From 85482241f208975fcaba8c1892d2cfc95f6ef5aa Mon Sep 17 00:00:00 2001 From: Konstantin Komarov Date: Thu, 3 Sep 2026 10:44:29 +0200 Subject: [PATCH 01/52] fs/ntfs3: document ads, nocase, and delalloc The 'ads', 'nocase', and 'delalloc' mount options are accepted by the parser and reported by ntfs_show_options(), but were never added to the mount option table in the documentation. Document them. Signed-off-by: Konstantin Komarov --- Documentation/filesystems/ntfs3.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Documentation/filesystems/ntfs3.rst b/Documentation/filesystems/ntfs3.rst index 2b86a9b3a6de..e1d86dcc3316 100644 --- a/Documentation/filesystems/ntfs3.rst +++ b/Documentation/filesystems/ntfs3.rst @@ -109,6 +109,29 @@ this table marked with no it means default is without **no**. Kernel. Not to be confused with NTFS ACLs. The option specified as acl enables support for POSIX ACLs. + * - ads + - Enable access to alternate data streams, i.e. named $DATA + attributes. A stream is addressed by appending a colon and the + stream name to the file name. Reading the pseudo-stream + ``query_streams`` returns the names of the streams attached to a + file, one per line:: + + cat file:query_streams # list stream names + cat file:ads1 # read a stream + touch file:ads2 # create a stream on an existing file + rm file:ads1 # remove a stream + + Creating a file together with a stream in a single call, and + renaming a stream, are not supported. Enabled by default. + + * - nocase + - Perform file name lookups case-insensitively. + + * - delalloc + - Delay block allocation for buffered writes until writeback, + rather than allocating as each write is issued. This lets the + driver allocate larger contiguous runs and reduces fragmentation. + Todo list ========= - Full journaling support over JBD. Currently journal replaying is supported From cd31336c23f6e40826675880396b45b32ef33fcb Mon Sep 17 00:00:00 2001 From: Konstantin Komarov Date: Thu, 3 Sep 2026 09:52:38 +0200 Subject: [PATCH 02/52] fs/ntfs3: convert to non-resident when the new size exceeds the record size attr_set_size_res() only checked whether the attribute still fits when the attribute was growing (dsize > 0), and only against sbi->max_bytes_per_attr. A resident attribute could therefore be resized to a value larger than a single MFT record can hold. Move the check ahead of the memmove() paths and also reject a new size larger than sbi->record_size, falling back to attr_make_nonresident() in both cases. Signed-off-by: Konstantin Komarov --- fs/ntfs3/attrib.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/fs/ntfs3/attrib.c b/fs/ntfs3/attrib.c index b1c315206ffa..833731d799a4 100644 --- a/fs/ntfs3/attrib.c +++ b/fs/ntfs3/attrib.c @@ -394,13 +394,15 @@ static int attr_set_size_res(struct ntfs_inode *ni, struct ATTRIB *attr, char *next = Add2Ptr(attr, asize); s64 dsize = ALIGN(new_size, 8) - ALIGN(rsize, 8); + if (new_size > sbi->record_size || + (dsize > 0 && used + dsize > sbi->max_bytes_per_attr)) { + return attr_make_nonresident(ni, attr, le, mi, new_size, run, + ins_attr, NULL); + } + if (dsize < 0) { memmove(next + dsize, next, tail); } else if (dsize > 0) { - if (used + dsize > sbi->max_bytes_per_attr) - return attr_make_nonresident(ni, attr, le, mi, new_size, - run, ins_attr, NULL); - memmove(next + dsize, next, tail); memset(next, 0, dsize); } From 6acb73ed40bc86c0f85d4af73a6f09c4ef733c13 Mon Sep 17 00:00:00 2001 From: Konstantin Komarov Date: Thu, 3 Sep 2026 09:55:44 +0200 Subject: [PATCH 03/52] fs/ntfs3: use MAX_LFS_FILESIZE for maxbytes with 64-bit clusters When CONFIG_NTFS3_64BIT_CLUSTER is enabled and the cluster count saturates the 64-bit VCN space, ntfs_init_from_boot() sets sbi->maxbytes to -1, while maxbytes_sparse and s_maxbytes are both set to MAX_LFS_FILESIZE just below. Set maxbytes to MAX_LFS_FILESIZE so all three limits agree. Signed-off-by: Konstantin Komarov --- fs/ntfs3/super.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/ntfs3/super.c b/fs/ntfs3/super.c index f4a42a0c73a4..d3093f72ea38 100644 --- a/fs/ntfs3/super.c +++ b/fs/ntfs3/super.c @@ -1210,7 +1210,7 @@ static int ntfs_init_from_boot(struct super_block *sb, u32 sector_size, #ifdef CONFIG_NTFS3_64BIT_CLUSTER if (clusters >= (1ull << (64 - cluster_bits))) - sbi->maxbytes = -1; + sbi->maxbytes = MAX_LFS_FILESIZE; sbi->maxbytes_sparse = MAX_LFS_FILESIZE; sb->s_maxbytes = MAX_LFS_FILESIZE; #else From ad39d8d90fa0fd99b2714d4c84c6f4e1c399f1f4 Mon Sep 17 00:00:00 2001 From: Weiming Shi Date: Fri, 26 Jun 2026 03:23:43 -0700 Subject: [PATCH 04/52] fs/ntfs3: take run_lock when freeing offs_folio in ni_decompress_file attr_wof_frame_info() allocates, caches and dereferences ni->file.offs_folio under down_write(&ni->file.run_lock). ni_decompress_file() frees the same folio with folio_put() without taking run_lock. When a WOF externally-compressed file is opened for write, ni_decompress_file() can drop the last reference while a concurrent O_RDONLY reader is dereferencing the folio in attr_wof_frame_info(), leading to a use-after-free: BUG: KASAN: use-after-free in attr_wof_frame_info (fs/ntfs3/attrib.c:1619) Read of size 4 at addr ffff8880120c9000 by task exploit Call Trace: attr_wof_frame_info (fs/ntfs3/attrib.c:1619) ni_read_frame (fs/ntfs3/frecord.c:2421) ni_read_folio_cmpr (fs/ntfs3/frecord.c:1916) ntfs_read_folio (fs/ntfs3/inode.c:648) read_pages (mm/readahead.c:181) page_cache_ra_unbounded (mm/readahead.c:292) force_page_cache_ra (mm/readahead.c:364) page_cache_sync_ra (mm/readahead.c:573) filemap_get_pages (mm/filemap.c:2688) filemap_read (mm/filemap.c:2806) generic_file_read_iter (mm/filemap.c:2994) ntfs_file_read_iter (fs/ntfs3/file.c:842) vfs_read (fs/read_write.c:574) __x64_sys_pread64 (fs/read_write.c:773) Take run_lock around the folio_put(), as is done for every other access to ni->file.offs_folio. Fixes: 4342306f0f0d ("fs/ntfs3: Add file operations and implementation") Reported-by: Xiang Mei Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Weiming Shi Signed-off-by: Konstantin Komarov --- fs/ntfs3/frecord.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fs/ntfs3/frecord.c b/fs/ntfs3/frecord.c index bead01a953f3..36bdb1983fef 100644 --- a/fs/ntfs3/frecord.c +++ b/fs/ntfs3/frecord.c @@ -2190,10 +2190,13 @@ int ni_decompress_file(struct ntfs_inode *ni) /* Clear cached flag. */ ni->ni_flags &= ~NI_FLAG_COMPRESSED_MASK; + /* offs_folio is accessed under run_lock in attr_wof_frame_info(). */ + down_write(&ni->file.run_lock); if (ni->file.offs_folio) { folio_put(ni->file.offs_folio); ni->file.offs_folio = NULL; } + up_write(&ni->file.run_lock); mapping->a_ops = &ntfs_aops; out: From bfa6296bb6a0e2333796a9507beb4605b92a9fdf Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Fri, 26 Jun 2026 17:49:14 +0800 Subject: [PATCH 05/52] fs/ntfs3: update ctime only on successful setxattr ntfs_setxattr() updates ctime and marks the inode dirty even when the xattr operation fails. Do that only on success. Fixes: 2d44667c306e ("fs/ntfs3: Update i_ctime when xattr is added") Signed-off-by: Baolin Liu Signed-off-by: Konstantin Komarov --- fs/ntfs3/xattr.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fs/ntfs3/xattr.c b/fs/ntfs3/xattr.c index 594ef6860b93..ab23caf7a218 100644 --- a/fs/ntfs3/xattr.c +++ b/fs/ntfs3/xattr.c @@ -976,8 +976,10 @@ static noinline int ntfs_setxattr(const struct xattr_handler *handler, NULL); out: - inode_set_ctime_current(inode); - mark_inode_dirty(inode); + if (!err) { + inode_set_ctime_current(inode); + mark_inode_dirty(inode); + } return err; } From a32e893a077a4b0d3b04c86cb7f69e354a187995 Mon Sep 17 00:00:00 2001 From: Weiming Shi Date: Thu, 25 Jun 2026 01:52:58 -0700 Subject: [PATCH 06/52] fs/ntfs3: zero frame pages on read error ni_read_frame() marks every frame page uptodate at the 'out:' label regardless of the return value. The pages come from ntfs_lock_new_page() and are not zeroed, and several error paths reach 'out:' before anything is written to them (e.g. a failed decompress_lznt()/decompress_lzx_xpress() on a corrupted chunk, or an allocation failure). A page marked uptodate is served directly from the page cache, so a later read() of the file returns the uninitialized page contents to userspace. On a crafted compressed image this leaks kernel memory, including pointers. Zero each page on the error path before marking it uptodate. The success path is unchanged. Fixes: 4342306f0f0d ("fs/ntfs3: Add file operations and implementation") Reported-by: Xiang Mei Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Weiming Shi Signed-off-by: Konstantin Komarov --- fs/ntfs3/frecord.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fs/ntfs3/frecord.c b/fs/ntfs3/frecord.c index 36bdb1983fef..4651dd8f6464 100644 --- a/fs/ntfs3/frecord.c +++ b/fs/ntfs3/frecord.c @@ -2524,6 +2524,8 @@ int ni_read_frame(struct ntfs_inode *ni, u64 frame_vbo, struct page **pages, out: for (i = 0; i < pages_per_frame; i++) { pg = pages[i]; + if (err) + clear_highpage(pg); SetPageUptodate(pg); } From 7a374d9be8267834dc534e55f01e5e01765cab4c Mon Sep 17 00:00:00 2001 From: Weiming Shi Date: Thu, 25 Jun 2026 05:07:42 -0700 Subject: [PATCH 07/52] ntfs3: fix out-of-bounds read in ntfs_read_mft() ntfs_read_mft() walks the attributes of an MFT record and, for each FILE_NAME attribute, compares the resident on-disk name with the name being looked up. The only size check before the compare is rsize >= SIZEOF_ATTRIBUTE_FILENAME, which covers the fixed header up to and including name_len, but not the variable-length name that follows it. name_len comes straight from disk and is not validated against the resident data_size. A crafted image can set name_len large while keeping data_size at the header size, so ntfs_cmp_names_cpu() reads name_len le16 characters past the end of the MFT record buffer: BUG: KASAN: slab-out-of-bounds in ntfs_cmp_names_cpu (fs/ntfs3/upcase.c:84) Read of size 2 at addr ffff888031879c00 by task a.out ntfs_cmp_names_cpu (fs/ntfs3/upcase.c:84) ntfs_iget5 (fs/ntfs3/inode.c:209) dir_search_u (fs/ntfs3/dir.c:265) ntfs_lookup (fs/ntfs3/namei.c:85) __lookup_slow (fs/namei.c:1915) lookup_slow (fs/namei.c:1932) path_lookupat (fs/namei.c:2278) vfs_statx (fs/stat.c) vfs_fstatat (fs/stat.c) __do_sys_newfstatat (fs/stat.c) Allocated by task: mi_init (fs/ntfs3/record.c) ntfs_iget5 (fs/ntfs3/inode.c) dir_search_u (fs/ntfs3/dir.c) ntfs_lookup (fs/ntfs3/namei.c) Check that the name fits in the resident data before using it. Fixes: 82cae269cfa9 ("fs/ntfs3: Add initialization of super block") Reported-by: Xiang Mei Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Weiming Shi Signed-off-by: Konstantin Komarov --- fs/ntfs3/inode.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fs/ntfs3/inode.c b/fs/ntfs3/inode.c index 56b4f6469a28..3b46726793e9 100644 --- a/fs/ntfs3/inode.c +++ b/fs/ntfs3/inode.c @@ -213,6 +213,11 @@ static int ntfs_read_mft(struct inode *inode, const struct cpu_str *name, names += 1; fname = Add2Ptr(attr, roff); + + /* Make sure the full name fits in the resident data. */ + if (rsize < fname_full_size(fname)) + goto out; + if (fname->type == FILE_NAME_DOS) goto next_attr; From 666af227b12fcc3f4ac9827d449ee0910f7bcf9f Mon Sep 17 00:00:00 2001 From: Yousef Alhouseen Date: Tue, 30 Jun 2026 23:17:43 +0200 Subject: [PATCH 08/52] ntfs3: reject empty iomap before reading its LCN attr_data_get_block() can return success with a zero run length when no cached or on-disk mapping covers a read VCN. In that case it does not initialize the LCN output. Check the returned length before comparing the LCN against the special mapping values. This preserves the existing -EINVAL result for an empty mapping without passing an uninitialized LCN to the comparisons. Fixes: ecbb433f9a8e ("fs/ntfs3: fold file size handling into ntfs_set_size()") Reported-by: syzbot+4b4ec878e25fafefa70f@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=4b4ec878e25fafefa70f Cc: stable@vger.kernel.org Signed-off-by: Yousef Alhouseen Signed-off-by: Konstantin Komarov --- fs/ntfs3/inode.c | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/fs/ntfs3/inode.c b/fs/ntfs3/inode.c index 3b46726793e9..5a93b0cc0caa 100644 --- a/fs/ntfs3/inode.c +++ b/fs/ntfs3/inode.c @@ -986,6 +986,8 @@ static int ntfs_iomap_begin(struct inode *inode, loff_t offset, loff_t length, if (err) { return err; } + if (!clen) + return -EINVAL; if (lcn == EOF_LCN) { /* request out of file. */ @@ -1021,11 +1023,6 @@ static int ntfs_iomap_begin(struct inode *inode, loff_t offset, loff_t length, return 0; } - if (!clen) { - /* broken file? */ - return -EINVAL; - } - iomap->bdev = inode->i_sb->s_bdev; iomap->offset = offset; iomap->length = ((loff_t)clen << cluster_bits) - off; From 5d3e03b70d05162ca49830efcd8b1cd2746a86a3 Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Tue, 30 Jun 2026 08:59:55 +0800 Subject: [PATCH 09/52] fs/ntfs3: reject FS_IOC_SETFSLABEL on readonly mounts FS_IOC_SETFSLABEL does not check for readonly mounts and can report success without persisting the label change. Return -EROFS on readonly mounts. Fixes: 21dc07ac9c25 ("ntfs3: add FS_IOC_SETFSLABEL ioctl") Signed-off-by: Baolin Liu Signed-off-by: Konstantin Komarov --- fs/ntfs3/file.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fs/ntfs3/file.c b/fs/ntfs3/file.c index 2abf334bfa0c..95dfe6878860 100644 --- a/fs/ntfs3/file.c +++ b/fs/ntfs3/file.c @@ -183,6 +183,9 @@ static int ntfs_ioctl_set_volume_label(struct ntfs_sb_info *sbi, u8 __user *buf) if (!capable(CAP_SYS_ADMIN)) return -EPERM; + if (sb_rdonly(sbi->sb)) + return -EROFS; + if (copy_from_user(user, buf, FSLABEL_MAX)) return -EFAULT; From 2f497d23652ff23da07b08cfb4b4a1c2a87f1892 Mon Sep 17 00:00:00 2001 From: Yousef Alhouseen Date: Sun, 28 Jun 2026 11:55:59 +0200 Subject: [PATCH 10/52] ntfs3: bound index head insertions by buffer capacity hdr_insert_head() shifts the existing index entries by ins_bytes without checking that the resulting used size fits within hdr->total. A directory index root larger than the newly allocated index buffer can therefore make the memmove write beyond the buffer. Reject insertions whose size would exceed the index header capacity and make both split callers unwind the new node when that happens. Fixes: 82cae269cfa9 ("fs/ntfs3: Add initialization of super block") Reported-by: syzbot+45ef5a8d661162757547@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=45ef5a8d661162757547 Cc: stable@vger.kernel.org Signed-off-by: Yousef Alhouseen Signed-off-by: Konstantin Komarov --- fs/ntfs3/index.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/fs/ntfs3/index.c b/fs/ntfs3/index.c index 689712d3463d..37e3e16b1f84 100644 --- a/fs/ntfs3/index.c +++ b/fs/ntfs3/index.c @@ -594,6 +594,8 @@ static const struct NTFS_DE *hdr_insert_head(struct INDEX_HDR *hdr, if (!e) return NULL; + if (size_add(used, ins_bytes) > le32_to_cpu(hdr->total)) + return NULL; /* Now we just make room for the inserted entries and jam it in. */ to_move = used - le32_to_cpu(hdr->de_off); @@ -1801,7 +1803,10 @@ static int indx_insert_into_root(struct ntfs_index *indx, struct ntfs_inode *ni, } /* Copy root entries into new buffer. */ - hdr_insert_head(hdr, re, to_move); + if (!hdr_insert_head(hdr, re, to_move)) { + err = -EINVAL; + goto out_put_n; + } /* Update bitmap attribute. */ indx_mark_used(indx, ni, new_vbn >> indx->idx2vbn_bits); @@ -1955,7 +1960,11 @@ static int indx_insert_into_buffer(struct ntfs_index *indx, /* Copy all the entries <= sp into the new buffer. */ de_t = hdr_first_de(hdr1); to_copy = PtrOffset(de_t, sp); - hdr_insert_head(hdr2, de_t, to_copy); + if (!hdr_insert_head(hdr2, de_t, to_copy)) { + err = -EINVAL; + put_indx_node(n2); + goto out; + } /* Remove all entries (sp including) from hdr1. */ used = used1 - to_copy - sp_size; From d0ac69ce3af4dbae01d4f064b7482f0c5440dbde Mon Sep 17 00:00:00 2001 From: "Xiang Mei (Microsoft)" Date: Tue, 7 Jul 2026 01:54:40 +0000 Subject: [PATCH 11/52] fs/ntfs3: check INDEX_ROOT resident size before use in ntfs_read_mft() When ntfs_read_mft() parses an ATTR_ROOT attribute it computes root = Add2Ptr(attr, roff); using the on-disk resident data offset (roff = attr->res.data_off) and then dereferences root->type and root->rule, and later passes root on to indx_init(). Unlike the ATTR_STD and ATTR_NAME cases, which validate that the resident data is large enough (asize >= sizeof(struct X) + roff and rsize >= sizeof(struct X)) before accessing it, the ATTR_ROOT case has no such check. mi_enum_attr() only guarantees data_off + data_size <= attribute size, so a crafted image can set data_off such that root lands at (or past) the end of the MFT record's slab allocation while still passing that check (e.g. data_size == 0). The subsequent read of root->type is then a slab-out-of-bounds read, reported by KASAN when mounting the image: BUG: KASAN: slab-out-of-bounds in ntfs_iget5 (fs/ntfs3/inode.c:283) Read of size 4 at addr ffff88800c06a400 by task exploit/142 Call Trace: ... [KASAN report internals] ntfs_iget5 (fs/ntfs3/inode.c:283 fs/ntfs3/inode.c:542) ntfs_fill_super (fs/ntfs3/super.c:1690) get_tree_bdev_flags (fs/super.c:1634) vfs_get_tree (fs/super.c:1694) fc_mount (fs/namespace.c:1198) path_mount (fs/namespace.c:3765 ...) __x64_sys_mount (fs/namespace.c:4174 ...) do_syscall_64 (arch/x86/entry/syscall_64.c:63) entry_SYSCALL_64_after_hwframe (arch/x86/entry/entry_64.S:121) The buggy address is located 0 bytes to the right of allocated 1024-byte region [ffff88800c06a000, ffff88800c06a400) which belongs to the cache kmalloc-1k of size 1024 Validate the resident data size in the ATTR_ROOT case the same way the neighbouring cases do, so that a truncated or mis-offset INDEX_ROOT is rejected before root is dereferenced. Fixes: 82cae269cfa9 ("fs/ntfs3: Add initialization of super block") Reported-by: AutonomousCodeSecurity@microsoft.com Signed-off-by: Xiang Mei (Microsoft) Signed-off-by: Konstantin Komarov --- fs/ntfs3/inode.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fs/ntfs3/inode.c b/fs/ntfs3/inode.c index 5a93b0cc0caa..946eabb2ce25 100644 --- a/fs/ntfs3/inode.c +++ b/fs/ntfs3/inode.c @@ -285,7 +285,9 @@ static int ntfs_read_mft(struct inode *inode, const struct cpu_str *name, break; case ATTR_ROOT: - if (attr->non_res) + if (attr->non_res || + asize < sizeof(struct INDEX_ROOT) + roff || + rsize < sizeof(struct INDEX_ROOT)) goto out; root = Add2Ptr(attr, roff); From a1972501ffb15313f7ae598989bb2cd32829e625 Mon Sep 17 00:00:00 2001 From: Zhan Xusheng Date: Fri, 4 Sep 2026 14:15:56 +0800 Subject: [PATCH 12/52] fs/ntfs3: fix valid_size not being updated in attr_allocate_frame() At the end of attr_allocate_frame() the on-disk valid_size is meant to be updated to the value passed in via @new_valid: valid_size = le64_to_cpu(attr_b->nres.valid_size); if (new_valid != valid_size) { attr_b->nres.valid_size = cpu_to_le64(valid_size); mi_b->dirty = true; } but it writes back the old valid_size that was just read instead of new_valid, so the field is never actually changed even though the record is marked dirty -- the intended update is a no-op. ni_update_parent() spells the same read-compare-write out correctly a few hundred lines away, storing the newly derived value: valid_le = cpu_to_le64(new_valid); if (valid_le != attr->nres.valid_size) { attr->nres.valid_size = valid_le; In practice that path, reached from ni_write_inode(), re-derives nres.valid_size from ni->i_valid on inode write-back, which normally masks this, so the visible impact is limited. The assignment here is still wrong though; store @new_valid as intended. Fixes: be71b5cba2e6 ("fs/ntfs3: Add attrib operations") Signed-off-by: Zhan Xusheng Signed-off-by: Konstantin Komarov --- fs/ntfs3/attrib.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/ntfs3/attrib.c b/fs/ntfs3/attrib.c index 833731d799a4..a9d97c970aef 100644 --- a/fs/ntfs3/attrib.c +++ b/fs/ntfs3/attrib.c @@ -1997,7 +1997,7 @@ int attr_allocate_frame(struct ntfs_inode *ni, CLST frame, size_t compr_size, valid_size = le64_to_cpu(attr_b->nres.valid_size); if (new_valid != valid_size) { - attr_b->nres.valid_size = cpu_to_le64(valid_size); + attr_b->nres.valid_size = cpu_to_le64(new_valid); mi_b->dirty = true; } } From 01e56820ef52d14a5560fb7c0c6f2f75ef6f9393 Mon Sep 17 00:00:00 2001 From: Konstantin Komarov Date: Mon, 21 Sep 2026 12:35:04 +0200 Subject: [PATCH 13/52] fs/ntfs3: validate EA entry layout against ef->size in ntfs_read_ea In ntfs_read_ea(), the ef->size branch only checks that the declared entry size fits the remaining buffer; unlike the zero-size branch it never verifies that the entry's own name and value fit inside ef->size. A crafted image can thus present an in-bounds ef->size with an arbitrary elength, which find_ea()/ntfs_get_ea() later use as the memcpy length, reading past the allocation and leaking adjacent heap memory: BUG: KASAN: slab-out-of-bounds in ntfs_get_ea (fs/ntfs3/xattr.c:302) Read of size 65535 at addr ffff888010770b0f by task exploit/142 ... __asan_memcpy (mm/kasan/shadow.c:105) ntfs_get_ea (fs/ntfs3/xattr.c:302) ntfs_getxattr (fs/ntfs3/xattr.c:848) __vfs_getxattr (fs/xattr.c:446) vfs_getxattr (fs/xattr.c:479) do_getxattr (fs/xattr.c:808) filename_getxattr (fs/xattr.c:837) path_getxattrat (fs/xattr.c:876) do_syscall_64 (arch/x86/entry/syscall_64.c:94) ... The buggy address is located 15 bytes inside of allocated 20-byte region, cache kmalloc-32 Validate the name/value layout in the ef->size branch too, bounding it against ef->size, as the zero-size branch validates it against the buffer. Fixes: be71b5cba2e6 ("fs/ntfs3: Add attrib operations") Reported-by: Weiming Shi Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Xiang Mei [almaz.alexandrovich@paragon-software.com: remove duplicate check of need > ea_size] Signed-off-by: Konstantin Komarov --- fs/ntfs3/xattr.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fs/ntfs3/xattr.c b/fs/ntfs3/xattr.c index ab23caf7a218..988eba963af9 100644 --- a/fs/ntfs3/xattr.c +++ b/fs/ntfs3/xattr.c @@ -165,6 +165,8 @@ static int ntfs_read_ea(struct ntfs_inode *ni, struct EA_FULL **ea, /* ef->size must fit the list and cover the record. */ if (ea_size > bytes || ea_size < need) goto out1; + if (bytes < offsetof(struct EA_FULL, name)) + goto out1; continue; } From 5b76487f94dacc2884135ce9a7bda61ce7ffa209 Mon Sep 17 00:00:00 2001 From: Xiang Mei Date: Fri, 3 Jul 2026 14:54:31 -0700 Subject: [PATCH 14/52] fs/ntfs3: validate $SDH entry size in ntfs_insert_security ntfs_insert_security() walks the $Secure::$SDH index and reads e->sec_hdr.size for every entry returned by indx_find()/indx_find_sort(). Those only guarantee the NTFS_DE envelope (e->size), so a crafted $SDH entry shorter than a full NTFS_DE_SDH at the tail of an index buffer makes the read of e->sec_hdr.size run past the allocation. Triggerable by an unprivileged setxattr(2) on a crafted NTFS image. BUG: KASAN: slab-out-of-bounds in ntfs_insert_security (fs/ntfs3/fsntfs.c:2148) Read of size 4 at addr ffff88800fa1de00 by task exploit/142 ntfs_insert_security (fs/ntfs3/fsntfs.c:2148) ntfs_setxattr (fs/ntfs3/xattr.c:947) __vfs_setxattr (fs/xattr.c:223) __vfs_setxattr_noperm (fs/xattr.c:257) vfs_setxattr (fs/xattr.c:344) filename_setxattr (fs/xattr.c:690) path_setxattrat (fs/xattr.c:734) __x64_sys_setxattr (fs/xattr.c:764) [...] The buggy address is located 0 bytes to the right of allocated 512-byte region [ffff88800fa1dc00, ffff88800fa1de00) Stop the scan when an entry is too small to hold a SECURITY_HDR. Fixes: 82cae269cfa9 ("fs/ntfs3: Add initialization of super block") Reported-by: Weiming Shi Signed-off-by: Xiang Mei Signed-off-by: Konstantin Komarov --- fs/ntfs3/fsntfs.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fs/ntfs3/fsntfs.c b/fs/ntfs3/fsntfs.c index 97c04ab2763a..b55311ccd229 100644 --- a/fs/ntfs3/fsntfs.c +++ b/fs/ntfs3/fsntfs.c @@ -2153,6 +2153,9 @@ int ntfs_insert_security(struct ntfs_sb_info *sbi, goto out; while (e) { + if (le16_to_cpu(e->de.size) < SIZEOF_SDH_DIRENTRY) + break; + if (le32_to_cpu(e->sec_hdr.size) == new_sec_size) { err = ntfs_read_run_nb(sbi, &ni->file.run, le64_to_cpu(e->sec_hdr.off), From eff11b5a1eacea8d17fa50ad0b1c2ef405341f2c Mon Sep 17 00:00:00 2001 From: Alexandro Calo Date: Tue, 7 Jul 2026 16:23:48 +0200 Subject: [PATCH 15/52] fs/ntfs3: Fix OOB write if ret == buf_len in ntfs_utf16_to_nls() 'utf16s_to_utf8s()' is documented to return at most 'buf_len', the caller does not check whether 'ret == buf_len' before writing the terminator. The 'buf[ret] = '\0'' could write a NULL terminator one byte past the buffer. Fix this up by limiting ret to the last valid buffer index before writing NULL. As long as 'utf16s_to_utf8s()' never return a negative value and 'buf_len' is guaranteed to be nonzero the patch is fine. Anyway, as defensive fixes, if(ret<0) and if(buf_len==0) could be added. Signed-off-by: Alexandro Calo Signed-off-by: Konstantin Komarov --- fs/ntfs3/dir.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fs/ntfs3/dir.c b/fs/ntfs3/dir.c index eb9152e9fa22..328c9a29df89 100644 --- a/fs/ntfs3/dir.c +++ b/fs/ntfs3/dir.c @@ -34,6 +34,9 @@ int ntfs_utf16_to_nls(struct ntfs_sb_info *sbi, const __le16 *name, u32 len, /* UTF-16 -> UTF-8 */ ret = utf16s_to_utf8s((wchar_t *)name, len, UTF16_LITTLE_ENDIAN, buf, buf_len); + if (ret >= buf_len) { + ret = buf_len-1; + } buf[ret] = '\0'; return ret; } From 5a5aef1142cd357897896d40f874849514b1ebbc Mon Sep 17 00:00:00 2001 From: Alexandro Calo Date: Wed, 8 Jul 2026 16:16:39 +0200 Subject: [PATCH 16/52] fs/ntfs3: Fix heap overflow after ALIGN in mi_pack_runs() When run_pack() returns a non-aligned byte count, ALIGN(err, 8) inflates it by 1-7 bytes, and since the destination of the memmove() is computed from that inflated value, the entire attribute tail could be copied 1-7 bytes past where the record buffer ends. The memmove() destination is next + new_run_size - run_size. For it to stay within the record, new_run_size must not exceed run_size + dsize, the space actually available. Since new_run_size = ALIGN(err, 8), a fix could be to ensures there is enough room that whatever ALIGN does to err, the result still fits within the available space. Restricting the budget of run_pack() should guarantee that. Something like: u32 avail = (run_size + dsize) & ~7u; err = run_pack(run, svcn, len, Add2Ptr(attr, run_off), avail, &plen); Now, run_pack() should never return 0. As now, the function returns at least 1. So this is fine. Negative values are caught as errors by if(err<0). The rounded-down avail may be smaller than the existing run size, if avail=0 run_pack is called and will write run_buf[0] = 0; that will be OOB, because the buffer size is zero, so imo the caller should avoid calling run_pack() with avail=0. After run_pack(), if plen = 0, then svcn + plen - 1 becomes svcn - 1. So, after run_pack(), it may be worth ensuring if(!plen). Lastly, I'm not sure if -ENOSPC is the right error code here, but it seems reasonable for a space condition. This heap out-of-bounds write requires a crafted filesystem image, which is not in the kernel threat model, but fixing memory errors would be nice to keep things secure. Signed-off-by: Alexandro Calo Signed-off-by: Konstantin Komarov --- fs/ntfs3/record.c | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/fs/ntfs3/record.c b/fs/ntfs3/record.c index 4f12ce15b03b..d7785f9e8e09 100644 --- a/fs/ntfs3/record.c +++ b/fs/ntfs3/record.c @@ -665,18 +665,32 @@ int mi_pack_runs(struct mft_inode *mi, struct ATTRIB *attr, u32 run_size = asize - run_off; u32 tail = used - aoff - asize; u32 dsize = sbi->record_size - used; + u32 avail; /* Make a maximum gap in current record. */ memmove(next + dsize, next, tail); + /* Leave room for the 8-byte ALIGN() to avoid OOB write */ + avail = (run_size + dsize) & ~7u; + + if (!avail) { + memmove(next, next + dsize, tail); + return -ENOSPC; + } + /* Pack as much as possible. */ - err = run_pack(run, svcn, len, Add2Ptr(attr, run_off), run_size + dsize, + err = run_pack(run, svcn, len, Add2Ptr(attr, run_off), avail, &plen); if (err < 0) { memmove(next, next + dsize, tail); return err; } + if (!plen) { + memmove(next, next + dsize, tail); + return -ENOSPC; + } + new_run_size = ALIGN(err, 8); memmove(next + new_run_size - run_size, next + dsize, tail); From 4d7e2c4c6329b80dd0ff5674fa23a92f9dcf5b16 Mon Sep 17 00:00:00 2001 From: Alexandro Calo Date: Wed, 8 Jul 2026 12:04:37 +0200 Subject: [PATCH 17/52] Fix OOB write if err == buflen in ntfs_readlink_hlp() ntfs_utf16_to_nls() may return buflen. The caller later uses the returned length as the index for writing the trailing NUL byte, so err == buflen writes one byte past the end of buffer. Fix this by limiting err to the last valid buffer index before writing NUL. ntfs_utf16_to_nls() returning a negative value is already handled by if (err < 0) goto out; As long as buflen is guaranteed to be nonzero the patch is fine. As a defensive fix if(buflen==0) could be added. This heap out-of-bounds write requires a crafted filesystem image, which is not in the kernel threat model, but fixing memory errors would be nice to keep things secure. Signed-off-by: Alexandro Calo Signed-off-by: Konstantin Komarov --- fs/ntfs3/inode.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fs/ntfs3/inode.c b/fs/ntfs3/inode.c index 946eabb2ce25..e6c96ef88b01 100644 --- a/fs/ntfs3/inode.c +++ b/fs/ntfs3/inode.c @@ -2251,6 +2251,9 @@ static noinline int ntfs_readlink_hlp(const struct dentry *link_de, if (err < 0) goto out; + if (err >= buflen) + err = buflen - 1; + /* Translate Windows '\' into Linux '/'. */ for (i = 0; i < err; i++) { if (buffer[i] == '\\') From a2914eb60a09dccfd3d22f324c7c6108cf6105ea Mon Sep 17 00:00:00 2001 From: Yi Xie Date: Tue, 7 Jul 2026 10:14:44 +0800 Subject: [PATCH 18/52] ntfs3: fix resident attribute handling in run load paths Don't dereference nres unless attr->non_res is set. ni_find_attr() can succeed with a resident record on a corrupted image, and run_unpack_ex() would mis-parse it as mapping pairs. Fix attr_load_runs(), attr_load_runs_vcn() and attr_set_size(). Fixes: 5ec75546dc71 ("ntfs: fix WARN_ON for resident attribute in ntfs_map_runlist_nolock()") Signed-off-by: Yi Xie Signed-off-by: Konstantin Komarov --- fs/ntfs3/attrib.c | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/fs/ntfs3/attrib.c b/fs/ntfs3/attrib.c index a9d97c970aef..a18c56663513 100644 --- a/fs/ntfs3/attrib.c +++ b/fs/ntfs3/attrib.c @@ -61,11 +61,16 @@ static int attr_load_runs(struct ATTRIB *attr, struct ntfs_inode *ni, struct runs_tree *run, const CLST *vcn) { int err; - CLST svcn = le64_to_cpu(attr->nres.svcn); - CLST evcn = le64_to_cpu(attr->nres.evcn); + CLST svcn, evcn; u32 asize; u16 run_off; + if (!attr->non_res) + return -EIO; + + svcn = le64_to_cpu(attr->nres.svcn); + evcn = le64_to_cpu(attr->nres.evcn); + if (svcn >= evcn + 1 || run_is_mapped_full(run, svcn, evcn)) return 0; @@ -558,6 +563,10 @@ int attr_set_size_ex(struct ntfs_inode *ni, enum ATTR_TYPE type, } next_le_1: + if (!attr->non_res) { + err = -EIO; + goto out; + } svcn = le64_to_cpu(attr->nres.svcn); evcn = le64_to_cpu(attr->nres.evcn); } @@ -1458,6 +1467,9 @@ int attr_load_runs_vcn(struct ntfs_inode *ni, enum ATTR_TYPE type, return -ENOENT; } + if (!attr->non_res) + return -EIO; + svcn = le64_to_cpu(attr->nres.svcn); evcn = le64_to_cpu(attr->nres.evcn); From e2fcb0040b0b4097b2e97f3fbe77d3b66df9e816 Mon Sep 17 00:00:00 2001 From: Ibrahim Hashimov Date: Thu, 9 Jul 2026 00:46:18 +0200 Subject: [PATCH 19/52] fs/ntfs3: fix integer overflow in find_log_rec() causing OOB read find_log_rec() validates the on-disk LFS_RECORD_HDR.client_data_len field (`len`, a raw u32 read straight from a mounted $LogFile page) against the log's total available space: len = le32_to_cpu(rh->client_data_len); rec_len = len + log->record_header_len; if (rec_len >= log->total_avail) return -EINVAL; The addition is unchecked. For len in [0xffffffd0, 0xffffffff] (i.e. log->record_header_len away from UINT_MAX), `rec_len` wraps to a small u32 value that trivially clears the `rec_len >= log->total_avail` guard, so a record whose declared length is actually ~4 GiB is reported as valid and in-page. Because find_log_rec() wrongly accepts the record, its caller read_log_rec_lcb() hands it back to log_replay() as good. On the transaction-table replay leg, log_replay() re-derives the record length directly from the same raw, still-unclamped client_data_len field and uses it to size the restart-table blob: rec_len = le32_to_cpu(frh->client_data_len); /* ~0xffffffff */ ... t16 = le16_to_cpu(lrh->redo_off); rt = Add2Ptr(lrh, t16); t32 = rec_len - t16; /* still ~4 GiB */ if (!check_rstbl(rt, t32)) return -EINVAL; check_rstbl()'s own `bytes < ts` bounds guard is defeated because the caller-supplied `bytes` (t32) is itself the poisoned ~4 GiB value, so the guard can never trigger. The subsequent entry-validation loop then walks `rt->used` (also attacker-controlled) entries of sizeof(struct RESTART_TABLE) == 0x18 bytes each straight past the end of the kmalloc(log->page_size) log-page buffer allocated in read_log_page(), producing an out-of-bounds read that is reachable simply by mounting a crafted $LogFile (no genuine dirty-page / crash-recovery state is required). Confirmed via KASAN as a slab-out-of-bounds read in check_rstbl(), called from log_replay() -> ntfs_loadlog_and_replay() -> ntfs_fill_super() -> mount(2). Fix the root cause instead of hardening every downstream consumer of the poisoned length: make the addition in find_log_rec() overflow-safe with check_add_overflow(), mirroring the pattern fs/ntfs3/run.c already uses to validate other attacker-controlled on-disk length/offset arithmetic decoded from MAPPING_PAIRS runs, e.g.: if (check_add_overflow(vcn64, len, &next_vcn)) return -EINVAL; ... if (check_add_overflow(lcn, len, &lcn_end)) return -EINVAL; (fs/ntfs3/run.c, run_unpack()). With the overflow rejected instead of silently wrapped, `rec_len` is only ever a true, non-wrapped sum, so the existing `rec_len >= log->total_avail` check now correctly rejects any oversized record up front. log_replay()'s transaction-table leg then never gets a chance to re-read the poisoned client_data_len, and check_rstbl() is never invoked with an attacker-inflated `bytes` for this path. Verified on a v6.19 KASAN build: mounting a crafted $LogFile whose client_data_len overflows trips a KASAN slab-out-of-bounds-read report in check_rstbl() before this fix; with check_add_overflow() applied, the same image is cleanly rejected during log replay and no KASAN report fires. Fixes: b46acd6a6a62 ("fs/ntfs3: Add NTFS journal") Cc: stable@vger.kernel.org Signed-off-by: Ibrahim Hashimov Assisted-by: AuditCode-AI:2026.07 Signed-off-by: Konstantin Komarov --- fs/ntfs3/fslog.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fs/ntfs3/fslog.c b/fs/ntfs3/fslog.c index ed50c1d0c23e..9a44b322ad60 100644 --- a/fs/ntfs3/fslog.c +++ b/fs/ntfs3/fslog.c @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -2457,7 +2458,8 @@ static int find_log_rec(struct ntfs_log *log, u64 lsn, struct lcb *lcb) * Check that the length field isn't greater than the total * available space the log file. */ - rec_len = len + log->record_header_len; + if (check_add_overflow(len, log->record_header_len, &rec_len)) + return -EINVAL; if (rec_len >= log->total_avail) return -EINVAL; From bf9a6578e03b065335836e025356b4b9170b9449 Mon Sep 17 00:00:00 2001 From: "Xiang Mei (Microsoft)" Date: Thu, 9 Jul 2026 22:05:01 +0000 Subject: [PATCH 20/52] fs/ntfs3: fix slab-out-of-bounds write in attr_insert_range() When fallocate(FALLOC_FL_INSERT_RANGE) grows a still-resident ntfs3 attribute, attr_insert_range() shifts the inline data using the insert length 'bytes' as both the destination offset and the copy size: memmove(data + bytes, data, bytes); Both are wrong: the shift must start at the insertion point 'vbo' and move only the data past it (data_size - vbo). When bytes > data_size the memmove runs off the end of the data_size + bytes buffer and overflows the adjacent slab object. Since vbo < data_size is already enforced above, the reworked offsets stay within the data_size + bytes buffer and cannot under/overflow. BUG: KASAN: slab-out-of-bounds in attr_insert_range (fs/ntfs3/attrib.c:2581) Write of size 512 at addr ffff8880135e8b10 by task exploit/142 ... __asan_memmove (mm/kasan/shadow.c:95) attr_insert_range (fs/ntfs3/attrib.c:2581) ntfs_fallocate (fs/ntfs3/file.c:618) vfs_fallocate (fs/open.c:338) __x64_sys_fallocate (fs/open.c:362) ... Fixes: 9256ec35359f ("fs/ntfs3: Refactoring attr_insert_range to restore after errors") Reported-by: AutonomousCodeSecurity@microsoft.com Signed-off-by: Xiang Mei (Microsoft) Signed-off-by: Konstantin Komarov --- fs/ntfs3/attrib.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/ntfs3/attrib.c b/fs/ntfs3/attrib.c index a18c56663513..bce7101b5823 100644 --- a/fs/ntfs3/attrib.c +++ b/fs/ntfs3/attrib.c @@ -2624,8 +2624,8 @@ int attr_insert_range(struct ntfs_inode *ni, u64 vbo, u64 bytes) char *data = Add2Ptr(attr_b, le16_to_cpu(attr_b->res.data_off)); - memmove(data + bytes, data, bytes); - memset(data, 0, bytes); + memmove(data + vbo + bytes, data + vbo, data_size - vbo); + memset(data + vbo, 0, bytes); goto done; } From 456189f9c714dee6716c2a3aad908e63830cac66 Mon Sep 17 00:00:00 2001 From: Michael Bommarito Date: Sat, 11 Jul 2026 11:26:30 -0400 Subject: [PATCH 21/52] fs/ntfs3: reject an oversized resident attribute on the inline iomap path attr_data_get_block_locked() maps a resident attribute as a one-page IOMAP_INLINE extent: it allocates a single page, copies the resident value into it with memcpy(), and hands that page to ntfs_iomap_begin() as the inline data. The copy length is the on-disk resident value length (res.data_size), and mi_enum_attr() only bounds that length against the MFT record size. A corrupted volume with MFT records larger than a page can therefore present a resident $DATA whose length exceeds PAGE_SIZE, and the memcpy() then writes past the single destination page. Impact: reading or writing a resident file on a mounted crafted NTFS volume whose MFT records are larger than a page overflows the one-page inline buffer in attr_data_get_block_locked() with attacker-controlled bytes. Reject such an attribute in attr_data_get_block_locked(), before the page is allocated and the copy is made. Fixes: 099ef9ab9203 ("fs/ntfs3: implement iomap-based file operations") Cc: stable@vger.kernel.org Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Michael Bommarito Signed-off-by: Konstantin Komarov --- fs/ntfs3/attrib.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/fs/ntfs3/attrib.c b/fs/ntfs3/attrib.c index bce7101b5823..3b4ef15162da 100644 --- a/fs/ntfs3/attrib.c +++ b/fs/ntfs3/attrib.c @@ -1045,6 +1045,21 @@ int attr_data_get_block_locked(struct ntfs_inode *ni, CLST vcn, CLST clen, if (!attr_b->non_res) { u32 data_size = le32_to_cpu(attr_b->res.data_size); + + /* + * A resident attribute is copied into a single page below + * (alloc_page() + memcpy()) and mapped as a one-page + * IOMAP_INLINE extent by ntfs_iomap_begin(). mi_enum_attr() + * only bounds the resident value length against the MFT record + * size, so a corrupted volume whose records are larger than a + * page can report data_size > PAGE_SIZE; copying that many bytes + * would overflow the single destination page. Reject it. + */ + if (data_size > PAGE_SIZE) { + err = -EINVAL; + goto out; + } + *lcn = RESIDENT_LCN; *len = data_size; if (res) { From f3b8ee6c05bed24a06192ea8e4cafcc06946c1bd Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Thu, 9 Jul 2026 15:18:49 +0800 Subject: [PATCH 22/52] fs/ntfs3: return -ERANGE for short xattr buffers ntfs3 currently returns -ENODATA when the xattr exists but the user buffer is too small. Return -ERANGE instead. Signed-off-by: Baolin Liu Signed-off-by: Konstantin Komarov --- fs/ntfs3/xattr.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fs/ntfs3/xattr.c b/fs/ntfs3/xattr.c index 988eba963af9..7f77df9c46f8 100644 --- a/fs/ntfs3/xattr.c +++ b/fs/ntfs3/xattr.c @@ -785,7 +785,7 @@ static int ntfs_getxattr(const struct xattr_handler *handler, struct dentry *de, if (!buffer) { err = sizeof(u8); } else if (size < sizeof(u8)) { - err = -ENODATA; + err = -ERANGE; } else { err = sizeof(u8); *(u8 *)buffer = le32_to_cpu(ni->std_fa); @@ -799,7 +799,7 @@ static int ntfs_getxattr(const struct xattr_handler *handler, struct dentry *de, if (!buffer) { err = sizeof(u32); } else if (size < sizeof(u32)) { - err = -ENODATA; + err = -ERANGE; } else { err = sizeof(u32); *(u32 *)buffer = le32_to_cpu(ni->std_fa); @@ -839,7 +839,7 @@ static int ntfs_getxattr(const struct xattr_handler *handler, struct dentry *de, if (!buffer) { err = sd_size; } else if (size < sd_size) { - err = -ENODATA; + err = -ERANGE; } else { err = sd_size; memcpy(buffer, sd, sd_size); From 60bd50014cc9a97c5e34a8bddd2bad4d2dc5acb7 Mon Sep 17 00:00:00 2001 From: Aldo Ariel Panzardo Date: Thu, 23 Jul 2026 19:10:45 -0300 Subject: [PATCH 23/52] fs/ntfs3: validate that EA entry size covers its name and value ntfs_read_ea() walks the on-disk $EA attribute and checks each EA_FULL entry for consistency. For an entry with a non-zero ef->size (the offset to the next entry) the loop only verifies that this stride fits in the remaining buffer, and then continues: if (ef->size) { ea_size = le32_to_cpu(ef->size); if (ea_size > bytes) goto out1; continue; } It never verifies that the entry's own name (ef->name_len) and value (ef->elength) actually fit inside ef->size. A later reader trusts ef->elength unconditionally; ntfs_get_ea() does: len = le16_to_cpu(ea->elength); /* up to 0xffff */ ... if (len > size) /* size is the user buffer */ return -ERANGE; memcpy(buffer, ea->name + ea->name_len + 1, len); A crafted image with a small but valid ef->size (e.g. 24) and ef->elength == 0xffff therefore passes validation, and a getxattr() with a large enough user buffer copies up to 64 KiB starting just past the short entry -- an out-of-bounds read of the ea_all allocation that leaks adjacent kernel heap memory to userspace. BUG: KASAN: slab-out-of-bounds in ntfs_get_ea+0x2b8/0x3f0 Read of size 65535 ... ntfs_get_ea -> ntfs_getxattr -> vfs_getxattr -> do_getxattr The zero-ef->size branch already computes the same struct_size() to derive the stride; mirror that in the non-zero branch and reject the entry when its declared size cannot hold the name and value. Fixes: 0e8235d28f3a ("fs/ntfs3: Check fields while reading") Cc: stable@vger.kernel.org Signed-off-by: Aldo Ariel Panzardo Signed-off-by: Konstantin Komarov --- fs/ntfs3/xattr.c | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/fs/ntfs3/xattr.c b/fs/ntfs3/xattr.c index 7f77df9c46f8..2cf45bdd93d5 100644 --- a/fs/ntfs3/xattr.c +++ b/fs/ntfs3/xattr.c @@ -167,6 +167,24 @@ static int ntfs_read_ea(struct ntfs_inode *ni, struct EA_FULL **ea, goto out1; if (bytes < offsetof(struct EA_FULL, name)) goto out1; + + /* Check if we can use fields ef->name_len and ef->elength. */ + if (bytes < offsetof(struct EA_FULL, name)) + goto out1; + + /* + * The declared entry size (ef->size) must be large + * enough to hold the name and value. Otherwise a later + * reader such as ntfs_get_ea() copies ef->elength bytes + * starting past the entry, reading out of bounds of the + * ea buffer and leaking adjacent heap memory to + * userspace via getxattr(). + */ + if (struct_size(ef, name, + 1 + ef->name_len + + le16_to_cpu(ef->elength)) > + ea_size) + goto out1; continue; } From 944ed7c00182deb1a8e9aa59d7c9d0a56f5c42f3 Mon Sep 17 00:00:00 2001 From: Matvey Kovalev Date: Sun, 19 Jul 2026 23:25:16 +0300 Subject: [PATCH 24/52] fs/ntfs3: remove redundant if-block in check_attr() Condition of this if-block repeats another one from if-block above. So, condition is always false. Found by Linux Verification Center (linuxtesting.org) with SVACE. Signed-off-by: Matvey Kovalev Signed-off-by: Konstantin Komarov --- fs/ntfs3/fslog.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/fs/ntfs3/fslog.c b/fs/ntfs3/fslog.c index 9a44b322ad60..5b7bac29b4e4 100644 --- a/fs/ntfs3/fslog.c +++ b/fs/ntfs3/fslog.c @@ -2785,9 +2785,6 @@ static inline bool check_attr(const struct MFT_REC *rec, return false; } - if (run_off > asize) - return false; - if (run_unpack(NULL, sbi, 0, svcn, evcn, svcn, Add2Ptr(attr, run_off), asize - run_off) < 0) { return false; From 131f7d7abf0f10edc6ea6b032f8a3908bdb13ce8 Mon Sep 17 00:00:00 2001 From: Dmitry Morgun Date: Sun, 12 Jul 2026 15:15:49 +0000 Subject: [PATCH 25/52] fs/ntfs3: check return values in indx_delete_entry() When the index tree collapses down to an empty root, indx_delete_entry() calls attr_set_size() and ni_remove_attr() for both ATTR_ALLOC and ATTR_BITMAP, but their return values are overwritten by the following call without being checked. In most other call sites of these functions the return value is checked, so add the same checks here in case an error at this point is meaningful. Found by Linux Verification Center (linuxtesting.org) with SVACE. Fixes: 82cae269cfa9 ("fs/ntfs3: Add initialization of super block") Signed-off-by: Dmitry Morgun Signed-off-by: Konstantin Komarov --- fs/ntfs3/index.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/fs/ntfs3/index.c b/fs/ntfs3/index.c index 37e3e16b1f84..bfc6d1f6d2fc 100644 --- a/fs/ntfs3/index.c +++ b/fs/ntfs3/index.c @@ -2711,18 +2711,28 @@ int indx_delete_entry(struct ntfs_index *indx, struct ntfs_inode *ni, err = attr_set_size(ni, ATTR_ALLOC, in->name, in->name_len, &indx->alloc_run, 0, NULL, false); + if (err) + goto out; + if (in->name == I30_NAME) i_size_write(&ni->vfs_inode, 0); err = ni_remove_attr(ni, ATTR_ALLOC, in->name, in->name_len, false, NULL); run_close(&indx->alloc_run); + if (err) + goto out; err = attr_set_size(ni, ATTR_BITMAP, in->name, in->name_len, &indx->bitmap_run, 0, NULL, false); + if (err) + goto out; + err = ni_remove_attr(ni, ATTR_BITMAP, in->name, in->name_len, false, NULL); run_close(&indx->bitmap_run); + if (err) + goto out; root = indx_get_root(indx, ni, &attr, &mi); if (!root) { From f3fb2530eac6d6e647488b97afca2f15d94b4451 Mon Sep 17 00:00:00 2001 From: Konstantin Komarov Date: Wed, 7 Oct 2026 11:21:00 +0200 Subject: [PATCH 26/52] fs/ntfs3: show supported options when an unknown one is given When an unrecognised mount option is passed, fs_parse() returns -ENOPARAM. Print the list of supported options alongside the key, so the correct spelling can be found directly. Signed-off-by: Konstantin Komarov --- fs/ntfs3/super.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/fs/ntfs3/super.c b/fs/ntfs3/super.c index d3093f72ea38..7fd83027007b 100644 --- a/fs/ntfs3/super.c +++ b/fs/ntfs3/super.c @@ -339,6 +339,17 @@ static int ntfs_fs_parse_param(struct fs_context *fc, int opt; opt = fs_parse(fc, ntfs_fs_parameters, param, &result); + + if (opt == -ENOPARAM && strcmp(param->key, "source")) { + /* Show mini help. */ + pr_err("ntfs3: unknown option \"%s\". Use uid,gid,umask," + "dmask,fmask,sys_immutable,discard,force,sparse," + "nohidden,hide_dot_files,windows_names,showmeta,acl," + "iocharset,prealloc,nocase,delalloc,ads", + param->key); + return -ENOPARAM; + } + if (opt < 0) return opt; From 4ef75fe998262d129d623289f0b2992654caa575 Mon Sep 17 00:00:00 2001 From: Konstantin Komarov Date: Wed, 7 Oct 2026 11:26:08 +0200 Subject: [PATCH 27/52] fs/ntfs3: zero newly allocated clusters when the request is smaller than the frame For compressed attributes, allocation is rounded up to a whole compression frame, so a request covering only part of a frame still allocates clst_per_frame clusters. The clusters outside the requested range are then allocated but never written by the caller. Until now 'zero' was set only when allocation had to start before the requested VCN. When it started at vcn0 but extended past the end of the request, the tail clusters were left holding whatever was previously on disk. Signed-off-by: Konstantin Komarov --- fs/ntfs3/attrib.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/ntfs3/attrib.c b/fs/ntfs3/attrib.c index 3b4ef15162da..cd6d4b75d328 100644 --- a/fs/ntfs3/attrib.c +++ b/fs/ntfs3/attrib.c @@ -1164,7 +1164,7 @@ int attr_data_get_block_locked(struct ntfs_inode *ni, CLST vcn, CLST clen, to_alloc = ((vcn0 + clen + clst_per_frame - 1) & cmask) - vcn; if (fr < clst_per_frame) fr = clst_per_frame; - if (vcn != vcn0) + if (vcn != vcn0 || clen < to_alloc) zero = true; /* Check if 'vcn' and 'vcn0' in different attribute segments. */ From de426c6554739ceb94ab6ae270bbd0ec6336ce77 Mon Sep 17 00:00:00 2001 From: Konstantin Komarov Date: Wed, 7 Oct 2026 23:39:51 +0200 Subject: [PATCH 28/52] fs/ntfs3: drop unused new_size argument from attr_make_nonresident() attr_make_nonresident() takes new_size but never uses it: the attribute's size is read from the record it is converting, and all three callers pass either the value already stored there or, in attr_set_size_res(), a size that the function ignores. Remove the argument. Signed-off-by: Konstantin Komarov --- fs/ntfs3/attrib.c | 13 ++++++------- fs/ntfs3/frecord.c | 5 ++--- fs/ntfs3/ntfs_fs.h | 4 ++-- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/fs/ntfs3/attrib.c b/fs/ntfs3/attrib.c index cd6d4b75d328..254037763a86 100644 --- a/fs/ntfs3/attrib.c +++ b/fs/ntfs3/attrib.c @@ -258,8 +258,8 @@ int attr_allocate_clusters(struct ntfs_sb_info *sbi, struct runs_tree *run, */ int attr_make_nonresident(struct ntfs_inode *ni, struct ATTRIB *attr, struct ATTR_LIST_ENTRY *le, struct mft_inode *mi, - u64 new_size, struct runs_tree *run, - struct ATTRIB **ins_attr, struct page *page) + struct runs_tree *run, struct ATTRIB **ins_attr, + struct page *page) { struct ntfs_sb_info *sbi; struct ATTRIB *attr_s; @@ -401,8 +401,8 @@ static int attr_set_size_res(struct ntfs_inode *ni, struct ATTRIB *attr, if (new_size > sbi->record_size || (dsize > 0 && used + dsize > sbi->max_bytes_per_attr)) { - return attr_make_nonresident(ni, attr, le, mi, new_size, run, - ins_attr, NULL); + return attr_make_nonresident(ni, attr, le, mi, run, ins_attr, + NULL); } if (dsize < 0) { @@ -2832,9 +2832,8 @@ int attr_force_nonresident(struct ntfs_inode *ni) } down_write(&ni->file.run_lock); - err = attr_make_nonresident(ni, attr, le, mi, - le32_to_cpu(attr->res.data_size), - &ni->file.run, &attr, NULL); + err = attr_make_nonresident(ni, attr, le, mi, &ni->file.run, &attr, + NULL); up_write(&ni->file.run_lock); return err; diff --git a/fs/ntfs3/frecord.c b/fs/ntfs3/frecord.c index 4651dd8f6464..b9831eea53d4 100644 --- a/fs/ntfs3/frecord.c +++ b/fs/ntfs3/frecord.c @@ -2574,9 +2574,8 @@ int ni_write_frame(struct ntfs_inode *ni, struct page **pages, if (!attr->non_res) { down_write(&ni->file.run_lock); - err = attr_make_nonresident(ni, attr, le, mi, - le32_to_cpu(attr->res.data_size), - &ni->file.run, &attr, pages[0]); + err = attr_make_nonresident(ni, attr, le, mi, &ni->file.run, + &attr, pages[0]); up_write(&ni->file.run_lock); if (err) goto out; diff --git a/fs/ntfs3/ntfs_fs.h b/fs/ntfs3/ntfs_fs.h index 5811d89d67b3..9321b2608e84 100644 --- a/fs/ntfs3/ntfs_fs.h +++ b/fs/ntfs3/ntfs_fs.h @@ -464,8 +464,8 @@ int attr_allocate_clusters(struct ntfs_sb_info *sbi, struct runs_tree *run, CLST *new_len); int attr_make_nonresident(struct ntfs_inode *ni, struct ATTRIB *attr, struct ATTR_LIST_ENTRY *le, struct mft_inode *mi, - u64 new_size, struct runs_tree *run, - struct ATTRIB **ins_attr, struct page *page); + struct runs_tree *run, struct ATTRIB **ins_attr, + struct page *page); int attr_set_size_ex(struct ntfs_inode *ni, enum ATTR_TYPE type, const __le16 *name, u8 name_len, struct runs_tree *run, u64 new_size, const u64 *new_valid, bool keep_prealloc, From 8f4736fe2f22a9823e356bb65bc3ac12d90b0a6b Mon Sep 17 00:00:00 2001 From: Konstantin Komarov Date: Wed, 7 Oct 2026 23:47:41 +0200 Subject: [PATCH 29/52] fs/ntfs3: minor code cleanups Use Add2Ptr() for pointer arithmetic in fill_name_de(), consistent with the rest of the driver, and reflow two statements to match the 80-column style. No functional change. Signed-off-by: Konstantin Komarov --- fs/ntfs3/inode.c | 3 +-- fs/ntfs3/namei.c | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/fs/ntfs3/inode.c b/fs/ntfs3/inode.c index e6c96ef88b01..1f1b2e5897e1 100644 --- a/fs/ntfs3/inode.c +++ b/fs/ntfs3/inode.c @@ -285,8 +285,7 @@ static int ntfs_read_mft(struct inode *inode, const struct cpu_str *name, break; case ATTR_ROOT: - if (attr->non_res || - asize < sizeof(struct INDEX_ROOT) + roff || + if (attr->non_res || asize < sizeof(struct INDEX_ROOT) + roff || rsize < sizeof(struct INDEX_ROOT)) goto out; diff --git a/fs/ntfs3/namei.c b/fs/ntfs3/namei.c index ec59bbabd3c5..46923fff0865 100644 --- a/fs/ntfs3/namei.c +++ b/fs/ntfs3/namei.c @@ -56,7 +56,7 @@ int fill_name_de(struct ntfs_sb_info *sbi, void *buf, const struct qstr *name, real_size = data_size + sizeof(struct NTFS_DE); aligned_size = ALIGN(data_size, 8) + sizeof(struct NTFS_DE); if (aligned_size > real_size) - memset((char *)buf + real_size, 0, aligned_size - real_size); + memset(Add2Ptr(buf, real_size), 0, aligned_size - real_size); e->size = cpu_to_le16(aligned_size); e->key_size = cpu_to_le16(data_size); @@ -231,8 +231,8 @@ static int ntfs_symlink(struct mnt_idmap *idmap, struct inode *dir, static struct dentry *ntfs_mkdir(struct mnt_idmap *idmap, struct inode *dir, struct dentry *dentry, umode_t mode) { - return ERR_PTR(ntfs_create_inode(idmap, dir, dentry, NULL, - mode, 0, NULL, 0, NULL)); + return ERR_PTR(ntfs_create_inode(idmap, dir, dentry, NULL, mode, 0, + NULL, 0, NULL)); } /* From 751e1ae6d28fde9bb54c42a1c0cd17951511948d Mon Sep 17 00:00:00 2001 From: Konstantin Komarov Date: Wed, 7 Oct 2026 23:52:59 +0200 Subject: [PATCH 30/52] fs/ntfs3: do not force allocation for IOMAP_ZERO on a partial cluster ntfs_iomap_begin() set 'rw' for IOMAP_ZERO whenever the end of the range fell inside a cluster, forcing the sparse path to allocate clusters in order to zero them. Drop the IOMAP_ZERO case and only clamp clen_max when 'rw' is already set. Signed-off-by: Konstantin Komarov --- fs/ntfs3/inode.c | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/fs/ntfs3/inode.c b/fs/ntfs3/inode.c index 1f1b2e5897e1..a3b2daaecb4d 100644 --- a/fs/ntfs3/inode.c +++ b/fs/ntfs3/inode.c @@ -968,9 +968,7 @@ static int ntfs_iomap_begin(struct inode *inode, loff_t offset, loff_t length, } } - if (IOMAP_ZERO == flags && (endbyte & sbi->cluster_mask)) { - rw = true; - } else if (rw) { + if (rw) { clen_max = bytes_to_cluster(sbi, endbyte) - vcn; } @@ -978,7 +976,7 @@ static int ntfs_iomap_begin(struct inode *inode, loff_t offset, loff_t length, * Force to allocate clusters if directIO(write) or writeback_range. * NOTE: attr_data_get_block allocates clusters only for sparse file. * Normal file allocates clusters in attr_set_size. - */ + */ no_da = flags == (IOMAP_DIRECT | IOMAP_WRITE) || srcmap == WB_NO_DA; err = attr_data_get_block(ni, vcn, clen_max, &lcn, &clen, @@ -987,8 +985,12 @@ static int ntfs_iomap_begin(struct inode *inode, loff_t offset, loff_t length, if (err) { return err; } - if (!clen) + if (!clen) { + /* Looks like file is broken. + * It is marked as bad via _ntfs_bad_inode + */ return -EINVAL; + } if (lcn == EOF_LCN) { /* request out of file. */ From acde4b7276d5434a4aa097d080c6d14e6381c7d2 Mon Sep 17 00:00:00 2001 From: Konstantin Komarov Date: Wed, 7 Oct 2026 23:55:36 +0200 Subject: [PATCH 31/52] fs/ntfs3: add 'ugm' mount option to control storing uid/gid/mode in xattrs On file creation ntfs3 stores the owner, group, mode and device number in the WSL extended attributes $LXUID, $LXGID, $LXMOD and $LXDEV, and may also write a POSIX ACL. These attributes are meaningless to Windows, and the packed size of the EAs is recorded in the directory entry as well, so every created file carries them. Add a 'ugm' mount option controlling this. It is enabled by default, preserving the current behaviour; mount with 'ugm=0' to create files without the WSL attributes. Signed-off-by: Konstantin Komarov --- fs/ntfs3/inode.c | 2 +- fs/ntfs3/ntfs_fs.h | 1 + fs/ntfs3/super.c | 13 ++++++++++++- fs/ntfs3/xattr.c | 7 +++++++ 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/fs/ntfs3/inode.c b/fs/ntfs3/inode.c index a3b2daaecb4d..7593a93466d9 100644 --- a/fs/ntfs3/inode.c +++ b/fs/ntfs3/inode.c @@ -1836,7 +1836,7 @@ int ntfs_create_inode(struct mnt_idmap *idmap, struct inode *dir, inode->i_flags |= S_NOSEC; } - if (!S_ISLNK(mode)) { + if (!S_ISLNK(mode) && sbi->options->ugm) { /* * ntfs_init_acl and ntfs_save_wsl_perm update extended attribute. * The packed size of extended attribute is stored in direntry too. diff --git a/fs/ntfs3/ntfs_fs.h b/fs/ntfs3/ntfs_fs.h index 9321b2608e84..90ccfde951dc 100644 --- a/fs/ntfs3/ntfs_fs.h +++ b/fs/ntfs3/ntfs_fs.h @@ -111,6 +111,7 @@ struct ntfs_mount_options { unsigned nocase : 1; /* case insensitive. */ unsigned delalloc : 1; /* delay allocation. */ unsigned ads : 1; /* ads support. */ + unsigned ugm : 1; /* store user/group/mode in xattr. */ }; /* Special value to unpack and deallocate. */ diff --git a/fs/ntfs3/super.c b/fs/ntfs3/super.c index 7fd83027007b..cf67d34568d8 100644 --- a/fs/ntfs3/super.c +++ b/fs/ntfs3/super.c @@ -275,6 +275,8 @@ enum Opt { Opt_delalloc_bool, Opt_ads, Opt_ads_bool, + Opt_ugm, + Opt_ugm_bool, Opt_err, }; @@ -303,6 +305,8 @@ static const struct fs_parameter_spec ntfs_fs_parameters[] = { fsparam_bool("delalloc", Opt_delalloc_bool), fsparam_flag("ads", Opt_ads), fsparam_bool("ads", Opt_ads_bool), + fsparam_flag("ugm", Opt_ugm), + fsparam_bool("ugm", Opt_ugm_bool), {} }; // clang-format on @@ -345,7 +349,7 @@ static int ntfs_fs_parse_param(struct fs_context *fc, pr_err("ntfs3: unknown option \"%s\". Use uid,gid,umask," "dmask,fmask,sys_immutable,discard,force,sparse," "nohidden,hide_dot_files,windows_names,showmeta,acl," - "iocharset,prealloc,nocase,delalloc,ads", + "iocharset,prealloc,nocase,delalloc,ads,ugm", param->key); return -ENOPARAM; } @@ -443,6 +447,12 @@ static int ntfs_fs_parse_param(struct fs_context *fc, case Opt_ads_bool: opts->ads = result.boolean; break; + case Opt_ugm: + opts->ugm = 1; + break; + case Opt_ugm_bool: + opts->ugm = result.boolean; + break; default: /* Should not be here unless we forget add case. */ return -EINVAL; @@ -1905,6 +1915,7 @@ static int ntfs_init_fs_context(struct fs_context *fc) opts->fs_fmask_inv = opts->fs_dmask_inv = ~current_umask(); opts->prealloc = 1; opts->ads = 1; + opts->ugm = 1; #ifdef CONFIG_NTFS3_FS_POSIX_ACL /* Set the default value 'acl' */ diff --git a/fs/ntfs3/xattr.c b/fs/ntfs3/xattr.c index 2cf45bdd93d5..d6ad06415318 100644 --- a/fs/ntfs3/xattr.c +++ b/fs/ntfs3/xattr.c @@ -1015,6 +1015,13 @@ int ntfs_save_wsl_perm(struct inode *inode, __le32 *ea_size) __le32 value; struct ntfs_inode *ni = ntfs_i(inode); + if (!ni->mi.sbi->options->ugm) { + /* Do not save uid/gid/mode/dev in xattr. */ + if (ea_size) + *ea_size = 0; + return 0; + } + ni_lock(ni); value = cpu_to_le32(i_uid_read(inode)); err = ntfs_set_ea(inode, "$LXUID", sizeof("$LXUID") - 1, &value, From 8100b9f6561f61529795bd271dd743bdeb06a073 Mon Sep 17 00:00:00 2001 From: Konstantin Komarov Date: Wed, 7 Oct 2026 23:59:11 +0200 Subject: [PATCH 32/52] fs/ntfs3: allow a zero cluster count for resident attributes ntfs_iomap_begin() rejected a zero cluster count from attr_data_get_block() as corruption. For a resident attribute this is a normal result: the data lives in the MFT record, so no clusters are mapped, and the resident case is handled further down and returns before the cluster count is used. Move the check below the RESIDENT_LCN and EOF_LCN handling, so it only applies to the mapped case where a zero count really does indicate a broken record. Signed-off-by: Konstantin Komarov --- fs/ntfs3/inode.c | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/fs/ntfs3/inode.c b/fs/ntfs3/inode.c index 7593a93466d9..4ec12d0184da 100644 --- a/fs/ntfs3/inode.c +++ b/fs/ntfs3/inode.c @@ -985,12 +985,6 @@ static int ntfs_iomap_begin(struct inode *inode, loff_t offset, loff_t length, if (err) { return err; } - if (!clen) { - /* Looks like file is broken. - * It is marked as bad via _ntfs_bad_inode - */ - return -EINVAL; - } if (lcn == EOF_LCN) { /* request out of file. */ @@ -1026,6 +1020,11 @@ static int ntfs_iomap_begin(struct inode *inode, loff_t offset, loff_t length, return 0; } + if (!clen) { + /* Looks like file is broken.*/ + return -EINVAL; + } + iomap->bdev = inode->i_sb->s_bdev; iomap->offset = offset; iomap->length = ((loff_t)clen << cluster_bits) - off; From 4bc61b5e98d4815ce062a0f95ab0da382018e857 Mon Sep 17 00:00:00 2001 From: Konstantin Komarov Date: Thu, 8 Oct 2026 00:02:58 +0200 Subject: [PATCH 33/52] fs/ntfs3: reserve a compression unit in maxbytes_sparse With 32-bit clusters, the maximum size of a sparse file was computed as the largest offset addressable by a 32-bit VCN. Reserve one compression unit below the limit so the rounded-up VCN still fits. Signed-off-by: Konstantin Komarov --- fs/ntfs3/super.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/fs/ntfs3/super.c b/fs/ntfs3/super.c index cf67d34568d8..0c347085cb85 100644 --- a/fs/ntfs3/super.c +++ b/fs/ntfs3/super.c @@ -1235,8 +1235,13 @@ static int ntfs_init_from_boot(struct super_block *sb, u32 sector_size, sbi->maxbytes_sparse = MAX_LFS_FILESIZE; sb->s_maxbytes = MAX_LFS_FILESIZE; #else - /* Maximum size for sparse file. */ - sbi->maxbytes_sparse = (1ull << (cluster_bits + 32)) - 1; + /* + * Maximum size for sparse file: + * 16 aligned up clusters should fit in 32 bits. + * xfstest 308 on sparse file. + */ + sbi->maxbytes_sparse = (1ull << (cluster_bits + 32)) - 1 - + (sbi->cluster_size << NTFS_LZNT_CUNIT); sb->s_maxbytes = 0xFFFFFFFFull << cluster_bits; #endif From fb058ccff5799ac4eb90ee183419835d3c0072f5 Mon Sep 17 00:00:00 2001 From: Konstantin Komarov Date: Thu, 8 Oct 2026 00:06:32 +0200 Subject: [PATCH 34/52] fs/ntfs3: take ni_lock() once in ntfs_readdir() ntfs_readdir() took ni_lock() to load subrecords, dropped it, and then took it again for the index walk. The double-checked pattern around ni_load_all_mi() and the separate out_unlock label exist only because of that split. Take the lock once for the whole function and drop the second acquisition, the inner re-check and the duplicate error label. Signed-off-by: Konstantin Komarov --- fs/ntfs3/dir.c | 44 +++++++++++++++++++------------------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/fs/ntfs3/dir.c b/fs/ntfs3/dir.c index 328c9a29df89..0998c98eb49c 100644 --- a/fs/ntfs3/dir.c +++ b/fs/ntfs3/dir.c @@ -519,23 +519,6 @@ static int ntfs_readdir(struct file *file, struct dir_context *ctx) if (!name) return -ENOMEM; - if (!ni->mi_loaded && ni->attr_list.size) { - /* - * Directory inode is locked for read. - * Load all subrecords to avoid 'write' access to 'ni' during - * directory reading. - */ - ni_lock(ni); - if (!ni->mi_loaded && ni->attr_list.size) { - err = ni_load_all_mi(ni); - if (!err) - ni->mi_loaded = true; - } - ni_unlock(ni); - if (err) - goto out; - } - /* * Keep directory metadata stable for the whole walk. Loading subrecords * once is not enough if concurrent writeback can still compact ATTR_LIST @@ -543,10 +526,22 @@ static int ntfs_readdir(struct file *file, struct dir_context *ctx) */ ni_lock(ni); + if (!ni->mi_loaded && ni->attr_list.size) { + /* + * Directory inode is locked for read. + * Load all subrecords to avoid 'write' access to 'ni' during + * directory reading. + */ + err = ni_load_all_mi(ni); + if (err) + goto out; + ni->mi_loaded = true; + } + root = indx_get_root(&ni->dir, ni, NULL, NULL); if (!root) { err = -EINVAL; - goto out_unlock; + goto out; } if (pos >= sbi->record_size) { @@ -557,7 +552,7 @@ static int ntfs_readdir(struct file *file, struct dir_context *ctx) */ err = ntfs_read_hdr(sbi, ni, &root->ihdr, 0, pos, name, ctx); if (err) - goto out_unlock; + goto out; bit = 0; } @@ -568,7 +563,7 @@ static int ntfs_readdir(struct file *file, struct dir_context *ctx) /* Get the next used index. */ err = indx_used_bit(&ni->dir, ni, &bit); if (err) - goto out_unlock; + goto out; if (bit == MINUS_ONE_T) { /* no more used indexes. end of dir. */ @@ -578,13 +573,13 @@ static int ntfs_readdir(struct file *file, struct dir_context *ctx) if (bit >= max_bit) { /* Corrupted directory. */ err = -EINVAL; - goto out_unlock; + goto out; } err = indx_read_ra(&ni->dir, ni, bit << ni->dir.idx2vbn_bits, &node, &file->f_ra); if (err) - goto out_unlock; + goto out; /* * Add each name from index in 'ctx'. @@ -593,13 +588,12 @@ static int ntfs_readdir(struct file *file, struct dir_context *ctx) ((u64)bit << index_bits) + sbi->record_size, pos, name, ctx); if (err) - goto out_unlock; + goto out; } -out_unlock: +out: ni_unlock(ni); -out: kfree(name); put_indx_node(node); From c9d3f5031aa911cf0da848d7f72ed0c7d626f713 Mon Sep 17 00:00:00 2001 From: Konstantin Komarov Date: Thu, 8 Oct 2026 00:09:00 +0200 Subject: [PATCH 35/52] fs/ntfs3: reject unaligned hole punching on compressed files attr_punch_hole() handles a range that is not cluster aligned by reporting the frame size back to the caller, which zeroes the partial clusters and retries. Return -EINVAL for an unaligned punch on a compressed attribute; the sparse case is unaffected. Signed-off-by: Konstantin Komarov --- fs/ntfs3/attrib.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/fs/ntfs3/attrib.c b/fs/ntfs3/attrib.c index 254037763a86..928c91b57da5 100644 --- a/fs/ntfs3/attrib.c +++ b/fs/ntfs3/attrib.c @@ -2386,6 +2386,12 @@ int attr_punch_hole(struct ntfs_inode *ni, u64 vbo, u64 bytes, u32 *frame_size) if ((vbo | bytes) & mask) { /* We have to zero a range(s). */ + if (is_attr_compressed(attr_b)) { + /* Compressed file punch must be frame aligned. */ + return -EINVAL; + } + + /* Sparse files can be punched. */ if (!frame_size) { /* Caller insists range is aligned. */ return -EINVAL; From ea25564bf05743da4f1e56ffe3e99aa8b4fa4d25 Mon Sep 17 00:00:00 2001 From: Konstantin Komarov Date: Thu, 8 Oct 2026 00:14:54 +0200 Subject: [PATCH 36/52] fs/ntfs3: fix stale attr_b in attr_allocate_frame() ni_insert_nonresident() can create an attribute list and move the base $DATA attribute to another MFT record, which invalidates attr_b and mi_b. attr_allocate_frame() kept using the stale pointers afterwards to update valid_size, writing into a record that no longer holds the attribute. The same situation is already handled in attr_data_get_block_locked(), which re-reads the base attribute after the layout changes. Look up attr_b again after the insertion, and read valid_size from the attribute before the layout can change. While here, flatten the 'if (attr)' block in attr_allocate_frame() by returning early to ins_ext, and simplify an empty-bodied if in attr_data_get_block_locked(). Signed-off-by: Konstantin Komarov --- fs/ntfs3/attrib.c | 111 ++++++++++++++++++++++++---------------------- 1 file changed, 58 insertions(+), 53 deletions(-) diff --git a/fs/ntfs3/attrib.c b/fs/ntfs3/attrib.c index 928c91b57da5..922134cabb05 100644 --- a/fs/ntfs3/attrib.c +++ b/fs/ntfs3/attrib.c @@ -1019,11 +1019,8 @@ int attr_data_get_block_locked(struct ntfs_inode *ni, CLST vcn, CLST clen, int step; again: - if (run_lookup_entry_da(run, da ? &ni->file.run_da : NULL, vcn, lcn, - len)) { - } else { + if (!run_lookup_entry_da(run, da ? run_da : NULL, vcn, lcn, len)) *len = 0; - } if (*len) { if (*lcn != SPARSE_LCN || !new) @@ -1812,7 +1809,7 @@ int attr_allocate_frame(struct ntfs_inode *ni, CLST frame, size_t compr_size, struct ATTR_LIST_ENTRY *le, *le_b; struct mft_inode *mi, *mi_b; CLST svcn, evcn1, next_svcn, len; - CLST vcn, end, clst_data; + CLST vcn, end, clst_data, alloc, evcn; u64 total_size, valid_size, data_size; le_b = NULL; @@ -1830,6 +1827,7 @@ int attr_allocate_frame(struct ntfs_inode *ni, CLST frame, size_t compr_size, svcn = le64_to_cpu(attr_b->nres.svcn); evcn1 = le64_to_cpu(attr_b->nres.evcn) + 1; data_size = le64_to_cpu(attr_b->nres.data_size); + valid_size = le64_to_cpu(attr_b->nres.valid_size); if (svcn <= vcn && vcn < evcn1) { attr = attr_b; @@ -1949,63 +1947,63 @@ int attr_allocate_frame(struct ntfs_inode *ni, CLST frame, size_t compr_size, attr = ni_find_attr(ni, attr, &le, ATTR_DATA, ni->file.ads.name, ni->file.ads.len, &svcn, &mi); - if (attr) { - CLST alloc = bytes_to_cluster( - sbi, le64_to_cpu(attr_b->nres.alloc_size)); - CLST evcn = le64_to_cpu(attr->nres.evcn); + if (!attr) + goto ins_ext; - if (end < next_svcn) - end = next_svcn; - while (end > evcn) { - /* Remove segment [svcn : evcn). */ - mi_remove_attr(NULL, mi, attr); + alloc = bytes_to_cluster(sbi, le64_to_cpu(attr_b->nres.alloc_size)); + evcn = le64_to_cpu(attr->nres.evcn); - if (!al_remove_le(ni, le)) { - err = -EINVAL; - goto out; - } + if (end < next_svcn) + end = next_svcn; + while (end > evcn) { + /* Remove segment [svcn : evcn). */ + mi_remove_attr(NULL, mi, attr); - if (evcn + 1 >= alloc) { - /* Last attribute segment. */ - evcn1 = evcn + 1; - goto ins_ext; - } - - if (ni_load_mi(ni, le, &mi)) { - attr = NULL; - goto out; - } - - attr = mi_find_attr(ni, mi, NULL, ATTR_DATA, - ni->file.ads.name, ni->file.ads.len, - &le->id); - if (!attr) { - err = -EINVAL; - goto out; - } - svcn = le64_to_cpu(attr->nres.svcn); - evcn = le64_to_cpu(attr->nres.evcn); + if (!al_remove_le(ni, le)) { + err = -EINVAL; + goto out; } - if (end < svcn) - end = svcn; + if (evcn + 1 >= alloc) { + /* Last attribute segment. */ + evcn1 = evcn + 1; + goto ins_ext; + } - err = attr_load_runs(attr, ni, run, &end); - if (err) + if (ni_load_mi(ni, le, &mi)) { + attr = NULL; goto out; + } - evcn1 = evcn + 1; - attr->nres.svcn = cpu_to_le64(next_svcn); - err = mi_pack_runs(mi, attr, run, evcn1 - next_svcn); - if (err) + attr = mi_find_attr(ni, mi, NULL, ATTR_DATA, ni->file.ads.name, + ni->file.ads.len, &le->id); + if (!attr) { + err = -EINVAL; goto out; - - le->vcn = cpu_to_le64(next_svcn); - ni->attr_list.dirty = true; - mi->dirty = true; - - next_svcn = le64_to_cpu(attr->nres.evcn) + 1; + } + svcn = le64_to_cpu(attr->nres.svcn); + evcn = le64_to_cpu(attr->nres.evcn); } + + if (end < svcn) + end = svcn; + + err = attr_load_runs(attr, ni, run, &end); + if (err) + goto out; + + evcn1 = evcn + 1; + attr->nres.svcn = cpu_to_le64(next_svcn); + err = mi_pack_runs(mi, attr, run, evcn1 - next_svcn); + if (err) + goto out; + + le->vcn = cpu_to_le64(next_svcn); + ni->attr_list.dirty = true; + mi->dirty = true; + + next_svcn = le64_to_cpu(attr->nres.evcn) + 1; + ins_ext: if (evcn1 > next_svcn) { err = ni_insert_nonresident(ni, ATTR_DATA, ni->file.ads.name, @@ -2014,6 +2012,14 @@ int attr_allocate_frame(struct ntfs_inode *ni, CLST frame, size_t compr_size, &attr, &mi, NULL); if (err) goto out; + /* Layout of records is changed. */ + attr_b = ni_find_attr(ni, NULL, NULL, ATTR_DATA, + ni->file.ads.name, ni->file.ads.len, NULL, + &mi_b); + if (!attr_b) { + err = -EINVAL; + goto out; + } } ok: run_truncate_around(run, vcn); @@ -2022,7 +2028,6 @@ int attr_allocate_frame(struct ntfs_inode *ni, CLST frame, size_t compr_size, if (new_valid > data_size) new_valid = data_size; - valid_size = le64_to_cpu(attr_b->nres.valid_size); if (new_valid != valid_size) { attr_b->nres.valid_size = cpu_to_le64(new_valid); mi_b->dirty = true; From 86ba91170c03e02eee767d82a9a7f2048e51a5e4 Mon Sep 17 00:00:00 2001 From: liyouhong Date: Wed, 29 Jul 2026 14:49:32 +0800 Subject: [PATCH 37/52] fs/ntfs3: fix ntfs_fnd leak when compare function is missing indx_insert_entry(), indx_delete_entry() and indx_update_dup() take a finder via fnd_get() before calling get_cmp_func(). If no compare function is available they return -EINVAL and never release the finder. Jump to the existing out path so fnd_put() still runs. Fixes: 3a2141b2f1c3 ("fs/ntfs3: resolve compare function in public index APIs") Signed-off-by: liyouhong Signed-off-by: Konstantin Komarov --- fs/ntfs3/index.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/fs/ntfs3/index.c b/fs/ntfs3/index.c index bfc6d1f6d2fc..9efd2ef02467 100644 --- a/fs/ntfs3/index.c +++ b/fs/ntfs3/index.c @@ -2058,7 +2058,8 @@ int indx_insert_entry(struct ntfs_index *indx, struct ntfs_inode *ni, cmp = get_cmp_func(root); if (unlikely(!cmp)) { WARN_ON_ONCE(1); - return -EINVAL; + err = -EINVAL; + goto out; } if (fnd_is_empty(fnd)) { @@ -2415,7 +2416,8 @@ int indx_delete_entry(struct ntfs_index *indx, struct ntfs_inode *ni, cmp = get_cmp_func(root); if (unlikely(!cmp)) { WARN_ON_ONCE(1); - return -EINVAL; + err = -EINVAL; + goto out; } /* Locate the entry to remove. */ @@ -2806,7 +2808,8 @@ int indx_update_dup(struct ntfs_inode *ni, struct ntfs_sb_info *sbi, cmp = get_cmp_func(root); if (unlikely(!cmp)) { WARN_ON_ONCE(1); - return -EINVAL; + err = -EINVAL; + goto out; } /* Find entry in directory. */ From 2727ac2ba87af3d5469a17648207fcfd72f6be7f Mon Sep 17 00:00:00 2001 From: Vidhu Sarwal Date: Mon, 13 Jul 2026 14:11:40 +0530 Subject: [PATCH 38/52] fs/ntfs3: prevent positive E_NTFS_NONRESIDENT from reaching writeback error handling attr_data_write_resident() returns the internal positive status code E_NTFS_NONRESIDENT when the DATA attribute becomes non-resident. ntfs_writepages() propagates this value into generic writeback error handling, which expects a standard negative errno and triggers WARN_ON_ONCE(err > 0). Translate positive E_NTFS_* status codes to -EIO before recording the writeback error. This prevents positive internal status codes from reaching generic writeback code while preserving the existing behaviour of treating this path as a writeback error. Reported-by: syzbot+57f9327d593d301ce2a2@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=57f9327d593d301ce2a2 Signed-off-by: Vidhu Sarwal Signed-off-by: Konstantin Komarov --- fs/ntfs3/inode.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/fs/ntfs3/inode.c b/fs/ntfs3/inode.c index 4ec12d0184da..850cdd4d583b 100644 --- a/fs/ntfs3/inode.c +++ b/fs/ntfs3/inode.c @@ -1232,6 +1232,15 @@ static int ntfs_writepages(struct address_space *mapping, folio_unlock(folio); if (err2) { + /* + * attr_data_write_resident() can return the + * positive internal code E_NTFS_NONRESIDENT; + * both mapping_set_error() and writeback_iter() + * need a negative errno and warn on a positive + * value. + */ + if (err2 > 0) + err2 = -EIO; mapping_set_error(mapping, err2); if (!err) err = err2; From 80218408836fa6fc91a9c26a9c014f81abf9e41f Mon Sep 17 00:00:00 2001 From: Weiming Shi Date: Sun, 12 Jul 2026 06:34:26 -0700 Subject: [PATCH 39/52] fs/ntfs3: do not mark frame pages uptodate on read error ni_read_frame() ends with a loop that marks every page of a compressed frame uptodate regardless of whether the read succeeded: out: for (i = 0; i < pages_per_frame; i++) { pg = pages[i]; SetPageUptodate(pg); } Non-critical frame pages come from __filemap_get_folio(FGP_CREAT) and are not zeroed. On an error path that reaches out: before frame_mem is written, these pages keep uninitialized page-allocator contents but are still marked uptodate. A later read() is then served from the page cache and returns that stale kernel memory to user space. For an unprivileged user mounting a crafted NTFS image this is a repeatable kernel memory disclosure. Mark the pages uptodate only on success; on error leave them !uptodate so the read path re-reads them or reports the error. Fixes: 4342306f0f0d ("fs/ntfs3: Add file operations and implementation") Reported-by: Xiang Mei Signed-off-by: Weiming Shi Signed-off-by: Konstantin Komarov --- fs/ntfs3/frecord.c | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/fs/ntfs3/frecord.c b/fs/ntfs3/frecord.c index b9831eea53d4..16ac847de48d 100644 --- a/fs/ntfs3/frecord.c +++ b/fs/ntfs3/frecord.c @@ -2290,7 +2290,6 @@ int ni_read_frame(struct ntfs_inode *ni, u64 frame_vbo, struct page **pages, u64 vbo_disk; size_t unc_size = 0; u32 frame_size, i, ondisk_size; - struct page *pg; struct ATTRIB *attr; CLST frame, clst_data; @@ -2522,11 +2521,10 @@ int ni_read_frame(struct ntfs_inode *ni, u64 frame_vbo, struct page **pages, #endif vunmap(frame_mem); out: - for (i = 0; i < pages_per_frame; i++) { - pg = pages[i]; - if (err) - clear_highpage(pg); - SetPageUptodate(pg); + if (!err) { + for (i = 0; i < pages_per_frame; i++) { + SetPageUptodate(pages[i]); + } } return err; From fb0f0ef7b5a6bc761b372ce00764d4b6e0b91a47 Mon Sep 17 00:00:00 2001 From: Hyeontae Lee Date: Fri, 31 Jul 2026 23:14:40 +0900 Subject: [PATCH 40/52] fs/ntfs3: bound CreateAttribute asize against record_size in do_action In do_action()'s CreateAttribute case (fslog.c:3286), attr2 points into the on-disk log record and asize = le32_to_cpu(attr2->size) is therefore attacker-controlled on mount. The guard bounds asize only against the log record itself, Add2Ptr(attr2, asize) > Add2Ptr(lrh, rec_len) where rec_len is the client data length, and separately bounds dlen against the MFT record. Nothing bounds asize against record_size, but all three writes that follow are into rec, a kmalloc(sbi->record_size) buffer allocated by mi_init() (record.c:105): memmove(Add2Ptr(attr, asize), attr, used - roff); memcpy(attr, attr2, asize); rec->used = cpu_to_le32(used + asize); With record_size 0x400, roff 0x38, used 0x160 and asize 0x800 the memcpy writes 2048 bytes at rec + 0x38, 0x438 bytes past the allocation, and rec->used is left at 0x960. dlen is 0x18 here, so the existing dlen check passes: dlen bounds nothing about the size of the copy. The equivalent non-replay path already refuses this. mi_insert_attr() performs the same insert and starts with (record.c:488): if (used + asize > sbi->record_size) return NULL; Apply the same bound, spelled as a subtraction to match the neighbouring clause. used <= record_size holds here because check_file_record() validates it (fslog.c:2826) and is called on this record at fslog.c:3172; roff <= used - 4 follows from check_if_attr() together with the ATTR_END check at fslog.c:2844, so bounding used + asize bounds both copies. The sibling WriteEndOfFileRecordSegment case does the equivalent check against record_size at fslog.c:3277. Reproduced by mounting a crafted image on v7.2-rc5 under KASAN: BUG: KASAN: slab-out-of-bounds in do_action.isra.0+0x41f6/0x83a0 Write of size 2048 at addr ffff888005c74838 by task mount/69 CPU: 0 UID: 0 PID: 69 Comm: mount Not tainted 7.2.0-rc5-00300-g8ba098e6b6ff #2 Call Trace: kasan_report+0xce/0x100 kasan_check_range+0x105/0x1b0 __asan_memcpy+0x3c/0x60 do_action.isra.0+0x41f6/0x83a0 log_replay+0x920a/0xd300 ntfs_loadlog_and_replay+0x3ef/0x510 ntfs_fill_super+0x1d23/0x4550 get_tree_bdev_flags+0x2ef/0x550 vfs_get_tree+0x82/0x2f0 fc_mount+0x10/0x1b0 path_mount+0x517/0x1df0 __x64_sys_mount+0x20b/0x270 do_syscall_64+0xf9/0x540 entry_SYSCALL_64_after_hwframe+0x77/0x7f Allocated by task 69: __kasan_kmalloc+0x8f/0xa0 __kmalloc_noprof+0x1b4/0x460 mi_init+0x81/0x110 mi_get+0x6a/0x220 do_action.isra.0+0x1db4/0x83a0 log_replay+0x920a/0xd300 ntfs_loadlog_and_replay+0x3ef/0x510 ntfs_fill_super+0x1d23/0x4550 The buggy address belongs to the object at ffff888005c74800 which belongs to the cache kmalloc-1k of size 1024 The buggy address is located 56 bytes inside of allocated 1024-byte region [ffff888005c74800, ffff888005c74c00) Fixes: b46acd6a6a62 ("fs/ntfs3: Add NTFS journal") Cc: stable@vger.kernel.org Assisted-by: Claude:claude-opus-5 Signed-off-by: Hyeontae Lee Signed-off-by: Konstantin Komarov --- fs/ntfs3/fslog.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/fs/ntfs3/fslog.c b/fs/ntfs3/fslog.c index 5b7bac29b4e4..9ec5d058aa3c 100644 --- a/fs/ntfs3/fslog.c +++ b/fs/ntfs3/fslog.c @@ -3317,10 +3317,15 @@ static int do_action(struct ntfs_log *log, struct OPEN_ATTR_ENRTY *oe, asize = le32_to_cpu(attr2->size); used = le32_to_cpu(rec->used); + /* + * attr2->size is taken from the log record and is bounded only + * by the log record length, not by the MFT record. Refuse an + * attribute that does not fit, as mi_insert_attr() does. + */ if (!check_if_attr(rec, lrh) || dlen < SIZEOF_RESIDENT || !IS_ALIGNED(asize, 8) || Add2Ptr(attr2, asize) > Add2Ptr(lrh, rec_len) || - dlen > record_size - used) { + dlen > record_size - used || asize > record_size - used) { goto dirty_vol; } From 7a5aa6c45072b54c8d97c5d54bbdf18aabf0ee6d Mon Sep 17 00:00:00 2001 From: Yun Zhou Date: Wed, 1 Jul 2026 09:30:49 +0800 Subject: [PATCH 41/52] ntfs3: release ni_lock before dir_emit in ntfs_readdir to fix deadlocks ntfs_readdir() holds ni_lock while calling ntfs_read_hdr(), which calls dir_emit() to copy directory entries to userspace. If the user buffer triggers a page fault, the fault handler may need mmap_lock or mapping->invalidate_lock, creating lock-order inversions with paths that acquire those locks before ni_lock (e.g. ntfs_fallocate, mmap). Fix by releasing ni_lock before calling ntfs_read_hdr(): - For non-root index nodes, indx_read_ra() returns data in a kmalloc'd buffer (node->index) that remains valid after ni_unlock. - For the root index header, the data lives in the main MFT record whose lifetime is bound to the inode. i_rwsem shared prevents concurrent directory modifications, so it is safe to access after ni_unlock. Reported-by: syzbot+7736960b837908f3a81d@syzkaller.appspotmail.com Reported-by: syzbot+262a71e9d2faf8747085@syzkaller.appspotmail.com Reported-by: syzbot+cfc6e8106bec1c524f7d@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=7736960b837908f3a81d Closes: https://syzkaller.appspot.com/bug?extid=262a71e9d2faf8747085 Closes: https://syzkaller.appspot.com/bug?extid=cfc6e8106bec1c524f7d Fixes: d62cf685d12e ("fs/ntfs3: hold ni_lock across readdir metadata walk") Signed-off-by: Yun Zhou Signed-off-by: Konstantin Komarov --- fs/ntfs3/dir.c | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/fs/ntfs3/dir.c b/fs/ntfs3/dir.c index 0998c98eb49c..c52d1bf9a5a4 100644 --- a/fs/ntfs3/dir.c +++ b/fs/ntfs3/dir.c @@ -547,12 +547,22 @@ static int ntfs_readdir(struct file *file, struct dir_context *ctx) if (pos >= sbi->record_size) { bit = (pos - sbi->record_size) >> index_bits; } else { + /* + * Release ni_lock before dir_emit (which may fault into + * user pages). root->ihdr lives in the main MFT record + * whose lifetime is bound to the inode, and i_rwsem shared + * prevents concurrent directory modifications. + */ + ni_unlock(ni); + /* * Add each name from root in 'ctx'. */ err = ntfs_read_hdr(sbi, ni, &root->ihdr, 0, pos, name, ctx); if (err) goto out; + + ni_lock(ni); bit = 0; } @@ -581,6 +591,12 @@ static int ntfs_readdir(struct file *file, struct dir_context *ctx) if (err) goto out; + /* + * node->index is a kmalloc'd buffer, safe after ni_unlock. + * Release lock before dir_emit to avoid lock-order issues. + */ + ni_unlock(ni); + /* * Add each name from index in 'ctx'. */ @@ -589,6 +605,8 @@ static int ntfs_readdir(struct file *file, struct dir_context *ctx) pos, name, ctx); if (err) goto out; + + ni_lock(ni); } out: From 408216b66afd9ddadaba2dce56fd4af6793e95a2 Mon Sep 17 00:00:00 2001 From: Hyeontae Lee Date: Mon, 3 Aug 2026 14:21:45 +0900 Subject: [PATCH 42/52] fs/ntfs3: bound DeleteAttribute asize against rec->used in do_action In do_action()'s DeleteAttribute case (fslog.c:3316), attr is Add2Ptr(rec, roff) with roff taken from the on-disk lrh->record_off, and asize = le32_to_cpu(attr->size) is read straight out of the MFT record: asize = le32_to_cpu(attr->size); used = le32_to_cpu(rec->used); if (!check_if_attr(rec, lrh)) goto dirty_vol; ... memmove(attr, Add2Ptr(attr, asize), used - asize - roff); check_if_attr() (fslog.c:2869) walks the attribute chain from rec->attr_off and returns o == ro. Its loop breaks on ATTR_END, so ro may legitimately equal the offset of the ATTR_END marker; that is exactly how CreateAttribute appends to a record. But check_file_record()'s walk (fslog.c:2831) also stops at ATTR_END, so check_attr() never inspects the bytes there. When roff is the ATTR_END offset, those never-validated bytes are what gets read as attr->size, and used - asize - roff underflows. The read itself is also unbounded: roff is a u16 and attr->size is dereferenced at fslog.c:3317, before check_if_attr() has constrained roff at all, so it can be up to ~64K past a record_size-sized object. Commit 0ca0485e4b2e ("fs/ntfs3: validate rec->used in journal-replay file record check") added at fslog.c:2844 if (used < PtrOffset(rec, attr) + sizeof(attr->type)) return false; and its changelog names this very memmove. It bounds rec->used against the ATTR_END offset; it does not bound asize. With record_size 0x400, attr_off 0x38, a valid three-attribute chain ending in ATTR_END at 0x158 and used 0x160, a record whose four bytes at 0x15c read 0x100 gives used - asize - roff = 0x160 - 0x100 - 0x158 = 0xffffff08 i.e. a ~4 GiB memmove whose source and destination both run off the end of rec, a kmalloc(sbi->record_size) object (record.c:105). The equivalent non-replay path already refuses this. mi_remove_attr() performs the same removal and starts with (record.c:548): if (aoff + asize > used) return false; Apply the same bound, spelled as a subtraction to match the neighbouring cases, and move the attr->size read after check_if_attr() so that the header is known to lie inside the record before it is dereferenced. Requiring roff + SIZEOF_RESIDENT <= used rejects the ATTR_END offset without rejecting any real attribute: for an attribute the walk validated, roff + asize <= off(ATTR_END) <= used - 4, and any attribute a DeleteAttribute record can legitimately name carries a full resident header, so roff + SIZEOF_RESIDENT <= roff + asize <= used - 4. check_attr() itself imposes no lower bound on asize, so a crafted sub-header asize can still pass the first test; the second test then bounds it and the memmove stays inside the record. Reproduced by mounting a crafted image on v7.2-rc5, which already contains 0ca0485e4b2e, under KASAN. The image is an ordinary mkfs.ntfs volume with four bytes of one MFT record changed and a crafted $LogFile: BUG: KASAN: slab-out-of-bounds in do_action.isra.0+0x3211/0x83c0 Read of size 4294967048 at addr ffff888002423258 by task mount/66 CPU: 1 UID: 0 PID: 66 Comm: mount Not tainted 7.2.0-rc5 #3 Call Trace: kasan_report+0xce/0x100 kasan_check_range+0x105/0x1b0 __asan_memmove+0x23/0x60 do_action.isra.0+0x3211/0x83c0 log_replay+0x920a/0xd300 ntfs_loadlog_and_replay+0x3ef/0x510 ntfs_fill_super+0x1d23/0x4550 get_tree_bdev_flags+0x2ef/0x550 vfs_get_tree+0x82/0x2f0 fc_mount+0x10/0x1b0 path_mount+0x517/0x1df0 __x64_sys_mount+0x20b/0x270 do_syscall_64+0xf9/0x540 entry_SYSCALL_64_after_hwframe+0x77/0x7f Allocated by task 66: __kasan_kmalloc+0x8f/0xa0 __kmalloc_noprof+0x1b4/0x460 mi_init+0x81/0x110 mi_get+0x6a/0x220 do_action.isra.0+0x1dbf/0x83c0 log_replay+0x920a/0xd300 ntfs_loadlog_and_replay+0x3ef/0x510 ntfs_fill_super+0x1d23/0x4550 The buggy address belongs to the object at ffff888002423000 which belongs to the cache kmalloc-1k of size 1024 The buggy address is located 600 bytes inside of allocated 1024-byte region [ffff888002423000, ffff888002423400) KASAN reports the source side because __asan_memmove() (mm/kasan/shadow.c:94) validates src before dest and returns without copying. Both ends are out of bounds, and on a kernel built without KASAN the copy is performed. rec->used is also left at used - asize by fslog.c:3323 before the memmove, so the record stays inconsistent even when the copy is suppressed; the same mount goes on to report "ino=1a, mi_enum_attr" and marks the volume dirty. Fixes: b46acd6a6a62 ("fs/ntfs3: Add NTFS journal") Cc: stable@vger.kernel.org Assisted-by: Claude:claude-opus-5 Signed-off-by: Hyeontae Lee Signed-off-by: Konstantin Komarov --- fs/ntfs3/fslog.c | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/fs/ntfs3/fslog.c b/fs/ntfs3/fslog.c index 9ec5d058aa3c..b2277d91a9df 100644 --- a/fs/ntfs3/fslog.c +++ b/fs/ntfs3/fslog.c @@ -3348,10 +3348,21 @@ static int do_action(struct ntfs_log *log, struct OPEN_ATTR_ENRTY *oe, break; case DeleteAttribute: - asize = le32_to_cpu(attr->size); used = le32_to_cpu(rec->used); - if (!check_if_attr(rec, lrh)) + /* + * check_if_attr() accepts a record_off that points at the + * ATTR_END marker, which is how CreateAttribute appends. The + * bytes there are not an attribute and check_attr() never + * validated them, so refuse an offset that cannot hold a + * resident header before reading attr->size, and then bound it + * as mi_remove_attr() does. + */ + if (!check_if_attr(rec, lrh) || roff + SIZEOF_RESIDENT > used) + goto dirty_vol; + + asize = le32_to_cpu(attr->size); + if (asize > used - roff) goto dirty_vol; rec->used = cpu_to_le32(used - asize); From f517cae6333121783043d9ba5142b50f5d1cf75b Mon Sep 17 00:00:00 2001 From: Hongling Zeng Date: Wed, 5 Aug 2026 14:45:59 +0800 Subject: [PATCH 43/52] ntfs3: fix mapping pairs bounds check for alignment In the UpdateMappingPairs action, the expansion check compares the unaligned new attribute size with the available record space. The new size is aligned to an 8-byte boundary before the mapping pairs are updated, so the check can underestimate the required space by up to seven bytes. Use the aligned size in the expansion check to ensure that the subsequent update cannot exceed the available record space. Fixes: b46acd6a6a62 ("fs/ntfs3: Add NTFS journal") Cc: stable@vger.kernel.org Signed-off-by: Hongling Zeng Signed-off-by: Konstantin Komarov --- fs/ntfs3/fslog.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fs/ntfs3/fslog.c b/fs/ntfs3/fslog.c index b2277d91a9df..77b216af1440 100644 --- a/fs/ntfs3/fslog.c +++ b/fs/ntfs3/fslog.c @@ -3436,7 +3436,8 @@ static int do_action(struct ntfs_log *log, struct OPEN_ATTR_ENRTY *oe, if (!check_if_attr(rec, lrh) || !attr->non_res || aoff < le16_to_cpu(attr->nres.run_off) || aoff > asize || - (nsize > asize && nsize - asize > record_size - used)) { + (nsize > asize && + ALIGN(nsize, 8) - asize > record_size - used)) { goto dirty_vol; } From ec67e8d61eb40a4e69b16d8931da217352447367 Mon Sep 17 00:00:00 2001 From: Slawomir Stepien Date: Wed, 15 Jul 2026 12:29:34 +0000 Subject: [PATCH 44/52] fs/ntfs3: handle partial allocation in attr_data_get_block_locked() When allocating clusters for a sparse or compressed attribute in attr_data_get_block_locked(), the requested virtual cluster number (vcn0) is aligned down to a frame boundary (vcn). The function then attempts to allocate a full frame of clusters. If the disk is nearly full and highly fragmented, attr_allocate_clusters() might only manage to allocate a small fragment and return success with a partial allocation length. If this partial allocation is so small that it does not even reach the originally requested vcn0 (i.e., end <= vcn0), vcn0 remains unallocated (a sparse hole). When the code subsequently calls run_lookup_entry() for vcn0, it finds the original SPARSE_LCN entry, which triggers a WARN_ON(1). Fix this by adding a check to verify if the partial allocation reached vcn0. If it did not (vcn0 >= end), treat this as an -ENOSPC failure and jump to the undo1 label. This safely rolls back the partial allocation, restores the original size, and repacks the runs, leaving the inode in a consistent state before returning the error to the caller. WARNING: fs/ntfs3/attrib.c:1236 at attr_data_get_block_locked+0x1980/0x25d0 fs/ntfs3/attrib.c:1236 RIP: 0010:attr_data_get_block_locked+0x1980/0x25d0 fs/ntfs3/attrib.c:1236 Call Trace: attr_data_get_block+0x1f1/0x2a0 fs/ntfs3/attrib.c:979 ntfs_fallocate+0xd3d/0xf70 fs/ntfs3/file.c:670 vfs_fallocate+0x663/0x7f0 fs/open.c:338 ksys_fallocate fs/open.c:362 [inline] __do_sys_fallocate fs/open.c:367 [inline] __se_sys_fallocate fs/open.c:365 [inline] __x64_sys_fallocate+0xbf/0x110 fs/open.c:365 do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline] do_syscall_64+0x174/0x580 arch/x86/entry/syscall_64.c:94 entry_SYSCALL_64_after_hwframe+0x77/0x7f Fixes: 099ef9ab9203 ("fs/ntfs3: implement iomap-based file operations") Assisted-by: Gemini:gemini-3.1-pro-preview Gemini:gemini-3-flash-preview syzbot Reported-by: syzbot+7be6ad5cb228286af3e3@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=7be6ad5cb228286af3e3 Link: https://syzkaller.appspot.com/ai_job?id=3c9dee6e-001d-4972-8bf6-f6c960708246 Signed-off-by: Slawomir Stepien Signed-off-by: Konstantin Komarov --- fs/ntfs3/attrib.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fs/ntfs3/attrib.c b/fs/ntfs3/attrib.c index 922134cabb05..a44c387a3085 100644 --- a/fs/ntfs3/attrib.c +++ b/fs/ntfs3/attrib.c @@ -1257,6 +1257,10 @@ int attr_data_get_block_locked(struct ntfs_inode *ni, CLST vcn, CLST clen, total_size = total_size0 + ((u64)*len << cluster_bits); if (vcn != vcn0) { + if (vcn0 >= end) { + err = -ENOSPC; + goto undo1; + } if (!run_lookup_entry(run, vcn0, lcn, len, NULL)) { err = -EINVAL; goto out; From 351817226fbf0031c4c63d1f5d7f15c3172312eb Mon Sep 17 00:00:00 2001 From: Yuejie Shi Date: Mon, 3 Aug 2026 12:34:32 +0800 Subject: [PATCH 45/52] fs/ntfs3: validate the EA value length in ntfs_read_ea() ntfs_read_ea() checks the $EA list for consistency, but it only recomputes the packed size of an entry when the entry's own size field is zero: for (off = 0; off < size; off += ea_size) { const struct EA_FULL *ef = Add2Ptr(ea_p, off); u32 bytes = size - off; ... if (ef->size) { ea_size = le32_to_cpu(ef->size); if (ea_size > bytes) goto out1; continue; /* <- elength never checked */ } ... ea_size = ALIGN(struct_size(ef, name, 1 + ef->name_len + le16_to_cpu(ef->elength)), 4); if (ea_size > bytes) goto out1; } ef->size is non-zero for every entry except the last one, and it is what ntfs3 itself writes (ntfs_set_ea() stores exactly that aligned packed size), so in practice elength is not validated at all. find_ea() then uses unpacked_ea_size(), which also prefers ea->size when it is set, so it agrees and hands the entry back. ntfs_get_ea() copies elength bytes with no further bound of its own: len = le16_to_cpu(ea->elength); ... if (len > size) { /* size is the *caller's* buffer */ err = -ERANGE; ... } memcpy(buffer, ea->name + ea->name_len + 1, len); so a caller that supplies a large enough buffer receives up to 64 KiB of whatever follows the small $EA allocation -- straight to userspace via getxattr(2). Compute the packed size unconditionally and require it to fit in the entry. Hoist the "bytes < offsetof(struct EA_FULL, name)" test above the ef->size branch as well: name_len and elength were being read there without it, which is itself a small over-read of the kmalloc'd buffer when fewer than 8 bytes are left. The old "bytes < sizeof(ef->size)" test is subsumed by it. # mkntfs image; create /mnt/ntfs/poc with user.ntfs6 set, then rewrite # that EA_FULL's elength from 0xf to 0xff00 on the raw device mount -t ntfs3 -o ro /dev/vda /mnt/ntfs getxattr("/mnt/ntfs/poc", "user.ntfs6", buf, 128 * 1024); BUG: KASAN: slab-out-of-bounds in ntfs_get_ea+0x294/0x3e0 Read of size 65280 at addr ffff0000cca53acf by task xattr_probe/142 __asan_memcpy+0x3c/0xa0 ntfs_get_ea+0x294/0x3e0 ntfs_getxattr+0x284/0x318 __vfs_getxattr+0x104/0x160 vfs_getxattr+0x1b4/0x1e0 do_getxattr+0xcc/0x230 path_getxattrat+0x184/0x270 __arm64_sys_getxattr+0x64/0x80 Allocated by task 142: __kmalloc_noprof+0x294/0x668 ntfs_read_ea+0x1e4/0x420 ntfs_get_ea+0x2c8/0x3e0 The buggy address belongs to the object at ffff0000cca53a80 which belongs to the cache kmalloc-96 of size 96 The object being over-read is the one ntfs_read_ea() just allocated, and it is 96 bytes. Entries written by ntfs3 and by Windows set size to the aligned packed size, so the new test does not reject anything a working filesystem produces. Fixes: be71b5cba2e6 ("fs/ntfs3: Add attrib operations") Cc: stable@vger.kernel.org Signed-off-by: Yuejie Shi [almaz.alexandrovich@paragon-software.com: applied as-is, logic is the same but the code is cleaner] Signed-off-by: Konstantin Komarov --- fs/ntfs3/xattr.c | 45 ++++++++++++++------------------------------- 1 file changed, 14 insertions(+), 31 deletions(-) diff --git a/fs/ntfs3/xattr.c b/fs/ntfs3/xattr.c index d6ad06415318..e1fa58aa0361 100644 --- a/fs/ntfs3/xattr.c +++ b/fs/ntfs3/xattr.c @@ -146,49 +146,32 @@ static int ntfs_read_ea(struct ntfs_inode *ni, struct EA_FULL **ea, for (off = 0; off < size; off += ea_size) { const struct EA_FULL *ef = Add2Ptr(ea_p, off); u32 bytes = size - off; - size_t need; - - /* Check if we can use field ea->size. */ - if (bytes < sizeof(ef->size)) - goto out1; + u32 packed_size; /* Check if we can use fields ef->name_len and ef->elength. */ if (bytes < offsetof(struct EA_FULL, name)) goto out1; - /* Size needed to hold this record's name and value. */ - need = struct_size(ef, name, - 1 + ef->name_len + le16_to_cpu(ef->elength)); + /* + * The name and the value have to fit in the entry whether or + * not ef->size is set, because ntfs_get_ea() copies elength + * bytes out of this buffer and checks them only against the + * caller supplied buffer size. + */ + packed_size = + ALIGN(struct_size(ef, name, + 1 + ef->name_len + + le16_to_cpu(ef->elength)), + 4); if (ef->size) { ea_size = le32_to_cpu(ef->size); - /* ef->size must fit the list and cover the record. */ - if (ea_size > bytes || ea_size < need) - goto out1; - if (bytes < offsetof(struct EA_FULL, name)) - goto out1; - - /* Check if we can use fields ef->name_len and ef->elength. */ - if (bytes < offsetof(struct EA_FULL, name)) - goto out1; - - /* - * The declared entry size (ef->size) must be large - * enough to hold the name and value. Otherwise a later - * reader such as ntfs_get_ea() copies ef->elength bytes - * starting past the entry, reading out of bounds of the - * ea buffer and leaking adjacent heap memory to - * userspace via getxattr(). - */ - if (struct_size(ef, name, - 1 + ef->name_len + - le16_to_cpu(ef->elength)) > - ea_size) + if (ea_size > bytes || packed_size > ea_size) goto out1; continue; } - ea_size = ALIGN(need, 4); + ea_size = packed_size; if (ea_size > bytes) goto out1; } From 857c06086af165087ff542b8cfa3519cd88d34bb Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Thu, 20 Aug 2026 17:04:58 +0800 Subject: [PATCH 46/52] ntfs3: add mount and log replay tracepoints Add ntfs3 tracepoints for mount and log replay paths. This adds trace events for ntfs_fill_super(), ntfs_init_from_boot(), and log_replay() to help observe mount setup, boot sector parsing, and $LogFile replay results. Signed-off-by: Baolin Liu Signed-off-by: Konstantin Komarov --- MAINTAINERS | 1 + fs/ntfs3/fslog.c | 3 ++ fs/ntfs3/super.c | 11 ++++- include/trace/events/ntfs3.h | 85 ++++++++++++++++++++++++++++++++++++ 4 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 include/trace/events/ntfs3.h diff --git a/MAINTAINERS b/MAINTAINERS index 3a19da74d00c..596f2552dc32 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -19502,6 +19502,7 @@ W: http://www.paragon-software.com/ T: git https://github.com/Paragon-Software-Group/linux-ntfs3.git F: Documentation/filesystems/ntfs3.rst F: fs/ntfs3/ +F: include/trace/events/ntfs3.h NTSYNC SYNCHRONIZATION PRIMITIVE DRIVER M: Elizabeth Figura diff --git a/fs/ntfs3/fslog.c b/fs/ntfs3/fslog.c index 77b216af1440..bb8d40bb5869 100644 --- a/fs/ntfs3/fslog.c +++ b/fs/ntfs3/fslog.c @@ -14,6 +14,7 @@ #include "debug.h" #include "ntfs.h" #include "ntfs_fs.h" +#include /* * LOG FILE structs @@ -5418,6 +5419,8 @@ int log_replay(struct ntfs_inode *ni, bool *initialized) else if (log->set_dirty) ntfs_set_state(sbi, NTFS_DIRTY_ERROR); + trace_ntfs3_log_replay(&ni->vfs_inode, *initialized, err); + kfree(log); return err; diff --git a/fs/ntfs3/super.c b/fs/ntfs3/super.c index 0c347085cb85..b04380e4245b 100644 --- a/fs/ntfs3/super.c +++ b/fs/ntfs3/super.c @@ -78,6 +78,9 @@ #include "lib/lib.h" #endif +#define CREATE_TRACE_POINTS +#include + #ifdef CONFIG_PRINTK /* * ntfs_printk - Trace warnings/notices/errors. @@ -991,7 +994,7 @@ static int ntfs_init_from_boot(struct super_block *sb, u32 sector_size, { struct ntfs_sb_info *sbi = sb->s_fs_info; int err; - u32 mb, gb, boot_sector_size, sct_per_clst, record_size; + u32 mb, gb, boot_sector_size = 0, sct_per_clst, record_size; u64 sectors, clusters, mlcn, mlcn2, mft_pos, mft2_pos, dev_size0; struct NTFS_BOOT *boot; struct buffer_head *bh; @@ -1264,6 +1267,8 @@ static int ntfs_init_from_boot(struct super_block *sb, u32 sector_size, } out: + trace_ntfs3_init_from_boot(sb, sector_size, boot_sector_size, + !!boot_block, err); brelse(bh); if (err == -EINVAL && !boot_block && dev_size0 > PAGE_SHIFT) { @@ -1782,12 +1787,16 @@ static int ntfs_fill_super(struct super_block *sb, struct fs_context *fc) } ntfs_create_procdir(sb); + trace_ntfs3_fill_super(sb, ro, sbi->cluster_size, sbi->record_size, + sbi->index_size, 0); return 0; put_inode_out: iput(inode); out: + trace_ntfs3_fill_super(sb, ro, sbi->cluster_size, sbi->record_size, + sbi->index_size, err); /* sbi->options == options */ if (options) { put_mount_options(sbi->options); diff --git a/include/trace/events/ntfs3.h b/include/trace/events/ntfs3.h new file mode 100644 index 000000000000..a45f919cb33e --- /dev/null +++ b/include/trace/events/ntfs3.h @@ -0,0 +1,85 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#undef TRACE_SYSTEM +#define TRACE_SYSTEM ntfs3 + +#if !defined(_TRACE_NTFS3_H) || defined(TRACE_HEADER_MULTI_READ) +#define _TRACE_NTFS3_H + +#include +#include + +TRACE_EVENT(ntfs3_fill_super, + TP_PROTO(struct super_block *sb, bool ro, u32 cluster_size, + u32 record_size, u32 index_size, int err), + TP_ARGS(sb, ro, cluster_size, record_size, index_size, err), + TP_STRUCT__entry( + __field(dev_t, dev) + __field(u32, cluster_size) + __field(u32, record_size) + __field(u32, index_size) + __field(int, err) + __field(bool, ro) + ), + TP_fast_assign( + __entry->dev = sb->s_bdev->bd_dev; + __entry->cluster_size = cluster_size; + __entry->record_size = record_size; + __entry->index_size = index_size; + __entry->err = err; + __entry->ro = ro; + ), + TP_printk("dev=(%d,%d) ro=%d cluster=%u record=%u index=%u err=%d", + MAJOR(__entry->dev), MINOR(__entry->dev), __entry->ro, + __entry->cluster_size, __entry->record_size, + __entry->index_size, __entry->err) +); + +TRACE_EVENT(ntfs3_init_from_boot, + TP_PROTO(struct super_block *sb, u32 media_sector_size, + u32 boot_sector_size, bool used_alt_boot, int err), + TP_ARGS(sb, media_sector_size, boot_sector_size, used_alt_boot, err), + TP_STRUCT__entry( + __field(dev_t, dev) + __field(u32, media_sector_size) + __field(u32, boot_sector_size) + __field(int, err) + __field(bool, used_alt_boot) + ), + TP_fast_assign( + __entry->dev = sb->s_bdev->bd_dev; + __entry->media_sector_size = media_sector_size; + __entry->boot_sector_size = boot_sector_size; + __entry->err = err; + __entry->used_alt_boot = used_alt_boot; + ), + TP_printk("dev=(%d,%d) media_sector=%u boot_sector=%u alt_boot=%d err=%d", + MAJOR(__entry->dev), MINOR(__entry->dev), + __entry->media_sector_size, __entry->boot_sector_size, + __entry->used_alt_boot, __entry->err) +); + +TRACE_EVENT(ntfs3_log_replay, + TP_PROTO(struct inode *inode, bool initialized, int err), + TP_ARGS(inode, initialized, err), + TP_STRUCT__entry( + __field(unsigned long, ino) + __field(loff_t, size) + __field(dev_t, dev) + __field(int, err) + __field(bool, initialized) + ), + TP_fast_assign( + __entry->ino = inode->i_ino; + __entry->size = i_size_read(inode); + __entry->dev = inode->i_sb->s_dev; + __entry->err = err; + __entry->initialized = initialized; + ), + TP_printk("dev=(%d,%d) ino=%lu size=%lld initialized=%d err=%d", + MAJOR(__entry->dev), MINOR(__entry->dev), __entry->ino, + __entry->size, __entry->initialized, __entry->err) +); + +#endif /* _TRACE_NTFS3_H */ + +#include From 2099db05335b5ffd64c8ad9a894a5f828e34e8ab Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Thu, 20 Aug 2026 17:04:59 +0800 Subject: [PATCH 47/52] ntfs3: add namei tracepoints Add ntfs3 tracepoints for namei operations. This adds trace events for ntfs_lookup() and ntfs_rename() to help observe directory lookup and rename activity. Signed-off-by: Baolin Liu Signed-off-by: Konstantin Komarov --- fs/ntfs3/namei.c | 5 ++++ include/trace/events/ntfs3.h | 44 ++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/fs/ntfs3/namei.c b/fs/ntfs3/namei.c index 46923fff0865..599434c9d714 100644 --- a/fs/ntfs3/namei.c +++ b/fs/ntfs3/namei.c @@ -13,6 +13,7 @@ #include "debug.h" #include "ntfs.h" #include "ntfs_fs.h" +#include /* * fill_name_de - Format NTFS_DE in @buf. @@ -77,6 +78,8 @@ static struct dentry *ntfs_lookup(struct inode *dir, struct dentry *dentry, struct inode *inode; int err; + trace_ntfs3_lookup(dir, dentry); + if (!uni) return ERR_PTR(-ENOMEM); @@ -299,6 +302,8 @@ static int ntfs_rename(struct mnt_idmap *idmap, struct inode *dir, if (unlikely(ntfs3_forced_shutdown(sb))) return -EIO; + trace_ntfs3_rename(dir, dentry, new_dir, new_dentry); + if (flags & ~RENAME_NOREPLACE) return -EINVAL; diff --git a/include/trace/events/ntfs3.h b/include/trace/events/ntfs3.h index a45f919cb33e..a1e4bdb6ec59 100644 --- a/include/trace/events/ntfs3.h +++ b/include/trace/events/ntfs3.h @@ -80,6 +80,50 @@ TRACE_EVENT(ntfs3_log_replay, __entry->size, __entry->initialized, __entry->err) ); +TRACE_EVENT(ntfs3_lookup, + TP_PROTO(struct inode *dir, struct dentry *dentry), + TP_ARGS(dir, dentry), + TP_STRUCT__entry( + __field(unsigned long, parent_ino) + __field(dev_t, dev) + __string(name, dentry->d_name.name) + ), + TP_fast_assign( + __entry->parent_ino = dir->i_ino; + __entry->dev = dir->i_sb->s_dev; + __assign_str(name); + ), + TP_printk("dev=(%d,%d) parent=%lu name=%s", + MAJOR(__entry->dev), MINOR(__entry->dev), + __entry->parent_ino, __get_str(name)) +); + +TRACE_EVENT(ntfs3_rename, + TP_PROTO(struct inode *dir, struct dentry *dentry, + struct inode *new_dir, struct dentry *new_dentry), + TP_ARGS(dir, dentry, new_dir, new_dentry), + TP_STRUCT__entry( + __field(unsigned long, dir_ino) + __field(unsigned long, new_dir_ino) + __field(unsigned long, ino) + __field(dev_t, dev) + __string(old_name, dentry->d_name.name) + __string(new_name, new_dentry->d_name.name) + ), + TP_fast_assign( + __entry->dir_ino = dir->i_ino; + __entry->new_dir_ino = new_dir->i_ino; + __entry->ino = d_inode(dentry)->i_ino; + __entry->dev = dir->i_sb->s_dev; + __assign_str(old_name); + __assign_str(new_name); + ), + TP_printk("dev=(%d,%d) dir=%lu new_dir=%lu ino=%lu old=%s new=%s", + MAJOR(__entry->dev), MINOR(__entry->dev), + __entry->dir_ino, __entry->new_dir_ino, __entry->ino, + __get_str(old_name), __get_str(new_name)) +); + #endif /* _TRACE_NTFS3_H */ #include From 0dca616f4667ae1ee327f0d5d5781e20e24c302e Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Thu, 20 Aug 2026 17:05:00 +0800 Subject: [PATCH 48/52] ntfs3: add create inode tracepoint Add an ntfs3 tracepoint for inode creation. This adds a trace event for ntfs_create_inode() to help observe inode creation activity. Signed-off-by: Baolin Liu Signed-off-by: Konstantin Komarov --- fs/ntfs3/inode.c | 3 +++ include/trace/events/ntfs3.h | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/fs/ntfs3/inode.c b/fs/ntfs3/inode.c index 850cdd4d583b..96e750c928cc 100644 --- a/fs/ntfs3/inode.c +++ b/fs/ntfs3/inode.c @@ -17,6 +17,7 @@ #include "debug.h" #include "ntfs.h" #include "ntfs_fs.h" +#include struct IGET5_PARAM { const struct MFT_REF *ref; @@ -1425,6 +1426,8 @@ int ntfs_create_inode(struct mnt_idmap *idmap, struct inode *dir, /* New file will be resident or non resident. */ const bool new_file_resident = 1; + trace_ntfs3_create_inode(dir, dentry); + if (!fnd) ni_lock_dir(dir_ni); diff --git a/include/trace/events/ntfs3.h b/include/trace/events/ntfs3.h index a1e4bdb6ec59..67a2bde0266d 100644 --- a/include/trace/events/ntfs3.h +++ b/include/trace/events/ntfs3.h @@ -124,6 +124,24 @@ TRACE_EVENT(ntfs3_rename, __get_str(old_name), __get_str(new_name)) ); +TRACE_EVENT(ntfs3_create_inode, + TP_PROTO(struct inode *dir, struct dentry *dentry), + TP_ARGS(dir, dentry), + TP_STRUCT__entry( + __field(unsigned long, parent_ino) + __field(dev_t, dev) + __string(name, dentry->d_name.name) + ), + TP_fast_assign( + __entry->parent_ino = dir->i_ino; + __entry->dev = dir->i_sb->s_dev; + __assign_str(name); + ), + TP_printk("dev=(%d,%d) parent=%lu name=%s", + MAJOR(__entry->dev), MINOR(__entry->dev), + __entry->parent_ino, __get_str(name)) +); + #endif /* _TRACE_NTFS3_H */ #include From 4feafef66a0f7b071e8aea653ca441b94769296f Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Thu, 20 Aug 2026 17:05:01 +0800 Subject: [PATCH 49/52] ntfs3: add directory index tracepoints Add ntfs3 tracepoints for directory index operations. This adds trace events for dir_search_u(), indx_find(), indx_insert_entry(), and indx_delete_entry() to help observe directory index lookup, insert, and delete activity. Signed-off-by: Baolin Liu Signed-off-by: Konstantin Komarov --- fs/ntfs3/dir.c | 3 ++ fs/ntfs3/index.c | 8 ++++ include/trace/events/ntfs3.h | 81 ++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+) diff --git a/fs/ntfs3/dir.c b/fs/ntfs3/dir.c index c52d1bf9a5a4..b54828735d55 100644 --- a/fs/ntfs3/dir.c +++ b/fs/ntfs3/dir.c @@ -14,6 +14,7 @@ #include "debug.h" #include "ntfs.h" #include "ntfs_fs.h" +#include /* Convert little endian UTF-16 to NLS string. */ int ntfs_utf16_to_nls(struct ntfs_sb_info *sbi, const __le16 *name, u32 len, @@ -284,6 +285,8 @@ struct inode *dir_search_flags(struct inode *dir, const struct cpu_str *uni, struct inode *inode = NULL; struct ntfs_fnd *fnd_a = NULL; + trace_ntfs3_dir_search_u(dir, uni ? uni->len : 0); + if (!fnd) { fnd_a = fnd_get(); if (!fnd_a) { diff --git a/fs/ntfs3/index.c b/fs/ntfs3/index.c index 9efd2ef02467..c06b36760448 100644 --- a/fs/ntfs3/index.c +++ b/fs/ntfs3/index.c @@ -13,6 +13,7 @@ #include "debug.h" #include "ntfs.h" #include "ntfs_fs.h" +#include static const struct INDEX_NAMES { const __le16 *name; @@ -1181,6 +1182,8 @@ int indx_find(struct ntfs_index *indx, struct ntfs_inode *ni, if (!root) root = indx_get_root(&ni->dir, ni, NULL, NULL); + trace_ntfs3_indx_find(&ni->vfs_inode, indx->type, key_len); + if (!root) { /* Should not happen. */ return -EINVAL; @@ -2062,6 +2065,9 @@ int indx_insert_entry(struct ntfs_index *indx, struct ntfs_inode *ni, goto out; } + trace_ntfs3_indx_insert_entry(&ni->vfs_inode, indx->type, + le16_to_cpu(new_de->key_size), undo); + if (fnd_is_empty(fnd)) { /* * Find the spot the tree where we want to @@ -2420,6 +2426,8 @@ int indx_delete_entry(struct ntfs_index *indx, struct ntfs_inode *ni, goto out; } + trace_ntfs3_indx_delete_entry(&ni->vfs_inode, indx->type, key_len); + /* Locate the entry to remove. */ err = indx_find(indx, ni, root, key, key_len, ctx, &diff, &e, fnd); if (err) diff --git a/include/trace/events/ntfs3.h b/include/trace/events/ntfs3.h index 67a2bde0266d..4ac05ed29e49 100644 --- a/include/trace/events/ntfs3.h +++ b/include/trace/events/ntfs3.h @@ -142,6 +142,87 @@ TRACE_EVENT(ntfs3_create_inode, __entry->parent_ino, __get_str(name)) ); +TRACE_EVENT(ntfs3_dir_search_u, + TP_PROTO(struct inode *dir, unsigned int name_len), + TP_ARGS(dir, name_len), + TP_STRUCT__entry( + __field(unsigned long, dir_ino) + __field(dev_t, dev) + __field(unsigned int, name_len) + ), + TP_fast_assign( + __entry->dir_ino = dir->i_ino; + __entry->dev = dir->i_sb->s_dev; + __entry->name_len = name_len; + ), + TP_printk("dev=(%d,%d) dir=%lu name_len=%u", + MAJOR(__entry->dev), MINOR(__entry->dev), + __entry->dir_ino, __entry->name_len) +); + +TRACE_EVENT(ntfs3_indx_find, + TP_PROTO(struct inode *inode, u8 type, size_t key_len), + TP_ARGS(inode, type, key_len), + TP_STRUCT__entry( + __field(unsigned long, ino) + __field(size_t, key_len) + __field(dev_t, dev) + __field(u8, type) + ), + TP_fast_assign( + __entry->ino = inode->i_ino; + __entry->key_len = key_len; + __entry->dev = inode->i_sb->s_dev; + __entry->type = type; + ), + TP_printk("dev=(%d,%d) ino=%lu type=%u key_len=%zu", + MAJOR(__entry->dev), MINOR(__entry->dev), + __entry->ino, __entry->type, __entry->key_len) +); + +TRACE_EVENT(ntfs3_indx_insert_entry, + TP_PROTO(struct inode *inode, u8 type, u16 key_len, bool undo), + TP_ARGS(inode, type, key_len, undo), + TP_STRUCT__entry( + __field(unsigned long, ino) + __field(dev_t, dev) + __field(u16, key_len) + __field(u8, type) + __field(bool, undo) + ), + TP_fast_assign( + __entry->ino = inode->i_ino; + __entry->dev = inode->i_sb->s_dev; + __entry->key_len = key_len; + __entry->type = type; + __entry->undo = undo; + ), + TP_printk("dev=(%d,%d) ino=%lu type=%u key_len=%u undo=%d", + MAJOR(__entry->dev), MINOR(__entry->dev), + __entry->ino, __entry->type, __entry->key_len, + __entry->undo) +); + +TRACE_EVENT(ntfs3_indx_delete_entry, + TP_PROTO(struct inode *inode, u8 type, u32 key_len), + TP_ARGS(inode, type, key_len), + TP_STRUCT__entry( + __field(unsigned long, ino) + __field(u32, key_len) + __field(dev_t, dev) + __field(u8, type) + ), + TP_fast_assign( + __entry->ino = inode->i_ino; + __entry->key_len = key_len; + __entry->dev = inode->i_sb->s_dev; + __entry->type = type; + ), + TP_printk("dev=(%d,%d) ino=%lu type=%u key_len=%u", + MAJOR(__entry->dev), MINOR(__entry->dev), + __entry->ino, __entry->type, __entry->key_len) +); + #endif /* _TRACE_NTFS3_H */ #include From 3bd6d6540dc62121a978f783a7e73d61ea00a007 Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Thu, 20 Aug 2026 17:05:02 +0800 Subject: [PATCH 50/52] ntfs3: add allocation tracepoints Add ntfs3 tracepoints for allocation operations. This adds trace events for attr_allocate_clusters(), attr_set_size_ex(), and attr_data_get_block() to help observe cluster allocation, size changes, and block mapping activity. Signed-off-by: Baolin Liu Signed-off-by: Konstantin Komarov --- fs/ntfs3/attrib.c | 12 ++++++ include/trace/events/ntfs3.h | 78 ++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/fs/ntfs3/attrib.c b/fs/ntfs3/attrib.c index a44c387a3085..805acc3ed869 100644 --- a/fs/ntfs3/attrib.c +++ b/fs/ntfs3/attrib.c @@ -13,6 +13,7 @@ #include "debug.h" #include "ntfs.h" #include "ntfs_fs.h" +#include /* * You can set external NTFS_MIN_LOG2_OF_CLUMP/NTFS_MAX_LOG2_OF_CLUMP to manage @@ -172,6 +173,8 @@ int attr_allocate_clusters(struct ntfs_sb_info *sbi, struct runs_tree *run, CLST flen, vcn0 = vcn, pre = pre_alloc ? *pre_alloc : 0; size_t cnt = run->count; + trace_ntfs3_attr_allocate_clusters(sbi->sb, vcn, lcn, len, opt); + for (;;) { err = ntfs_look_for_free_space(sbi, lcn, len + pre, &lcn, &flen, opt); @@ -470,6 +473,12 @@ int attr_set_size_ex(struct ntfs_inode *ni, enum ATTR_TYPE type, goto bad_inode; } + trace_ntfs3_attr_set_size_ex(&ni->vfs_inode, le32_to_cpu(type), + attr_b->non_res ? + le64_to_cpu(attr_b->nres.data_size) : + le32_to_cpu(attr_b->res.data_size), + new_size, keep_prealloc, no_da); + if (!attr_b->non_res) { err = attr_set_size_res(ni, attr_b, le_b, mi_b, new_size, run, &attr_b); @@ -971,6 +980,9 @@ int attr_data_get_block(struct ntfs_inode *ni, CLST vcn, CLST clen, CLST *lcn, if (res) *res = NULL; + trace_ntfs3_attr_data_get_block(&ni->vfs_inode, vcn, clen, new, zero, + no_da); + /* Try to find in cache. */ down_read(&ni->file.run_lock); if (run_lookup_entry_da(&ni->file.run, !no_da ? &ni->file.run_da : NULL, diff --git a/include/trace/events/ntfs3.h b/include/trace/events/ntfs3.h index 4ac05ed29e49..e39765bc062f 100644 --- a/include/trace/events/ntfs3.h +++ b/include/trace/events/ntfs3.h @@ -223,6 +223,84 @@ TRACE_EVENT(ntfs3_indx_delete_entry, __entry->ino, __entry->type, __entry->key_len) ); +TRACE_EVENT(ntfs3_attr_allocate_clusters, + TP_PROTO(struct super_block *sb, u64 vcn, u64 lcn, u64 len, u32 opt), + TP_ARGS(sb, vcn, lcn, len, opt), + TP_STRUCT__entry( + __field(u64, vcn) + __field(u64, lcn) + __field(u64, len) + __field(dev_t, dev) + __field(u32, opt) + ), + TP_fast_assign( + __entry->vcn = vcn; + __entry->lcn = lcn; + __entry->len = len; + __entry->dev = sb->s_dev; + __entry->opt = opt; + ), + TP_printk("dev=(%d,%d) vcn=%llu lcn=%llu len=%llu opt=0x%x", + MAJOR(__entry->dev), MINOR(__entry->dev), + __entry->vcn, __entry->lcn, __entry->len, __entry->opt) +); + +TRACE_EVENT(ntfs3_attr_set_size_ex, + TP_PROTO(struct inode *inode, u32 type, u64 old_size, u64 new_size, + bool keep_prealloc, bool no_da), + TP_ARGS(inode, type, old_size, new_size, keep_prealloc, no_da), + TP_STRUCT__entry( + __field(u64, old_size) + __field(u64, new_size) + __field(unsigned long, ino) + __field(dev_t, dev) + __field(u32, type) + __field(bool, keep_prealloc) + __field(bool, no_da) + ), + TP_fast_assign( + __entry->old_size = old_size; + __entry->new_size = new_size; + __entry->ino = inode->i_ino; + __entry->dev = inode->i_sb->s_dev; + __entry->type = type; + __entry->keep_prealloc = keep_prealloc; + __entry->no_da = no_da; + ), + TP_printk("dev=(%d,%d) ino=%lu type=0x%x old_size=%llu new_size=%llu keep_prealloc=%d no_da=%d", + MAJOR(__entry->dev), MINOR(__entry->dev), __entry->ino, + __entry->type, __entry->old_size, __entry->new_size, + __entry->keep_prealloc, __entry->no_da) +); + +TRACE_EVENT(ntfs3_attr_data_get_block, + TP_PROTO(struct inode *inode, u64 vcn, u64 clen, bool *new, + bool zero, bool no_da), + TP_ARGS(inode, vcn, clen, new, zero, no_da), + TP_STRUCT__entry( + __field(u64, vcn) + __field(u64, clen) + __field(unsigned long, ino) + __field(dev_t, dev) + __field(bool, create) + __field(bool, zero) + __field(bool, no_da) + ), + TP_fast_assign( + __entry->vcn = vcn; + __entry->clen = clen; + __entry->ino = inode->i_ino; + __entry->dev = inode->i_sb->s_dev; + __entry->create = !!new; + __entry->zero = zero; + __entry->no_da = no_da; + ), + TP_printk("dev=(%d,%d) ino=%lu vcn=%llu clen=%llu create=%d zero=%d no_da=%d", + MAJOR(__entry->dev), MINOR(__entry->dev), __entry->ino, + __entry->vcn, __entry->clen, __entry->create, + __entry->zero, __entry->no_da) +); + #endif /* _TRACE_NTFS3_H */ #include From 7f7141354bedb7f0257b119768d8c3141dcb9453 Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Thu, 20 Aug 2026 17:05:03 +0800 Subject: [PATCH 51/52] ntfs3: add iomap tracepoints Add ntfs3 tracepoints for iomap operations. This adds trace events for ntfs_iomap_begin() and ntfs_iomap_end() to help observe iomap mapping activity. Signed-off-by: Baolin Liu Signed-off-by: Konstantin Komarov --- fs/ntfs3/inode.c | 4 +++ include/trace/events/ntfs3.h | 50 ++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/fs/ntfs3/inode.c b/fs/ntfs3/inode.c index 96e750c928cc..8adbdf1a7d11 100644 --- a/fs/ntfs3/inode.c +++ b/fs/ntfs3/inode.c @@ -954,6 +954,8 @@ static int ntfs_iomap_begin(struct inode *inode, loff_t offset, loff_t length, bool new_clst = false; bool no_da; bool zero = false; + + trace_ntfs3_iomap_begin(inode, offset, length, flags); if (unlikely(ntfs3_forced_shutdown(sbi->sb))) return -EIO; @@ -1091,6 +1093,8 @@ static int ntfs_iomap_end(struct inode *inode, loff_t pos, loff_t length, struct ntfs_inode *ni = ntfs_i(inode); loff_t endbyte = pos + written; + trace_ntfs3_iomap_end(inode, pos, length, written, flags); + if ((flags & IOMAP_WRITE) || (flags & IOMAP_ZERO)) { if (iomap->type == IOMAP_INLINE) { u32 data_size; diff --git a/include/trace/events/ntfs3.h b/include/trace/events/ntfs3.h index e39765bc062f..e0c0c44ce318 100644 --- a/include/trace/events/ntfs3.h +++ b/include/trace/events/ntfs3.h @@ -301,6 +301,56 @@ TRACE_EVENT(ntfs3_attr_data_get_block, __entry->zero, __entry->no_da) ); +TRACE_EVENT(ntfs3_iomap_begin, + TP_PROTO(struct inode *inode, loff_t offset, loff_t length, + unsigned int flags), + TP_ARGS(inode, offset, length, flags), + TP_STRUCT__entry( + __field(unsigned long, ino) + __field(loff_t, offset) + __field(loff_t, length) + __field(dev_t, dev) + __field(unsigned int, flags) + ), + TP_fast_assign( + __entry->ino = inode->i_ino; + __entry->offset = offset; + __entry->length = length; + __entry->dev = inode->i_sb->s_dev; + __entry->flags = flags; + ), + TP_printk("dev=(%d,%d) ino=%lu offset=%lld length=%lld flags=0x%x", + MAJOR(__entry->dev), MINOR(__entry->dev), + __entry->ino, __entry->offset, __entry->length, + __entry->flags) +); + +TRACE_EVENT(ntfs3_iomap_end, + TP_PROTO(struct inode *inode, loff_t pos, loff_t length, + ssize_t written, unsigned int flags), + TP_ARGS(inode, pos, length, written, flags), + TP_STRUCT__entry( + __field(unsigned long, ino) + __field(loff_t, pos) + __field(loff_t, length) + __field(ssize_t, written) + __field(dev_t, dev) + __field(unsigned int, flags) + ), + TP_fast_assign( + __entry->ino = inode->i_ino; + __entry->pos = pos; + __entry->length = length; + __entry->written = written; + __entry->dev = inode->i_sb->s_dev; + __entry->flags = flags; + ), + TP_printk("dev=(%d,%d) ino=%lu pos=%lld length=%lld written=%zd flags=0x%x", + MAJOR(__entry->dev), MINOR(__entry->dev), + __entry->ino, __entry->pos, __entry->length, + __entry->written, __entry->flags) +); + #endif /* _TRACE_NTFS3_H */ #include From ee1cb3443cff5a800948b662756f75b17ca63fe2 Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Thu, 20 Aug 2026 17:05:04 +0800 Subject: [PATCH 52/52] ntfs3: add file I/O tracepoints Add ntfs3 tracepoints for file I/O operations. This adds trace events for ntfs_file_read_iter() and ntfs_file_write_iter() to help observe file read and write activity. Signed-off-by: Baolin Liu Signed-off-by: Konstantin Komarov --- fs/ntfs3/file.c | 5 +++++ include/trace/events/ntfs3.h | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/fs/ntfs3/file.c b/fs/ntfs3/file.c index 95dfe6878860..c11da747270f 100644 --- a/fs/ntfs3/file.c +++ b/fs/ntfs3/file.c @@ -20,6 +20,7 @@ #include "debug.h" #include "ntfs.h" #include "ntfs_fs.h" +#include /* * cifx, btrfs, exfat, ext4, f2fs use this constant. @@ -827,6 +828,8 @@ static ssize_t ntfs_file_read_iter(struct kiocb *iocb, struct iov_iter *iter) unsigned int dio_flags; ssize_t ret; + trace_ntfs3_file_read_iter(iocb, iter); + ret = check_read_restriction(inode); if (ret) return ret; @@ -1247,6 +1250,8 @@ static ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from) loff_t vbo, endbyte; ssize_t ret, err; + trace_ntfs3_file_write_iter(iocb, from); + if (!inode_trylock(inode)) { if (iocb->ki_flags & IOCB_NOWAIT) return -EAGAIN; diff --git a/include/trace/events/ntfs3.h b/include/trace/events/ntfs3.h index e0c0c44ce318..750300068984 100644 --- a/include/trace/events/ntfs3.h +++ b/include/trace/events/ntfs3.h @@ -351,6 +351,39 @@ TRACE_EVENT(ntfs3_iomap_end, __entry->written, __entry->flags) ); +DECLARE_EVENT_CLASS(ntfs3_file_class, + TP_PROTO(struct kiocb *iocb, struct iov_iter *iter), + TP_ARGS(iocb, iter), + TP_STRUCT__entry( + __field(loff_t, size) + __field(loff_t, offset) + __field(size_t, count) + __field(unsigned long, ino) + __field(dev_t, dev) + ), + TP_fast_assign( + __entry->size = i_size_read(file_inode(iocb->ki_filp)); + __entry->offset = iocb->ki_pos; + __entry->count = iov_iter_count(iter); + __entry->ino = file_inode(iocb->ki_filp)->i_ino; + __entry->dev = file_inode(iocb->ki_filp)->i_sb->s_dev; + ), + TP_printk("dev=(%d,%d) ino=%lu size=%lld pos=%lld bytecount=%zu", + MAJOR(__entry->dev), MINOR(__entry->dev), + __entry->ino, __entry->size, __entry->offset, + __entry->count) +); + +DEFINE_EVENT(ntfs3_file_class, ntfs3_file_read_iter, + TP_PROTO(struct kiocb *iocb, struct iov_iter *iter), + TP_ARGS(iocb, iter) +); + +DEFINE_EVENT(ntfs3_file_class, ntfs3_file_write_iter, + TP_PROTO(struct kiocb *iocb, struct iov_iter *iter), + TP_ARGS(iocb, iter) +); + #endif /* _TRACE_NTFS3_H */ #include