From 3d3de2aee17d1431694aa085039479b5679e5ad4 Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Fri, 21 Aug 2026 13:32:30 +0800 Subject: [PATCH 01/23] ntfs: return DT_UNKNOWN on inode lookup failure in readdir ntfs_reparse_tag_dt_types() returns PTR_ERR(vi) when ntfs_iget() fails, but its return type is unsigned int and the caller passes the value straight to dir_emit() as d_type. A stale or corrupt MFT reference in a directory index thus makes readdir report a garbage d_type value to userspace. Return DT_UNKNOWN on lookup failure instead. Fixes: fc053f05ca28 ("ntfs: add reparse and ea operations") Signed-off-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/reparse.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/ntfs/reparse.c b/fs/ntfs/reparse.c index 5e483a2f9060..1cc6dfe19fed 100644 --- a/fs/ntfs/reparse.c +++ b/fs/ntfs/reparse.c @@ -405,7 +405,7 @@ unsigned int ntfs_reparse_tag_dt_types(struct ntfs_volume *vol, unsigned long mr vi = ntfs_iget(vol->sb, mref); if (IS_ERR(vi)) - return PTR_ERR(vi); + return DT_UNKNOWN; reparse_attr = (struct reparse_point *)ntfs_attr_readall(NTFS_I(vi), AT_REPARSE_POINT, NULL, 0, &attr_size); From 9692b1b4fc00cf89628bc43f71729ab21f14f8d3 Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Fri, 21 Aug 2026 13:32:32 +0800 Subject: [PATCH 02/23] ntfs: propagate reparse index insertion failure update_reparse_data() ignores the return value of set_reparse_index(). When index insertion fails, the code removes the just-written reparse data as cleanup but still returns 0, so symlink(2) (and WSL special file creation) reports success while no reparse data exists on disk. When there was no previous reparse data (oldsize == 0), the failure was likewise silently ignored. Propagate the error to the caller. Fixes: fc053f05ca28 ("ntfs: add reparse and ea operations") Signed-off-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/reparse.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fs/ntfs/reparse.c b/fs/ntfs/reparse.c index 1cc6dfe19fed..1a6073e22677 100644 --- a/fs/ntfs/reparse.c +++ b/fs/ntfs/reparse.c @@ -694,8 +694,9 @@ static int update_reparse_data(struct ntfs_inode *ni, struct ntfs_index_context goto put_rp_inode; } - if (set_reparse_index(ni, xr, ((const struct reparse_point *)value)->reparse_tag) && - oldsize > 0) { + err = set_reparse_index(ni, xr, + ((const struct reparse_point *)value)->reparse_tag); + if (err && oldsize > 0) { /* * If cannot index, try to remove the reparse * data and log the error. There will be an From ada728801999e25e610091362447372f1b25dd25 Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Fri, 21 Aug 2026 13:32:33 +0800 Subject: [PATCH 03/23] ntfs: return -ERANGE for undersized xattr buffer When the value buffer passed to getxattr(2) for system.dos_attrib, system.ntfs_attrib or system.ntfs_attrib_be is smaller than the attribute value, ntfs_getxattr() returns -ENODATA, which tells userspace the attribute does not exist. The xattr API expects -ERANGE in this case, and ntfs_get_ea() in the same file already returns -ERANGE for regular EAs. Fixes: fc053f05ca28 ("ntfs: add reparse and ea operations") Signed-off-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/ea.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c index cdd306933d73..f6dbdfe6ff15 100644 --- a/fs/ntfs/ea.c +++ b/fs/ntfs/ea.c @@ -615,7 +615,7 @@ static int ntfs_getxattr(const struct xattr_handler *handler, if (!buffer) { err = sizeof(u8); } else if (size < sizeof(u8)) { - err = -ENODATA; + err = -ERANGE; } else { err = sizeof(u8); *(u8 *)buffer = (u8)(le32_to_cpu(ni->flags) & 0x3F); @@ -628,7 +628,7 @@ static int ntfs_getxattr(const struct xattr_handler *handler, if (!buffer) { err = sizeof(u32); } else if (size < sizeof(u32)) { - err = -ENODATA; + err = -ERANGE; } else { err = sizeof(u32); *(u32 *)buffer = le32_to_cpu(ni->flags); From 8efe00b098b5b3618c885d2a35a5edccfbfbec7d Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Fri, 21 Aug 2026 13:32:34 +0800 Subject: [PATCH 04/23] ntfs: preserve error code in ntfs_resident_attr_record_add() ntfs_resident_attr_record_add() collapses every failure to -EIO at its put_err_out label. This defeats the resident-to-non-resident fallback in ntfs_attr_add(), which relies on seeing -ENOSPC to convert the attribute when the MFT record has no room, and also hides -EEXIST and -ENOMEM from callers. Return the actual error code. Every path reaching the label has err set to a negative errno. Fixes: 495e90fa3348 ("ntfs: update attrib operations") Signed-off-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/attrib.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c index 60264833bb63..3663259f0b7d 100644 --- a/fs/ntfs/attrib.c +++ b/fs/ntfs/attrib.c @@ -2500,7 +2500,7 @@ int ntfs_resident_attr_record_add(struct ntfs_inode *ni, __le32 type, return offset; put_err_out: ntfs_attr_put_search_ctx(ctx); - return -EIO; + return err; } /* From ba1b61ddaa764f31b14abe1d547049682cc5824e Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Fri, 21 Aug 2026 13:32:35 +0800 Subject: [PATCH 05/23] ntfs: return real error from ntfs_non_resident_attr_record_add() ntfs_non_resident_attr_record_add() returns -1 at its put_err_out label, which callers propagate as -EPERM to userspace. Return the actual error code. Every path reaching the label has err set to a negative errno. Fixes: 495e90fa3348 ("ntfs: update attrib operations") Signed-off-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/attrib.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c index 3663259f0b7d..4dff5c3f779b 100644 --- a/fs/ntfs/attrib.c +++ b/fs/ntfs/attrib.c @@ -2639,7 +2639,7 @@ static int ntfs_non_resident_attr_record_add(struct ntfs_inode *ni, __le32 type, return offset; put_err_out: ntfs_attr_put_search_ctx(ctx); - return -1; + return err; } /* From cf06dcd572845723821b54a608fc2da995c3c8e2 Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Fri, 21 Aug 2026 13:32:36 +0800 Subject: [PATCH 06/23] ntfs: fix kmap_local leak in write_mft_record_nolock() error paths write_mft_record_nolock() maps the MFT record folio with kmap_local_folio(), but the pre_write_mst_fixup() and bio_add_folio() failure paths jump to the error label without unmapping it. kmap_local mappings are stack-ordered per task, so leaking one corrupts the nesting for any outer mapping. Unmap the folio on those error paths too. Fixes: 115380f9a2f9 ("ntfs: update mft operations") Signed-off-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/mft.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fs/ntfs/mft.c b/fs/ntfs/mft.c index 984a0827f9ac..69b007e574fc 100644 --- a/fs/ntfs/mft.c +++ b/fs/ntfs/mft.c @@ -580,7 +580,7 @@ int write_mft_record_nolock(struct ntfs_inode *ni, struct mft_record *m, int syn err = pre_write_mst_fixup((struct ntfs_record *)fixup_m, vol->mft_record_size); if (err) { ntfs_error(vol->sb, "Failed to apply mst fixups!"); - goto err_out; + goto unmap_err_out; } folio_size = vol->mft_record_size / ni->mft_lcn_count; @@ -645,6 +645,8 @@ int write_mft_record_nolock(struct ntfs_inode *ni, struct mft_record *m, int syn return 0; put_bio_out: bio_put(bio); +unmap_err_out: + kunmap_local(kaddr); err_out: /* * The caller should mark the base inode as bad so no more I/O From be9e89ccb8e52a3e4b67feeb03ebd8133091dc7e Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Fri, 21 Aug 2026 13:32:37 +0800 Subject: [PATCH 07/23] ntfs: only count successfully cleared runs when freeing clusters ntfs_cluster_free_from_rl_nolock() adds a run's length to nr_freed whenever the error bookkeeping condition is false, which includes cases where ntfs_bitmap_clear_run() actually failed - e.g. a second run failing with the same errno as an earlier one, or any failure after a non-ENOMEM error was already recorded. Since a failed ntfs_bitmap_clear_run() rolls back its partial modifications, no bits were cleared for that run, yet its length still inflates vol->free_clusters, corrupting statfs output and the allocator's free space gate. Only count runs whose bitmap clear succeeded. Fixes: 11ccc9107dc4 ("ntfs: update runlist handling and cluster allocator") Signed-off-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/lcnalloc.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fs/ntfs/lcnalloc.c b/fs/ntfs/lcnalloc.c index aa2e017a4384..795f71d26895 100644 --- a/fs/ntfs/lcnalloc.c +++ b/fs/ntfs/lcnalloc.c @@ -53,10 +53,10 @@ int ntfs_cluster_free_from_rl_nolock(struct ntfs_volume *vol, if (rl->lcn < 0) continue; err = ntfs_bitmap_clear_run(lcnbmp_vi, rl->lcn, rl->length); - if (unlikely(err && (!ret || ret == -ENOMEM) && ret != err)) - ret = err; - else + if (likely(!err)) nr_freed += rl->length; + else if (!ret || ret == -ENOMEM) + ret = err; } ntfs_inc_free_clusters(vol, nr_freed); ntfs_debug("Done."); From 5f2a22b36fe34c98f6d5e35ddb759ee53d684145 Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Fri, 21 Aug 2026 13:32:38 +0800 Subject: [PATCH 08/23] ntfs: skip free cluster decrement when rollback fails When the rollback in __ntfs_cluster_free() fails, the recursive call returns a negative errno and the subsequent ntfs_dec_free_clusters(vol, delta) subtracts that negative value, adding bogus clusters to the counter on an already-failing volume. Skip the decrement when the rollback failed. Fixes: 11ccc9107dc4 ("ntfs: update runlist handling and cluster allocator") Signed-off-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/lcnalloc.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fs/ntfs/lcnalloc.c b/fs/ntfs/lcnalloc.c index 795f71d26895..0d6cd08ee2e7 100644 --- a/fs/ntfs/lcnalloc.c +++ b/fs/ntfs/lcnalloc.c @@ -1045,8 +1045,9 @@ s64 __ntfs_cluster_free(struct ntfs_inode *ni, const s64 start_vcn, s64 count, "Failed to rollback (error %i). Leaving inconsistent metadata! Unmount and run chkdsk.", (int)delta); NVolSetErrors(vol); + } else { + ntfs_dec_free_clusters(vol, delta); } - ntfs_dec_free_clusters(vol, delta); up_write(&vol->lcnbmp_lock); memalloc_nofs_restore(memalloc_flags); ntfs_error(vol->sb, "Aborting (error %i).", err); From 0e4c839905418d55bafe571a92533a1d1ac7b0a8 Mon Sep 17 00:00:00 2001 From: Dennis Tighe Date: Sun, 23 Aug 2026 22:08:40 -0700 Subject: [PATCH 09/23] ntfs: do not mark the volume clean in sync_fs when errors were recorded ntfs_put_super() and the remount-read-only path both clear the dirty bit only when NVolErrors(vol) is false. ntfs_sync_fs() clears it unconditionally, so any sync() on a volume that recorded an error marks that volume clean. A volume without this set is then seen as not needing recovery and it does not run one, so whatever went wrong is never repaired. This change skips resetting the dirty bit when there are volume errors. Reproduced on a volume whose $MFTMirr does not match $MFT, which sets the error flag while leaving the mount read-write: after a write and a sync, the on-disk volume flags read 0x0000 with this driver and 0x0001 with the guard in place. Fixes: 6251f0b0de7d ("ntfs: update super block operations") Assisted-by: claude:claude-opus-5 Signed-off-by: Dennis Tighe Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/super.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c index 30481e5d5dd4..a4dc64fb89ed 100644 --- a/fs/ntfs/super.c +++ b/fs/ntfs/super.c @@ -1862,7 +1862,8 @@ static int ntfs_sync_fs(struct super_block *sb, int wait) return 0; /* If there are some dirty buffers in the bdev inode */ - if (ntfs_clear_volume_flags(vol, VOLUME_IS_DIRTY)) { + if (!NVolErrors(vol) && + ntfs_clear_volume_flags(vol, VOLUME_IS_DIRTY)) { ntfs_warning(sb, "Failed to clear dirty bit in volume information flags. Run chkdsk."); err = -EIO; } From 8d139e3635c86e2c97c78d55538ce0b00b6d9986 Mon Sep 17 00:00:00 2001 From: Hongling Zeng Date: Mon, 24 Aug 2026 13:41:15 +0800 Subject: [PATCH 10/23] ntfs: fix incorrect MFT record pointer passed to ntfs_attr_record_resize ntfs_new_attr_flags() passes the wrong MFT record to ntfs_attr_record_resize(). When the attribute is in an extent record, ctx->mrec points to the extent but the function receives the base record pointer m, causing incorrect size calculations in memmove. Fix by passing ctx->mrec (the actual MFT record containing the attribute) instead of m (the base MFT record) to ntfs_attr_record_resize(). Fixes: fc053f05ca28 ("ntfs: add reparse and ea operations") Cc: stable@vger.kernel.org Signed-off-by: Hongling Zeng Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/ea.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c index f6dbdfe6ff15..9f0222c172d9 100644 --- a/fs/ntfs/ea.c +++ b/fs/ntfs/ea.c @@ -764,7 +764,7 @@ static int ntfs_new_attr_flags(struct ntfs_inode *ni, __le32 fattr) memmove((u8 *)a + mp_ofs, (u8 *)a + old_mp_ofs, mp_size); } - err = ntfs_attr_record_resize(m, a, arec_size); + err = ntfs_attr_record_resize(ctx->mrec, a, arec_size); if (unlikely(err)) goto err_out; From 607a9478833db656e7ceac8e9e382fa4acfde545 Mon Sep 17 00:00:00 2001 From: Wentao Guan Date: Tue, 25 Aug 2026 13:46:59 +0800 Subject: [PATCH 11/23] ntfs: treat any nonzero dio zero-range return as an error ntfs_dio_zero_range() returns either 0 or a negative errno from blkdev_issue_zeroout(); it never returns a positive value. The zeroing failure check in ntfs_attr_fallocate() therefore never fired, so a failed zeroing operation was silently ignored: the loop kept going, the newly allocated clusters were folded into initialized_size and the write could succeed leaving stale on-disk data. Treat any nonzero return as an error and abort the allocation. Fixes: 495e90fa33482 ("ntfs: update attrib operations") Assisted-by: atomcode:deepseek-v4-flash Signed-off-by: Wentao Guan Signed-off-by: Namjae Jeon --- fs/ntfs/attrib.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c index 4dff5c3f779b..c55dc47d2261 100644 --- a/fs/ntfs/attrib.c +++ b/fs/ntfs/attrib.c @@ -5704,7 +5704,7 @@ int ntfs_attr_fallocate(struct ntfs_inode *ni, loff_t start, loff_t byte_len, bo lcn << vol->cluster_size_bits, alloc_cnt << vol->cluster_size_bits); - if (err > 0) + if (err) goto out; } From a79899ca38af4ce5518a59a22f0eb1f59f7e6089 Mon Sep 17 00:00:00 2001 From: Hongling Zeng Date: Tue, 25 Aug 2026 17:54:05 +0800 Subject: [PATCH 12/23] ntfs: fix undefined behavior in mft/index record size calculation The boot sector validation allows clusters_per_mft_record and clusters_per_index_record to range from 0xE1 (-31) to 0xF7 (-9) when interpreted as signed values. When these are used as negative shift counts in expressions like `1 << -clusters_per_mft_record`, values like 0xE1 cause `1 << 31`, which shifts into the sign bit of a 32-bit signed integer, resulting in undefined behavior. Fix by using unsigned shift (1U << ...) instead of signed shift. This prevents undefined behavior while preserving the full valid range of negative values (-31 to -9) that may appear in NTFS boot sectors. The encoding scheme uses negative values to represent record sizes smaller than cluster_size: -log2(record_size). Common values include -10 (1024 bytes) for mft_record_size and -12 (4096 bytes) for index_record_size. Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2") Cc: stable@vger.kernel.org Signed-off-by: Hongling Zeng Reviewed-by: Baolin Liu Signed-off-by: Namjae Jeon --- fs/ntfs/super.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c index a4dc64fb89ed..2df64712335a 100644 --- a/fs/ntfs/super.c +++ b/fs/ntfs/super.c @@ -695,7 +695,7 @@ static bool parse_ntfs_boot_sector(struct ntfs_volume *vol, * = -log2(mft_record_size) bytes. mft_record_size normaly is * 1024 bytes, which is encoded as 0xF6 (-10 in decimal). */ - vol->mft_record_size = 1 << -clusters_per_mft_record; + vol->mft_record_size = 1U << -clusters_per_mft_record; vol->mft_record_size_mask = vol->mft_record_size - 1; vol->mft_record_size_bits = ffs(vol->mft_record_size) - 1; ntfs_debug("vol->mft_record_size = %i (0x%x)", vol->mft_record_size, @@ -732,7 +732,7 @@ static bool parse_ntfs_boot_sector(struct ntfs_volume *vol, * index_record_size normaly equals 4096 bytes, which is * encoded as 0xF4 (-12 in decimal). */ - vol->index_record_size = 1 << -clusters_per_index_record; + vol->index_record_size = 1U << -clusters_per_index_record; vol->index_record_size_mask = vol->index_record_size - 1; vol->index_record_size_bits = ffs(vol->index_record_size) - 1; ntfs_debug("vol->index_record_size = %i (0x%x)", From c8504fc1245f5322af5fa5c325ab05f9cf792b87 Mon Sep 17 00:00:00 2001 From: Dennis Tighe Date: Tue, 25 Aug 2026 21:44:24 -0700 Subject: [PATCH 13/23] ntfs: bound $AttrDef table walk to the loaded table size ntfs_attr_find_in_attrdef() walks the in-memory $AttrDef table, but the loop condition bounds only the start of each entry, not the whole entry: for (ad = vol->attrdef; (u8 *)ad - (u8 *)vol->attrdef < vol->attrdef_size && ad->type; ++ad) struct attr_def is 160 bytes; the guard reads ad->type at offset 128 and the loop body reads further fields. vol->attrdef is kvzalloc(i_size), where i_size is the on-disk $AttrDef data size, checked in load_and_init_attrdef() only as 0 < i_size <= 0x7fffffff. A volume whose $AttrDef data size is smaller than one entry (e.g. 120 bytes) makes the read of ad->type run past the allocation. Creating a file reaches this through ntfs_attr_size_bounds_check() and reads out of bounds: BUG: KASAN: slab-out-of-bounds in ntfs_attr_find_in_attrdef+0x66/0xa0 Read of size 4 at addr ffff888005833280 by task init/1 ntfs_attr_find_in_attrdef ntfs_attr_size_bounds_check ntfs_attr_can_be_non_resident ntfs_attr_add Require the whole entry to lie within attrdef_size in the loop guard, and reject at mount a $AttrDef too small to hold one attr_def entry. Fixes: 1e9ea7e04472 ("Revert "fs: Remove NTFS classic"") Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Dennis Tighe Signed-off-by: Namjae Jeon --- fs/ntfs/attrib.c | 4 ++-- fs/ntfs/super.c | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c index c55dc47d2261..b3e941423a3f 100644 --- a/fs/ntfs/attrib.c +++ b/fs/ntfs/attrib.c @@ -1737,8 +1737,8 @@ static struct attr_def *ntfs_attr_find_in_attrdef(const struct ntfs_volume *vol, struct attr_def *ad; WARN_ON(!type); - for (ad = vol->attrdef; (u8 *)ad - (u8 *)vol->attrdef < - vol->attrdef_size && ad->type; ++ad) { + for (ad = vol->attrdef; (u8 *)ad - (u8 *)vol->attrdef <= + vol->attrdef_size - (s32)sizeof(*ad) && ad->type; ++ad) { /* We have not found it yet, carry on searching. */ if (likely(le32_to_cpu(ad->type) < le32_to_cpu(type))) continue; diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c index 2df64712335a..155dca5db7a9 100644 --- a/fs/ntfs/super.c +++ b/fs/ntfs/super.c @@ -1241,9 +1241,9 @@ static bool load_and_init_attrdef(struct ntfs_volume *vol) goto failed; } NInoSetSparseDisabled(NTFS_I(ino)); - /* The size of FILE_AttrDef must be above 0 and fit inside 31 bits. */ + /* FILE_AttrDef must hold at least one entry and fit inside 31 bits. */ i_size = i_size_read(ino); - if (i_size <= 0 || i_size > 0x7fffffff) + if (i_size < (s64)sizeof(struct attr_def) || i_size > 0x7fffffff) goto iput_failed; vol->attrdef = kvzalloc(i_size, GFP_NOFS); if (!vol->attrdef) From 323751a604e7533fa473874d999371592a614207 Mon Sep 17 00:00:00 2001 From: Dennis Tighe Date: Tue, 25 Aug 2026 22:09:34 -0700 Subject: [PATCH 14/23] ntfs: reject invalid sectors_per_cluster in the boot sector is_boot_sector_ntfs() checks the boot sector's sectors_per_cluster field with a range test that rejects 0x81..0xf3 but accepts 0 and other non-power-of-two counts. A zero value reaches parse_ntfs_boot_sector(): sectors_per_cluster_bits = ffs(sectors_per_cluster) - 1; ... vol->cluster_size = vol->sector_size << sectors_per_cluster_bits; ffs(0) is 0, so sectors_per_cluster_bits becomes (unsigned)-1 and the shift is undefined: UBSAN: shift-out-of-bounds in fs/ntfs/super.c:673:39 shift exponent 4294967295 is too large for 32-bit type 'int' This change rejects any non-power-of-two value, since it feeds the aforementioned shift via ffs() - 1, which only yields the correct shift for a power of two. Fixes: 6251f0b0de7d ("ntfs: update super block operations") Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Dennis Tighe Signed-off-by: Namjae Jeon --- fs/ntfs/super.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c index 155dca5db7a9..60d43339c590 100644 --- a/fs/ntfs/super.c +++ b/fs/ntfs/super.c @@ -557,8 +557,8 @@ static bool is_boot_sector_ntfs(const struct super_block *sb, * Check sectors per cluster value is valid and the cluster size * is not above the maximum (2MB). */ - if (b->bpb.sectors_per_cluster > 0x80 && - b->bpb.sectors_per_cluster < 0xf4) + if (b->bpb.sectors_per_cluster < 0xf4 && + !is_power_of_2(b->bpb.sectors_per_cluster)) goto not_ntfs; /* Check reserved/unused fields are really zero. */ From 6faa235a649e78a82e3230b849607f446ba65ed5 Mon Sep 17 00:00:00 2001 From: Dennis Tighe Date: Sun, 23 Aug 2026 00:13:25 -0700 Subject: [PATCH 15/23] ntfs: compute bi_sector in 512-byte units bi_sector counts in 512 byte sectors and not in multiples of the volume's sector size. Under "normal" circumstances (with 512 byte sectors in NTFS) the current code works as is; however, when we have a 4k sector size on the volume the current usage of NTFS_B_TO_SECTOR() and ntfs_bytes_to_sector() end up converting to the number of 4k sectors after mount. Reads work today on 4k volumes as bdev-io.c as performing the shift correctly inline. With writes, we end up with significant silent disk corruption on these volumes. This fixes changes to use the new ntfs_bytes_to_bio_sector() function everywhere we're performing this calculation (including the existing read path). For the change in inode.c it removes a dead code block rather than updating. Fixes: 40796051991d ("ntfs: update in-memory, on-disk structures and headers") Assisted-by: Claude:claude-opus-5 Signed-off-by: Dennis Tighe Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/bdev-io.c | 2 +- fs/ntfs/compress.c | 2 +- fs/ntfs/inode.c | 10 ++-------- fs/ntfs/mft.c | 12 ++++++------ fs/ntfs/ntfs.h | 10 ++++------ 5 files changed, 14 insertions(+), 22 deletions(-) diff --git a/fs/ntfs/bdev-io.c b/fs/ntfs/bdev-io.c index 86db4d9298ed..4f27eed3b072 100644 --- a/fs/ntfs/bdev-io.c +++ b/fs/ntfs/bdev-io.c @@ -34,7 +34,7 @@ int ntfs_bdev_read(struct block_device *bdev, char *data, loff_t start, size_t s int error; struct bio *bio; blk_opf_t op; - sector_t sector = start >> SECTOR_SHIFT; + sector_t sector = ntfs_bytes_to_bio_sector(start); if (start & (SECTOR_SIZE - 1)) return -EINVAL; diff --git a/fs/ntfs/compress.c b/fs/ntfs/compress.c index 2225630b19d7..197d8607fc63 100644 --- a/fs/ntfs/compress.c +++ b/fs/ntfs/compress.c @@ -1414,7 +1414,7 @@ static int ntfs_write_cb(struct ntfs_inode *ni, loff_t pos, struct page **pages, 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); + bio->bi_iter.bi_sector = ntfs_bytes_to_bio_sector(bio_pos); for (i = 0; bio_size; i++) { unsigned int len = min_t(unsigned int, bio_size, PAGE_SIZE); diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c index 32edb4045178..5aedc045f65a 100644 --- a/fs/ntfs/inode.c +++ b/fs/ntfs/inode.c @@ -1852,7 +1852,7 @@ int ntfs_read_inode_mount(struct inode *vi) struct mft_record *m = NULL; struct attr_record *a; struct ntfs_attr_search_ctx *ctx; - unsigned int i, nr_blocks; + unsigned int i; int err; size_t new_rl_count; @@ -1896,11 +1896,6 @@ int ntfs_read_inode_mount(struct inode *vi) goto err_out; } - /* Determine the first block of the $MFT/$DATA attribute. */ - nr_blocks = ntfs_bytes_to_sector(vol, vol->mft_record_size); - if (!nr_blocks) - nr_blocks = 1; - /* Load $MFT/$DATA's first mft record. */ err = ntfs_bdev_read(sb->s_bdev, (char *)m, ntfs_cluster_to_bytes(vol, vol->mft_lcn), i); @@ -3780,8 +3775,7 @@ static s64 __ntfs_inode_non_resident_attr_pwrite(struct inode *vi, bio = bio_alloc(vol->sb->s_bdev, 1, REQ_OP_WRITE, GFP_NOIO); bio->bi_iter.bi_sector = - ntfs_bytes_to_sector(vol, - ntfs_cluster_to_bytes(vol, lcn) + + ntfs_bytes_to_bio_sector(ntfs_cluster_to_bytes(vol, lcn) + lcn_folio_off); length = min_t(unsigned long, diff --git a/fs/ntfs/mft.c b/fs/ntfs/mft.c index 69b007e574fc..7e58c99f1728 100644 --- a/fs/ntfs/mft.c +++ b/fs/ntfs/mft.c @@ -499,8 +499,8 @@ int ntfs_sync_mft_mirror(struct ntfs_volume *vol, const u64 mft_no, bio = bio_alloc(vol->sb->s_bdev, 1, REQ_OP_WRITE, GFP_NOIO); bio->bi_iter.bi_sector = - NTFS_B_TO_SECTOR(vol, NTFS_CLU_TO_B(vol, vol->mftmirr_lcn) + - lcn_folio_off + folio_ofs); + ntfs_bytes_to_bio_sector(NTFS_CLU_TO_B(vol, vol->mftmirr_lcn) + + lcn_folio_off + folio_ofs); if (bio_add_folio(bio, folio, vol->mft_record_size, folio_ofs)) err = submit_bio_wait(bio); @@ -592,8 +592,8 @@ int write_mft_record_nolock(struct ntfs_inode *ni, struct mft_record *m, int syn bio = bio_alloc(vol->sb->s_bdev, 1, REQ_OP_WRITE, GFP_NOIO); bio->bi_iter.bi_sector = - NTFS_B_TO_SECTOR(vol, NTFS_CLU_TO_B(vol, ni->mft_lcn[i]) + - clu_off); + ntfs_bytes_to_bio_sector(NTFS_CLU_TO_B(vol, ni->mft_lcn[i]) + + clu_off); if (!bio_add_folio(bio, folio, folio_size, ni->folio_ofs + offset)) { @@ -2742,8 +2742,8 @@ static int ntfs_write_mft_block(struct folio *folio, struct writeback_control *w bio = bio_alloc(vol->sb->s_bdev, 1, REQ_OP_WRITE, GFP_NOIO); bio->bi_iter.bi_sector = - ntfs_bytes_to_sector(vol, - ntfs_cluster_to_bytes(vol, lcn) + off); + ntfs_bytes_to_bio_sector( + ntfs_cluster_to_bytes(vol, lcn) + off); } if (vol->cluster_size == NTFS_BLOCK_SIZE && diff --git a/fs/ntfs/ntfs.h b/fs/ntfs/ntfs.h index df5a75d506f6..45f77848a9cf 100644 --- a/fs/ntfs/ntfs.h +++ b/fs/ntfs/ntfs.h @@ -19,6 +19,7 @@ #include #include #include +#include #include #include "volume.h" @@ -71,8 +72,6 @@ #define NTFS_CLU_TO_POFS(vol, clu) (((u64)(clu) << (vol)->cluster_size_bits) & \ ~PAGE_MASK) -#define NTFS_B_TO_SECTOR(vol, b) ((b) >> ((vol)->sb)->s_blocksize_bits) - enum { NTFS_BLOCK_SIZE = 512, NTFS_BLOCK_SIZE_BITS = 9, @@ -154,11 +153,10 @@ static inline u64 ntfs_cluster_to_poff(const struct ntfs_volume *vol, return (clu << vol->cluster_size_bits) & ~PAGE_MASK; } -/* Convert byte offset to sector (block) number. */ -static inline sector_t ntfs_bytes_to_sector(const struct ntfs_volume *vol, - u64 bytes) +/* Convert a byte offset on the volume to a bio sector number. */ +static inline sector_t ntfs_bytes_to_bio_sector(u64 bytes) { - return bytes >> vol->sb->s_blocksize_bits; + return bytes >> SECTOR_SHIFT; } /* Global variables. */ From acb1095fd2db884b417cb70808c886e4b615ff05 Mon Sep 17 00:00:00 2001 From: Hongling Zeng Date: Mon, 24 Aug 2026 15:59:35 +0800 Subject: [PATCH 16/23] ntfs: fix memmove overlap in ntfs_new_attr_flags When the record shrinks while the payload offsets increase (e.g., enabling compression reduces padding, making arec_size < old_arec_size, but the header grows by 8 bytes), moving the name first can overwrite the old mapping_pairs before they are copied. Move mapping_pairs first in this case. Since mp_ofs is derived from name_ofs, they always change in the same direction. Checking name_ofs alone is sufficient. Fixes: fc053f05ca28 ("ntfs: add reparse and ea operations") Cc: stable@vger.kernel.org Signed-off-by: Hongling Zeng Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/ea.c | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c index 9f0222c172d9..0bc29bf1f050 100644 --- a/fs/ntfs/ea.c +++ b/fs/ntfs/ea.c @@ -753,15 +753,36 @@ static int ntfs_new_attr_flags(struct ntfs_inode *ni, __le32 fattr) old_arec_size = le32_to_cpu(a->length); /* - * Move payloads before shrinking the record. Otherwise resizing moves + * Move payloads before shrinking the record. Otherwise resizing moves * the following attribute over the old payload before it can be copied. + * + * When offsets increase, move mapping_pairs first to avoid name + * overwriting the start of mapping_pairs. */ 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); + if (name_ofs > old_name_ofs) { + /* Payload offsets increased: move mapping pairs first. */ + if (mp_ofs != old_mp_ofs) + memmove((u8 *)a + mp_ofs, + (u8 *)a + old_mp_ofs, + mp_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)); + } else { + /* Payload offsets decreased or unchanged: move name first. */ + 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(ctx->mrec, a, arec_size); From 67aded1da114dc44808315f249bd9e7e440f799d Mon Sep 17 00:00:00 2001 From: Hongling Zeng Date: Thu, 27 Aug 2026 13:58:44 +0800 Subject: [PATCH 17/23] ntfs: fix race between fallocate and mmap reads The fallocate implementation only takes invalidate_lock for punch hole, collapse range, and insert range operations. For standard allocation modes (mode == 0, FALLOC_FL_KEEP_SIZE), the lock is not held. During ntfs_attr_fallocate(), new clusters are mapped to the runlist via ntfs_attr_map_cluster() before being zeroed by ntfs_dio_zero_range(). This creates a window where concurrent mmap page faults can read uninitialized disk data. Since mmap uses filemap_fault() which takes invalidate_lock in shared mode, it can fault in pages during this window and expose old disk contents to userspace. This is an information leak and data integrity issue. Fix by taking invalidate_lock for all fallocate operations, not just for punch/collapse/insert modes. This prevents concurrent page faults from accessing unzeroed clusters during the allocation window. Fixes: 495e90fa3348 ("ntfs: update attrib operations") Cc: stable@vger.kernel.org Reviewed-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Hongling Zeng Signed-off-by: Namjae Jeon --- fs/ntfs/file.c | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c index 88747217ba61..1969e4f444f7 100644 --- a/fs/ntfs/file.c +++ b/fs/ntfs/file.c @@ -1116,7 +1116,6 @@ static long ntfs_fallocate(struct file *file, int mode, loff_t offset, loff_t le struct ntfs_volume *vol = ni->vol; int err = 0; loff_t old_size; - bool map_locked = false; if (mode & ~(NTFS_FALLOC_FL_SUPPORTED)) return -EOPNOTSUPP; @@ -1148,16 +1147,13 @@ static long ntfs_fallocate(struct file *file, int mode, loff_t offset, loff_t le inode_lock(vi); if (NInoCompressed(ni) || NInoEncrypted(ni) || NInoWofCompressed(ni)) { - err = -EOPNOTSUPP; - goto out; + inode_unlock(vi); + return -EOPNOTSUPP; } inode_dio_wait(vi); - if (mode & (FALLOC_FL_PUNCH_HOLE | FALLOC_FL_COLLAPSE_RANGE | - FALLOC_FL_INSERT_RANGE)) { - filemap_invalidate_lock(vi->i_mapping); - map_locked = true; - } + /* Take invalidate_lock for all fallocate operations to prevent races */ + filemap_invalidate_lock(vi->i_mapping); switch (mode & FALLOC_FL_MODE_MASK) { case FALLOC_FL_ALLOCATE_RANGE: @@ -1182,8 +1178,7 @@ static long ntfs_fallocate(struct file *file, int mode, loff_t offset, loff_t le err = file_modified(file); out: - if (map_locked) - filemap_invalidate_unlock(vi->i_mapping); + filemap_invalidate_unlock(vi->i_mapping); if (!err) { if (mode == 0 && NInoNonResident(ni) && offset > old_size) { From ac727d86fb84bdc9626ba9c756c26767459f3083 Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Thu, 27 Aug 2026 14:43:17 +0800 Subject: [PATCH 18/23] ntfs: leave HasEA flag untouched on setxattr failure In ntfs_set_ea(), the exit path unconditionally updates the HasEA flag based on ea_info_qsize. When an error occurs before ea_info_qsize is updated, NInoClearHasEA() hides existing on-disk EAs until the inode is evicted. Only update the flag on success. Fixes: fc053f05ca28 ("ntfs: add reparse and ea operations") Signed-off-by: Baolin Liu Signed-off-by: Namjae Jeon --- fs/ntfs/ea.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c index 0bc29bf1f050..3f4ba7667522 100644 --- a/fs/ntfs/ea.c +++ b/fs/ntfs/ea.c @@ -404,10 +404,12 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len, *packed_ea_size = p_ea_info->ea_length; mark_mft_record_dirty(ni); out: - if (ea_info_qsize > 0) - NInoSetHasEA(ni); - else - NInoClearHasEA(ni); + if (!err) { + if (ea_info_qsize > 0) + NInoSetHasEA(ni); + else + NInoClearHasEA(ni); + } kvfree(ea_buf); kvfree(old_ea_buf); From 41a52ba4a5fe25b2cca431fe76fb5f3d8ad35139 Mon Sep 17 00:00:00 2001 From: Zhan Xusheng Date: Fri, 28 Aug 2026 12:58:43 +0800 Subject: [PATCH 19/23] ntfs: read WOF chunks outside the decompression lock WOF decompression uses four module-global workspaces, one per compression format, each with a static mutex. ntfs_read_wof_compressed_block() takes that mutex once and holds it across the whole chunk loop, so both block reads run inside it: mutex_lock(ws->lock); for each chunk { parse_wof_chunk_table(..., ws->input, ...); /* reads disk */ ntfs_read_wof_chunk(..., ws->input, ...); /* reads disk */ decompress into ws->output; } mutex_unlock(ws->lock); Readers of system-compressed files then serialise system-wide on the disk waits, not just on the decompressor scratch the lock exists for. One reader sleeping in submit_bio_wait() blocks all the rest. The waits dominate. Reading an 8 MiB xpress4k file (2048 chunks at a 48% compressed ratio, so 2048 acquisitions and 4096 block reads) and timing ws->lock against the part of it spent in ntfs_bdev_read(): backing store held of that in I/O held after virtio, host page cache 348 ms 321 ms (92%) 24.6 ms virtio, throttled 100 MB/s 978 ms 948 ms (96%) 36.6 ms The page-cache row is a lower bound, having no seek cost at all, and the share still grows with slower storage because only the wait scales while decompression stays near 26 ms. The reads are inside the lock only because they land in ws->input, a buffer shared through the workspace. Nothing else requires it: parse_wof_chunk_table() and ntfs_read_wof_chunk() already take the buffer as a parameter and both set *chunk_mem to a pointer inside it, so a caller-owned buffer works unchanged. Allocate that buffer per call, do both reads without the lock, and take the lock only around decompression, which is the step needing ws->output and ws->scratch. squashfs is arranged this way already: its squashfs_decompress() is handed a bio that has been read, and locks only for the CPU work. Block reads are unchanged in number, they just no longer run under the lock, and hold time stops tracking device speed. This also unnests two per-inode locks from the global one, runlist->lock taken by both reads and base_ni->mrec_lock taken for a resident stream. A resident chunk needs no I/O at all, yet used to queue behind a reader blocked in submit_bio_wait() and then take mrec_lock inside the global mutex. The buffer is 4608 bytes for xpress4k and at most 33280 for lzx32k. This path already does GFP_NOFS allocations per call in ntfs_attr_iget(), and in ntfs_attr_get_search_ctx() for a resident stream, so one more does not change how it behaves under memory pressure. The workspace keeps output and scratch, 4 KiB to 32 KiB and 6224 bytes (xpress) or 10240 (lzx), and its "already allocated" test moves from ws->input to ws->output. The lock is now taken per chunk rather than per call, which differs only for a folio spanning several chunks: a few more uncontended mutex operations in exchange for not holding it across the reads between them. Verified under QEMU against an uncompressed copy of the same data, on an 8 MiB file and a 100000 byte one, the latter covering the tail chunk that is not a full comp_unit. Signed-off-by: Zhan Xusheng Signed-off-by: Namjae Jeon --- fs/ntfs/wof.c | 127 +++++++++++++++++++++++++++++++------------------- 1 file changed, 79 insertions(+), 48 deletions(-) diff --git a/fs/ntfs/wof.c b/fs/ntfs/wof.c index 8f84c2212eee..9847259e5b1a 100644 --- a/fs/ntfs/wof.c +++ b/fs/ntfs/wof.c @@ -39,8 +39,6 @@ struct ntfs_wof_workspace { struct mutex *lock; const struct ntfs_codec_ops *codec; u32 comp_unit; - void *input; - size_t input_size; void *output; void *scratch; }; @@ -97,30 +95,36 @@ static struct ntfs_wof_workspace *ntfs_wof_workspace(u8 block_size_bits) } } +/* + * Size of the buffer a chunk is read into. A chunk is read straight off the + * device, so the buffer has to hold @comp_unit bytes plus the leading partial + * sector. + */ +static size_t ntfs_wof_input_size(const struct ntfs_wof_workspace *ws) +{ + return round_up((size_t)ws->comp_unit + 511, 512); +} + static int ntfs_wof_workspace_prepare(struct ntfs_wof_workspace *ws) { - void *input, *output, *scratch; + void *output, *scratch; size_t scratch_size; - if (ws->input) + if (ws->output) return 0; - ws->input_size = round_up((size_t)ws->comp_unit + 511, 512); scratch_size = ws->codec->scratch_size(ws->comp_unit); if (!scratch_size) return -EINVAL; - input = kvmalloc(ws->input_size, GFP_NOFS); output = kvmalloc(ws->comp_unit, GFP_NOFS); scratch = kvzalloc(scratch_size, GFP_NOFS); - if (!input || !output || !scratch) { - kvfree(input); + if (!output || !scratch) { kvfree(output); kvfree(scratch); return -ENOMEM; } - ws->input = input; ws->output = output; ws->scratch = scratch; return 0; @@ -134,10 +138,8 @@ void ntfs_wof_free_workspaces(void) struct ntfs_wof_workspace *ws = ntfs_wof_workspaces[i]; mutex_lock(ws->lock); - kvfree(ws->input); kvfree(ws->output); kvfree(ws->scratch); - ws->input = NULL; ws->output = NULL; ws->scratch = NULL; mutex_unlock(ws->lock); @@ -602,6 +604,51 @@ static int ntfs_wof_try_direct(struct ntfs_wof_workspace *ws, chunk_end, src, src_len, dst_len); } +/* + * Decompress one chunk into @folio. Only this step needs the workspace, so it + * is the only step that takes the workspace lock. + */ +static int ntfs_wof_decompress_chunk(struct ntfs_wof_workspace *ws, + struct ntfs_volume *vol, + struct address_space *mapping, + struct folio *folio, loff_t folio_start, + loff_t folio_end, u64 chunk_file_offset, + char *chunk_mem, u32 chunk_size, + u32 decomp_size) +{ + loff_t chunk_end = chunk_file_offset + decomp_size; + loff_t copy_start, copy_end; + int err; + + mutex_lock(ws->lock); + err = ntfs_wof_workspace_prepare(ws); + if (err) + goto out_unlock; + + err = ntfs_wof_try_direct(ws, mapping, folio, chunk_file_offset, + chunk_end, chunk_mem, chunk_size, + decomp_size); + if (err != -EAGAIN) + goto out_unlock; + + err = ntfs_wof_decode(ws, chunk_mem, chunk_size, ws->output, + decomp_size); + if (err) { + ntfs_error(vol->sb, "Decompression failed: %d", err); + err = -EINVAL; + goto out_unlock; + } + + copy_start = max_t(loff_t, folio_start, chunk_file_offset); + copy_end = min_t(loff_t, folio_end, chunk_file_offset + decomp_size); + memcpy_to_folio(folio, copy_start - folio_start, + ws->output + copy_start - chunk_file_offset, + copy_end - copy_start); +out_unlock: + mutex_unlock(ws->lock); + return err; +} + int ntfs_read_wof_compressed_block(struct folio *folio) { struct address_space *mapping = folio->mapping; @@ -613,6 +660,8 @@ int ntfs_read_wof_compressed_block(struct folio *folio) loff_t folio_start = folio_pos(folio); loff_t folio_end = folio_next_pos(folio); char *chunk_mem; + void *input; + size_t input_size; u32 decomp_size; u64 chunk_count, chunk_idx, last_chunk, chunk_offset; int err = 0; @@ -652,10 +701,12 @@ int ntfs_read_wof_compressed_block(struct folio *folio) goto out_iput; } - mutex_lock(ws->lock); - err = ntfs_wof_workspace_prepare(ws); - if (err) - goto out_unlock_ws; + input_size = ntfs_wof_input_size(ws); + input = kvmalloc(input_size, GFP_NOFS); + if (!input) { + err = -ENOMEM; + goto out_iput; + } chunk_idx = div_u64(folio_start, ws->comp_unit); last_chunk = @@ -663,55 +714,35 @@ int ntfs_read_wof_compressed_block(struct folio *folio) chunk_count = DIV_ROUND_UP_ULL(i_size, ws->comp_unit); for (; chunk_idx <= last_chunk; chunk_idx++) { u32 chunk_size; - u64 chunk_file_offset; - loff_t chunk_end, copy_start, copy_end; decomp_size = chunk_idx + 1 == chunk_count ? i_size - chunk_idx * ws->comp_unit : ws->comp_unit; err = parse_wof_chunk_table(ni, wof_ni, chunk_idx, chunk_count, decomp_size, &chunk_offset, - &chunk_size, ws->input, - ws->input_size); + &chunk_size, input, input_size); if (err) - goto out_unlock_ws; + goto out_free_input; err = ntfs_read_wof_chunk(vol, wof_ni, chunk_offset, chunk_size, - ws->input, ws->input_size, - &chunk_mem); + input, input_size, &chunk_mem); if (err) - goto out_unlock_ws; + goto out_free_input; - chunk_file_offset = chunk_idx * ws->comp_unit; - chunk_end = chunk_file_offset + decomp_size; - err = ntfs_wof_try_direct(ws, mapping, folio, chunk_file_offset, - chunk_end, chunk_mem, chunk_size, - decomp_size); - if (!err) - continue; - if (err != -EAGAIN) - goto out_unlock_ws; - - err = ntfs_wof_decode(ws, chunk_mem, chunk_size, ws->output, - decomp_size); - if (err) { - ntfs_error(vol->sb, "Decompression failed: %d", err); - err = -EINVAL; - goto out_unlock_ws; - } - copy_start = max_t(loff_t, folio_start, chunk_file_offset); - copy_end = min_t(loff_t, folio_end, - chunk_file_offset + decomp_size); - memcpy_to_folio(folio, copy_start - folio_start, - ws->output + copy_start - chunk_file_offset, - copy_end - copy_start); + err = ntfs_wof_decompress_chunk(ws, vol, mapping, folio, + folio_start, folio_end, + chunk_idx * ws->comp_unit, + chunk_mem, chunk_size, + decomp_size); + if (err) + goto out_free_input; } if (folio_end > i_size) folio_zero_segment(folio, i_size - folio_start, folio_size(folio)); -out_unlock_ws: - mutex_unlock(ws->lock); +out_free_input: + kvfree(input); out_iput: iput(wof_inode); out: From 03c6ecc4b4b13a3901f207152451fdd2d82e40c4 Mon Sep 17 00:00:00 2001 From: Jacopo Labardi Date: Sun, 30 Aug 2026 02:15:36 +0200 Subject: [PATCH 20/23] ntfs: fix FITRIM range alignment ntfs_trim_fs() aligns the start of a free extent up to the device discard granularity, but derives the discard length by aligning the original extent length down. When the free extent start is not discard-aligned, adding that length to the aligned start can extend the discard past the free extent and into allocated clusters. For example, with 4 KiB clusters and 32 KiB discard granularity, the free extent [4 KiB, 36 KiB) becomes the discard range [32 KiB, 64 KiB), so 28 KiB beyond the free extent may be discarded. Align the absolute end of the free extent down and derive the length from the two aligned endpoints. Skip extents that contain no full discard unit. Reproduced with a 4 KiB-cluster NTFS filesystem on scsi_debug configured for 32 KiB discard granularity and read-zero-after-trim. Before this change, FITRIM zeroed seven allocated 4 KiB clusters following an unaligned 32 KiB hole. With this change, the same data remains intact across FITRIM and remount. Fixes: 11ccc9107dc4 ("ntfs: update runlist handling and cluster allocator") Cc: stable@vger.kernel.org Assisted-by: OpenAI Codex:GPT-5.6 Sol Max Signed-off-by: Jacopo Labardi Signed-off-by: Namjae Jeon --- fs/ntfs/bitmap.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/fs/ntfs/bitmap.c b/fs/ntfs/bitmap.c index b1436b3151b9..1840b7d84c62 100644 --- a/fs/ntfs/bitmap.c +++ b/fs/ntfs/bitmap.c @@ -64,7 +64,7 @@ int ntfs_trim_fs(struct ntfs_volume *vol, struct fstrim_range *range) end = start_buf; while (end < end_buf) { - u64 aligned_start, aligned_count; + u64 aligned_start, aligned_end, aligned_count; u64 start = find_next_zero_bit(bitmap, end_buf - start_buf, end - start_buf) + start_buf; if (start >= end_buf) @@ -74,8 +74,10 @@ int ntfs_trim_fs(struct ntfs_volume *vol, struct fstrim_range *range) start - start_buf) + start_buf; aligned_start = ALIGN(ntfs_cluster_to_bytes(vol, start), dq); - aligned_count = - ALIGN_DOWN(ntfs_cluster_to_bytes(vol, end - start), dq); + aligned_end = ALIGN_DOWN(ntfs_cluster_to_bytes(vol, end), dq); + if (aligned_start >= aligned_end) + continue; + aligned_count = aligned_end - aligned_start; if (aligned_count >= range->minlen) { ret = blkdev_issue_discard(vol->sb->s_bdev, aligned_start >> 9, aligned_count >> 9, GFP_NOFS); From 4dc8f4ee2d46d5d1e749ddd1b94912c72e796162 Mon Sep 17 00:00:00 2001 From: Hongling Zeng Date: Wed, 26 Aug 2026 13:59:53 +0800 Subject: [PATCH 21/23] ntfs: handle signal interruption in fallocate The ntfs_attr_fallocate() function checks for pending signals during allocation loops and exits early via 'out' label. However, when a signal interrupts the operation with err == 0, the function returns 0 (success) instead of -EINTR. The signal_pending() checks at the allocation loops jump to 'out' without setting err = -EINTR, so the function returns success even when interrupted by a signal. Set err = -EINTR when jumping to the signal exit path, and only override when no other error is pending. This ensures: - Allocation interrupted by signal returns -EINTR - Allocation that completed successfully before signal arrived returns 0 - Other errors are preserved and not overwritten by -EINTR Fixes: 495e90fa3348 ("ntfs: update attrib operations") Cc: stable@vger.kernel.org Signed-off-by: Hongling Zeng Reviewed-by: Baolin Liu Signed-off-by: Namjae Jeon --- fs/ntfs/attrib.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c index b3e941423a3f..848a0d338b89 100644 --- a/fs/ntfs/attrib.c +++ b/fs/ntfs/attrib.c @@ -5709,7 +5709,7 @@ int ntfs_attr_fallocate(struct ntfs_inode *ni, loff_t start, loff_t byte_len, bo } if (signal_pending(current)) - goto out; + goto signal_out; vcn += alloc_cnt; try_alloc_cnt -= alloc_cnt; @@ -5730,7 +5730,7 @@ int ntfs_attr_fallocate(struct ntfs_inode *ni, loff_t start, loff_t byte_len, bo up_write(&ni->runlist.lock); mutex_unlock(&ni->mrec_lock); if (err || signal_pending(current)) - goto out; + goto signal_out; vcn += alloc_cnt; try_alloc_cnt -= alloc_cnt; @@ -5756,4 +5756,8 @@ int ntfs_attr_fallocate(struct ntfs_inode *ni, loff_t start, loff_t byte_len, bo mutex_unlock(&ni->mrec_lock); out: return err >= 0 ? 0 : err; +signal_out: + if (!err) + err = -EINTR; + goto out; } From 9cc5761b8f28f9cef72061094eb5e37e2cd44d97 Mon Sep 17 00:00:00 2001 From: Hongling Zeng Date: Mon, 31 Aug 2026 16:30:14 +0800 Subject: [PATCH 22/23] ntfs: take invalidate_lock in ntfs_setattr_size() ntfs_setattr_size() updates i_size and resizes the on-disk attribute without holding mapping->invalidate_lock. Page faults take the lock shared, so a fault racing the resize can resolve a VCN against the transient runlist state of ntfs_non_resident_attr_expand() and fail with a spurious SIGBUS, and can interleave with the size-change epilogue (truncate_pagecache(), i_size_write(), pagecache_isize_extended()). Take invalidate_lock exclusively around the whole resize after inode_dio_wait(), matching the fallocate path and other filesystems such as xfs, which wraps truncate in its mmaplock (= invalidate_lock). Fixes: 9c87959601e8 ("ntfs: update file operations") Cc: stable@vger.kernel.org Reviewed-by: Hyunchul Lee Reviewed-by: Baolin Liu Signed-off-by: Hongling Zeng Signed-off-by: Namjae Jeon --- fs/ntfs/file.c | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c index 1969e4f444f7..585ab2145797 100644 --- a/fs/ntfs/file.c +++ b/fs/ntfs/file.c @@ -270,18 +270,25 @@ static int ntfs_setattr_size(struct inode *vi, struct iattr *attr) return err; inode_dio_wait(vi); + + /* + * Serialize with page faults and pagecache instantiation so that + * readers cannot observe the size change until the attribute + * updates below have completed. + */ + filemap_invalidate_lock(vi->i_mapping); 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 + } else { truncate_setsize(vi, attr->ia_size); + } err = ntfs_truncate_vfs(vi, attr->ia_size, old_size); - if (err) { + if (err) i_size_write(vi, old_size); - return err; - } + filemap_invalidate_unlock(vi->i_mapping); return err; } From 0fecc393f2060e6bc25138df32cb923ec7071c6b Mon Sep 17 00:00:00 2001 From: Hongling Zeng Date: Mon, 31 Aug 2026 16:32:52 +0800 Subject: [PATCH 23/23] ntfs: take invalidate_lock in ntfs_filemap_page_mkwrite() ntfs_filemap_page_mkwrite() calls iomap_page_mkwrite() without holding mapping->invalidate_lock, so a concurrent truncate or fallocate can be in the middle of invalidating pagecache and rewriting the runlist while the write fault maps blocks and dirties the folio. This races with ntfs_attr_fallocate(), which merges clusters into the in-memory runlist, drops the runlist lock, and only afterwards zeroes the newly allocated clusters on disk; and with the punch-hole/insert/collapse paths that free clusters after truncating the cache. Per Documentation/filesystems/locking.rst, ->page_mkwrite() must ensure there are no truncate/invalidate races, "usually mapping->invalidate_lock is suitable for proper serialization". xfs takes its mmaplock (= the invalidate_lock rwsem) shared in exactly this path. Take invalidate_lock shared around iomap_page_mkwrite(). The read-only fault path is already covered because filemap_fault() itself grabs invalidate_lock shared on instantiation/read paths; only page_mkwrite was bypassing it in this driver. Fixes: 9c87959601e8 ("ntfs: update file operations") Cc: stable@vger.kernel.org Reviewed-by: Hyunchul Lee Reviewed-by: Baolin Liu Signed-off-by: Hongling Zeng Co-developed-by: Namjae Jeon Signed-off-by: Namjae Jeon --- fs/ntfs/file.c | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c index 585ab2145797..8164326b7812 100644 --- a/fs/ntfs/file.c +++ b/fs/ntfs/file.c @@ -676,6 +676,7 @@ static ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from) static vm_fault_t ntfs_filemap_page_mkwrite(struct vm_fault *vmf) { struct inode *inode = file_inode(vmf->vma->vm_file); + struct address_space *mapping = inode->i_mapping; vm_fault_t ret; if (NInoWofCompressed(NTFS_I(inode))) @@ -684,7 +685,14 @@ static vm_fault_t ntfs_filemap_page_mkwrite(struct vm_fault *vmf) sb_start_pagefault(inode->i_sb); file_update_time(vmf->vma->vm_file); + /* + * Serialize against truncate/fallocate which hold the lock + * exclusively while invalidating pagecache and changing extents. + */ + filemap_invalidate_lock_shared(mapping); ret = iomap_page_mkwrite(vmf, &ntfs_page_mkwrite_iomap_ops, NULL); + filemap_invalidate_unlock_shared(mapping); + sb_end_pagefault(inode->i_sb); return ret; } @@ -1185,13 +1193,15 @@ static long ntfs_fallocate(struct file *file, int mode, loff_t offset, loff_t le err = file_modified(file); out: + if (!err && mode == 0 && NInoNonResident(ni) && + offset > old_size) { + truncate_pagecache(vi, old_size); + pagecache_isize_extended(vi, old_size, offset); + } + filemap_invalidate_unlock(vi->i_mapping); + if (!err) { - if (mode == 0 && NInoNonResident(ni) && - 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)); mark_inode_dirty(vi);