From 523307b8516fc740895238af8473aa0630b3e088 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sat, 11 Jul 2026 16:36:55 +0900 Subject: [PATCH 01/35] ntfs: preserve RECALL_ON_OPEN on WSL special-file reparse points When creating a WSL special file (socket, fifo, character or block device), __ntfs_create() sets FILE_ATTRIBUTE_RECALL_ON_OPEN in ni->flags as valid_reparse_data() requires for these tags. This flag is intentionally absent from $FILE_NAME, so the subsequent reload ni->flags = fn->file_attributes; drops it from ni->flags, the authoritative copy written back to $STANDARD_INFORMATION. The on-disk file_attributes becomes 0x00000404 instead of 0x00040404, and after a remount valid_reparse_data() rejects the reparse point while fsck reports "$REPARSE_POINT data is corrupted". Preserve the RECALL_ON_OPEN bit across the reload. Symlinks do not set that bit, so they are unaffected. Fixes: af0db57d4293 ("ntfs: update inode operations") Signed-off-by: Namjae Jeon --- fs/ntfs/namei.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fs/ntfs/namei.c b/fs/ntfs/namei.c index 5ff25e9aaa32..cd403b1d99ee 100644 --- a/fs/ntfs/namei.c +++ b/fs/ntfs/namei.c @@ -685,7 +685,8 @@ static struct ntfs_inode *__ntfs_create(struct mnt_idmap *idmap, struct inode *d mutex_unlock(&dir_ni->mrec_lock); mutex_unlock(&ni->mrec_lock); - ni->flags = fn->file_attributes; + ni->flags = fn->file_attributes | + (ni->flags & FILE_ATTRIBUTE_RECALL_ON_OPEN); /* Set the sequence number. */ vi->i_generation = ni->seq_no; set_nlink(vi, 1); From 8bed376124ab4505b70083a2b91f2c7ef6d51e24 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Fri, 10 Jul 2026 14:22:57 +0900 Subject: [PATCH 02/35] ntfs: harden runlist realloc size calculations Add a shared helper to safely convert runlist element counts to byte sizes using overflow checks, and use it in both ntfs_rl_realloc() and ntfs_rl_realloc_nofail(). Fixes: 11ccc9107dc4 ("ntfs: update runlist handling and cluster allocator") Co-developed-by: Alper Mudar Signed-off-by: Alper Mudar Tested-by: Alper Mudar Signed-off-by: Namjae Jeon --- fs/ntfs/runlist.c | 50 +++++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/fs/ntfs/runlist.c b/fs/ntfs/runlist.c index cbb6576cf725..8e0fd400e7f7 100644 --- a/fs/ntfs/runlist.c +++ b/fs/ntfs/runlist.c @@ -71,29 +71,46 @@ static inline void ntfs_rl_mc(struct runlist_element *dstbase, int dst, * On success, return a pointer to the newly allocated, or recycled, memory. * On error, return -errno. */ -struct runlist_element *ntfs_rl_realloc(struct runlist_element *rl, - int old_size, int new_size) +static inline struct runlist_element *ntfs_rl_realloc_gfp(struct runlist_element *rl, + int old_size, int new_size, gfp_t gfp) { struct runlist_element *new_rl; + size_t new_bytes; + + if (old_size < 0 || new_size < 0) + return ERR_PTR(-EINVAL); - old_size = old_size * sizeof(*rl); - new_size = new_size * sizeof(*rl); if (old_size == new_size) return rl; - new_rl = kvzalloc(new_size, GFP_NOFS); + if (check_mul_overflow(new_size, sizeof(*rl), &new_bytes)) + return ERR_PTR(-EINVAL); + + new_rl = kvzalloc(new_bytes, gfp); if (unlikely(!new_rl)) return ERR_PTR(-ENOMEM); if (likely(rl != NULL)) { - if (unlikely(old_size > new_size)) - old_size = new_size; - memcpy(new_rl, rl, old_size); + size_t old_bytes; + + if (check_mul_overflow(old_size, sizeof(*rl), &old_bytes)) { + kvfree(new_rl); + return ERR_PTR(-EINVAL); + } + if (unlikely(old_bytes > new_bytes)) + old_bytes = new_bytes; + memcpy(new_rl, rl, old_bytes); kvfree(rl); } return new_rl; } +struct runlist_element *ntfs_rl_realloc(struct runlist_element *rl, + int old_size, int new_size) +{ + return ntfs_rl_realloc_gfp(rl, old_size, new_size, GFP_NOFS); +} + /* * ntfs_rl_realloc_nofail - Reallocate memory for runlists * @rl: original runlist @@ -118,21 +135,8 @@ struct runlist_element *ntfs_rl_realloc(struct runlist_element *rl, static inline struct runlist_element *ntfs_rl_realloc_nofail(struct runlist_element *rl, int old_size, int new_size) { - struct runlist_element *new_rl; - - old_size = old_size * sizeof(*rl); - new_size = new_size * sizeof(*rl); - if (old_size == new_size) - return rl; - - new_rl = kvmalloc(new_size, GFP_NOFS | __GFP_NOFAIL); - if (likely(rl != NULL)) { - if (unlikely(old_size > new_size)) - old_size = new_size; - memcpy(new_rl, rl, old_size); - kvfree(rl); - } - return new_rl; + return ntfs_rl_realloc_gfp(rl, old_size, new_size, + GFP_NOFS | __GFP_NOFAIL); } /* From 4e646ecd44759e552b0b9ccd995f3f608daab414 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Mon, 13 Jul 2026 16:49:57 +0900 Subject: [PATCH 03/35] ntfs: drop stale page-cache when shrinking a non-resident attr ntfs_non_resident_attr_shrink() shrinks attribute sizes but fails to trim the page cache. This leaves orphaned dirty folios beyond the new end of the attribute, leading to writeback failures (-ENOENT), data loss, and $EA chain corruption. Fix this by truncating the page cache to the new size immediately after updating the sizes, preventing writeback from flushing out-of-range folios. Fixes: 495e90fa3348 ("ntfs: update attrib operations") Signed-off-by: Namjae Jeon --- fs/ntfs/attrib.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c index 239b7bcbaedf..58f32aac5f61 100644 --- a/fs/ntfs/attrib.c +++ b/fs/ntfs/attrib.c @@ -4293,6 +4293,16 @@ static int ntfs_non_resident_attr_shrink(struct ntfs_inode *ni, const s64 newsiz ni->initialized_size = newsize; ctx->attr->data.non_resident.initialized_size = cpu_to_le64(newsize); } + + /* + * Drop any page-cache folios that now lie beyond the shrunk + * attribute. The clusters backing them have just been freed and the + * runlist truncated, so leaving stale dirty folios around makes a + * later writeback map a vcn past the new allocation, which fails with + * -ENOENT and loses the write. + */ + truncate_inode_pages(VFS_I(ni)->i_mapping, newsize); + /* Update data size in the index. */ if (ni->type == AT_DATA && ni->name == AT_UNNAMED) NInoSetFileNameDirty(ni); From aabd574b13368bbd9419ecf52dcac238c0dec5d6 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Tue, 14 Jul 2026 14:49:51 +0900 Subject: [PATCH 04/35] ntfs: validate final EA attribute size A replacement first removes the existing EA record, then adds the replacement. Check the size of that final $EA stream before mutating the current stream. This avoids committing the shortened $EA stream or $EA_INFORMATION before discovering that the replacement exceeds the AttrDef size limit. Fixes: fc053f05ca28 ("ntfs: add reparse and ea operations") Signed-off-by: Namjae Jeon --- fs/ntfs/ea.c | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c index 0cd192752b7c..0eba3f41c7bb 100644 --- a/fs/ntfs/ea.c +++ b/fs/ntfs/ea.c @@ -250,6 +250,14 @@ create_ea_info: goto out; } + /* Check the final $EA size before removing the old entry. */ + if (val_size && + ntfs_attr_size_bounds_check(ni->vol, AT_EA, + ea_info_qsize - ea_size + new_ea_size)) { + err = -EFBIG; + goto out; + } + p_ea = (struct ea_attr *)(ea_buf + ea_off); if (val_size && @@ -285,6 +293,12 @@ create_ea_info: err = -ENODATA; goto out; } + + if (ntfs_attr_size_bounds_check(ni->vol, AT_EA, + ea_info_qsize + new_ea_size)) { + err = -EFBIG; + goto out; + } } kvfree(ea_buf); @@ -312,8 +326,7 @@ alloc_new_ea: p_ea_info->ea_length = cpu_to_le16(ea_packed); p_ea_info->ea_query_length = cpu_to_le32(ea_info_qsize + new_ea_size); - if (ea_packed > 0xffff || - ntfs_attr_size_bounds_check(ni->vol, AT_EA, new_ea_size)) { + if (ea_packed > 0xffff) { err = -EFBIG; goto out; } From 76d544b677717fedafb58efb0aeb038e73e00332 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Tue, 14 Jul 2026 14:50:01 +0900 Subject: [PATCH 05/35] ntfs: remove empty EA attribute pair Removing the final xattr leaves an empty $EA stream. An empty $EA attribute paired with $EA_INFORMATION is not a valid EA chain and ntfsck reports it as corrupt. Remove both attributes when the final EA entry is deleted. Fixes: fc053f05ca28 ("ntfs: add reparse and ea operations") Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/ea.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c index 0eba3f41c7bb..88bfd6560692 100644 --- a/fs/ntfs/ea.c +++ b/fs/ntfs/ea.c @@ -275,6 +275,15 @@ create_ea_info: ea_info_qsize -= ea_size; p_ea_info->ea_query_length = cpu_to_le32(ea_info_qsize); + if ((flags & XATTR_REPLACE) && !val_size && !ea_info_qsize) { + err = ntfs_attr_remove(ni, AT_EA, AT_UNNAMED, 0); + if (err) + goto out; + + err = ntfs_attr_remove(ni, AT_EA_INFORMATION, AT_UNNAMED, 0); + goto out; + } + err = ntfs_write_ea(ni, AT_EA_INFORMATION, (char *)p_ea_info, 0, sizeof(struct ea_information), false); if (err) From d915b6f4c4539db7b8837af859dbed9628c6a71c Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Tue, 14 Jul 2026 14:50:34 +0900 Subject: [PATCH 06/35] ntfs: rewrite EA stream before updating metadata Updating an EA removes the old record and appends its replacement. Build the complete $EA stream in memory and rewrite it from offset zero, rather than committing a compacted stream followed by a separate append. generic/642 shows that the append path can leave an invalid record layout on disk, including when a new EA entry is added. When removing an EA, write the compacted stream before updating $EA_INFORMATION and restore the original pair if the metadata update fails. When the final EA entry is removed the $EA/$EA_INFORMATION pair is torn down. If removing $EA_INFORMATION fails after $EA has already been removed, the original $EA is restored so the two attributes stay consistent. Fixes: fc053f05ca28 ("ntfs: add reparse and ea operations") Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/ea.c | 65 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 46 insertions(+), 19 deletions(-) diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c index 88bfd6560692..d0044c61152a 100644 --- a/fs/ntfs/ea.c +++ b/fs/ntfs/ea.c @@ -196,6 +196,9 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len, struct ea_attr *p_ea; u32 ea_info_qsize = 0; char *ea_buf = NULL; + char *new_ea_buf; + char *old_ea_buf = NULL; + struct ea_information old_ea_info; size_t new_ea_size = ALIGN(struct_size(p_ea, ea_name, 1 + name_len + val_size), 4); s64 ea_off, ea_info_size, all_ea_size, ea_size; @@ -249,6 +252,14 @@ create_ea_info: err = -EEXIST; goto out; } + if ((flags & XATTR_REPLACE) && !val_size) { + old_ea_info = *p_ea_info; + old_ea_buf = kvmemdup(ea_buf, all_ea_size, GFP_NOFS); + if (!old_ea_buf) { + err = -ENOMEM; + goto out; + } + } /* Check the final $EA size before removing the old entry. */ if (val_size && @@ -281,20 +292,33 @@ create_ea_info: goto out; err = ntfs_attr_remove(ni, AT_EA_INFORMATION, AT_UNNAMED, 0); + if (err) { + /* Restore the original $EA if $EA_INFORMATION removal failed. */ + ntfs_attr_add(ni, AT_EA, AT_UNNAMED, 0, old_ea_buf, + all_ea_size); + ea_info_qsize = le32_to_cpu(old_ea_info.ea_query_length); + } goto out; } - err = ntfs_write_ea(ni, AT_EA_INFORMATION, (char *)p_ea_info, 0, - sizeof(struct ea_information), false); - if (err) - goto out; - - err = ntfs_write_ea(ni, AT_EA, ea_buf, 0, ea_info_qsize, true); - if (err) - goto out; - if ((flags & XATTR_REPLACE) && !val_size) { - /* Remove xattr. */ + err = ntfs_write_ea(ni, AT_EA, ea_buf, 0, ea_info_qsize, + true); + if (err) { + ntfs_write_ea(ni, AT_EA, old_ea_buf, 0, + all_ea_size, false); + goto out; + } + + err = ntfs_write_ea(ni, AT_EA_INFORMATION, (char *)p_ea_info, + 0, sizeof(struct ea_information), false); + if (err) { + ntfs_write_ea(ni, AT_EA, old_ea_buf, 0, + all_ea_size, false); + ntfs_write_ea(ni, AT_EA_INFORMATION, + (char *)&old_ea_info, 0, + sizeof(old_ea_info), false); + } goto out; } } else { @@ -309,21 +333,23 @@ create_ea_info: goto out; } } - kvfree(ea_buf); - alloc_new_ea: - ea_buf = kzalloc(new_ea_size, GFP_NOFS); - if (!ea_buf) { + new_ea_buf = kvzalloc(ea_info_qsize + new_ea_size, GFP_NOFS); + if (!new_ea_buf) { err = -ENOMEM; goto out; } + if (ea_info_qsize) + memcpy(new_ea_buf, ea_buf, ea_info_qsize); + kvfree(ea_buf); + ea_buf = new_ea_buf; + p_ea = (struct ea_attr *)(ea_buf + ea_info_qsize); /* * EA and REPARSE_POINT compatibility not checked any more, * required by Windows 10, but having both may lead to * problems with earlier versions. */ - p_ea = (struct ea_attr *)ea_buf; memcpy(p_ea->ea_name, name, name_len); p_ea->ea_name_length = name_len; p_ea->ea_name[name_len] = 0; @@ -344,13 +370,13 @@ alloc_new_ea: * no EA or EA_INFORMATION : add them */ if (!ntfs_attr_exist(ni, AT_EA, AT_UNNAMED, 0)) { - err = ntfs_attr_add(ni, AT_EA, AT_UNNAMED, 0, (char *)p_ea, - new_ea_size); + err = ntfs_attr_add(ni, AT_EA, AT_UNNAMED, 0, ea_buf, + ea_info_qsize + new_ea_size); if (err) goto out; } else { - err = ntfs_write_ea(ni, AT_EA, (char *)p_ea, ea_info_qsize, - new_ea_size, false); + err = ntfs_write_ea(ni, AT_EA, ea_buf, 0, + ea_info_qsize + new_ea_size, true); if (err) goto out; } @@ -370,6 +396,7 @@ out: NInoClearHasEA(ni); kvfree(ea_buf); + kvfree(old_ea_buf); kvfree(p_ea_info); return err; From 7e2a1c554bc482c0ab0f72b26093e48db982059d Mon Sep 17 00:00:00 2001 From: Alexandro Calo Date: Wed, 15 Jul 2026 23:50:29 +0900 Subject: [PATCH 07/35] ntfs: Fix min_len for compressed/sparse attributes in ntfs_non_resident_attr_value_is_valid() Here the attribute validator computes a single min_len = 64 (as the end of initialized_size) for all non-resident attributes regardless of the flags field. This is correct for regular non-resident attributes but for sparse or compressed non-resident attributes the fixed header is 8 bytes longer, it includes a compressed_size field at bytes 64-71, min_len should be 72. Since the validator lets a sparse/compressed attr_record be less than the correct length, caller's accesses to compressed_size (e.g., ntfs_read_locked_inode() or ntfs_attr_update_mapping_pairs()) can extend past the attribute declared boundary. This can cause OOB reads or OOB writes past the MFT record buffer if the attribute is positioned near the end of the MFT record. The compressed_size field is accessed from: - ntfs_read_locked_inode() - ntfs_read_locked_attr_inode() - ntfs_attr_open() - ntfs_attr_update_mapping_pairs() ntfs_attr_make_non_resident() seems to be safe. Fixing this by raising min_len for sparse/compressed attributes in the validator. The OOB reads and the OOB writes require a crafted filesystem image, which is not in the kernel threat model, anyway, fixing memory errors would be nice to keep things secure. Signed-off-by: Alexandro Calo Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/attrib.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c index 58f32aac5f61..d354c3b0fae1 100644 --- a/fs/ntfs/attrib.c +++ b/fs/ntfs/attrib.c @@ -697,6 +697,11 @@ static bool ntfs_non_resident_attr_value_is_valid(const struct attr_record *a) attr_len = le32_to_cpu(a->length); min_len = offsetof(struct attr_record, data.non_resident.initialized_size) + sizeof(a->data.non_resident.initialized_size); + + /* Sparse and compressed attributes have the extra compressed_size field */ + if (a->flags & (ATTR_IS_SPARSE | ATTR_COMPRESSION_MASK)) + min_len += sizeof(a->data.non_resident.compressed_size); + if (attr_len < min_len) return false; From f52d94c4b424cc50fe3b3799c2d0a38a37b02530 Mon Sep 17 00:00:00 2001 From: "Matthew Wilcox (Oracle)" Date: Wed, 15 Jul 2026 10:38:00 +0900 Subject: [PATCH 08/35] ntfs: Inline zero_partial_compressed_page() zero_partial_compressed_page() has one caller and the next commit will make changes to it that make it inelegant to split across two functions. Fixes: 495e90fa3348 ("ntfs: update attrib operations") Signed-off-by: Matthew Wilcox (Oracle) Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/compress.c | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/fs/ntfs/compress.c b/fs/ntfs/compress.c index 76bd806b41ed..c904858dff3d 100644 --- a/fs/ntfs/compress.c +++ b/fs/ntfs/compress.c @@ -96,26 +96,6 @@ void free_compression_buffers(void) mutex_unlock(&ntfs_cb_lock); } -/* - * zero_partial_compressed_page - zero out of bounds compressed page region - * @page: page to zero - * @initialized_size: initialized size of the attribute - */ -static void zero_partial_compressed_page(struct page *page, - const s64 initialized_size) -{ - u8 *kp = page_address(page); - unsigned int kp_ofs; - - ntfs_debug("Zeroing page region outside initialized size."); - if (((s64)page->__folio_index << PAGE_SHIFT) >= initialized_size) { - clear_page(kp); - return; - } - kp_ofs = initialized_size & ~PAGE_MASK; - memset(kp + kp_ofs, 0, PAGE_SIZE - kp_ofs); -} - /* * handle_bounds_compressed_page - test for&handle out of bounds compressed page * @page: page to check and handle @@ -126,8 +106,18 @@ static inline void handle_bounds_compressed_page(struct page *page, const loff_t i_size, const s64 initialized_size) { if ((page->__folio_index >= (initialized_size >> PAGE_SHIFT)) && - (initialized_size < i_size)) - zero_partial_compressed_page(page, initialized_size); + (initialized_size < i_size)) { + u8 *kp = page_address(page); + unsigned int kp_ofs; + + ntfs_debug("Zeroing page region outside initialized size."); + if (((s64)page->__folio_index << PAGE_SHIFT) >= initialized_size) { + clear_page(kp); + return; + } + kp_ofs = initialized_size & ~PAGE_MASK; + memset(kp + kp_ofs, 0, PAGE_SIZE - kp_ofs); + } } /* From 0fed76692f8aa7caea97f2235e6489aca715d270 Mon Sep 17 00:00:00 2001 From: "Matthew Wilcox (Oracle)" Date: Wed, 15 Jul 2026 10:38:01 +0900 Subject: [PATCH 09/35] ntfs: Remove use of __folio_index in handle_bounds_compressed_page() Nobody is supposed to use page->__folio_index. Use page_offset() instead, and simplify by working exclusively in loff_t instead of mixing up loff_t and pgoff_t. Link: https://lore.kernel.org/all/20260608210618.3437216-3-willy@infradead.org/ Fixes: 495e90fa3348 ("ntfs: update attrib operations") Signed-off-by: Matthew Wilcox (Oracle) Co-developed-by: Hyunchul Lee Signed-off-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/compress.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fs/ntfs/compress.c b/fs/ntfs/compress.c index c904858dff3d..8078041b8796 100644 --- a/fs/ntfs/compress.c +++ b/fs/ntfs/compress.c @@ -105,13 +105,15 @@ void free_compression_buffers(void) static inline void handle_bounds_compressed_page(struct page *page, const loff_t i_size, const s64 initialized_size) { - if ((page->__folio_index >= (initialized_size >> PAGE_SHIFT)) && + loff_t pos = page_offset(page); + + if ((pos + PAGE_SIZE > initialized_size) && (initialized_size < i_size)) { u8 *kp = page_address(page); unsigned int kp_ofs; ntfs_debug("Zeroing page region outside initialized size."); - if (((s64)page->__folio_index << PAGE_SHIFT) >= initialized_size) { + if (pos >= initialized_size) { clear_page(kp); return; } From 3149f7a0070055722285f12f8eab86605b6d1e09 Mon Sep 17 00:00:00 2001 From: "Matthew Wilcox (Oracle)" Date: Wed, 15 Jul 2026 10:38:02 +0900 Subject: [PATCH 10/35] ntfs: Use zero_user_segment() in handle_bounds_compressed_page() This fixes handle_bounds_compressed_page() on highmem memory as page_address() does not work on memory which has been kmap_local(), only on kmap() memory. Fixes: 495e90fa3348 ("ntfs: update attrib operations") Signed-off-by: Matthew Wilcox (Oracle) Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/compress.c | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/fs/ntfs/compress.c b/fs/ntfs/compress.c index 8078041b8796..f03fae63c199 100644 --- a/fs/ntfs/compress.c +++ b/fs/ntfs/compress.c @@ -109,16 +109,16 @@ static inline void handle_bounds_compressed_page(struct page *page, if ((pos + PAGE_SIZE > initialized_size) && (initialized_size < i_size)) { - u8 *kp = page_address(page); - unsigned int kp_ofs; + size_t offset; ntfs_debug("Zeroing page region outside initialized size."); - if (pos >= initialized_size) { - clear_page(kp); - return; - } - kp_ofs = initialized_size & ~PAGE_MASK; - memset(kp + kp_ofs, 0, PAGE_SIZE - kp_ofs); + if (pos >= initialized_size) + offset = 0; + else + offset = offset_in_page(initialized_size); + zero_user_segment(page, offset, PAGE_SIZE); + } else { + flush_dcache_page(page); } } @@ -223,7 +223,6 @@ return_error: */ handle_bounds_compressed_page(dp, i_size, initialized_size); - flush_dcache_page(dp); kunmap_local(page_address(dp)); SetPageUptodate(dp); unlock_page(dp); @@ -759,7 +758,6 @@ lock_retry_remap: */ handle_bounds_compressed_page(page, i_size, initialized_size); - flush_dcache_page(page); kunmap_local(page_address(page)); SetPageUptodate(page); unlock_page(page); From cb6831717ad06db992b2041f736d5369ec2d6198 Mon Sep 17 00:00:00 2001 From: "Matthew Wilcox (Oracle)" Date: Wed, 15 Jul 2026 10:38:03 +0900 Subject: [PATCH 11/35] ntfs: Remove references to page->__folio_index Pages don't have indexes, folios have indexes. Correct this in ntfs_read_compressed_block() and also remove a use of page->mapping while I'm in here. Also convert the calls to unlock_page() and flush_dcache_page(). Fixes: 495e90fa3348 ("ntfs: update attrib operations") Signed-off-by: Matthew Wilcox (Oracle) Cc: Christoph Hellwig Cc: Hyunchul Lee Cc: Namjae Jeon Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/compress.c | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/fs/ntfs/compress.c b/fs/ntfs/compress.c index f03fae63c199..d83d3e8e06ae 100644 --- a/fs/ntfs/compress.c +++ b/fs/ntfs/compress.c @@ -456,14 +456,14 @@ int ntfs_read_compressed_block(struct folio *folio) struct page *page = &folio->page; loff_t i_size; s64 initialized_size; - struct address_space *mapping = page->mapping; + struct address_space *mapping = folio->mapping; struct ntfs_inode *ni = NTFS_I(mapping->host); struct ntfs_volume *vol = ni->vol; struct super_block *sb = vol->sb; struct runlist_element *rl; unsigned long flags; u8 *cb, *cb_pos, *cb_end; - unsigned long offset, index = page->__folio_index; + unsigned long offset, index = folio->index; u32 cb_size = ni->itype.compressed.block_size; u64 cb_size_mask = cb_size - 1UL; s64 vcn; @@ -812,14 +812,16 @@ lock_retry_remap: for (cur_page = 0; cur_page < max_page; cur_page++) { page = pages[cur_page]; if (page) { + folio = page_folio(page); + ntfs_error(vol->sb, "Still have pages left! Terminating them with extreme prejudice. Inode 0x%llx, page index 0x%lx.", - ni->mft_no, page->__folio_index); - flush_dcache_page(page); + ni->mft_no, folio->index); + flush_dcache_folio(folio); kunmap_local(page_address(page)); - unlock_page(page); + folio_unlock(folio); if (cur_page != xpage) - put_page(page); + folio_put(folio); pages[cur_page] = NULL; } } From 513c0772542b8a7aeb494febea4907f79491f132 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Mon, 15 Jun 2026 21:07:19 +0900 Subject: [PATCH 12/35] ntfs: fix kmap_local_page() usage in compress Several compressed I/O paths discard the address returned by kmap_local_page() and later access or unmap the page using page_address(). This is invalid for highmem pages, and local mappings must also be unmapped using the address returned by kmap_local_page(). Map each destination page in ntfs_decompress() only while producing the current sub-block. Use memcpy_from_page(), memcpy_to_page(), and memzero_page() for the other page accesses. Remove unnecessary local mappings from ntfs_write_cb(), where pages are accessed through the vmap() mapping. Fixes: 495e90fa3348 ("ntfs: update attrib operations") Reported-by: Matthew Wilcox Signed-off-by: Namjae Jeon --- fs/ntfs/compress.c | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/fs/ntfs/compress.c b/fs/ntfs/compress.c index d83d3e8e06ae..e866f43ca30b 100644 --- a/fs/ntfs/compress.c +++ b/fs/ntfs/compress.c @@ -177,6 +177,7 @@ static int ntfs_decompress(struct page *dest_pages[], int completed_pages[], /* Variables for uncompressed data / destination. */ struct page *dp; /* Current destination page being worked on. */ + u8 *dp_kaddr; /* Local kmap for the current destination page. */ u8 *dp_addr; /* Current pointer into dp. */ u8 *dp_sb_start; /* Start of current sub-block in dp. */ u8 *dp_sb_end; /* End of current sb in dp (dp_sb_start + NTFS_SB_SIZE). */ @@ -191,6 +192,7 @@ static int ntfs_decompress(struct page *dest_pages[], int completed_pages[], /* Default error code. */ int err = -EOVERFLOW; + dp_kaddr = NULL; ntfs_debug("Entering, cb_size = 0x%x.", cb_size); do_next_sb: ntfs_debug("Beginning sub-block at offset = 0x%zx in the cb.", @@ -223,7 +225,6 @@ return_error: */ handle_bounds_compressed_page(dp, i_size, initialized_size); - kunmap_local(page_address(dp)); SetPageUptodate(dp); unlock_page(dp); if (di == xpage) @@ -269,7 +270,8 @@ return_error: } /* We have a valid destination page. Setup the destination pointers. */ - dp_addr = (u8 *)page_address(dp) + do_sb_start; + dp_kaddr = kmap_local_page(dp); + dp_addr = dp_kaddr + do_sb_start; /* Now, we are ready to process the current sub-block (sb). */ if (!(le16_to_cpup((__le16 *)cb) & NTFS_SB_IS_COMPRESSED)) { @@ -290,6 +292,8 @@ return_error: /* Advance destination position to next sub-block. */ *dest_ofs += NTFS_SB_SIZE; *dest_ofs &= ~PAGE_MASK; + kunmap_local(dp_kaddr); + dp_kaddr = NULL; if (!(*dest_ofs)) { finalize_page: /* @@ -324,6 +328,8 @@ do_next_tag: } /* We have finished the current sub-block. */ *dest_ofs &= ~PAGE_MASK; + kunmap_local(dp_kaddr); + dp_kaddr = NULL; if (!(*dest_ofs)) goto finalize_page; goto do_next_sb; @@ -429,6 +435,8 @@ do_next_tag: goto do_next_tag; return_overflow: + if (dp_kaddr) + kunmap_local(dp_kaddr); ntfs_error(NULL, "Failed. Returning -EOVERFLOW."); goto return_error; } @@ -557,7 +565,6 @@ int ntfs_read_compressed_block(struct folio *folio) * least wasting our time. */ if (!PageDirty(page) && (!PageUptodate(page))) { - kmap_local_page(page); continue; } unlock_page(page); @@ -643,8 +650,7 @@ lock_retry_remap: } lock_page(lpage); - memcpy(cb_pos, page_address(lpage) + page_ofs, - vol->cluster_size); + memcpy_from_page(cb_pos, lpage, page_ofs, vol->cluster_size); unlock_page(lpage); put_page(lpage); cb_pos += vol->cluster_size; @@ -683,14 +689,7 @@ lock_retry_remap: for (; cur_page < cb_max_page; cur_page++) { page = pages[cur_page]; if (page) { - if (likely(!cur_ofs)) - clear_page(page_address(page)); - else - memset(page_address(page) + cur_ofs, 0, - PAGE_SIZE - - cur_ofs); - flush_dcache_page(page); - kunmap_local(page_address(page)); + memzero_page(page, cur_ofs, PAGE_SIZE - cur_ofs); SetPageUptodate(page); unlock_page(page); if (cur_page == xpage) @@ -708,8 +707,7 @@ lock_retry_remap: if (cb_max_ofs && cb_pos < cb_end) { page = pages[cur_page]; if (page) - memset(page_address(page) + cur_ofs, 0, - cb_max_ofs - cur_ofs); + memzero_page(page, cur_ofs, cb_max_ofs - cur_ofs); /* * No need to update cb_pos at this stage: * cb_pos += cb_max_ofs - cur_ofs; @@ -730,7 +728,7 @@ lock_retry_remap: for (; cur_page < cb_max_page; cur_page++) { page = pages[cur_page]; if (page) - memcpy(page_address(page) + cur_ofs, cb_pos, + memcpy_to_page(page, cur_ofs, cb_pos, PAGE_SIZE - cur_ofs); cb_pos += PAGE_SIZE - cur_ofs; cur_ofs = 0; @@ -741,7 +739,7 @@ lock_retry_remap: if (cb_max_ofs && cb_pos < cb_end) { page = pages[cur_page]; if (page) - memcpy(page_address(page) + cur_ofs, cb_pos, + memcpy_to_page(page, cur_ofs, cb_pos, cb_max_ofs - cur_ofs); cb_pos += cb_max_ofs - cur_ofs; cur_ofs = cb_max_ofs; @@ -758,7 +756,6 @@ lock_retry_remap: */ handle_bounds_compressed_page(page, i_size, initialized_size); - kunmap_local(page_address(page)); SetPageUptodate(page); unlock_page(page); if (cur2_page == xpage) @@ -794,7 +791,6 @@ lock_retry_remap: page = pages[prev_cur_page]; if (page) { flush_dcache_page(page); - kunmap_local(page_address(page)); unlock_page(page); if (prev_cur_page != xpage) put_page(page); @@ -818,7 +814,6 @@ lock_retry_remap: "Still have pages left! Terminating them with extreme prejudice. Inode 0x%llx, page index 0x%lx.", ni->mft_no, folio->index); flush_dcache_folio(folio); - kunmap_local(page_address(page)); folio_unlock(folio); if (cur_page != xpage) folio_put(folio); @@ -856,7 +851,6 @@ err_out: page = pages[i]; if (page) { flush_dcache_page(page); - kunmap_local(page_address(page)); unlock_page(page); if (i != xpage) put_page(page); @@ -1308,7 +1302,6 @@ static int ntfs_write_cb(struct ntfs_inode *ni, loff_t pos, struct page **pages, } pages_disk[i] = pg; lock_page(pg); - kmap_local_page(pg); } outbuf = vmap(pages_disk, pages_count, VM_MAP, PAGE_KERNEL); @@ -1443,7 +1436,6 @@ out: for (i = 0; i < pages_count; i++) { pg = pages_disk[i]; if (pg) { - kunmap_local(page_address(pg)); unlock_page(pg); put_page(pg); } From afc49a445c670e16dab4132d350983d96f49813a Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Wed, 15 Jul 2026 14:25:10 +0900 Subject: [PATCH 13/35] ntfs: file extension before write submission Prepare non-resident file allocation and initialized-size extension in ->write_iter() before entering the buffered or direct iomap write paths. Previously, the iomap write callback extended initialized_size. When a direct write started beyond initialized_size, ntfs_extend_initialized_size() used iomap_zero_range() to zero the gap through the page cache. This created dirty folios after iomap DIO had invalidated its target cache range. The bsync path then had to synchronously write back the entire zeroed gap to prevent the post-DIO invalidation from encountering a dirty boundary folio. Move allocation and initialized-size preparation ahead of iomap submission. For DIO, kiocb_invalidate_pages() now sees any dirty boundary folio created by iomap_zero_range(), writes it back when necessary, and invalidates it before the direct I/O is issued. This removes the explicit synchronous writeback of the zeroed gap while preserving the required boundary-folio ordering. Keep compressed writes out of the early initialized-size extension so their existing write path can zero uninitialized data before compression. Move compressed-file allocation expansion to write_iter as well, eliminating the now-redundant expansion from ntfs_compress_write(). Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/compress.c | 10 ---------- fs/ntfs/file.c | 43 +++++++++++++++++++++++++++++++++++++++++-- fs/ntfs/inode.c | 6 +----- fs/ntfs/inode.h | 2 +- fs/ntfs/iomap.c | 34 +--------------------------------- 5 files changed, 44 insertions(+), 51 deletions(-) diff --git a/fs/ntfs/compress.c b/fs/ntfs/compress.c index e866f43ca30b..fe1877b86f49 100644 --- a/fs/ntfs/compress.c +++ b/fs/ntfs/compress.c @@ -1459,16 +1459,6 @@ int ntfs_compress_write(struct ntfs_inode *ni, loff_t pos, size_t count, size_t written = 0; struct address_space *mapping = VFS_I(ni)->i_mapping; - if (NInoCompressed(ni) && pos + count > ni->allocated_size) { - int err; - loff_t end = pos + count; - - err = ntfs_attr_expand(ni, end, - round_up(end, ni->itype.compressed.block_size)); - if (err) - return err; - } - pages = kmalloc_array(pages_per_cb, sizeof(struct page *), GFP_NOFS); if (!pages) return -ENOMEM; diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c index 6a7b638e523d..9061f8f77f7e 100644 --- a/fs/ntfs/file.c +++ b/fs/ntfs/file.c @@ -535,6 +535,31 @@ out: return ret; } +static int ntfs_expand_for_write(struct ntfs_inode *ni, loff_t end) +{ + struct ntfs_volume *vol = ni->vol; + loff_t prealloc_size = 0; + int err; + + if (end <= ni->data_size) + return 0; + + if (NInoCompressed(ni)) { + if (end > ni->allocated_size) + prealloc_size = round_up(end, + ni->itype.compressed.block_size); + } else if (end > ni->allocated_size && + end < ni->allocated_size + vol->preallocated_size) { + prealloc_size = ni->allocated_size + vol->preallocated_size; + } + + mutex_lock(&ni->mrec_lock); + err = ntfs_attr_expand(ni, end, prealloc_size); + mutex_unlock(&ni->mrec_lock); + + return err; +} + static ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from) { struct file *file = iocb->ki_filp; @@ -543,7 +568,7 @@ static ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from) struct ntfs_volume *vol = ni->vol; ssize_t ret; ssize_t count; - loff_t pos; + loff_t pos, end; int err; loff_t old_data_size, old_init_size; @@ -580,10 +605,24 @@ static ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from) pos = iocb->ki_pos; count = ret; + end = pos + count; old_data_size = ni->data_size; old_init_size = ni->initialized_size; + if (end > old_data_size) { + ret = ntfs_expand_for_write(ni, end); + if (ret < 0) + goto out; + } + + if (NInoNonResident(ni) && !NInoCompressed(ni) && + end > old_init_size) { + ret = ntfs_extend_initialized_size(vi, pos, end); + if (ret < 0) + goto out; + } + if (NInoNonResident(ni) && NInoCompressed(ni)) { ret = ntfs_compress_write(ni, pos, count, from); if (ret > 0) @@ -655,7 +694,7 @@ static int ntfs_file_mmap_prepare(struct vm_area_desc *desc) from + desc->end - desc->start); if (NTFS_I(inode)->initialized_size < to) { - err = ntfs_extend_initialized_size(inode, to, to, false); + err = ntfs_extend_initialized_size(inode, to, to); if (err) return err; } diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c index 7381a18cfadd..50e244aa372e 100644 --- a/fs/ntfs/inode.c +++ b/fs/ntfs/inode.c @@ -2401,7 +2401,7 @@ int ntfs_show_options(struct seq_file *sf, struct dentry *root) } int ntfs_extend_initialized_size(struct inode *vi, const loff_t offset, - const loff_t new_size, bool bsync) + const loff_t new_size) { struct ntfs_inode *ni = NTFS_I(vi); loff_t old_init_size; @@ -2428,10 +2428,6 @@ int ntfs_extend_initialized_size(struct inode *vi, const loff_t offset, &ntfs_iomap_folio_ops, NULL); if (err) return err; - if (bsync) - err = filemap_write_and_wait_range(vi->i_mapping, - old_init_size, - offset - 1); } diff --git a/fs/ntfs/inode.h b/fs/ntfs/inode.h index 9aacd5787ffe..c6d065aaecd5 100644 --- a/fs/ntfs/inode.h +++ b/fs/ntfs/inode.h @@ -352,7 +352,7 @@ static inline void ntfs_commit_inode(struct inode *vi) int ntfs_inode_sync_filename(struct ntfs_inode *ni); int ntfs_extend_initialized_size(struct inode *vi, const loff_t offset, - const loff_t new_size, bool bsync); + const loff_t new_size); void ntfs_set_vfs_operations(struct inode *inode, mode_t mode, dev_t dev); struct folio *ntfs_get_locked_folio(struct address_space *mapping, pgoff_t index, pgoff_t end_index, struct file_ra_state *ra); diff --git a/fs/ntfs/iomap.c b/fs/ntfs/iomap.c index 52eecf5cb256..26a1831a2c18 100644 --- a/fs/ntfs/iomap.c +++ b/fs/ntfs/iomap.c @@ -675,21 +675,7 @@ static int ntfs_write_iomap_begin_non_resident(struct inode *inode, loff_t offse loff_t length, unsigned int flags, struct iomap *iomap, int ntfs_iomap_flags) { - struct ntfs_inode *ni = NTFS_I(inode); - - if (ntfs_iomap_flags & (NTFS_IOMAP_FLAGS_BEGIN | NTFS_IOMAP_FLAGS_DIO) && - offset + length > ni->initialized_size) { - int ret; - - ret = ntfs_extend_initialized_size(inode, offset, - offset + length, - ntfs_iomap_flags & - NTFS_IOMAP_FLAGS_DIO); - if (ret < 0) - return ret; - } - - mutex_lock(&ni->mrec_lock); + mutex_lock(&NTFS_I(inode)->mrec_lock); if (ntfs_iomap_flags & NTFS_IOMAP_FLAGS_BEGIN) return ntfs_write_simple_iomap_begin_non_resident(inode, offset, length, iomap); @@ -705,28 +691,10 @@ static int __ntfs_write_iomap_begin(struct inode *inode, loff_t offset, struct iomap *iomap, int ntfs_iomap_flags) { struct ntfs_inode *ni = NTFS_I(inode); - loff_t end = offset + length; if (NVolShutdown(ni->vol)) return -EIO; - if (ntfs_iomap_flags & (NTFS_IOMAP_FLAGS_BEGIN | NTFS_IOMAP_FLAGS_DIO) && - end > ni->data_size) { - struct ntfs_volume *vol = ni->vol; - int ret; - - mutex_lock(&ni->mrec_lock); - if (end > ni->allocated_size && - end < ni->allocated_size + vol->preallocated_size) - ret = ntfs_attr_expand(ni, end, - ni->allocated_size + vol->preallocated_size); - else - ret = ntfs_attr_expand(ni, end, 0); - mutex_unlock(&ni->mrec_lock); - if (ret) - return ret; - } - if (!NInoNonResident(ni)) { mutex_lock(&ni->mrec_lock); return ntfs_write_iomap_begin_resident(inode, offset, iomap); From ecee2c2e91c82d7eecd62106f5835c6b3d1ae699 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Wed, 15 Jul 2026 15:31:06 +0900 Subject: [PATCH 14/35] ntfs: use pagecache_isize_extended() on size extension When extending file size, call truncate_pagecache() first, then update i_size, and use pagecache_isize_extended() instead of manual iomap_zero_range(). This ensures the straddling folio is properly marked RO so page_mkwrite() is called and post-EOF area is zeroed. Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/file.c | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c index 9061f8f77f7e..c8e49f83fd92 100644 --- a/fs/ntfs/file.c +++ b/fs/ntfs/file.c @@ -268,23 +268,19 @@ static int ntfs_setattr_size(struct inode *vi, struct iattr *attr) return err; inode_dio_wait(vi); - truncate_setsize(vi, attr->ia_size); + if (attr->ia_size > old_size) { + truncate_pagecache(vi, old_size); + i_size_write(vi, attr->ia_size); + pagecache_isize_extended(vi, old_size, attr->ia_size); + } else + truncate_setsize(vi, attr->ia_size); + err = ntfs_truncate_vfs(vi, attr->ia_size, old_size); if (err) { i_size_write(vi, old_size); return err; } - if (NInoNonResident(ni) && attr->ia_size > old_size && - old_size % PAGE_SIZE != 0) { - loff_t len = min_t(loff_t, - round_up(old_size, PAGE_SIZE) - old_size, - attr->ia_size - old_size); - err = iomap_zero_range(vi, old_size, len, - NULL, &ntfs_seek_iomap_ops, - &ntfs_iomap_folio_ops, NULL); - } - return err; } @@ -1165,13 +1161,9 @@ out: filemap_invalidate_unlock(vi->i_mapping); if (!err) { if (mode == 0 && NInoNonResident(ni) && - offset > old_size && old_size % PAGE_SIZE != 0) { - loff_t len = min_t(loff_t, - round_up(old_size, PAGE_SIZE) - old_size, - offset - old_size); - err = iomap_zero_range(vi, old_size, len, NULL, - &ntfs_seek_iomap_ops, - &ntfs_iomap_folio_ops, NULL); + offset > old_size) { + truncate_pagecache(vi, old_size); + pagecache_isize_extended(vi, old_size, offset); } NInoSetFileNameDirty(ni); inode_set_mtime_to_ts(vi, inode_set_ctime_current(vi)); From c4c3e3a745e14677a5e9685eb416cdf6b1aebdfe Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Tue, 14 Jul 2026 18:48:40 +0900 Subject: [PATCH 15/35] MAINTAINERS: update mailing list address for ntfs Add the newly created official mailing list for the ntfs. This mailing list will be shared and used for both the kernel driver and the ntfsprogs-plus utility project. Signed-off-by: Namjae Jeon --- MAINTAINERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MAINTAINERS b/MAINTAINERS index f37a81950e25..aae3fec5e80f 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -19280,7 +19280,7 @@ F: drivers/ntb/hw/intel/ NTFS FILESYSTEM M: Namjae Jeon M: Hyunchul Lee -L: linux-fsdevel@vger.kernel.org +L: ntfs@lists.linux.dev S: Maintained T: git git://git.kernel.org/pub/scm/linux/kernel/git/linkinjeon/ntfs.git F: Documentation/filesystems/ntfs.rst From 63e61fb2a90ebab523377e159979f4f22b1e0cd8 Mon Sep 17 00:00:00 2001 From: Mohammad Shahid Date: Fri, 3 Jul 2026 23:03:01 +0530 Subject: [PATCH 16/35] ntfs: reparse: remove redundant NULL checks before kvfree() kvfree() safely handles NULL pointers, so the explicit NULL checks before calling kvfree() are unnecessary. This issue was reported by ifnullfree.cocci. Signed-off-by: Mohammad Shahid Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/reparse.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/fs/ntfs/reparse.c b/fs/ntfs/reparse.c index fa523dc3691e..ced4c675d91f 100644 --- a/fs/ntfs/reparse.c +++ b/fs/ntfs/reparse.c @@ -317,8 +317,7 @@ unsigned int ntfs_make_symlink(struct ntfs_inode *ni) } else ni->flags &= ~FILE_ATTR_REPARSE_POINT; - if (reparse_attr) - kvfree(reparse_attr); + kvfree(reparse_attr); return mode; } @@ -358,8 +357,7 @@ unsigned int ntfs_reparse_tag_dt_types(struct ntfs_volume *vol, unsigned long mr } } - if (reparse_attr) - kvfree(reparse_attr); + kvfree(reparse_attr); iput(vi); return dt_type; From 5757eadea4aefbe981bc4bcfdcfcad9c48950147 Mon Sep 17 00:00:00 2001 From: Mohammad Shahid Date: Sat, 4 Jul 2026 16:40:57 +0530 Subject: [PATCH 17/35] ntfs: mft: use kmemdup() instead of kmalloc() and memcpy() Use kmemdup() instead of a separate kmalloc() and memcpy() pair, simplifying the code while preserving the existing behavior. This issue was reported by memdup.cocci. Signed-off-by: Mohammad Shahid Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/mft.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fs/ntfs/mft.c b/fs/ntfs/mft.c index fd20d7abd6f5..f95e433885a0 100644 --- a/fs/ntfs/mft.c +++ b/fs/ntfs/mft.c @@ -2420,7 +2420,7 @@ mft_rec_already_initialized: * record. */ - (*ni)->mrec = kmalloc(vol->mft_record_size, GFP_NOFS); + (*ni)->mrec = kmemdup(m, vol->mft_record_size, GFP_NOFS); if (!(*ni)->mrec) { folio_unlock(folio); kunmap_local(m); @@ -2429,7 +2429,6 @@ mft_rec_already_initialized: goto undo_mftbmp_alloc; } - memcpy((*ni)->mrec, m, vol->mft_record_size); post_read_mst_fixup((struct ntfs_record *)(*ni)->mrec, vol->mft_record_size); ntfs_mft_mark_dirty(folio); folio_unlock(folio); From 1598068dca0f8ddedb48f01e06212374bbfe32fa Mon Sep 17 00:00:00 2001 From: Mohammad Shahid Date: Sat, 4 Jul 2026 20:16:54 +0530 Subject: [PATCH 18/35] ntfs: dir: use kmemdup() instead of kmalloc() and memcpy() Use kmemdup() instead of a separate kmalloc() and memcpy() pair, simplifying the code while preserving the existing behavior. This issue was reported by memdup.cocci. Signed-off-by: Mohammad Shahid Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/dir.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fs/ntfs/dir.c b/fs/ntfs/dir.c index 6fa9ae3377cb..2d594cbb4ebe 100644 --- a/fs/ntfs/dir.c +++ b/fs/ntfs/dir.c @@ -966,13 +966,14 @@ filldir: */ private = file->private_data; kfree(private->key); - private->key = kmalloc(le16_to_cpu(next->key_length), GFP_KERNEL); + private->key = kmemdup(&next->key.file_name, + le16_to_cpu(next->key_length), + GFP_KERNEL); if (!private->key) { err = -ENOMEM; goto out; } - memcpy(private->key, &next->key.file_name, le16_to_cpu(next->key_length)); private->key_length = next->key_length; break; } From 66b74e83daa6a7071ddcbf047aad9c0b5046b6a8 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Mon, 20 Jul 2026 17:42:21 +0900 Subject: [PATCH 19/35] ntfs: propagate compression context allocation errors ntfs_compress_block() returns -ENOMEM when its compression context cannot be allocated, but its unsigned return type turns the error into a large positive value. ntfs_write_cb() then hides the allocation failure. Use a signed return type and propagate negative errors to the caller. Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/compress.c | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/fs/ntfs/compress.c b/fs/ntfs/compress.c index fe1877b86f49..6b78a8efe3ac 100644 --- a/fs/ntfs/compress.c +++ b/fs/ntfs/compress.c @@ -1070,10 +1070,10 @@ static void ntfs_skip_position(struct compress_context *pctx, const int i) * * Returns the size of the compressed block, including the * header (minimal size is 2, maximum size is 4098) - * 0 if an error has been met. + * A negative error code if an error has been met. */ -static unsigned int ntfs_compress_block(const char *inbuf, const int bufsize, - char *outbuf) +static int ntfs_compress_block(const char *inbuf, const int bufsize, + char *outbuf) { struct compress_context *pctx; int i; /* current position */ @@ -1264,7 +1264,8 @@ static int ntfs_write_cb(struct ntfs_inode *ni, loff_t pos, struct page **pages, char *outbuf = NULL, *pbuf, *inbuf; u32 compsz, p, insz = pages_per_cb << PAGE_SHIFT; s32 rounded, bio_size; - unsigned int sz, bsz; + int sz; + unsigned int bsz; bool fail = false, allzeroes; /* a single compressed zero */ static char onezero[] = {0x01, 0xb0, 0x00, 0x00}; @@ -1319,6 +1320,10 @@ static int ntfs_write_cb(struct ntfs_inode *ni, loff_t pos, struct page **pages, bsz = insz - p; pbuf = &outbuf[compsz]; sz = ntfs_compress_block(&inbuf[p], bsz, pbuf); + if (sz < 0) { + err = sz; + goto out; + } /* fail if all the clusters (or more) are needed */ if (!sz || ((compsz + sz + vol->cluster_size + 2) > ni->itype.compressed.block_size)) From 6a4cbd059b01dc51eaa221a15f6cc9774801da99 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Tue, 21 Jul 2026 11:57:36 +0900 Subject: [PATCH 20/35] ntfs: support large pages in compressed writes ntfs_compress_write() derives its page count by shifting the compression block size and assumes that every compression block begins at a page boundary. This produces a zero page count for small compression blocks on large-page systems and ignores an in-page compression block offset. Map every page covering the compression block, pass the in-page offset to ntfs_write_cb(), and stage uncompressed output in page-aligned pages. Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/compress.c | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/fs/ntfs/compress.c b/fs/ntfs/compress.c index 6b78a8efe3ac..006b8831836c 100644 --- a/fs/ntfs/compress.c +++ b/fs/ntfs/compress.c @@ -1258,11 +1258,11 @@ static int ntfs_compress_block(const char *inbuf, const int bufsize, } static int ntfs_write_cb(struct ntfs_inode *ni, loff_t pos, struct page **pages, - int pages_per_cb) + int pages_per_cb, unsigned int page_offset) { struct ntfs_volume *vol = ni->vol; - char *outbuf = NULL, *pbuf, *inbuf; - u32 compsz, p, insz = pages_per_cb << PAGE_SHIFT; + char *outbuf = NULL, *pbuf, *inbuf, *in_mapping; + u32 compsz, p, insz = ni->itype.compressed.block_size; s32 rounded, bio_size; int sz; unsigned int bsz; @@ -1284,14 +1284,15 @@ static int ntfs_write_cb(struct ntfs_inode *ni, loff_t pos, struct page **pages, loff_t new_length; s64 new_vcn; - inbuf = vmap(pages, pages_per_cb, VM_MAP, PAGE_KERNEL_RO); - if (!inbuf) + in_mapping = vmap(pages, pages_per_cb, VM_MAP, PAGE_KERNEL_RO); + if (!in_mapping) return -ENOMEM; + inbuf = in_mapping + page_offset; /* may need 2 extra bytes per block and 2 more bytes */ pages_disk = kcalloc(pages_count, sizeof(struct page *), GFP_NOFS); if (!pages_disk) { - vunmap(inbuf); + vunmap(in_mapping); return -ENOMEM; } @@ -1361,7 +1362,9 @@ static int ntfs_write_cb(struct ntfs_inode *ni, loff_t pos, struct page **pages, err = 0; goto out; } else { + memcpy(outbuf, inbuf, insz); bio_size = insz; + pages = pages_disk; } new_vcn = ntfs_bytes_to_cluster(vol, @@ -1420,7 +1423,8 @@ setup_bio: GFP_NOIO); bio->bi_iter.bi_sector = ntfs_bytes_to_sector(vol, - ntfs_cluster_to_bytes(vol, bio_lcn + i)); + ntfs_cluster_to_bytes(vol, bio_lcn) + + ((s64)i << PAGE_SHIFT)); } if (!bio_add_page(bio, pages[i], page_size, 0)) { @@ -1437,7 +1441,8 @@ setup_bio: err = submit_bio_wait(bio); bio_put(bio); out: - vunmap(outbuf); + if (outbuf) + vunmap(outbuf); for (i = 0; i < pages_count; i++) { pg = pages_disk[i]; if (pg) { @@ -1446,7 +1451,7 @@ out: } } kfree(pages_disk); - vunmap(inbuf); + vunmap(in_mapping); NInoSetFileNameDirty(ni); mark_mft_record_dirty(ni); @@ -1458,12 +1463,15 @@ int ntfs_compress_write(struct ntfs_inode *ni, loff_t pos, size_t count, { struct folio *folio; struct page **pages = NULL, *page; - int pages_per_cb = ni->itype.compressed.block_size >> PAGE_SHIFT; + int pages_per_cb; int cb_size = ni->itype.compressed.block_size, cb_off, err = 0; int i, ip; size_t written = 0; struct address_space *mapping = VFS_I(ni)->i_mapping; + pages_per_cb = DIV_ROUND_UP(offset_in_page(pos & ~(cb_size - 1)) + + cb_size, PAGE_SIZE); + pages = kmalloc_array(pages_per_cb, sizeof(struct page *), GFP_NOFS); if (!pages) return -ENOMEM; @@ -1471,6 +1479,7 @@ int ntfs_compress_write(struct ntfs_inode *ni, loff_t pos, size_t count, while (count) { pgoff_t index; size_t copied, bytes; + unsigned int page_offset; int off; off = pos & (cb_size - 1); @@ -1479,6 +1488,8 @@ int ntfs_compress_write(struct ntfs_inode *ni, loff_t pos, size_t count, bytes = count; cb_off = pos & ~(cb_size - 1); + page_offset = offset_in_page(cb_off); + pages_per_cb = DIV_ROUND_UP(page_offset + cb_size, PAGE_SIZE); index = cb_off >> PAGE_SHIFT; if (unlikely(fault_in_iov_iter_readable(from, bytes))) { @@ -1527,7 +1538,7 @@ int ntfs_compress_write(struct ntfs_inode *ni, loff_t pos, size_t count, } } - err = ntfs_write_cb(ni, pos, pages, pages_per_cb); + err = ntfs_write_cb(ni, pos, pages, pages_per_cb, page_offset); for (i = 0; i < pages_per_cb; i++) { folio = page_folio(pages[i]); From d605e3f941c860b33717db77eb7123e481d64962 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Mon, 20 Jul 2026 17:43:15 +0900 Subject: [PATCH 21/35] ntfs: punch all-zero compressed blocks When a rewritten compression block consists entirely of zeroes, ntfs_write_cb() returns without replacing its existing runlist mapping. The old on-disk contents therefore remain visible after cache eviction. Punch the compression unit so that reads resolve it as a sparse block and release any clusters that held the previous contents. Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/compress.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/fs/ntfs/compress.c b/fs/ntfs/compress.c index 006b8831836c..33ed0456bf7e 100644 --- a/fs/ntfs/compress.c +++ b/fs/ntfs/compress.c @@ -1279,9 +1279,10 @@ static int ntfs_write_cb(struct ntfs_inode *ni, loff_t pos, struct page **pages, int i, err; int pages_count = (round_up(ni->itype.compressed.block_size + 2 * (ni->itype.compressed.block_size / NTFS_SB_SIZE) + 2, PAGE_SIZE)) / PAGE_SIZE; + u32 cb_clusters = ni->itype.compressed.block_clusters; size_t new_rl_count; struct bio *bio = NULL; - loff_t new_length; + loff_t cb_pos, new_length; s64 new_vcn; in_mapping = vmap(pages, pages_per_cb, VM_MAP, PAGE_KERNEL_RO); @@ -1351,6 +1352,9 @@ static int ntfs_write_cb(struct ntfs_inode *ni, loff_t pos, struct page **pages, } } + cb_pos = pos & ~((loff_t)ni->itype.compressed.block_size - 1); + new_vcn = ntfs_bytes_to_cluster(vol, cb_pos); + if (!fail && !allzeroes) { outbuf[compsz++] = 0; outbuf[compsz++] = 0; @@ -1359,7 +1363,7 @@ static int ntfs_write_cb(struct ntfs_inode *ni, loff_t pos, struct page **pages, bio_size = rounded; pages = pages_disk; } else if (allzeroes) { - err = 0; + err = ntfs_non_resident_attr_punch_hole(ni, new_vcn, cb_clusters); goto out; } else { memcpy(outbuf, inbuf, insz); @@ -1367,8 +1371,6 @@ static int ntfs_write_cb(struct ntfs_inode *ni, loff_t pos, struct page **pages, pages = pages_disk; } - new_vcn = ntfs_bytes_to_cluster(vol, - pos & ~((loff_t)ni->itype.compressed.block_size - 1)); new_length = ntfs_bytes_to_cluster(vol, round_up(bio_size, vol->cluster_size)); err = ntfs_non_resident_attr_punch_hole(ni, new_vcn, ni->itype.compressed.block_clusters); From 75291ba474386706d6ce069b7bd766135153ee3f Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Mon, 20 Jul 2026 17:43:34 +0900 Subject: [PATCH 22/35] ntfs: write compressed data before replacing old clusters ntfs_write_cb() punches the old compression unit and publishes the new mapping before submitting the replacement data. An allocation or I/O failure after the punch loses the previous contents and can leave the mapping pointing at unwritten clusters. Allocate and write the replacement clusters first. Replace the runlist only after the synchronous write succeeds, and free new clusters on failure. Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/compress.c | 63 ++++++++++++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/fs/ntfs/compress.c b/fs/ntfs/compress.c index 33ed0456bf7e..5b5cd494e5d8 100644 --- a/fs/ntfs/compress.c +++ b/fs/ntfs/compress.c @@ -1373,10 +1373,6 @@ static int ntfs_write_cb(struct ntfs_inode *ni, loff_t pos, struct page **pages, new_length = ntfs_bytes_to_cluster(vol, round_up(bio_size, vol->cluster_size)); - err = ntfs_non_resident_attr_punch_hole(ni, new_vcn, ni->itype.compressed.block_clusters); - if (err < 0) - goto out; - rlc = ntfs_cluster_alloc(vol, new_vcn, new_length, -1, DATA_ZONE, false, true, true); if (IS_ERR(rlc)) { @@ -1385,28 +1381,6 @@ static int ntfs_write_cb(struct ntfs_inode *ni, loff_t pos, struct page **pages, } bio_lcn = rlc->lcn; - down_write(&ni->runlist.lock); - rl = ntfs_runlists_merge(&ni->runlist, rlc, 0, &new_rl_count); - if (IS_ERR(rl)) { - up_write(&ni->runlist.lock); - ntfs_error(vol->sb, "Failed to merge runlists"); - err = PTR_ERR(rl); - if (ntfs_cluster_free_from_rl(vol, rlc)) - ntfs_error(vol->sb, "Failed to free hot clusters."); - kvfree(rlc); - goto out; - } - - ni->runlist.count = new_rl_count; - ni->runlist.rl = rl; - - err = ntfs_attr_update_mapping_pairs(ni, 0); - up_write(&ni->runlist.lock); - if (err) { - err = -EIO; - goto out; - } - i = 0; while (bio_size > 0) { int page_size; @@ -1423,6 +1397,10 @@ setup_bio: if (!bio) { bio = bio_alloc(vol->sb->s_bdev, 1, REQ_OP_WRITE, GFP_NOIO); + if (!bio) { + err = -ENOMEM; + goto free_rlc; + } bio->bi_iter.bi_sector = ntfs_bytes_to_sector(vol, ntfs_cluster_to_bytes(vol, bio_lcn) + @@ -1433,7 +1411,7 @@ setup_bio: err = submit_bio_wait(bio); bio_put(bio); if (err) - goto out; + goto free_rlc; bio = NULL; goto setup_bio; } @@ -1442,6 +1420,37 @@ setup_bio: err = submit_bio_wait(bio); bio_put(bio); + if (err) + goto free_rlc; + + /* Do not discard the old compression block until the new one is safe. */ + err = ntfs_non_resident_attr_punch_hole(ni, new_vcn, cb_clusters); + if (err) + goto free_rlc; + + down_write(&ni->runlist.lock); + rl = ntfs_runlists_merge(&ni->runlist, rlc, 0, &new_rl_count); + if (IS_ERR(rl)) { + up_write(&ni->runlist.lock); + ntfs_error(vol->sb, "Failed to merge runlists"); + err = PTR_ERR(rl); + goto free_rlc; + } + + ni->runlist.count = new_rl_count; + ni->runlist.rl = rl; + rlc = NULL; + + err = ntfs_attr_update_mapping_pairs(ni, 0); + up_write(&ni->runlist.lock); + if (err) + err = -EIO; + goto out; + +free_rlc: + if (ntfs_cluster_free_from_rl(vol, rlc)) + ntfs_error(vol->sb, "Failed to free hot clusters."); + kvfree(rlc); out: if (outbuf) vunmap(outbuf); From 306f066a64d44b59e1a0ed11193b8ba14571b5b2 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Tue, 21 Jul 2026 11:58:08 +0900 Subject: [PATCH 23/35] ntfs: fix initialized size and page state after compressed writes The write iterator now expands attributes before calling ntfs_compress_write(), so compressed writes must not expand the attribute themselves. However, the compressed path still needs to reject zero-byte iterator copies, advance initialized_size after successful I/O, and invalidate modified folios after a failed compression-unit write. Reject no-progress copies, persist the new initialized size on success, and clear folio uptodate state when the synchronous write fails. Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/compress.c | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/fs/ntfs/compress.c b/fs/ntfs/compress.c index 5b5cd494e5d8..a993cba3b961 100644 --- a/fs/ntfs/compress.c +++ b/fs/ntfs/compress.c @@ -1549,13 +1549,26 @@ int ntfs_compress_write(struct ntfs_inode *ni, loff_t pos, size_t count, } } - err = ntfs_write_cb(ni, pos, pages, pages_per_cb, page_offset); + if (!copied) { + err = -EFAULT; + goto release_pages; + } + err = ntfs_write_cb(ni, pos, pages, pages_per_cb, page_offset); + if (!err && pos + copied > ni->initialized_size) { + mutex_lock(&ni->mrec_lock); + err = ntfs_attr_set_initialized_size(ni, pos + copied); + mutex_unlock(&ni->mrec_lock); + } + +release_pages: for (i = 0; i < pages_per_cb; i++) { folio = page_folio(pages[i]); - if (i < ip) { + if (!err) { folio_clear_dirty(folio); folio_mark_uptodate(folio); + } else { + folio_clear_uptodate(folio); } folio_unlock(folio); folio_put(folio); From a8af29cd136fc7319dc601abc03bd29abf9844d9 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Tue, 21 Jul 2026 08:44:57 +0900 Subject: [PATCH 24/35] ntfs: reuse the compression context during writes ntfs_compress_block() allocates and initializes a roughly 40 KiB match finder context for every 4 KiB sub-block. A 64 KiB compression unit thus performs sixteen large allocations even though the calls are serialized. Allocate one context for the complete write request and reset its hash chains for each sub-block as before. Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/compress.c | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/fs/ntfs/compress.c b/fs/ntfs/compress.c index a993cba3b961..f073a53f6136 100644 --- a/fs/ntfs/compress.c +++ b/fs/ntfs/compress.c @@ -1072,10 +1072,9 @@ static void ntfs_skip_position(struct compress_context *pctx, const int i) * header (minimal size is 2, maximum size is 4098) * A negative error code if an error has been met. */ -static int ntfs_compress_block(const char *inbuf, const int bufsize, - char *outbuf) +static int ntfs_compress_block(struct compress_context *pctx, + const char *inbuf, const int bufsize, char *outbuf) { - struct compress_context *pctx; int i; /* current position */ int j; /* end of best match from current position */ int k; /* end of best match from next position */ @@ -1090,10 +1089,6 @@ static int ntfs_compress_block(const char *inbuf, const int bufsize, int tag; /* current value of tag */ int ntag; /* count of bits still undefined in tag */ - pctx = kvzalloc(sizeof(struct compress_context), GFP_NOFS); - if (!pctx) - return -ENOMEM; - /* * All hash chains start as empty. The special value '-1' indicates the * end of each hash chain. @@ -1249,16 +1244,12 @@ static int ntfs_compress_block(const char *inbuf, const int bufsize, xout = NTFS_SB_SIZE + 2; } - /* - * Free the compression context and return the total number of bytes - * written to 'outbuf'. - */ - kvfree(pctx); return xout; } static int ntfs_write_cb(struct ntfs_inode *ni, loff_t pos, struct page **pages, - int pages_per_cb, unsigned int page_offset) + int pages_per_cb, unsigned int page_offset, + struct compress_context *ctx) { struct ntfs_volume *vol = ni->vol; char *outbuf = NULL, *pbuf, *inbuf, *in_mapping; @@ -1321,7 +1312,7 @@ static int ntfs_write_cb(struct ntfs_inode *ni, loff_t pos, struct page **pages, else bsz = insz - p; pbuf = &outbuf[compsz]; - sz = ntfs_compress_block(&inbuf[p], bsz, pbuf); + sz = ntfs_compress_block(ctx, &inbuf[p], bsz, pbuf); if (sz < 0) { err = sz; goto out; @@ -1472,6 +1463,7 @@ out: int ntfs_compress_write(struct ntfs_inode *ni, loff_t pos, size_t count, struct iov_iter *from) { + struct compress_context *ctx; struct folio *folio; struct page **pages = NULL, *page; int pages_per_cb; @@ -1486,6 +1478,11 @@ int ntfs_compress_write(struct ntfs_inode *ni, loff_t pos, size_t count, pages = kmalloc_array(pages_per_cb, sizeof(struct page *), GFP_NOFS); if (!pages) return -ENOMEM; + ctx = kvzalloc_obj(*ctx, GFP_NOFS); + if (!ctx) { + kfree(pages); + return -ENOMEM; + } while (count) { pgoff_t index; @@ -1554,7 +1551,7 @@ int ntfs_compress_write(struct ntfs_inode *ni, loff_t pos, size_t count, goto release_pages; } - err = ntfs_write_cb(ni, pos, pages, pages_per_cb, page_offset); + err = ntfs_write_cb(ni, pos, pages, pages_per_cb, page_offset, ctx); if (!err && pos + copied > ni->initialized_size) { mutex_lock(&ni->mrec_lock); err = ntfs_attr_set_initialized_size(ni, pos + copied); @@ -1584,6 +1581,7 @@ release_pages: } out: + kvfree(ctx); kfree(pages); if (err < 0) written = err; From e2b309a870f996de4eb7af1268fb57ac09c6d182 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Tue, 21 Jul 2026 08:45:56 +0900 Subject: [PATCH 25/35] ntfs: reuse compression output workspace across write units ntfs_write_cb() allocates output pages and creates input and output vmaps for every compression unit. Sequential writes repeatedly pay those allocation and page-table costs even though each unit has the same maximum output size. Allocate and map the output workspace once per write request. Access input sub-blocks with kmap_local_page(), and reuse the output pages and mapping for every compression unit in the request. Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/compress.c | 135 ++++++++++++++++++++++++++++----------------- 1 file changed, 85 insertions(+), 50 deletions(-) diff --git a/fs/ntfs/compress.c b/fs/ntfs/compress.c index f073a53f6136..f3c14518f78e 100644 --- a/fs/ntfs/compress.c +++ b/fs/ntfs/compress.c @@ -894,6 +894,12 @@ struct compress_context { s16 prev[NTFS_SB_SIZE]; }; +struct ntfs_compress_workspace { + struct page **pages; + char *outbuf; + unsigned int nr_pages; +}; + /* * Hash the next 3-byte sequence in the input buffer */ @@ -1247,12 +1253,69 @@ static int ntfs_compress_block(struct compress_context *pctx, return xout; } +static int ntfs_compress_workspace_init(struct ntfs_inode *ni, + struct ntfs_compress_workspace *ws) +{ + unsigned int size, i; + + size = ni->itype.compressed.block_size + 2 * + (ni->itype.compressed.block_size / NTFS_SB_SIZE) + 2; + ws->nr_pages = DIV_ROUND_UP(size, PAGE_SIZE); + ws->pages = kcalloc(ws->nr_pages, sizeof(*ws->pages), GFP_NOFS); + if (!ws->pages) + return -ENOMEM; + + for (i = 0; i < ws->nr_pages; i++) { + ws->pages[i] = alloc_page(GFP_NOFS); + if (!ws->pages[i]) + goto free_pages; + } + + ws->outbuf = vmap(ws->pages, ws->nr_pages, VM_MAP, PAGE_KERNEL); + if (!ws->outbuf) + goto free_pages; + return 0; + +free_pages: + while (i) + put_page(ws->pages[--i]); + kfree(ws->pages); + return -ENOMEM; +} + +static void ntfs_compress_workspace_free(struct ntfs_compress_workspace *ws) +{ + unsigned int i; + + vunmap(ws->outbuf); + for (i = 0; i < ws->nr_pages; i++) + put_page(ws->pages[i]); + kfree(ws->pages); +} + +static void ntfs_copy_cb(struct page **pages, int pages_per_cb, + unsigned int page_offset, + struct ntfs_compress_workspace *ws, unsigned int bytes) +{ + unsigned int copied = 0, i; + + for (i = 0; i < pages_per_cb && copied < bytes; i++) { + unsigned int offset = i ? 0 : page_offset; + unsigned int len = min(bytes - copied, PAGE_SIZE - offset); + void *addr = kmap_local_page(pages[i]); + + memcpy(ws->outbuf + copied, addr + offset, len); + kunmap_local(addr); + copied += len; + } +} + static int ntfs_write_cb(struct ntfs_inode *ni, loff_t pos, struct page **pages, int pages_per_cb, unsigned int page_offset, - struct compress_context *ctx) + struct compress_context *ctx, struct ntfs_compress_workspace *ws) { struct ntfs_volume *vol = ni->vol; - char *outbuf = NULL, *pbuf, *inbuf, *in_mapping; + char *outbuf = ws->outbuf, *pbuf; u32 compsz, p, insz = ni->itype.compressed.block_size; s32 rounded, bio_size; int sz; @@ -1264,55 +1327,32 @@ static int ntfs_write_cb(struct ntfs_inode *ni, loff_t pos, struct page **pages, static char twozeroes[] = {0x02, 0xb0, 0x00, 0x00, 0x00}; /* more compressed zeroes, to be followed by some count */ static char morezeroes[] = {0x03, 0xb0, 0x02, 0x00}; - struct page **pages_disk = NULL, *pg; s64 bio_lcn; struct runlist_element *rlc, *rl; int i, err; - int pages_count = (round_up(ni->itype.compressed.block_size + 2 * - (ni->itype.compressed.block_size / NTFS_SB_SIZE) + 2, PAGE_SIZE)) / PAGE_SIZE; u32 cb_clusters = ni->itype.compressed.block_clusters; size_t new_rl_count; struct bio *bio = NULL; loff_t cb_pos, new_length; s64 new_vcn; - in_mapping = vmap(pages, pages_per_cb, VM_MAP, PAGE_KERNEL_RO); - if (!in_mapping) - return -ENOMEM; - inbuf = in_mapping + page_offset; - - /* may need 2 extra bytes per block and 2 more bytes */ - pages_disk = kcalloc(pages_count, sizeof(struct page *), GFP_NOFS); - if (!pages_disk) { - vunmap(in_mapping); - return -ENOMEM; - } - - for (i = 0; i < pages_count; i++) { - pg = alloc_page(GFP_KERNEL); - if (!pg) { - err = -ENOMEM; - goto out; - } - pages_disk[i] = pg; - lock_page(pg); - } - - outbuf = vmap(pages_disk, pages_count, VM_MAP, PAGE_KERNEL); - if (!outbuf) { - err = -ENOMEM; - goto out; - } - compsz = 0; allzeroes = true; for (p = 0; (p < insz) && !fail; p += NTFS_SB_SIZE) { + unsigned int input_offset = page_offset + p; + unsigned int page_idx = input_offset >> PAGE_SHIFT; + const char *input; + void *addr; + if ((p + NTFS_SB_SIZE) < insz) bsz = NTFS_SB_SIZE; else bsz = insz - p; pbuf = &outbuf[compsz]; - sz = ntfs_compress_block(ctx, &inbuf[p], bsz, pbuf); + addr = kmap_local_page(pages[page_idx]); + input = addr + offset_in_page(input_offset); + sz = ntfs_compress_block(ctx, input, bsz, pbuf); + kunmap_local(addr); if (sz < 0) { err = sz; goto out; @@ -1352,14 +1392,12 @@ static int ntfs_write_cb(struct ntfs_inode *ni, loff_t pos, struct page **pages, rounded = ((compsz - 1) | (vol->cluster_size - 1)) + 1; memset(&outbuf[compsz], 0, rounded - compsz); bio_size = rounded; - pages = pages_disk; } else if (allzeroes) { err = ntfs_non_resident_attr_punch_hole(ni, new_vcn, cb_clusters); goto out; } else { - memcpy(outbuf, inbuf, insz); + ntfs_copy_cb(pages, pages_per_cb, page_offset, ws, insz); bio_size = insz; - pages = pages_disk; } new_length = ntfs_bytes_to_cluster(vol, round_up(bio_size, vol->cluster_size)); @@ -1398,7 +1436,7 @@ setup_bio: ((s64)i << PAGE_SHIFT)); } - if (!bio_add_page(bio, pages[i], page_size, 0)) { + if (!bio_add_page(bio, ws->pages[i], page_size, 0)) { err = submit_bio_wait(bio); bio_put(bio); if (err) @@ -1443,17 +1481,6 @@ free_rlc: ntfs_error(vol->sb, "Failed to free hot clusters."); kvfree(rlc); out: - if (outbuf) - vunmap(outbuf); - for (i = 0; i < pages_count; i++) { - pg = pages_disk[i]; - if (pg) { - unlock_page(pg); - put_page(pg); - } - } - kfree(pages_disk); - vunmap(in_mapping); NInoSetFileNameDirty(ni); mark_mft_record_dirty(ni); @@ -1463,6 +1490,7 @@ out: int ntfs_compress_write(struct ntfs_inode *ni, loff_t pos, size_t count, struct iov_iter *from) { + struct ntfs_compress_workspace ws = {}; struct compress_context *ctx; struct folio *folio; struct page **pages = NULL, *page; @@ -1483,6 +1511,12 @@ int ntfs_compress_write(struct ntfs_inode *ni, loff_t pos, size_t count, kfree(pages); return -ENOMEM; } + err = ntfs_compress_workspace_init(ni, &ws); + if (err) { + kvfree(ctx); + kfree(pages); + return err; + } while (count) { pgoff_t index; @@ -1551,7 +1585,7 @@ int ntfs_compress_write(struct ntfs_inode *ni, loff_t pos, size_t count, goto release_pages; } - err = ntfs_write_cb(ni, pos, pages, pages_per_cb, page_offset, ctx); + err = ntfs_write_cb(ni, pos, pages, pages_per_cb, page_offset, ctx, &ws); if (!err && pos + copied > ni->initialized_size) { mutex_lock(&ni->mrec_lock); err = ntfs_attr_set_initialized_size(ni, pos + copied); @@ -1581,6 +1615,7 @@ release_pages: } out: + ntfs_compress_workspace_free(&ws); kvfree(ctx); kfree(pages); if (err < 0) From 512a7a631e43cd1cdd3e3872ac6e3350bb0c1551 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Tue, 21 Jul 2026 16:03:01 +0900 Subject: [PATCH 26/35] ntfs: submit one bio per compressed write unit ntfs_write_cb() allocates a single-vector bio and synchronously submits it whenever another output page cannot be added. A 64 KiB uncompressed unit therefore requires up to sixteen separate bio submissions. Allocate enough vectors for the complete unit, add all output pages, and perform one synchronous submission. Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/compress.c | 43 +++++++++++-------------------------------- 1 file changed, 11 insertions(+), 32 deletions(-) diff --git a/fs/ntfs/compress.c b/fs/ntfs/compress.c index f3c14518f78e..a3af669b1008 100644 --- a/fs/ntfs/compress.c +++ b/fs/ntfs/compress.c @@ -1327,7 +1327,7 @@ static int ntfs_write_cb(struct ntfs_inode *ni, loff_t pos, struct page **pages, static char twozeroes[] = {0x02, 0xb0, 0x00, 0x00, 0x00}; /* more compressed zeroes, to be followed by some count */ static char morezeroes[] = {0x03, 0xb0, 0x02, 0x00}; - s64 bio_lcn; + s64 bio_lcn, bio_pos; struct runlist_element *rlc, *rl; int i, err; u32 cb_clusters = ni->itype.compressed.block_clusters; @@ -1410,41 +1410,20 @@ static int ntfs_write_cb(struct ntfs_inode *ni, loff_t pos, struct page **pages, } bio_lcn = rlc->lcn; - i = 0; - while (bio_size > 0) { - int page_size; + bio_pos = ntfs_cluster_to_bytes(vol, bio_lcn); + bio = bio_alloc(vol->sb->s_bdev, DIV_ROUND_UP(bio_size, PAGE_SIZE), + REQ_OP_WRITE, GFP_NOIO); + bio->bi_iter.bi_sector = ntfs_bytes_to_sector(vol, bio_pos); - if (bio_size >= PAGE_SIZE) { - page_size = PAGE_SIZE; - bio_size -= PAGE_SIZE; - } else { - page_size = bio_size; - bio_size = 0; - } + for (i = 0; bio_size; i++) { + unsigned int len = min_t(unsigned int, bio_size, PAGE_SIZE); -setup_bio: - if (!bio) { - bio = bio_alloc(vol->sb->s_bdev, 1, REQ_OP_WRITE, - GFP_NOIO); - if (!bio) { - err = -ENOMEM; - goto free_rlc; - } - bio->bi_iter.bi_sector = - ntfs_bytes_to_sector(vol, - ntfs_cluster_to_bytes(vol, bio_lcn) + - ((s64)i << PAGE_SHIFT)); - } - - if (!bio_add_page(bio, ws->pages[i], page_size, 0)) { - err = submit_bio_wait(bio); + if (bio_add_page(bio, ws->pages[i], len, 0) != len) { + err = -EIO; bio_put(bio); - if (err) - goto free_rlc; - bio = NULL; - goto setup_bio; + goto free_rlc; } - i++; + bio_size -= len; } err = submit_bio_wait(bio); From b4a21e6ddb06021c95975cbfd4c21c68d0ae5d84 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Tue, 21 Jul 2026 08:46:33 +0900 Subject: [PATCH 27/35] ntfs: skip reads for full compression unit overwrites ntfs_compress_write() reads every page in a compression unit before copying new data into it. The read is unnecessary when an aligned write replaces every byte covered by the page-cache folios. Detect full page-aligned compression unit overwrites and grab locked cache folios without reading them. Keep the read-modify-write path for partial units and units that cover only part of a large page. Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/compress.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/fs/ntfs/compress.c b/fs/ntfs/compress.c index a3af669b1008..ea29fade9b9b 100644 --- a/fs/ntfs/compress.c +++ b/fs/ntfs/compress.c @@ -1501,6 +1501,7 @@ int ntfs_compress_write(struct ntfs_inode *ni, loff_t pos, size_t count, pgoff_t index; size_t copied, bytes; unsigned int page_offset; + bool full_cb; int off; off = pos & (cb_size - 1); @@ -1512,6 +1513,8 @@ int ntfs_compress_write(struct ntfs_inode *ni, loff_t pos, size_t count, page_offset = offset_in_page(cb_off); pages_per_cb = DIV_ROUND_UP(page_offset + cb_size, PAGE_SIZE); index = cb_off >> PAGE_SHIFT; + full_cb = !off && bytes == cb_size && !page_offset && + !(cb_size & (PAGE_SIZE - 1)); if (unlikely(fault_in_iov_iter_readable(from, bytes))) { err = -EFAULT; @@ -1519,7 +1522,10 @@ int ntfs_compress_write(struct ntfs_inode *ni, loff_t pos, size_t count, } for (i = 0; i < pages_per_cb; i++) { - folio = read_mapping_folio(mapping, index + i, NULL); + if (full_cb) + folio = filemap_grab_folio(mapping, index + i); + else + folio = read_mapping_folio(mapping, index + i, NULL); if (IS_ERR(folio)) { for (ip = 0; ip < i; ip++) { folio_unlock(page_folio(pages[ip])); @@ -1529,7 +1535,8 @@ int ntfs_compress_write(struct ntfs_inode *ni, loff_t pos, size_t count, goto out; } - folio_lock(folio); + if (!full_cb) + folio_lock(folio); pages[i] = folio_page(folio, 0); } From e791930240a548be33ece8bf7515dcb1177a354a Mon Sep 17 00:00:00 2001 From: Hyunchul Lee Date: Tue, 21 Jul 2026 15:57:11 +0900 Subject: [PATCH 28/35] ntfs: fix resident conversion in ntfs_new_attr_flags When setting sparse/compressed flags on a resident attribute, the function skipped the resident-to-non-resident conversion and terminated. Signed-off-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/ea.c | 94 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 72 insertions(+), 22 deletions(-) diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c index d0044c61152a..95587b9a7129 100644 --- a/fs/ntfs/ea.c +++ b/fs/ntfs/ea.c @@ -630,6 +630,7 @@ static int ntfs_new_attr_flags(struct ntfs_inode *ni, __le32 fattr) struct mft_record *m; struct attr_record *a; __le16 new_aflags; + u16 old_name_ofs, old_mp_ofs; int mp_size, mp_ofs, name_ofs, arec_size, err; m = map_mft_record(ni); @@ -662,8 +663,10 @@ static int ntfs_new_attr_flags(struct ntfs_inode *ni, __le32 fattr) else new_aflags &= ~ATTR_IS_COMPRESSED; - if (new_aflags == a->flags) - return 0; + if (new_aflags == a->flags) { + err = 0; + goto err_out; + } if ((new_aflags & (ATTR_IS_SPARSE | ATTR_IS_COMPRESSED)) == (ATTR_IS_SPARSE | ATTR_IS_COMPRESSED)) { @@ -672,15 +675,40 @@ static int ntfs_new_attr_flags(struct ntfs_inode *ni, __le32 fattr) goto err_out; } - if (!a->non_resident) - goto out; + if (!a->non_resident) { + if (!(new_aflags & (ATTR_IS_SPARSE | ATTR_IS_COMPRESSED))) + return 0; - if (a->data.non_resident.data_size) { - pr_err("Can't change sparsed/compressed for non-empty file\n"); - err = -EOPNOTSUPP; - goto err_out; + if (le32_to_cpu(a->data.resident.value_length)) { + pr_err("Can't change sparse/compressed for non-empty file"); + err = -EOPNOTSUPP; + goto err_out; + } + + err = ntfs_attr_make_non_resident(ni, 0); + if (err) + goto err_out; + + ntfs_attr_reinit_search_ctx(ctx); + err = ntfs_attr_lookup(ni->type, ni->name, + ni->name_len, CASE_SENSITIVE, + 0, NULL, 0, ctx); + if (err) { + err = -EINVAL; + goto err_out; + } + a = ctx->attr; + } else { + if (a->data.non_resident.data_size) { + pr_err("Can't change sparsed/compressed for non-empty file"); + err = -EOPNOTSUPP; + goto err_out; + } } + old_name_ofs = le16_to_cpu(a->name_offset); + old_mp_ofs = le16_to_cpu(a->data.non_resident.mapping_pairs_offset); + if (new_aflags & (ATTR_IS_SPARSE | ATTR_IS_COMPRESSED)) name_ofs = (offsetof(struct attr_record, data.non_resident.compressed_size) + @@ -703,6 +731,25 @@ static int ntfs_new_attr_flags(struct ntfs_inode *ni, __le32 fattr) if (unlikely(err)) goto err_out; + /* + * When compressed/sparse state changes, the non-resident header grows or + * shrinks by the compressed_size field. Update the in-record payload layout + * to match the new offsets before exposing the new mapping_pairs_offset. + */ + if (name_ofs > old_name_ofs) { + if (mp_ofs != old_mp_ofs) + memmove((u8 *)a + mp_ofs, (u8 *)a + old_mp_ofs, mp_size); + if (a->name_length) + memmove((u8 *)a + name_ofs, (u8 *)a + old_name_ofs, + a->name_length * sizeof(__le16)); + } else { + if (a->name_length && name_ofs != old_name_ofs) + memmove((u8 *)a + name_ofs, (u8 *)a + old_name_ofs, + a->name_length * sizeof(__le16)); + if (mp_ofs != old_mp_ofs) + memmove((u8 *)a + mp_ofs, (u8 *)a + old_mp_ofs, mp_size); + } + if (new_aflags & (ATTR_IS_SPARSE | ATTR_IS_COMPRESSED)) { a->data.non_resident.compression_unit = 0; if (new_aflags & ATTR_IS_COMPRESSED || ni->vol->major_ver < 3) @@ -723,28 +770,31 @@ static int ntfs_new_attr_flags(struct ntfs_inode *ni, __le32 fattr) ni->itype.compressed.block_size_bits = 0; ni->itype.compressed.block_clusters = 0; } - - if (new_aflags & ATTR_IS_SPARSE) { - NInoSetSparse(ni); - ni->flags |= FILE_ATTR_SPARSE_FILE; - } - - if (new_aflags & ATTR_IS_COMPRESSED) { - NInoSetCompressed(ni); - ni->flags |= FILE_ATTR_COMPRESSED; - } } else { - ni->flags &= ~(FILE_ATTR_SPARSE_FILE | FILE_ATTR_COMPRESSED); a->data.non_resident.compression_unit = 0; - NInoClearSparse(ni); - NInoClearCompressed(ni); } a->name_offset = cpu_to_le16(name_ofs); a->data.non_resident.mapping_pairs_offset = cpu_to_le16(mp_ofs); -out: a->flags = new_aflags; + + if (new_aflags & ATTR_IS_SPARSE) { + NInoSetSparse(ni); + ni->flags |= FILE_ATTR_SPARSE_FILE; + } else { + NInoClearSparse(ni); + ni->flags &= ~FILE_ATTR_SPARSE_FILE; + } + + if (new_aflags & ATTR_IS_COMPRESSED) { + NInoSetCompressed(ni); + ni->flags |= FILE_ATTR_COMPRESSED; + } else { + NInoClearCompressed(ni); + ni->flags &= ~FILE_ATTR_COMPRESSED; + } + mark_mft_record_dirty(ctx->ntfs_ino); err_out: if (ctx) From 7cc56fea68daea52983a863473b6100869e6c5a9 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Tue, 21 Jul 2026 15:58:08 +0900 Subject: [PATCH 29/35] ntfs: move attribute payload before shrinking its record ntfs_new_attr_flags() resizes the non-resident attribute record before moving its name and mapping pairs to their shorter-header offsets when compression or sparse state is cleared. Shrinking the record first moves the following attribute over the tail of the old record. The subsequent memmove() therefore copies bytes from that following attribute instead of the old mapping pairs. Re-enabling compression on an empty file persists those bytes as a malformed mapping pairs array, which ntfsck reports as a missing or invalid run length. Move the payload before shrinking the record, while retaining the existing resize-before-move ordering when growing it. Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/ea.c | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c index 95587b9a7129..4fb10d51211c 100644 --- a/fs/ntfs/ea.c +++ b/fs/ntfs/ea.c @@ -631,7 +631,7 @@ static int ntfs_new_attr_flags(struct ntfs_inode *ni, __le32 fattr) struct attr_record *a; __le16 new_aflags; u16 old_name_ofs, old_mp_ofs; - int mp_size, mp_ofs, name_ofs, arec_size, err; + int mp_size, mp_ofs, name_ofs, old_arec_size, arec_size, err; m = map_mft_record(ni); if (IS_ERR(m)) @@ -726,6 +726,19 @@ static int ntfs_new_attr_flags(struct ntfs_inode *ni, __le32 fattr) mp_ofs = (name_ofs + a->name_length * sizeof(__le16) + 7) & ~7; arec_size = (mp_ofs + mp_size + 7) & ~7; + old_arec_size = le32_to_cpu(a->length); + + /* + * Move payloads before shrinking the record. Otherwise resizing moves + * the following attribute over the old payload before it can be copied. + */ + if (arec_size < old_arec_size) { + if (a->name_length && name_ofs != old_name_ofs) + memmove((u8 *)a + name_ofs, (u8 *)a + old_name_ofs, + a->name_length * sizeof(__le16)); + if (mp_ofs != old_mp_ofs) + memmove((u8 *)a + mp_ofs, (u8 *)a + old_mp_ofs, mp_size); + } err = ntfs_attr_record_resize(m, a, arec_size); if (unlikely(err)) @@ -736,18 +749,12 @@ static int ntfs_new_attr_flags(struct ntfs_inode *ni, __le32 fattr) * shrinks by the compressed_size field. Update the in-record payload layout * to match the new offsets before exposing the new mapping_pairs_offset. */ - if (name_ofs > old_name_ofs) { + if (arec_size > old_arec_size) { if (mp_ofs != old_mp_ofs) memmove((u8 *)a + mp_ofs, (u8 *)a + old_mp_ofs, mp_size); if (a->name_length) memmove((u8 *)a + name_ofs, (u8 *)a + old_name_ofs, a->name_length * sizeof(__le16)); - } else { - if (a->name_length && name_ofs != old_name_ofs) - memmove((u8 *)a + name_ofs, (u8 *)a + old_name_ofs, - a->name_length * sizeof(__le16)); - if (mp_ofs != old_mp_ofs) - memmove((u8 *)a + mp_ofs, (u8 *)a + old_mp_ofs, mp_size); } if (new_aflags & (ATTR_IS_SPARSE | ATTR_IS_COMPRESSED)) { From 5b4020fbd223c5d84908b12ae5f6def4fd8ac162 Mon Sep 17 00:00:00 2001 From: Alexandro Calo Date: Fri, 17 Jul 2026 12:14:51 +0200 Subject: [PATCH 30/35] ntfs: Fix index_root heap OOB write in ntfs_ir_to_ib() ntfs_ir_to_ib copies all entries from index_root into a freshly allocated index_block_size-byte buffer without verifying that the entries fit in the available space. The entries in index_root may be larger than the usable entry space in the index block. This can cause OOB writes past the end of the allocation. The validator ntfs_index_root_inconsistent() checks that entries are self-consistent within the IR value, but never cross-checks them against index_block_size. There is no bounds check in ntfs_ir_to_ib() before the memcpy. Fixing this at the sink in ntfs_ir_to_ib() since ntfs_index_root_inconsistent() validates the logical consistency of index_root as a structure and a root with large entries is a structurally valid root. The bug is a size conflict of ntfs_ir_to_ib(). Also, the validator is called once per inode load in ntfs_read_locked_inode() while ntfs_ir_to_ib() is only called during a reparent, a check there adds no overhead to the common path. Moreover, even a future call path that bypasses the validator would still be protected. With NULL as first parameter of ntfs_error(), the volume error flag is never set by this call, so the device name will be absent from the error message. In any case, that the caller, ntfs_ir_reparent(), prints an error message that includes the device name on NULL returns. I think this is the best solution available without adding 'struct super_block *sb' as a parameter to ntfs_ir_to_ib(). This heap out-of-bounds write is triggered by a crafted filesystem image, which is not in the kernel threat model, anyway, fixing memory errors would be nice to keep things secure. Fixes: 0a8ac0c1fa0b ("ntfs: update directory operations") Signed-off-by: Alexandro Calo Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/index.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/fs/ntfs/index.c b/fs/ntfs/index.c index faa7ee920a3a..409759eab55d 100644 --- a/fs/ntfs/index.c +++ b/fs/ntfs/index.c @@ -1112,6 +1112,7 @@ static struct index_block *ntfs_ir_to_ib(struct index_root *ir, s64 ib_vcn) struct index_entry *ie_last; char *ies_start, *ies_end; int i; + u32 ib_cap; ntfs_debug("Entering\n"); @@ -1127,6 +1128,16 @@ static struct index_block *ntfs_ir_to_ib(struct index_root *ir, s64 ib_vcn) * as well, which can never have any data. */ i = (char *)ie_last - ies_start + le16_to_cpu(ie_last->length); + + /* Entries must fit in the allocated index block */ + ib_cap = le32_to_cpu(ib->index.allocated_size) - + le32_to_cpu(ib->index.entries_offset); + if ((u32)i > ib_cap) { + ntfs_error(NULL, "Entries (%d B) exceed IB capacity", i); + kvfree(ib); + return NULL; + } + memcpy(ntfs_ie_get_first(&ib->index), ies_start, i); ib->index.flags = ir->index.flags; From 8e38918c249693eab832cb7c447ec7f7517843d9 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sat, 25 Jul 2026 12:00:00 +0900 Subject: [PATCH 31/35] ntfs: apply Windows name checks only with windows_names The windows_names mount option is documented to reject names containing characters forbidden by Windows. However, ntfs_check_bad_windows_name() unconditionally rejects those characters before checking the mount option. Move the character validation after the option check so a default NTFS mount accepts POSIX names such as names containing ':'. Mounts using windows_names retain the existing Windows-compatible validation, including reserved device names and trailing spaces or dots. Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/namei.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fs/ntfs/namei.c b/fs/ntfs/namei.c index cd403b1d99ee..aee7aabaf059 100644 --- a/fs/ntfs/namei.c +++ b/fs/ntfs/namei.c @@ -61,12 +61,12 @@ static int ntfs_check_bad_windows_name(struct ntfs_volume *vol, const __le16 *wc, unsigned int wc_len) { - if (ntfs_check_bad_char(wc, wc_len)) - return -EINVAL; - if (!NVolCheckWindowsNames(vol)) return 0; + if (ntfs_check_bad_char(wc, wc_len)) + return -EINVAL; + /* Check for trailing space or dot. */ if (wc_len > 0 && (wc[wc_len - 1] == cpu_to_le16(' ') || From 5d89971138816e5d8b47e4957c847cbb2c905387 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Mon, 27 Jul 2026 18:07:15 +0900 Subject: [PATCH 32/35] ntfs: respect per-file chmod mode over mount masks fmask and dmask provide the default permissions for files without WSL metadata. Once chmod stores a mode in $LXMOD, however, that per-file mode must take precedence so selected files can retain permissions such as execute across remounts. Record whether $LXMOD was found while loading an inode and apply the mount masks only when it is absent. Do not remask the in-memory mode after setattr persists it. Continue loading $LXMOD even when optional $LXUID or $LXGID metadata is missing, since chmod may create only $LXMOD. Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/ea.c | 20 +++++++++----------- fs/ntfs/ea.h | 3 ++- fs/ntfs/file.c | 10 ++++------ fs/ntfs/inode.c | 15 +++++++++------ fs/ntfs/namei.c | 4 ---- 5 files changed, 24 insertions(+), 28 deletions(-) diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c index 4fb10d51211c..4fbea76afe7e 100644 --- a/fs/ntfs/ea.c +++ b/fs/ntfs/ea.c @@ -406,37 +406,35 @@ out: * Check for the presence of an EA "$LXDEV" (used by WSL) * and return its value as a device address */ -int ntfs_ea_get_wsl_inode(struct inode *inode, dev_t *rdevp, unsigned int flags) +int ntfs_ea_get_wsl_inode(struct inode *inode, dev_t *rdevp, unsigned int flags, + bool *has_lxmod) { int err; __le32 v; + *has_lxmod = false; + if (!(flags & NTFS_VOL_UID)) { /* Load uid to lxuid EA */ err = ntfs_get_ea(inode, "$LXUID", sizeof("$LXUID") - 1, &v, sizeof(v)); - if (err < 0) - return err; - if (err != sizeof(v)) - return -EIO; - i_uid_write(inode, le32_to_cpu(v)); + if (err == sizeof(v)) + i_uid_write(inode, le32_to_cpu(v)); } if (!(flags & NTFS_VOL_GID)) { /* Load gid to lxgid EA */ err = ntfs_get_ea(inode, "$LXGID", sizeof("$LXGID") - 1, &v, sizeof(v)); - if (err < 0) - return err; - if (err != sizeof(v)) - return -EIO; - i_gid_write(inode, le32_to_cpu(v)); + if (err == sizeof(v)) + i_gid_write(inode, le32_to_cpu(v)); } /* Load mode to lxmod EA */ err = ntfs_get_ea(inode, "$LXMOD", sizeof("$LXMOD") - 1, &v, sizeof(v)); if (err == sizeof(v)) { inode->i_mode = le32_to_cpu(v); + *has_lxmod = true; } else { /* Everyone gets all permissions. */ inode->i_mode |= 0777; diff --git a/fs/ntfs/ea.h b/fs/ntfs/ea.h index 1f63bd55e057..acb39c2a6fbc 100644 --- a/fs/ntfs/ea.h +++ b/fs/ntfs/ea.h @@ -10,7 +10,8 @@ extern const struct xattr_handler *const ntfs_xattr_handlers[]; int ntfs_ea_set_wsl_not_symlink(struct ntfs_inode *ni, mode_t mode, dev_t dev); -int ntfs_ea_get_wsl_inode(struct inode *inode, dev_t *rdevp, unsigned int flags); +int ntfs_ea_get_wsl_inode(struct inode *inode, dev_t *rdevp, unsigned int flags, + bool *has_lxmod); int ntfs_ea_set_wsl_inode(struct inode *inode, dev_t rdev, __le16 *ea_size, unsigned int flags); ssize_t ntfs_listxattr(struct dentry *dentry, char *buffer, size_t size); diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c index c8e49f83fd92..d4282822b3ce 100644 --- a/fs/ntfs/file.c +++ b/fs/ntfs/file.c @@ -342,14 +342,12 @@ int ntfs_setattr(struct mnt_idmap *idmap, struct dentry *dentry, if (ia_valid & ATTR_MODE) flags |= NTFS_EA_MODE; - if (S_ISDIR(vi->i_mode)) - vi->i_mode &= ~vol->dmask; - else - vi->i_mode &= ~vol->fmask; - mutex_lock(&ni->mrec_lock); - ntfs_ea_set_wsl_inode(vi, 0, NULL, flags); + err = ntfs_ea_set_wsl_inode(vi, 0, NULL, flags); mutex_unlock(&ni->mrec_lock); + if (err) + goto out; + } mark_inode_dirty(vi); diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c index 50e244aa372e..39c7fd8c1149 100644 --- a/fs/ntfs/inode.c +++ b/fs/ntfs/inode.c @@ -682,6 +682,7 @@ static int ntfs_read_locked_inode(struct inode *vi) unsigned int name_len = 4, flags = 0; int extend_sys = 0; dev_t dev = 0; + bool has_lxmod = false; bool vol_err = true; ntfs_debug("Entering for i_ino 0x%llx.", ni->mft_no); @@ -862,7 +863,7 @@ skip_attr_list_load: err = ntfs_attr_lookup(AT_EA_INFORMATION, NULL, 0, 0, 0, NULL, 0, ctx); if (!err) { NInoSetHasEA(ni); - ntfs_ea_get_wsl_inode(vi, &dev, flags); + ntfs_ea_get_wsl_inode(vi, &dev, flags, &has_lxmod); } if (ni->flags & FILE_ATTR_REPARSE_POINT) { @@ -886,16 +887,18 @@ skip_attr_list_load: if (S_ISDIR(vi->i_mode)) { /* - * Apply the directory permissions mask set in the mount - * options. + * Apply the directory permissions mask set in the mount options + * when no per-file WSL mode is present. */ - vi->i_mode &= ~vol->dmask; + if (!has_lxmod) + vi->i_mode &= ~vol->dmask; /* Things break without this kludge! */ if (vi->i_nlink > 1) set_nlink(vi, 1); } else { - /* Apply the file permissions mask set in the mount options. */ - vi->i_mode &= ~vol->fmask; + /* Apply the file permissions mask when no WSL mode is present. */ + if (!has_lxmod) + vi->i_mode &= ~vol->fmask; } /* diff --git a/fs/ntfs/namei.c b/fs/ntfs/namei.c index aee7aabaf059..96045face63f 100644 --- a/fs/ntfs/namei.c +++ b/fs/ntfs/namei.c @@ -424,8 +424,6 @@ static struct ntfs_inode *__ntfs_create(struct mnt_idmap *idmap, struct inode *d * directories, also setup the index values to the defaults. */ if (S_ISDIR(mode)) { - mode &= ~vol->dmask; - NInoSetMstProtected(ni); ni->itype.index.block_size = 4096; ni->itype.index.block_size_bits = ntfs_ffs(4096) - 1; @@ -439,8 +437,6 @@ static struct ntfs_inode *__ntfs_create(struct mnt_idmap *idmap, struct inode *d ni->itype.index.vcn_size_bits = vol->sector_size_bits; } - } else { - mode &= ~vol->fmask; } if (IS_RDONLY(vi)) From b4be3a47f8ba4dc0e9de706e2159eac6a05342b4 Mon Sep 17 00:00:00 2001 From: Bryam Vargas Date: Sun, 26 Jul 2026 18:29:11 -0500 Subject: [PATCH 33/35] ntfs: bound the free-cluster bitmap scan to the volume vol->lcn_empty_bits_per_page is sized from vol->nr_clusters at mount, but ntfs_cluster_alloc() bounds its scan of that array by the size of $Bitmap. Those are independent on-disk quantities and the mount-time check only rejects a $Bitmap that is too small, so an image whose $Bitmap covers more clusters than the volume has lets the scan index past the array. A run whose LCN lies in that gap takes the allocator straight there, since the caller passes the file's own last LCN as its locality hint. KASAN reports a slab out-of-bounds read when a file on such a volume is extended. Clamp the scan to what that array covers, mirroring the max_index calculation the mount-time scan already uses, and reject a decoded LCN at or beyond nr_clusters in the mapping pairs decoder. Conforming volumes are unaffected. Fixes: 11ccc9107dc4 ("ntfs: update runlist handling and cluster allocator") Cc: stable@vger.kernel.org Signed-off-by: Bryam Vargas Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/lcnalloc.c | 7 ++++++- fs/ntfs/runlist.c | 7 +++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/fs/ntfs/lcnalloc.c b/fs/ntfs/lcnalloc.c index 835a041023a2..aa2e017a4384 100644 --- a/fs/ntfs/lcnalloc.c +++ b/fs/ntfs/lcnalloc.c @@ -298,7 +298,12 @@ struct runlist_element *ntfs_cluster_alloc(struct ntfs_volume *vol, const s64 st clusters = count; rlpos = rlsize = 0; mapping = lcnbmp_vi->i_mapping; - i_size = i_size_read(lcnbmp_vi); + /* + * lcn_empty_bits_per_page is sized from nr_clusters, but $Bitmap can + * cover more clusters than that; bound the scan by the array. + */ + i_size = min_t(s64, i_size_read(lcnbmp_vi), + ((s64)vol->nr_clusters + 7) >> 3); while (1) { ntfs_debug("Start of outer while loop: done_zones 0x%x, search_zone %i, pass %i, zone_start 0x%llx, zone_end 0x%llx, bmp_initial_pos 0x%llx, bmp_pos 0x%llx, rlpos %i, rlsize %i.", done_zones, search_zone, pass, diff --git a/fs/ntfs/runlist.c b/fs/ntfs/runlist.c index 8e0fd400e7f7..17eb275a21ff 100644 --- a/fs/ntfs/runlist.c +++ b/fs/ntfs/runlist.c @@ -884,6 +884,13 @@ struct runlist_element *ntfs_mapping_pairs_decompress(const struct ntfs_volume * ntfs_error(vol->sb, "lcn == -1"); } #endif + /* Check lcn is within the volume. */ + if (unlikely(lcn >= (s64)vol->nr_clusters)) { + ntfs_error(vol->sb, + "LCN >= nr_clusters in mapping pairs array."); + goto err_out; + } + /* Check lcn is not below -1. */ if (unlikely(lcn < -1)) { ntfs_error(vol->sb, "Invalid s64 < -1 in mapping pairs array."); From 6b022dbb947995d1471c0b214b915e747b6cbef1 Mon Sep 17 00:00:00 2001 From: Pisit Preechapramoth Date: Tue, 28 Jul 2026 21:58:58 +0900 Subject: [PATCH 34/35] ntfs: reject unprivileged writes to reserved $LX* xattrs Reject setxattr of the reserved $LXUID, $LXGID, $LXMOD and $LXDEV names from userspace unless the caller has CAP_SYS_ADMIN. Fixes: fc053f05ca28 ("ntfs: add reparse and ea operations") Signed-off-by: Pisit Preechapramoth Signed-off-by: Namjae Jeon --- fs/ntfs/ea.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c index 4fbea76afe7e..fc6cec7ce130 100644 --- a/fs/ntfs/ea.c +++ b/fs/ntfs/ea.c @@ -808,6 +808,12 @@ err_out: return err; } +static bool ntfs_is_reserved_lxattr(const char *name) +{ + return !strcmp(name, "$LXUID") || !strcmp(name, "$LXGID") || + !strcmp(name, "$LXMOD") || !strcmp(name, "$LXDEV"); +} + static int ntfs_setxattr(const struct xattr_handler *handler, struct mnt_idmap *idmap, struct dentry *unused, struct inode *inode, const char *name, const void *value, @@ -820,6 +826,9 @@ static int ntfs_setxattr(const struct xattr_handler *handler, if (NVolShutdown(ni->vol)) return -EIO; + if (ntfs_is_reserved_lxattr(name) && !capable(CAP_SYS_ADMIN)) + return -EPERM; + if (!strcmp(name, SYSTEM_DOS_ATTRIB)) { if (sizeof(u8) != size) { err = -EINVAL; From 5a46d8b2b9bfff4a47f9d20be74984b5754994e7 Mon Sep 17 00:00:00 2001 From: Dmitry Antipov Date: Mon, 27 Jul 2026 18:37:49 +0300 Subject: [PATCH 35/35] ntfs: simplify ntfs_reparse_set_native_symlink() Avoid redundant 'strlen()' and use the convenient 'strreplace()' to simplify 'ntfs_reparse_set_native_symlink()'. Signed-off-by: Dmitry Antipov Signed-off-by: Namjae Jeon --- fs/ntfs/reparse.c | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/fs/ntfs/reparse.c b/fs/ntfs/reparse.c index ced4c675d91f..0d3988992119 100644 --- a/fs/ntfs/reparse.c +++ b/fs/ntfs/reparse.c @@ -892,12 +892,7 @@ int ntfs_reparse_set_native_symlink(struct ntfs_inode *ni, err = ntfs_set_ntfs_reparse_data(ni, (char *)reparse, total_reparse_len); if (!err) { - int len = strlen(sub_name); - - for (i = 0; i < len; i++) { - if (sub_name[i] == '\\') - sub_name[i] = '/'; - } + strreplace(sub_name, '\\', '/'); ni->target = sub_name; sub_name = NULL; if (prt_sub_shared)