From 3378667c6e6929b4292817de0710063b7ce305b8 Mon Sep 17 00:00:00 2001 From: Hyunchul Lee Date: Mon, 21 Sep 2026 15:44:03 +0900 Subject: ntfs: read MFT writeback bounds under size_lock Read the MFT inode size and allocated size under the size_lock critical section before deriving the folio writeback limits. This prevents writeback from combining an old allocation boundary with a concurrently updated file size while the MFT is extended. Fixes: 115380f9a2f9 ("ntfs: update mft operations") Cc: stable@vger.kernel.org Signed-off-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/mft.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/mft.c b/fs/ntfs/mft.c index 4b7449495375..c9ea11198aa6 100644 --- a/fs/ntfs/mft.c +++ b/fs/ntfs/mft.c @@ -2901,9 +2901,17 @@ static int ntfs_write_mft_block(struct folio *folio, struct writeback_control *w struct ntfs_inode *tni; s64 lcn; s64 vcn = ntfs_pidx_to_cluster(vol, folio->index); - s64 end_vcn = ntfs_bytes_to_cluster(vol, ni->allocated_size); + s64 end_vcn; unsigned int folio_sz; - loff_t i_size = i_size_read(vi); + loff_t i_size; + s64 allocated_size; + unsigned long flags; + + read_lock_irqsave(&ni->size_lock, flags); + i_size = i_size_read(vi); + allocated_size = ni->allocated_size; + read_unlock_irqrestore(&ni->size_lock, flags); + end_vcn = ntfs_bytes_to_cluster(vol, allocated_size); ntfs_debug("Entering for inode 0x%llx, attribute type 0x%x, folio index 0x%lx.", ni->mft_no, ni->type, folio->index); -- cgit v1.2.3 From e67150128d1780b8801932eb97f861c1d02cf873 Mon Sep 17 00:00:00 2001 From: Hyunchul Lee Date: Mon, 21 Sep 2026 15:44:04 +0900 Subject: ntfs: validate the MFT I/O geometry MFT writes must consider both the device logical block size and the NTFS record geometry. Validate those values at mount time, then cache the MFT I/O unit used by all MFT write paths. Use the larger of the device logical block and MFT record sizes as the MFT I/O unit. Keep valid geometries that the current write paths cannot safely handle read-only until their mapping support is generalized. Fixes: 115380f9a2f9 ("ntfs: update mft operations") Cc: stable@vger.kernel.org Signed-off-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/super.c | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- fs/ntfs/volume.h | 8 ++++++ 2 files changed, 82 insertions(+), 2 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c index 4066bacabe37..99a95ee32f22 100644 --- a/fs/ntfs/super.c +++ b/fs/ntfs/super.c @@ -286,6 +286,12 @@ static int ntfs_reconfigure(struct fs_context *fc) static const char *es = ". Cannot remount read-write."; /* Remounting read-write. */ + if (!vol->mft_write_supported) { + ntfs_error(sb, + "MFT writeback is unsupported for this geometry%s", + es); + return -EROFS; + } if (NVolErrors(vol)) { ntfs_error(sb, "Volume has errors and is read-only%s", es); @@ -530,6 +536,8 @@ out: static bool is_boot_sector_ntfs(const struct super_block *sb, const struct ntfs_boot_sector *b, const bool silent) { + u16 sector_size = le16_to_cpu(b->bpb.bytes_per_sector); + /* * Check that checksum == sum of u32 values from b to the checksum * field. If checksum is zero, no checking is done. We will work when @@ -550,8 +558,8 @@ static bool is_boot_sector_ntfs(const struct super_block *sb, if (b->oem_id != magicNTFS) goto not_ntfs; /* Check bytes per sector value is between 256 and 4096. */ - if (le16_to_cpu(b->bpb.bytes_per_sector) < 0x100 || - le16_to_cpu(b->bpb.bytes_per_sector) > 0x1000) + if (sector_size < 0x100 || sector_size > 0x1000 || + !is_power_of_2(sector_size)) goto not_ntfs; /* * Check sectors per cluster value is valid and the cluster size @@ -632,6 +640,60 @@ static char *read_ntfs_boot_sector(struct super_block *sb, return boot_sector; } +static bool ntfs_validate_mft_io_geometry(struct ntfs_volume *vol, + const unsigned int logical_block_size) +{ + struct super_block *sb = vol->sb; + u32 mft_io_unit_size = 0; + bool mft_write_supported = true; + + if (!is_power_of_2(logical_block_size) || + logical_block_size > PAGE_SIZE || + !is_power_of_2(vol->sector_size) || + vol->sector_size < logical_block_size || + vol->sector_size % logical_block_size || + !is_power_of_2(vol->cluster_size) || + !is_power_of_2(vol->mft_record_size) || + vol->mft_record_size > PAGE_SIZE) + goto err; + + mft_io_unit_size = max(logical_block_size, vol->mft_record_size); + if (mft_io_unit_size > PAGE_SIZE || + mft_io_unit_size % logical_block_size || + mft_io_unit_size % vol->mft_record_size) + goto err; + + /* + * The current direct-write path stores at most two MFT runlist + * segments. Keep valid but larger records read-only until that path + * can map an arbitrary number of segments. + */ + if (vol->mft_record_size > 2 * (u64)vol->cluster_size) + mft_write_supported = false; + + /* + * A containing device block is currently mapped through one MFT + * runlist element. Keep valid geometries that require crossing a + * cluster read-only until the mapping is generalized. + */ + if (mft_io_unit_size > vol->mft_record_size && + (vol->cluster_size < mft_io_unit_size || + vol->cluster_size % mft_io_unit_size)) + mft_write_supported = false; + + vol->mft_io_unit_size = mft_io_unit_size; + vol->mft_write_supported = mft_write_supported; + return true; + +err: + ntfs_error(sb, + "Unsupported MFT I/O geometry (logical %u, block %lu, sector %u, cluster %u, MFT record %u, I/O unit %u).", + logical_block_size, sb->s_blocksize, + (unsigned int)vol->sector_size, vol->cluster_size, + vol->mft_record_size, mft_io_unit_size); + return false; +} + /* * parse_ntfs_boot_sector - parse the boot sector and store the data in @vol * @vol: volume structure to initialise with data from boot sector @@ -644,9 +706,11 @@ static bool parse_ntfs_boot_sector(struct ntfs_volume *vol, const struct ntfs_boot_sector *b) { unsigned int sectors_per_cluster, sectors_per_cluster_bits, nr_hidden_sects; + unsigned int logical_block_size; int clusters_per_mft_record, clusters_per_index_record; u64 ll; + logical_block_size = bdev_logical_block_size(vol->sb->s_bdev); vol->sector_size = le16_to_cpu(b->bpb.bytes_per_sector); vol->sector_size_bits = ffs(vol->sector_size) - 1; ntfs_debug("vol->sector_size = %i (0x%x)", vol->sector_size, @@ -719,6 +783,9 @@ static bool parse_ntfs_boot_sector(struct ntfs_volume *vol, ntfs_warning(vol->sb, "Mft record size (%i) is smaller than the sector size (%i).", vol->mft_record_size, vol->sector_size); } + if (!ntfs_validate_mft_io_geometry(vol, logical_block_size)) + return false; + clusters_per_index_record = b->clusters_per_index_record; ntfs_debug("clusters_per_index_record = %i (0x%x)", clusters_per_index_record, clusters_per_index_record); @@ -2290,6 +2357,11 @@ static int ntfs_fill_super(struct super_block *sb, struct fs_context *fc) ntfs_error(sb, "Unsupported NTFS filesystem."); goto err_out_now; } + if (!vol->mft_write_supported && !sb_rdonly(sb)) { + sb->s_flags |= SB_RDONLY; + ntfs_warning(sb, + "MFT writeback is unsupported for this geometry. Mounting read-only."); + } if (vol->sector_size > blocksize) { blocksize = sb_set_blocksize(sb, vol->sector_size); diff --git a/fs/ntfs/volume.h b/fs/ntfs/volume.h index bc85a9592245..8c17a66b4f22 100644 --- a/fs/ntfs/volume.h +++ b/fs/ntfs/volume.h @@ -43,6 +43,7 @@ * @mft_record_size: in bytes * @mft_record_size_mask: mft_record_size - 1 * @mft_record_size_bits: log2(mft_record_size) + * @mft_write_supported: Whether the MFT write paths support the geometry. * @index_record_size: in bytes * @index_record_size_mask: index_record_size - 1 * @index_record_size_bits: log2(index_record_size) @@ -111,6 +112,13 @@ struct ntfs_volume { u32 mft_record_size; u32 mft_record_size_mask; u8 mft_record_size_bits; + bool mft_write_supported; + /* + * Unit size used for MFT I/O. This is the MFT record size when + * it is at least as large as the device logical block, or the + * containing device logical block when the record is smaller. + */ + u32 mft_io_unit_size; u32 index_record_size; u32 index_record_size_mask; u8 index_record_size_bits; -- cgit v1.2.3 From 6978406d5f78c19a122aadf844b4118c2a07d8be Mon Sep 17 00:00:00 2001 From: Hyunchul Lee Date: Mon, 21 Sep 2026 15:44:05 +0900 Subject: ntfs: write MFT mirror data in mapped I/O units Replace the record-specific mirror writer with a unit-based helper and use a stack-allocated BIO for synchronous mirror writes. Route write_mft_record_nolock() and ntfs_write_mft_block() through the common mirror primitive. The helper only returns errors, So callers handle volume error reporting and retry policy. Fixes: 115380f9a2f9 ("ntfs: update mft operations") Cc: stable@vger.kernel.org Signed-off-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/mft.c | 142 ++++++++++++++++++++++++++++++---------------------------- fs/ntfs/mft.h | 2 - 2 files changed, 73 insertions(+), 71 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/mft.c b/fs/ntfs/mft.c index c9ea11198aa6..b102e0204eba 100644 --- a/fs/ntfs/mft.c +++ b/fs/ntfs/mft.c @@ -429,6 +429,12 @@ void __mark_mft_record_dirty(struct ntfs_inode *ni) __mark_inode_dirty(VFS_I(base_ni), I_DIRTY_DATASYNC); } +struct ntfs_mft_io_unit { + sector_t sector; + unsigned int folio_ofs; + unsigned int len; +}; + /* * ntfs_bio_end_io - bio completion callback for MFT record writes * @@ -445,65 +451,60 @@ static void ntfs_bio_end_io(struct bio *bio) } /* - * ntfs_sync_mft_mirror - synchronize an mft record to the mft mirror - * @vol: ntfs volume on which the mft record to synchronize resides - * @mft_no: mft record number of mft record to synchronize - * @m: mapped, mst protected (extent) mft record to synchronize - * - * Write the mapped, mst protected (extent) mft record @m with mft record - * number @mft_no to the mft mirror ($MFTMirr) of the ntfs volume @vol. - * - * On success return 0. On error return -errno and set the volume errors flag - * in the ntfs volume @vol. - * - * NOTE: We always perform synchronous i/o. + * Write one MFT I/O unit to $MFTMirr. The source folio contains the MST + * protected image that will be written to $MFT. */ -int ntfs_sync_mft_mirror(struct ntfs_volume *vol, const u64 mft_no, - struct mft_record *m) +static int ntfs_sync_mft_mirror_unit(struct ntfs_volume *vol, + struct folio *source, u64 mirror_file_ofs, + const struct ntfs_mft_io_unit *unit) { - u8 *kmirr; - struct folio *folio; - unsigned int folio_ofs; - int err = 0; - struct bio *bio; + struct folio *mirror; + struct bio_vec bvec; + struct bio bio; + u64 mirror_size; + unsigned int mirror_ofs; + u8 *src, *dst; + int err; - ntfs_debug("Entering for inode 0x%llx.", mft_no); + if (unlikely(!vol->mftmirr_ino)) + return -EIO; + + mirror_size = (u64)vol->mftmirr_size * vol->mft_record_size; + if (mirror_file_ofs >= mirror_size || + unit->len > mirror_size - mirror_file_ofs) + return -EIO; + if (unit->folio_ofs + unit->len > folio_size(source)) + return -EIO; - if (unlikely(!vol->mftmirr_ino)) { - /* This could happen during umount... */ + mirror = read_mapping_folio(vol->mftmirr_ino->i_mapping, + mirror_file_ofs >> PAGE_SHIFT, NULL); + if (IS_ERR(mirror)) + return PTR_ERR(mirror); + + folio_lock(mirror); + if (folio_test_writeback(mirror)) + folio_wait_writeback(mirror); + mirror_ofs = mirror_file_ofs - folio_pos(mirror); + if (mirror_ofs + unit->len > folio_size(mirror)) { err = -EIO; - goto err_out; - } - /* Get the page containing the mirror copy of the mft record @m. */ - folio = read_mapping_folio(vol->mftmirr_ino->i_mapping, - NTFS_MFT_NR_TO_PIDX(vol, mft_no), NULL); - if (IS_ERR(folio)) { - ntfs_error(vol->sb, "Failed to map mft mirror page."); - err = PTR_ERR(folio); - goto err_out; + goto out_unlock; } - folio_lock(folio); - folio_clear_uptodate(folio); - /* Offset of the mft mirror record inside the page. */ - folio_ofs = NTFS_MFT_NR_TO_POFS(vol, mft_no); - /* The address in the page of the mirror copy of the mft record @m. */ - kmirr = kmap_local_folio(folio, 0) + folio_ofs; - /* Copy the mst protected mft record to the mirror. */ - memcpy(kmirr, m, vol->mft_record_size); - kunmap_local(kmirr); - - bio = bio_alloc(vol->sb->s_bdev, 1, REQ_OP_WRITE, GFP_NOIO); - bio->bi_iter.bi_sector = - ntfs_bytes_to_bio_sector(NTFS_CLU_TO_B(vol, vol->mftmirr_lcn) + - ((u64)folio->index << PAGE_SHIFT) + - folio_ofs); - - if (bio_add_folio(bio, folio, vol->mft_record_size, folio_ofs)) - err = submit_bio_wait(bio); - else + folio_clear_uptodate(mirror); + src = kmap_local_folio(source, unit->folio_ofs); + dst = kmap_local_folio(mirror, mirror_ofs); + memcpy(dst, src, unit->len); + kunmap_local(dst); + kunmap_local(src); + + bio_init(&bio, vol->sb->s_bdev, &bvec, 1, REQ_OP_WRITE); + bio.bi_iter.bi_sector = ntfs_bytes_to_bio_sector(NTFS_CLU_TO_B(vol, vol->mftmirr_lcn) + + mirror_file_ofs); + if (!bio_add_folio(&bio, mirror, unit->len, mirror_ofs)) err = -EIO; - bio_put(bio); + else + err = submit_bio_wait(&bio); + bio_uninit(&bio); /* * The in-memory mirror is now valid because we just memcpy()'d the @@ -512,23 +513,26 @@ int ntfs_sync_mft_mirror(struct ntfs_volume *vol, const u64 mft_no, * the stale on-disk mirror and overwrite this copy. The error is * propagated to the caller via @err. */ - folio_mark_uptodate(folio); + folio_mark_uptodate(mirror); - folio_unlock(folio); - folio_put(folio); - if (likely(!err)) { - ntfs_debug("Done."); - } else { - ntfs_error(vol->sb, "I/O error while writing mft mirror record 0x%llx!", mft_no); -err_out: - ntfs_error(vol->sb, - "Failed to synchronize $MFTMirr (error code %i). Volume will be left marked dirty on umount. Run chkdsk on the partition after umounting to correct this.", - err); - NVolSetErrors(vol); - } +out_unlock: + folio_unlock(mirror); + folio_put(mirror); return err; } +static int ntfs_sync_mft_mirror_record(struct ntfs_volume *vol, + struct folio *source, const u64 mft_no) +{ + struct ntfs_mft_io_unit unit = { + .folio_ofs = NTFS_MFT_NR_TO_POFS(vol, mft_no), + .len = vol->mft_record_size, + }; + + return ntfs_sync_mft_mirror_unit(vol, source, + (u64)mft_no * vol->mft_record_size, &unit); +} + /* * write_mft_record_nolock - write out a mapped (extent) mft record * @ni: ntfs inode describing the mapped (extent) mft record @@ -600,8 +604,8 @@ int write_mft_record_nolock(struct ntfs_inode *ni, struct mft_record *m, int syn /* Synchronize the mft mirror now if not @sync. */ if (!sync && ni->mft_no < vol->mftmirr_size) { - int sub_err = ntfs_sync_mft_mirror(vol, ni->mft_no, - fixup_m); + int sub_err = ntfs_sync_mft_mirror_record(vol, folio, + ni->mft_no); if (unlikely(sub_err) && !err) err = sub_err; } @@ -624,7 +628,8 @@ int write_mft_record_nolock(struct ntfs_inode *ni, struct mft_record *m, int syn /* If @sync, now synchronize the mft mirror. */ if (sync && ni->mft_no < vol->mftmirr_size) { - int sub_err = ntfs_sync_mft_mirror(vol, ni->mft_no, fixup_m); + int sub_err = + ntfs_sync_mft_mirror_record(vol, folio, ni->mft_no); if (unlikely(sub_err) && !err) err = sub_err; @@ -3031,8 +3036,7 @@ flush_bio: prev_mft_ofs = mft_ofs; if (mft_no < vol->mftmirr_size) { - int sub_err = ntfs_sync_mft_mirror(vol, mft_no, - (struct mft_record *)(kaddr + mft_ofs)); + int sub_err = ntfs_sync_mft_mirror_record(vol, folio, mft_no); if (unlikely(sub_err) && !err) err = sub_err; diff --git a/fs/ntfs/mft.h b/fs/ntfs/mft.h index ed5c1d595c0d..eb8a6f0d7944 100644 --- a/fs/ntfs/mft.h +++ b/fs/ntfs/mft.h @@ -42,8 +42,6 @@ static inline void mark_mft_record_dirty(struct ntfs_inode *ni) __mark_mft_record_dirty(ni); } -int ntfs_sync_mft_mirror(struct ntfs_volume *vol, const u64 mft_no, - struct mft_record *m); int write_mft_record_nolock(struct ntfs_inode *ni, struct mft_record *m, int sync); /* -- cgit v1.2.3 From 81745a99d100143990a4ab29fd5e9acc0c55e069 Mon Sep 17 00:00:00 2001 From: Hyunchul Lee Date: Mon, 21 Sep 2026 15:44:06 +0900 Subject: ntfs: serialize raw MFT updates against folio writeback Because MFT folio writeback can remain active until its asynchronous BIO completes, raw MFT record formatting and allocation must not modify the folio while it is still used as a BIO source. Wait for existing folio writeback after taking the folio lock in both raw MFT update paths. Fixes: 115380f9a2f9 ("ntfs: update mft operations") Cc: stable@vger.kernel.org Signed-off-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/mft.c | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'fs') diff --git a/fs/ntfs/mft.c b/fs/ntfs/mft.c index b102e0204eba..c7577c322454 100644 --- a/fs/ntfs/mft.c +++ b/fs/ntfs/mft.c @@ -2022,6 +2022,8 @@ static int ntfs_mft_record_format(const struct ntfs_volume *vol, const s64 mft_n return PTR_ERR(folio); } folio_lock(folio); + if (folio_test_writeback(folio)) + folio_wait_writeback(folio); folio_clear_uptodate(folio); m = (struct mft_record *)((u8 *)kmap_local_folio(folio, 0) + ofs); err = ntfs_mft_record_layout(vol, mft_no, m); @@ -2539,6 +2541,8 @@ mft_rec_already_initialized: goto undo_mftbmp_alloc; } folio_lock(folio); + if (folio_test_writeback(folio)) + folio_wait_writeback(folio); folio_clear_uptodate(folio); m = (struct mft_record *)((u8 *)kmap_local_folio(folio, 0) + ofs); /* If we just formatted the mft record no need to do it again. */ -- cgit v1.2.3 From 7780f99835103177332da9c4903808c1c2c7cfc6 Mon Sep 17 00:00:00 2001 From: Hyunchul Lee Date: Mon, 21 Sep 2026 15:44:07 +0900 Subject: ntfs: track direct MFT writes until BIO completion Use an aggregate parent BIO, with optional chained child BIOs, for direct MFT record writes. Keep the folio reference and PG_writeback set until all physical segments complete. For asynchronous writes, release the parent BIO embedded in the write context before ending folio writeback. Synchronous callers retain ownership of the parent BIO after waiting. Wait for any prior folio writeback before modifying a direct MFT record. Fixes: 115380f9a2f9 ("ntfs: update mft operations") Cc: stable@vger.kernel.org Signed-off-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/mft.c | 236 ++++++++++++++++++++++++++++++++++++++++++++------------ fs/ntfs/mft.h | 15 ++-- fs/ntfs/super.c | 10 +++ 3 files changed, 206 insertions(+), 55 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/mft.c b/fs/ntfs/mft.c index c7577c322454..51066b403757 100644 --- a/fs/ntfs/mft.c +++ b/fs/ntfs/mft.c @@ -10,6 +10,7 @@ #include #include +#include #include #include "bitmap.h" @@ -450,6 +451,91 @@ static void ntfs_bio_end_io(struct bio *bio) bio_put(bio); } +struct ntfs_mft_write_ctx { + struct folio *folio; + struct address_space *mapping; + struct ntfs_volume *vol; + struct completion *done; + int error; + bool writeback_started; + struct bio bio; +}; + +static struct bio_set ntfs_mft_bioset; + +int ntfs_mft_bioset_init(void) +{ + return bioset_init(&ntfs_mft_bioset, BIO_POOL_SIZE, + offsetof(struct ntfs_mft_write_ctx, bio), + BIOSET_NEED_BVECS); +} + +void ntfs_mft_bioset_exit(void) +{ + bioset_exit(&ntfs_mft_bioset); +} + +static void ntfs_mft_end_io(struct bio *bio) +{ + struct ntfs_mft_write_ctx *ctx = + container_of(bio, struct ntfs_mft_write_ctx, bio); + struct folio *folio = ctx->folio; + struct completion *done = ctx->done; + int err; + + if (bio->bi_status) + err = blk_status_to_errno(bio->bi_status); + else + err = ctx->error; + if (err) { + mapping_set_error(ctx->mapping, err); + NVolSetErrors(ctx->vol); + ntfs_error(ctx->vol->sb, "I/O error while writing MFT: %d", + err); + } + + if (!done) + bio_put(bio); + folio_end_writeback(folio); + folio_put(folio); + if (done) + complete(done); +} + +static void ntfs_start_mft_writeback(struct ntfs_mft_write_ctx *ctx) +{ + if (ctx->writeback_started) + return; + + inode_attach_wb(ctx->mapping->host, NULL); + folio_get(ctx->folio); + folio_start_writeback(ctx->folio); + ctx->writeback_started = true; +} + +static struct bio *ntfs_alloc_mft_parent_bio(struct ntfs_volume *vol, + struct folio *folio, + struct completion *done) +{ + struct ntfs_mft_write_ctx *ctx; + struct bio *parent; + + parent = bio_alloc_bioset(vol->sb->s_bdev, 1, REQ_OP_WRITE, GFP_NOIO, + &ntfs_mft_bioset); + if (!parent) + return NULL; + + ctx = container_of(parent, struct ntfs_mft_write_ctx, bio); + ctx->folio = folio; + ctx->mapping = folio->mapping; + ctx->vol = vol; + ctx->done = done; + ctx->error = 0; + ctx->writeback_started = false; + parent->bi_end_io = ntfs_mft_end_io; + return parent; +} + /* * Write one MFT I/O unit to $MFTMirr. The source folio contains the MST * protected image that will be written to $MFT. @@ -533,6 +619,42 @@ static int ntfs_sync_mft_mirror_record(struct ntfs_volume *vol, (u64)mft_no * vol->mft_record_size, &unit); } +static int ntfs_prepare_mft_record_io_units(struct ntfs_inode *ni, + struct ntfs_mft_io_unit units[2]) +{ + struct ntfs_volume *vol = ni->vol; + u64 record_byte = (u64)ni->mft_no * vol->mft_record_size; + u64 disk_byte; + unsigned int nr_units = ni->mft_lcn_count; + unsigned int cluster_ofs; + unsigned int i; + + if (!nr_units || nr_units > ARRAY_SIZE(ni->mft_lcn)) + return -EIO; + for (i = 0; i < nr_units; i++) + if (ni->mft_lcn[i] < 0) + return -EIO; + + cluster_ofs = ntfs_bytes_to_cluster_off(vol, record_byte); + disk_byte = NTFS_CLU_TO_B(vol, ni->mft_lcn[0]) + cluster_ofs; + units[0] = (struct ntfs_mft_io_unit){ + .sector = ntfs_bytes_to_bio_sector(disk_byte), + .folio_ofs = ni->folio_ofs, + .len = vol->mft_record_size, + }; + if (nr_units == 1) + return 1; + + units[0].len = vol->cluster_size; + disk_byte = NTFS_CLU_TO_B(vol, ni->mft_lcn[1]); + units[1] = (struct ntfs_mft_io_unit){ + .sector = ntfs_bytes_to_bio_sector(disk_byte), + .folio_ofs = ni->folio_ofs + vol->cluster_size, + .len = vol->mft_record_size - vol->cluster_size, + }; + return 2; +} + /* * write_mft_record_nolock - write out a mapped (extent) mft record * @ni: ntfs inode describing the mapped (extent) mft record @@ -553,17 +675,23 @@ int write_mft_record_nolock(struct ntfs_inode *ni, struct mft_record *m, int syn { struct ntfs_volume *vol = ni->vol; struct folio *folio = ni->folio; - int err = 0, i = 0; + DECLARE_COMPLETION_ONSTACK(done); + struct ntfs_mft_io_unit units[2]; + struct ntfs_mft_write_ctx *ctx; + struct bio *parent, *child = NULL; + unsigned int nr_units; + int err = 0; u8 *kaddr; struct mft_record *fixup_m; - struct bio *bio; - unsigned int offset = 0, folio_size; ntfs_debug("Entering for inode 0x%llx.", ni->mft_no); WARN_ON(NInoAttr(ni)); WARN_ON(!folio_test_locked(folio)); + if (folio_test_writeback(folio)) + folio_wait_writeback(folio); + /* * If the struct ntfs_inode is clean no need to do anything. If it is dirty, * mark it as clean now so that it can be redirtied later on if needed. @@ -573,6 +701,11 @@ int write_mft_record_nolock(struct ntfs_inode *ni, struct mft_record *m, int syn if (!NInoTestClearDirty(ni)) goto done; + err = ntfs_prepare_mft_record_io_units(ni, units); + if (err < 0) + goto err_out; + nr_units = err; + kaddr = kmap_local_folio(folio, 0); fixup_m = (struct mft_record *)(kaddr + ni->folio_ofs); memcpy(fixup_m, m, vol->mft_record_size); @@ -584,69 +717,74 @@ int write_mft_record_nolock(struct ntfs_inode *ni, struct mft_record *m, int syn goto unmap_err_out; } - folio_size = vol->mft_record_size / ni->mft_lcn_count; - while (i < ni->mft_lcn_count) { - unsigned int clu_off; - - clu_off = (unsigned int)((s64)ni->mft_no * vol->mft_record_size + offset) & - vol->cluster_size_mask; - - bio = bio_alloc(vol->sb->s_bdev, 1, REQ_OP_WRITE, GFP_NOIO); - bio->bi_iter.bi_sector = - ntfs_bytes_to_bio_sector(NTFS_CLU_TO_B(vol, ni->mft_lcn[i]) + - clu_off); + parent = ntfs_alloc_mft_parent_bio(vol, folio, sync ? &done : NULL); + if (!parent) { + err = -ENOMEM; + goto unmap_err_out; + } + ctx = container_of(parent, struct ntfs_mft_write_ctx, bio); - if (!bio_add_folio(bio, folio, folio_size, - ni->folio_ofs + offset)) { - err = -EIO; - goto put_bio_out; + parent->bi_iter.bi_sector = units[0].sector; + if (!bio_add_folio(parent, folio, units[0].len, units[0].folio_ofs)) { + err = -EIO; + goto put_bios_out; + } + + if (nr_units == 2) { + if (bio_end_sector(parent) != units[1].sector || + !bio_add_folio(parent, folio, units[1].len, + units[1].folio_ofs)) { + child = bio_alloc(vol->sb->s_bdev, 1, REQ_OP_WRITE, + GFP_NOIO); + if (!child) { + err = -ENOMEM; + goto put_bios_out; + } + child->bi_iter.bi_sector = units[1].sector; + if (!bio_add_folio(child, folio, units[1].len, + units[1].folio_ofs)) { + err = -EIO; + goto put_bios_out; + } } + } - /* Synchronize the mft mirror now if not @sync. */ - if (!sync && ni->mft_no < vol->mftmirr_size) { - int sub_err = ntfs_sync_mft_mirror_record(vol, folio, - ni->mft_no); - if (unlikely(sub_err) && !err) - err = sub_err; - } + if (ni->mft_no < vol->mftmirr_size) { + err = ntfs_sync_mft_mirror_record(vol, folio, ni->mft_no); + if (err) + ctx->error = err; + } - if (sync) { - int sub_err = submit_bio_wait(bio); + kunmap_local(kaddr); - bio_put(bio); - if (unlikely(sub_err) && !err) - err = sub_err; - } else { - folio_get(folio); - bio->bi_private = folio; - bio->bi_end_io = ntfs_bio_end_io; - submit_bio(bio); - } - offset += vol->cluster_size; - i++; + ntfs_start_mft_writeback(ctx); + if (child) { + bio_chain(child, parent); + submit_bio(child); } + submit_bio(parent); - /* If @sync, now synchronize the mft mirror. */ - if (sync && ni->mft_no < vol->mftmirr_size) { - int sub_err = - ntfs_sync_mft_mirror_record(vol, folio, ni->mft_no); - - if (unlikely(sub_err) && !err) - err = sub_err; + if (sync) { + wait_for_completion(&done); + if (!err && parent->bi_status) + err = blk_status_to_errno(parent->bi_status); + bio_put(parent); } - kunmap_local(kaddr); + if (unlikely(err)) { /* I/O error during writing. This is really bad! */ ntfs_error(vol->sb, "I/O error while writing mft record 0x%llx! Marking base inode as bad. You should unmount the volume and run chkdsk.", ni->mft_no); - goto err_out; + return err; } done: ntfs_debug("Done."); return 0; -put_bio_out: - bio_put(bio); +put_bios_out: + if (child) + bio_put(child); + bio_put(parent); unmap_err_out: kunmap_local(kaddr); err_out: diff --git a/fs/ntfs/mft.h b/fs/ntfs/mft.h index eb8a6f0d7944..28a79bf4c1c6 100644 --- a/fs/ntfs/mft.h +++ b/fs/ntfs/mft.h @@ -42,6 +42,8 @@ static inline void mark_mft_record_dirty(struct ntfs_inode *ni) __mark_mft_record_dirty(ni); } +int ntfs_mft_bioset_init(void); +void ntfs_mft_bioset_exit(void); int write_mft_record_nolock(struct ntfs_inode *ni, struct mft_record *m, int sync); /* @@ -51,16 +53,17 @@ int write_mft_record_nolock(struct ntfs_inode *ni, struct mft_record *m, int syn * @sync: if true, wait for i/o completion * * This is just a wrapper for write_mft_record_nolock() (see mft.c), which - * locks the page for the duration of the write. This ensures that there are - * no race conditions between writing the mft record via the dirty inode code - * paths and via the page cache write back code paths or between writing - * neighbouring mft records residing in the same page. + * locks the folio for the duration of the write. The folio lock prevents + * races between writing the mft record via the dirty inode code paths and via + * page cache writeback or between neighbouring mft records residing in the + * same folio. * * Locking the page also serializes us against ->read_folio() if the page is not * uptodate. * - * On success, clean the mft record and return 0. On error, leave the mft - * record dirty and return -errno. + * On success, clean the mft record and return 0. On allocation failure, + * redirty the record for retry and return 0. On other errors, return -errno + * and mark the volume with errors. */ static inline int write_mft_record(struct ntfs_inode *ni, struct mft_record *m, int sync) { diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c index 99a95ee32f22..49ec1eb1520c 100644 --- a/fs/ntfs/super.c +++ b/fs/ntfs/super.c @@ -18,6 +18,7 @@ #include "sysctl.h" #include "logfile.h" #include "index.h" +#include "mft.h" #include "ntfs.h" #include "ea.h" #include "volume.h" @@ -2685,6 +2686,13 @@ static int __init init_ntfs_fs(void) return err; } + err = ntfs_mft_bioset_init(); + if (err) { + pr_crit("Failed to initialize NTFS MFT bioset!\n"); + ntfs_workqueue_destroy(); + return err; + } + ntfs_index_ctx_cache = kmem_cache_create(ntfs_index_ctx_cache_name, sizeof(struct ntfs_index_context), 0 /* offset */, SLAB_HWCACHE_ALIGN, NULL /* ctor */); @@ -2752,6 +2760,7 @@ name_err_out: actx_err_out: kmem_cache_destroy(ntfs_index_ctx_cache); ictx_err_out: + ntfs_mft_bioset_exit(); if (!err) { pr_crit("Aborting NTFS filesystem driver registration...\n"); err = -ENOMEM; @@ -2770,6 +2779,7 @@ static void __exit exit_ntfs_fs(void) * destroy cache. */ rcu_barrier(); + ntfs_mft_bioset_exit(); #ifdef CONFIG_NTFS_FS_WOF_COMPRESSION ntfs_wof_free_workspaces(); #endif -- cgit v1.2.3 From e543dc6c5f1ed3ffa17a6e4ef522470cf521c8d6 Mon Sep 17 00:00:00 2001 From: Hyunchul Lee Date: Mon, 21 Sep 2026 15:44:08 +0900 Subject: ntfs: write MFT folios through mapped I/O units Replace the legacy MFT folio writeback loop with a mapped I/O-unit writeback path. Map the I/O units covering selected records under the runlist lock and submit them through an aggregate parent BIO with optional chained child BIOs. Defer transient mapping failures, preserve partial-progress redirty semantics, and complete folio writeback from aggregate parent completion. Do not set defer for records that generic writeback does not own. Dirty FILE records remain under direct inode writeback. other rejected records and the special $MFT extent are skipped rather than treated as transient failures. Fixes: 115380f9a2f9 ("ntfs: update mft operations") Cc: stable@vger.kernel.org Signed-off-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/mft.c | 483 +++++++++++++++++++++++++++++++++++----------------------- 1 file changed, 290 insertions(+), 193 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/mft.c b/fs/ntfs/mft.c index 51066b403757..bbbe0d4426ec 100644 --- a/fs/ntfs/mft.c +++ b/fs/ntfs/mft.c @@ -10,8 +10,10 @@ #include #include +#include #include #include +#include #include "bitmap.h" #include "lcnalloc.h" @@ -436,21 +438,6 @@ struct ntfs_mft_io_unit { unsigned int len; }; -/* - * ntfs_bio_end_io - bio completion callback for MFT record writes - * - * Decrements the folio reference count that was incremented before - * submit_bio(). This prevents a race condition where umount could - * evict the inode and release the folio while I/O is still in flight, - * potentially causing data corruption or use-after-free. - */ -static void ntfs_bio_end_io(struct bio *bio) -{ - if (bio->bi_private) - folio_put((struct folio *)bio->bi_private); - bio_put(bio); -} - struct ntfs_mft_write_ctx { struct folio *folio; struct address_space *mapping; @@ -584,8 +571,8 @@ static int ntfs_sync_mft_mirror_unit(struct ntfs_volume *vol, kunmap_local(src); bio_init(&bio, vol->sb->s_bdev, &bvec, 1, REQ_OP_WRITE); - bio.bi_iter.bi_sector = ntfs_bytes_to_bio_sector(NTFS_CLU_TO_B(vol, vol->mftmirr_lcn) + - mirror_file_ofs); + bio.bi_iter.bi_sector = ntfs_bytes_to_bio_sector( + NTFS_CLU_TO_B(vol, vol->mftmirr_lcn) + mirror_file_ofs); if (!bio_add_folio(&bio, mirror, unit->len, mirror_ofs)) err = -EIO; else @@ -857,7 +844,8 @@ static int ntfs_test_inode_wb(struct inode *vi, u64 ino, void *data) * it in @ref_vi instead of calling iput() directly. The caller must call * iput() on @ref_vi after releasing the folio lock. * - * Return 'true' if the mft record may be written out and 'false' if not. + * Return 'true' if the mft record may be written out by this path and 'false' + * if direct inode writeback owns it. * * The caller has locked the page and cleared the uptodate flag on it which * means that we can safely write out any dirty mft records that do not have @@ -954,9 +942,10 @@ static bool ntfs_may_write_mft_record(struct ntfs_volume *vol, const u64 mft_no, mft_no); /* * The write has to occur while we hold the mft record lock so - * return the locked ntfs inode. + * return the locked ntfs inode and its vfs inode reference. */ *locked_ni = ni; + *ref_vi = vi; return true; } ntfs_debug("Inode 0x%llx is not in icache.", mft_no); @@ -3001,19 +2990,141 @@ sync_rollback: return err; } -static s64 lcn_from_index(struct ntfs_volume *vol, struct ntfs_inode *ni, - unsigned long index) +static bool ntfs_add_mft_io_unit(struct bio *bio, struct folio *folio, + const struct ntfs_mft_io_unit *unit) { - s64 vcn; - s64 lcn; + if (!bio->bi_iter.bi_size) + bio->bi_iter.bi_sector = unit->sector; + else if (bio_end_sector(bio) != unit->sector) + return false; + + return bio_add_folio(bio, folio, unit->len, unit->folio_ofs); +} + +static void ntfs_release_mft_write_refs(struct ntfs_inode **locked_nis, + unsigned int nr_locked_nis, + struct inode **ref_inos, + unsigned int nr_ref_inos) +{ + while (nr_locked_nis-- > 0) { + struct ntfs_inode *tni = locked_nis[nr_locked_nis]; + + mutex_unlock(&tni->mrec_lock); + atomic_dec(&tni->count); + } + + while (nr_ref_inos-- > 0) + iput(ref_inos[nr_ref_inos]); +} + +static int ntfs_map_mft_io_locked(struct ntfs_inode *ni, u64 folio_byte, + struct ntfs_mft_io_unit *unit) +{ + struct ntfs_volume *vol = ni->vol; + u64 file_ofs = folio_byte + unit->folio_ofs; + s64 vcn, lcn; + + lockdep_assert_held(&ni->runlist.lock); + if (!ni->runlist.rl) + return -EIO; + + vcn = ntfs_bytes_to_cluster(vol, file_ofs); + /* + * $MFT is fully mapped at mount time and extensions merge already + * mapped runs under the write lock. Do not remap here: the generic + * helper may drop this read lock while upgrading it. + */ + lcn = ntfs_rl_vcn_to_lcn(ni->runlist.rl, vcn); + if (lcn == LCN_RL_NOT_MAPPED) + return -EAGAIN; + if (lcn < 0) + return -EIO; + if (ntfs_bytes_to_cluster_off(vol, file_ofs) + unit->len > + vol->cluster_size) + return -EIO; + + unit->sector = ntfs_bytes_to_bio_sector( + NTFS_CLU_TO_B(vol, lcn) + + ntfs_bytes_to_cluster_off(vol, file_ofs)); + return 0; +} - vcn = ntfs_pidx_to_cluster(vol, index); +static int ntfs_map_mft_io_for_folio(struct ntfs_inode *ni, u64 folio_byte, + struct ntfs_mft_io_unit *unit) +{ + int err; - down_read(&ni->runlist.lock); - lcn = ntfs_attr_vcn_to_lcn_nolock(ni, vcn, false); + if (!down_read_trylock(&ni->runlist.lock)) + return -EAGAIN; + err = ntfs_map_mft_io_locked(ni, folio_byte, unit); up_read(&ni->runlist.lock); + return err; +} + +static int ntfs_prepare_mft_folio_units(struct ntfs_inode *ni, u64 folio_byte, + u64 file_limit, + const unsigned long *record_writable, + struct ntfs_mft_io_unit *units, + unsigned int *nr_units, + unsigned int max_units, bool *defer) +{ + struct ntfs_volume *vol = ni->vol; + u64 folio_end = folio_byte + PAGE_SIZE; + u64 unit_byte = folio_byte; + + while (unit_byte < folio_end && unit_byte < file_limit) { + struct ntfs_mft_io_unit unit; + u64 record_byte; + u64 cluster_end = ntfs_cluster_to_bytes( + vol, ntfs_bytes_to_cluster(vol, unit_byte) + 1); + u64 record_end = + round_down(unit_byte, (u64)vol->mft_record_size) + + vol->mft_record_size; + u64 unit_end = min3(record_end, cluster_end, folio_end); + bool writable = false; + int err; + + record_byte = round_down(unit_byte, (u64)vol->mft_record_size); + while (record_byte < min(unit_end, file_limit)) { + unsigned int record; + + record = div_u64(record_byte - folio_byte, + vol->mft_record_size); + if (test_bit(record, record_writable)) { + writable = true; + break; + } + record_byte += vol->mft_record_size; + } + if (!writable) + goto next; + + unit = (struct ntfs_mft_io_unit){ + .folio_ofs = unit_byte - folio_byte, + .len = unit_end - unit_byte, + }; + err = ntfs_map_mft_io_for_folio(ni, folio_byte, &unit); + if (err == -EAGAIN) { + *defer = true; + goto next; + } + if (err) + return err; + if (*nr_units >= max_units) + return -EOVERFLOW; + units[(*nr_units)++] = unit; +next: + unit_byte = unit_end; + } + return 0; +} - return lcn; +static void ntfs_mft_write_error(struct ntfs_volume *vol, + struct address_space *mapping, int err) +{ + mapping_set_error(mapping, err); + NVolSetErrors(vol); + ntfs_error(vol->sb, "Error while writing MFT folio: %d", err); } /* @@ -3022,9 +3133,8 @@ static s64 lcn_from_index(struct ntfs_volume *vol, struct ntfs_inode *ni, * @wbc: Writeback control structure * * This function is called as part of the address_space_operations - * .writepages implementation for the $MFT inode (or $MFTMirr). - * It handles writing one folio (normally 4KiB page) worth of MFT records - * to the underlying block device. + * .writepages implementation for the $MFT inode. Despite its historical + * name, it handles one folio containing MFT records. * * Return: 0 on success, or -errno on error. */ @@ -3034,202 +3144,189 @@ static int ntfs_write_mft_block(struct folio *folio, struct writeback_control *w struct inode *vi = mapping->host; struct ntfs_inode *ni = NTFS_I(vi); struct ntfs_volume *vol = ni->vol; - u8 *kaddr; - struct ntfs_inode **locked_nis __free(kfree) = kmalloc_objs(struct ntfs_inode *, - PAGE_SIZE / NTFS_BLOCK_SIZE, - GFP_NOFS); - int nr_locked_nis = 0, err = 0, mft_ofs, prev_mft_ofs; - struct inode **ref_inos __free(kfree) = kmalloc_objs(struct inode *, - PAGE_SIZE / NTFS_BLOCK_SIZE, - GFP_NOFS); - int nr_ref_inos = 0; - struct bio *bio = NULL; - u64 mft_no; - struct ntfs_inode *tni; - s64 lcn; - s64 vcn = ntfs_pidx_to_cluster(vol, folio->index); - s64 end_vcn; - unsigned int folio_sz; + struct ntfs_inode **locked_nis __free(kfree) = + kmalloc_objs(struct ntfs_inode *, PAGE_SIZE / NTFS_BLOCK_SIZE, + GFP_NOFS); + struct inode **ref_inos __free(kfree) = + kmalloc_objs(struct inode *, PAGE_SIZE / NTFS_BLOCK_SIZE, GFP_NOFS); + struct ntfs_mft_io_unit *units __free(kfree) = NULL; + struct bio *parent = NULL, *child = NULL; + struct ntfs_mft_write_ctx *ctx = NULL; + u8 *kaddr = NULL; + DECLARE_BITMAP(record_writable, PAGE_SIZE / NTFS_BLOCK_SIZE) = {}; + u64 folio_byte, file_limit, folio_end, mirror_size; + unsigned int nr_records, nr_units = 0, max_units; + unsigned int nr_locked_nis = 0, nr_ref_inos = 0; + unsigned int record, unit_idx; + unsigned long flags; loff_t i_size; s64 allocated_size; - unsigned long flags; + bool defer = false, redirty = false; + int err = 0; read_lock_irqsave(&ni->size_lock, flags); i_size = i_size_read(vi); allocated_size = ni->allocated_size; read_unlock_irqrestore(&ni->size_lock, flags); - end_vcn = ntfs_bytes_to_cluster(vol, allocated_size); - ntfs_debug("Entering for inode 0x%llx, attribute type 0x%x, folio index 0x%lx.", - ni->mft_no, ni->type, folio->index); + ntfs_debug("Entering for inode 0x%llx, folio index 0x%lx.", ni->mft_no, + folio->index); + WARN_ON(!folio_test_locked(folio)); - if (!locked_nis || !ref_inos) { - folio_redirty_for_writepage(wbc, folio); - folio_unlock(folio); - return -ENOMEM; + folio_byte = folio_pos(folio); + folio_end = folio_byte + PAGE_SIZE; + mirror_size = (u64)vol->mftmirr_size * vol->mft_record_size; + + if (i_size <= folio_byte) + folio_zero_segment(folio, 0, PAGE_SIZE); + else if ((u64)i_size < folio_end) + folio_zero_segment(folio, i_size - folio_byte, PAGE_SIZE); + + file_limit = min_t(u64, i_size, allocated_size); + nr_records = PAGE_SIZE / vol->mft_record_size; + max_units = PAGE_SIZE / min(vol->cluster_size, vol->mft_record_size); + units = kmalloc_array(max_units, sizeof(*units), GFP_NOFS); + if (!locked_nis || !ref_inos || !units) { + err = -ENOMEM; + goto out_noio; } - /* We have to zero every time due to mmap-at-end-of-file. */ - if (folio->index >= (i_size >> folio_shift(folio))) - /* The page straddles i_size. */ - folio_zero_segment(folio, - offset_in_folio(folio, i_size), - folio_size(folio)); + kaddr = kmap_local_folio(folio, 0); + folio_clear_uptodate(folio); + + for (record = 0; record < nr_records; record++) { + struct ntfs_inode *tni = NULL; + struct inode *ref_vi = NULL; + u64 record_byte = + folio_byte + (u64)record * vol->mft_record_size; + u64 mft_no; + + if (record_byte >= file_limit) + continue; + mft_no = record_byte >> vol->mft_record_size_bits; + if (!ntfs_may_write_mft_record( + vol, mft_no, + (struct mft_record *)(kaddr + + record * vol->mft_record_size), + &tni, &ref_vi)) { + if (ref_vi) + ref_inos[nr_ref_inos++] = ref_vi; + continue; + } + if (ref_vi) + ref_inos[nr_ref_inos++] = ref_vi; + if (tni) { + locked_nis[nr_locked_nis++] = tni; + if (tni->nr_extents < 0 && + tni->ext.base_ntfs_ino == NTFS_I(vol->mft_ino)) + continue; + } + __set_bit(record, record_writable); + } - lcn = lcn_from_index(vol, ni, folio->index); - if (lcn <= LCN_HOLE) { + err = ntfs_prepare_mft_folio_units(ni, folio_byte, file_limit, + record_writable, units, &nr_units, + max_units, &defer); + if (err) + goto out_noio; + if (!nr_units) { + if (defer) + goto out_noio; + folio_mark_uptodate(folio); + kunmap_local(kaddr); folio_start_writeback(folio); folio_unlock(folio); folio_end_writeback(folio); - return -EIO; + ntfs_release_mft_write_refs(locked_nis, nr_locked_nis, ref_inos, + nr_ref_inos); + return 0; } - /* Map folio so we can access its contents. */ - kaddr = kmap_local_folio(folio, 0); - /* Clear the page uptodate flag whilst the mst fixups are applied. */ - folio_clear_uptodate(folio); + parent = ntfs_alloc_mft_parent_bio(vol, folio, NULL); + if (!parent) { + err = -ENOMEM; + goto out_noio; + } + ctx = container_of(parent, struct ntfs_mft_write_ctx, bio); - for (mft_ofs = 0; mft_ofs < PAGE_SIZE && vcn < end_vcn; - mft_ofs += vol->mft_record_size) { - /* Get the mft record number. */ - mft_no = (((s64)folio->index << PAGE_SHIFT) + mft_ofs) >> - vol->mft_record_size_bits; - vcn = ntfs_mft_no_to_cluster(vol, mft_no); - /* Check whether to write this mft record. */ - tni = NULL; - if (ntfs_may_write_mft_record(vol, mft_no, - (struct mft_record *)(kaddr + mft_ofs), - &tni, &ref_inos[nr_ref_inos])) { - unsigned int mft_record_off = 0; - s64 vcn_off = vcn; - s64 rl_len = 0; + if (!ntfs_add_mft_io_unit(parent, folio, &units[0])) { + err = -EIO; + goto out_noio; + } - /* - * The record should be written. If a locked ntfs - * inode was returned, add it to the array of locked - * ntfs inodes. - */ - if (tni) - locked_nis[nr_locked_nis++] = tni; - else if (ref_inos[nr_ref_inos]) - nr_ref_inos++; - - if (bio && (mft_ofs != prev_mft_ofs + vol->mft_record_size)) { -flush_bio: - bio->bi_end_io = ntfs_bio_end_io; - submit_bio(bio); - bio = NULL; - } + for (unit_idx = 0; unit_idx < nr_units; unit_idx++) { + struct ntfs_mft_io_unit *unit = &units[unit_idx]; + u64 file_ofs = folio_byte + unit->folio_ofs; - if (vol->cluster_size < folio_size(folio)) { - struct runlist_element *rl; + if (unit_idx) { + struct bio *target = child ? child : parent; - down_write(&ni->runlist.lock); - rl = ntfs_attr_vcn_to_rl(ni, vcn_off, &lcn); - if (!IS_ERR(rl)) - rl_len = rl->length - (vcn_off - rl->vcn); - up_write(&ni->runlist.lock); - if (IS_ERR(rl) || lcn < 0) { - err = -EIO; - goto unm_done; + if (!ntfs_add_mft_io_unit(target, folio, unit)) { + if (child) { + ntfs_start_mft_writeback(ctx); + bio_chain(child, parent); + submit_bio(child); + child = NULL; } - - if (bio && - (bio_end_sector(bio) >> (vol->cluster_size_bits - 9)) != - lcn) { - bio->bi_end_io = ntfs_bio_end_io; - submit_bio(bio); - bio = NULL; + child = bio_alloc(vol->sb->s_bdev, 1, + REQ_OP_WRITE, GFP_NOIO); + if (!child) { + defer = true; + break; + } + if (!ntfs_add_mft_io_unit(child, folio, unit)) { + bio_put(child); + child = NULL; + if (!ctx->error) + ctx->error = -EIO; + redirty = true; + err = -EIO; + break; } } + } + if (file_ofs < mirror_size) { + int mirror_err = ntfs_sync_mft_mirror_unit( + vol, folio, file_ofs, unit); - if (!bio) { - unsigned int off; - - off = ((mft_no << vol->mft_record_size_bits) + - mft_record_off) & vol->cluster_size_mask; - - bio = bio_alloc(vol->sb->s_bdev, 1, REQ_OP_WRITE, - GFP_NOIO); - bio->bi_iter.bi_sector = - ntfs_bytes_to_bio_sector( - ntfs_cluster_to_bytes(vol, lcn) + off); - } - - if (vol->cluster_size == NTFS_BLOCK_SIZE && - (mft_record_off || - rl_len == 1 || - mft_ofs + NTFS_BLOCK_SIZE >= PAGE_SIZE)) - folio_sz = NTFS_BLOCK_SIZE; - else - folio_sz = vol->mft_record_size; - if (!bio_add_folio(bio, folio, folio_sz, - mft_ofs + mft_record_off)) { - err = -EIO; - bio_put(bio); - goto unm_done; - } - mft_record_off += folio_sz; - - if (mft_record_off != vol->mft_record_size) { - vcn_off++; - goto flush_bio; - } - prev_mft_ofs = mft_ofs; - - if (mft_no < vol->mftmirr_size) { - int sub_err = ntfs_sync_mft_mirror_record(vol, folio, mft_no); - - if (unlikely(sub_err) && !err) - err = sub_err; - } - } else if (ref_inos[nr_ref_inos]) - nr_ref_inos++; + if (mirror_err && !ctx->error) + ctx->error = mirror_err; + } } - if (bio) { - bio->bi_end_io = ntfs_bio_end_io; - submit_bio(bio); + if (child) { + ntfs_start_mft_writeback(ctx); + bio_chain(child, parent); + submit_bio(child); } -unm_done: + ntfs_start_mft_writeback(ctx); folio_mark_uptodate(folio); + if (defer || redirty) + folio_redirty_for_writepage(wbc, folio); kunmap_local(kaddr); - - folio_start_writeback(folio); + kaddr = NULL; folio_unlock(folio); - folio_end_writeback(folio); - - /* Unlock any locked inodes. */ - while (nr_locked_nis-- > 0) { - struct ntfs_inode *base_tni; - - tni = locked_nis[nr_locked_nis]; - mutex_unlock(&tni->mrec_lock); - - /* Get the base inode. */ - mutex_lock(&tni->extent_lock); - if (tni->nr_extents >= 0) - base_tni = tni; - else - base_tni = tni->ext.base_ntfs_ino; - mutex_unlock(&tni->extent_lock); - ntfs_debug("Unlocking %s inode 0x%llx.", - tni == base_tni ? "base" : "extent", - tni->mft_no); - atomic_dec(&tni->count); - iput(VFS_I(base_tni)); - } + submit_bio(parent); + ntfs_release_mft_write_refs(locked_nis, nr_locked_nis, ref_inos, + nr_ref_inos); + return 0; - /* Dropping deferred references */ - while (nr_ref_inos-- > 0) { - if (ref_inos[nr_ref_inos]) - iput(ref_inos[nr_ref_inos]); +out_noio: + if (kaddr) { + folio_mark_uptodate(folio); + kunmap_local(kaddr); } - - if (unlikely(err && err != -ENOMEM)) - NVolSetErrors(vol); - if (likely(!err)) - ntfs_debug("Done."); + if (err && err != -ENOMEM) + ntfs_mft_write_error(vol, mapping, err); + if (parent) + bio_put(parent); + if (redirty || defer || err == -ENOMEM) + folio_redirty_for_writepage(wbc, folio); + folio_unlock(folio); + ntfs_release_mft_write_refs(locked_nis, nr_locked_nis, ref_inos, + nr_ref_inos); + if (err == -ENOMEM || (defer && !err)) + return 0; return err; } -- cgit v1.2.3 From 65811a614bc9a567b6a27f24de3c187dcc2aec32 Mon Sep 17 00:00:00 2001 From: Hyunchul Lee Date: Mon, 21 Sep 2026 15:44:09 +0900 Subject: ntfs: write MFT records through containing I/O units Use the configured MFT I/O unit for direct writes, mirror updates, and generic folio writeback. On native 4Kn devices, multiple MFT records can share a device block, so submit complete containing units while preserving unselected neighboring records. Track selected records separately from records owned by direct inode writeback, and retain the existing runlist and cluster-boundary checks. Force direct MFT writes to complete synchronously when the I/O unit is larger than an individual record. the following patch restores caller-controlled completion. Fixes: 115380f9a2f9 ("ntfs: update mft operations") Cc: stable@vger.kernel.org Signed-off-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/mft.c | 61 +++++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 45 insertions(+), 16 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/mft.c b/fs/ntfs/mft.c index bbbe0d4426ec..77acc26a85d9 100644 --- a/fs/ntfs/mft.c +++ b/fs/ntfs/mft.c @@ -597,13 +597,18 @@ out_unlock: static int ntfs_sync_mft_mirror_record(struct ntfs_volume *vol, struct folio *source, const u64 mft_no) { + u64 mirror_file_ofs = (u64)mft_no * vol->mft_record_size; struct ntfs_mft_io_unit unit = { .folio_ofs = NTFS_MFT_NR_TO_POFS(vol, mft_no), .len = vol->mft_record_size, }; - return ntfs_sync_mft_mirror_unit(vol, source, - (u64)mft_no * vol->mft_record_size, &unit); + mirror_file_ofs = + round_down(mirror_file_ofs, (u64)vol->mft_io_unit_size); + unit.folio_ofs = round_down(unit.folio_ofs, vol->mft_io_unit_size); + unit.len = vol->mft_io_unit_size; + + return ntfs_sync_mft_mirror_unit(vol, source, mirror_file_ofs, &unit); } static int ntfs_prepare_mft_record_io_units(struct ntfs_inode *ni, @@ -623,6 +628,18 @@ static int ntfs_prepare_mft_record_io_units(struct ntfs_inode *ni, return -EIO; cluster_ofs = ntfs_bytes_to_cluster_off(vol, record_byte); + if (vol->mft_io_unit_size > vol->mft_record_size) { + cluster_ofs = round_down(cluster_ofs, vol->mft_io_unit_size); + disk_byte = NTFS_CLU_TO_B(vol, ni->mft_lcn[0]) + cluster_ofs; + units[0] = (struct ntfs_mft_io_unit){ + .sector = ntfs_bytes_to_bio_sector(disk_byte), + .folio_ofs = round_down(ni->folio_ofs, + vol->mft_io_unit_size), + .len = vol->mft_io_unit_size, + }; + return 1; + } + disk_byte = NTFS_CLU_TO_B(vol, ni->mft_lcn[0]) + cluster_ofs; units[0] = (struct ntfs_mft_io_unit){ .sector = ntfs_bytes_to_bio_sector(disk_byte), @@ -676,6 +693,8 @@ int write_mft_record_nolock(struct ntfs_inode *ni, struct mft_record *m, int syn WARN_ON(NInoAttr(ni)); WARN_ON(!folio_test_locked(folio)); + if (vol->mft_io_unit_size > vol->mft_record_size) + sync = 1; if (folio_test_writeback(folio)) folio_wait_writeback(folio); @@ -3063,7 +3082,7 @@ static int ntfs_map_mft_io_for_folio(struct ntfs_inode *ni, u64 folio_byte, static int ntfs_prepare_mft_folio_units(struct ntfs_inode *ni, u64 folio_byte, u64 file_limit, - const unsigned long *record_writable, + const unsigned long *record_selected, struct ntfs_mft_io_unit *units, unsigned int *nr_units, unsigned int max_units, bool *defer) @@ -3075,28 +3094,37 @@ static int ntfs_prepare_mft_folio_units(struct ntfs_inode *ni, u64 folio_byte, while (unit_byte < folio_end && unit_byte < file_limit) { struct ntfs_mft_io_unit unit; u64 record_byte; - u64 cluster_end = ntfs_cluster_to_bytes( - vol, ntfs_bytes_to_cluster(vol, unit_byte) + 1); - u64 record_end = - round_down(unit_byte, (u64)vol->mft_record_size) + - vol->mft_record_size; - u64 unit_end = min3(record_end, cluster_end, folio_end); - bool writable = false; + u64 unit_end; + u64 io_unit_end; + u64 cluster_end; + bool selected = false; int err; + io_unit_end = + round_down(unit_byte, (u64)vol->mft_io_unit_size) + + vol->mft_io_unit_size; + cluster_end = ntfs_cluster_to_bytes( + vol, ntfs_bytes_to_cluster(vol, unit_byte) + 1); + unit_end = min3(io_unit_end, cluster_end, folio_end); + record_byte = round_down(unit_byte, (u64)vol->mft_record_size); while (record_byte < min(unit_end, file_limit)) { unsigned int record; record = div_u64(record_byte - folio_byte, vol->mft_record_size); - if (test_bit(record, record_writable)) { - writable = true; + if (test_bit(record, record_selected)) { + selected = true; break; } record_byte += vol->mft_record_size; } - if (!writable) + /* + * Direct inode writeback owns unselected records. Their folio + * images are stable while this folio is locked, so a containing + * unit can preserve them when writing a selected record. + */ + if (!selected) goto next; unit = (struct ntfs_mft_io_unit){ @@ -3153,7 +3181,7 @@ static int ntfs_write_mft_block(struct folio *folio, struct writeback_control *w struct bio *parent = NULL, *child = NULL; struct ntfs_mft_write_ctx *ctx = NULL; u8 *kaddr = NULL; - DECLARE_BITMAP(record_writable, PAGE_SIZE / NTFS_BLOCK_SIZE) = {}; + DECLARE_BITMAP(record_selected, PAGE_SIZE / NTFS_BLOCK_SIZE) = {}; u64 folio_byte, file_limit, folio_end, mirror_size; unsigned int nr_records, nr_units = 0, max_units; unsigned int nr_locked_nis = 0, nr_ref_inos = 0; @@ -3221,11 +3249,11 @@ static int ntfs_write_mft_block(struct folio *folio, struct writeback_control *w tni->ext.base_ntfs_ino == NTFS_I(vol->mft_ino)) continue; } - __set_bit(record, record_writable); + __set_bit(record, record_selected); } err = ntfs_prepare_mft_folio_units(ni, folio_byte, file_limit, - record_writable, units, &nr_units, + record_selected, units, &nr_units, max_units, &defer); if (err) goto out_noio; @@ -3309,6 +3337,7 @@ static int ntfs_write_mft_block(struct folio *folio, struct writeback_control *w submit_bio(parent); ntfs_release_mft_write_refs(locked_nis, nr_locked_nis, ref_inos, nr_ref_inos); + return 0; out_noio: -- cgit v1.2.3 From 2f49869cc13b759952427c9b1288ac786ffc5aa0 Mon Sep 17 00:00:00 2001 From: Hyunchul Lee Date: Mon, 21 Sep 2026 15:44:10 +0900 Subject: ntfs: use sync flag for direct MFT writes Use the supplied sync flag for direct MFT writes instead of forcing containing-unit writes to complete synchronously. Pass sync=1 from ntfs_mft_record_free() so the MFT bitmap entry is not released until the record write completes. Other direct-write callers keep the completion mode requested by their caller. The folio lock serializes write preparation, and PG_writeback keeps the whole unit stable until all chained BIOs complete, so asynchronous containing-unit writes remain safe. Fixes: 115380f9a2f9 ("ntfs: update mft operations") Cc: stable@vger.kernel.org Signed-off-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/mft.c | 17 ++++++++++------- fs/ntfs/mft.h | 14 ++++++++------ 2 files changed, 18 insertions(+), 13 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/mft.c b/fs/ntfs/mft.c index 77acc26a85d9..8bb8b4085c8b 100644 --- a/fs/ntfs/mft.c +++ b/fs/ntfs/mft.c @@ -671,9 +671,13 @@ static int ntfs_prepare_mft_record_io_units(struct ntfs_inode *ni, * * We only write the mft record if the ntfs inode @ni is dirty. * - * On success, clean the mft record and return 0. - * On error (specifically ENOMEM), we redirty the record so it can be retried. - * For other errors, we mark the volume with errors. + * On success, clean the mft record and return 0. On ENOMEM, redirty the + * record so it can be retried. Asynchronous callers return success after + * redirtying while synchronous callers receive the error. For other errors, + * mark the volume with errors. + * + * If @sync is false, PG_writeback keeps the folio stable and serializes later + * writers until the I/O completes. */ int write_mft_record_nolock(struct ntfs_inode *ni, struct mft_record *m, int sync) { @@ -693,8 +697,6 @@ int write_mft_record_nolock(struct ntfs_inode *ni, struct mft_record *m, int syn WARN_ON(NInoAttr(ni)); WARN_ON(!folio_test_locked(folio)); - if (vol->mft_io_unit_size > vol->mft_record_size) - sync = 1; if (folio_test_writeback(folio)) folio_wait_writeback(folio); @@ -804,7 +806,8 @@ err_out: ntfs_error(vol->sb, "Not enough memory to write mft record. Redirtying so the write is retried later."); mark_mft_record_dirty(ni); - err = 0; + if (!sync) + err = 0; } else NVolSetErrors(vol); return err; @@ -2959,7 +2962,7 @@ int ntfs_mft_record_free(struct ntfs_volume *vol, struct ntfs_inode *ni) * record to be freed is guaranteed to do it already. */ NInoSetDirty(ni); - err = write_mft_record(ni, ni_mrec, 0); + err = write_mft_record(ni, ni_mrec, 1); if (err) goto sync_rollback; diff --git a/fs/ntfs/mft.h b/fs/ntfs/mft.h index 28a79bf4c1c6..d2a31205e08c 100644 --- a/fs/ntfs/mft.h +++ b/fs/ntfs/mft.h @@ -53,17 +53,19 @@ int write_mft_record_nolock(struct ntfs_inode *ni, struct mft_record *m, int syn * @sync: if true, wait for i/o completion * * This is just a wrapper for write_mft_record_nolock() (see mft.c), which - * locks the folio for the duration of the write. The folio lock prevents - * races between writing the mft record via the dirty inode code paths and via - * page cache writeback or between neighbouring mft records residing in the - * same folio. + * locks the folio while preparing the write. write_mft_record_nolock() waits + * for prior folio writeback before modifying the folio and keeps PG_writeback + * set until the submitted I/O completes. Together these serialize dirty + * inode writes, page cache writeback, and neighbouring mft record writes in + * the same folio. * * Locking the page also serializes us against ->read_folio() if the page is not * uptodate. * * On success, clean the mft record and return 0. On allocation failure, - * redirty the record for retry and return 0. On other errors, return -errno - * and mark the volume with errors. + * redirty the record for retry. Asynchronous callers return 0 after + * redirtying, while synchronous callers receive -ENOMEM. On other errors, + * return -errno and mark the volume with errors. */ static inline int write_mft_record(struct ntfs_inode *ni, struct mft_record *m, int sync) { -- cgit v1.2.3 From c3448f5b0db12c22001821fa72788648a693c9d2 Mon Sep 17 00:00:00 2001 From: Hyunchul Lee Date: Mon, 21 Sep 2026 22:40:16 +0900 Subject: ntfs: trigger page_mkwrite for native 4Kn size extension On native 4Kn volumes, pagecache_isize_extended() can leave a writable mapping for a partial old-EOF page. A subsequent shared mmap write can then bypass ntfs_filemap_page_mkwrite(), leaving NTFS initialized_size at the old EOF. Invalidate the partial old-EOF page after size extension, independent of the filesystem block size, so the next mmap write enters the NTFS page_mkwrite path. Apply the same helper to zero-fill fallocate extensions and compare the actual new size after the operation so extensions overlapping the old EOF are handled as well. Fixes: 115380f9a2f9 ("ntfs: update mft operations") Cc: stable@vger.kernel.org Signed-off-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/file.c | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c index 007d1614b9ac..7c7cbd16e944 100644 --- a/fs/ntfs/file.c +++ b/fs/ntfs/file.c @@ -252,6 +252,22 @@ static int ntfs_file_fsync(struct file *filp, loff_t start, loff_t end, return ret; } +static void ntfs_pagecache_extend(struct inode *vi, loff_t from, loff_t to) +{ + pagecache_isize_extended(vi, from, to); + + /* + * NTFS initialized_size is byte-granular, so a partial old-EOF page + * must stay write-protected until page_mkwrite() updates it. The + * generic helper skips this when the block size is at least a page, + * the extension ends before the rounded block boundary, or that + * boundary is page-aligned. + */ + if (from < to && from & (PAGE_SIZE - 1)) + unmap_mapping_range(vi->i_mapping, round_down(from, PAGE_SIZE), + PAGE_SIZE, 0); +} + static int ntfs_setattr_size(struct inode *vi, struct iattr *attr) { struct ntfs_inode *ni = NTFS_I(vi); @@ -281,7 +297,7 @@ static int ntfs_setattr_size(struct inode *vi, struct iattr *attr) 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); + ntfs_pagecache_extend(vi, old_size, attr->ia_size); } else { truncate_setsize(vi, attr->ia_size); } @@ -1131,7 +1147,7 @@ static long ntfs_fallocate(struct file *file, int mode, loff_t offset, loff_t le struct ntfs_inode *ni = NTFS_I(vi); struct ntfs_volume *vol = ni->vol; int err = 0; - loff_t old_size; + loff_t old_size, new_size; if (mode & ~(NTFS_FALLOC_FL_SUPPORTED)) return -EOPNOTSUPP; @@ -1159,13 +1175,12 @@ static long ntfs_fallocate(struct file *file, int mode, loff_t offset, loff_t le return err; } - old_size = i_size_read(vi); - inode_lock(vi); if (NInoCompressed(ni) || NInoEncrypted(ni) || NInoWofCompressed(ni)) { inode_unlock(vi); return -EOPNOTSUPP; } + old_size = i_size_read(vi); inode_dio_wait(vi); /* Take invalidate_lock for all fallocate operations to prevent races */ @@ -1194,10 +1209,12 @@ 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); + if (!err && mode == 0 && NInoNonResident(ni)) { + new_size = i_size_read(vi); + if (new_size > old_size) { + truncate_pagecache(vi, old_size); + ntfs_pagecache_extend(vi, old_size, new_size); + } } filemap_invalidate_unlock(vi->i_mapping); -- cgit v1.2.3 From 8af3725917e90559b7723b308bac49550556bd85 Mon Sep 17 00:00:00 2001 From: Hyunchul Lee Date: Mon, 21 Sep 2026 15:44:12 +0900 Subject: ntfs: wait for attribute folio writeback before update ntfs_inode_attr_pwrite() locks an attribute folio before copying a new attribute image into it, but taking the folio lock does not wait for an already-running writeback BIO to complete. The writeback path can unlock the folio while PG_writeback remains set and the BIO still references the folio memory. Fixes: 115380f9a2f9 ("ntfs: update mft operations") Cc: stable@vger.kernel.org Signed-off-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/inode.c | 2 ++ 1 file changed, 2 insertions(+) (limited to 'fs') diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c index a777de8a80c7..b3c5219c7b77 100644 --- a/fs/ntfs/inode.c +++ b/fs/ntfs/inode.c @@ -3739,6 +3739,8 @@ static s64 __ntfs_inode_non_resident_attr_pwrite(struct inode *vi, folio_lock(folio); } + folio_wait_writeback(folio); + if (count == PAGE_SIZE) { offset = 0; attr_len = count; -- cgit v1.2.3 From b7de03adad59073116e8c51cfc6a5f67c5f39416 Mon Sep 17 00:00:00 2001 From: Hyunchul Lee Date: Sun, 13 Sep 2026 09:02:36 +0800 Subject: ntfs: fix lookup of POSIX names with Windows-invalid characters POSIX FILE_NAME entries may contain characters such as '?' that are invalid in the Windows namespace. Directory index lookup currently passes an invalid-character error value to ntfs_collate_names(), causing lookup and removal of such names to fail with -EINVAL. Allow callers to disable invalid-character errors when collation is used for index traversal, and suppress those errors when comparing POSIX FILE_NAME attributes. Signed-off-by: Hyunchul Lee Tested-by: Baolin Liu Signed-off-by: Namjae Jeon --- fs/ntfs/attrib.c | 8 ++++---- fs/ntfs/collate.c | 6 +++--- fs/ntfs/dir.c | 8 ++++---- fs/ntfs/ntfs.h | 4 ++-- fs/ntfs/unistr.c | 29 ++++++++++++++++++++--------- 5 files changed, 33 insertions(+), 22 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c index c949ff765075..eb6a166cfcc7 100644 --- a/fs/ntfs/attrib.c +++ b/fs/ntfs/attrib.c @@ -909,7 +909,7 @@ static int ntfs_attr_find(const __le32 type, const __le16 *name, rc = ntfs_collate_names(name, name_len, (__le16 *)((u8 *)a + le16_to_cpu(a->name_offset)), - a->name_length, 1, IGNORE_CASE, + a->name_length, true, IGNORE_CASE, upcase, upcase_len); /* * If @name collates before a->name, there is no @@ -922,7 +922,7 @@ static int ntfs_attr_find(const __le32 type, const __le16 *name, continue; rc = ntfs_collate_names(name, name_len, (__le16 *)((u8 *)a + le16_to_cpu(a->name_offset)), - a->name_length, 1, CASE_SENSITIVE, + a->name_length, true, CASE_SENSITIVE, upcase, upcase_len); if (rc == -1) return -ENOENT; @@ -1313,7 +1313,7 @@ find_attr_list_attr: register int rc; rc = ntfs_collate_names(name, name_len, al_name, - al_name_len, 1, IGNORE_CASE, + al_name_len, true, IGNORE_CASE, vol->upcase, vol->upcase_len); /* * If @name collates before al_name, there is no @@ -1326,7 +1326,7 @@ find_attr_list_attr: continue; rc = ntfs_collate_names(name, name_len, al_name, - al_name_len, 1, CASE_SENSITIVE, + al_name_len, true, CASE_SENSITIVE, vol->upcase, vol->upcase_len); if (rc == -1) goto not_found; diff --git a/fs/ntfs/collate.c b/fs/ntfs/collate.c index 77e34038902d..885efebe8959 100644 --- a/fs/ntfs/collate.c +++ b/fs/ntfs/collate.c @@ -100,11 +100,11 @@ static int ntfs_collate_file_name(struct ntfs_volume *vol, { int rc; - rc = ntfs_file_compare_values(data1, data2, -EINVAL, - IGNORE_CASE, vol->upcase, vol->upcase_len); + rc = ntfs_file_compare_values(data1, data2, + true, IGNORE_CASE, vol->upcase, vol->upcase_len); if (!rc) rc = ntfs_file_compare_values(data1, data2, - -EINVAL, CASE_SENSITIVE, vol->upcase, vol->upcase_len); + true, CASE_SENSITIVE, vol->upcase, vol->upcase_len); return rc; } diff --git a/fs/ntfs/dir.c b/fs/ntfs/dir.c index df60138f9b2d..54b706d807b4 100644 --- a/fs/ntfs/dir.c +++ b/fs/ntfs/dir.c @@ -238,7 +238,7 @@ found_it: */ rc = ntfs_collate_names(uname, uname_len, (__le16 *)&ie->key.file_name.file_name, - ie->key.file_name.file_name_length, 1, + ie->key.file_name.file_name_length, false, IGNORE_CASE, vol->upcase, vol->upcase_len); /* * If uname collates before the name of the current entry, there @@ -257,7 +257,7 @@ found_it: */ rc = ntfs_collate_names(uname, uname_len, (__le16 *)&ie->key.file_name.file_name, - ie->key.file_name.file_name_length, 1, + ie->key.file_name.file_name_length, false, CASE_SENSITIVE, vol->upcase, vol->upcase_len); if (rc == -1) break; @@ -474,7 +474,7 @@ found_it2: */ rc = ntfs_collate_names(uname, uname_len, (__le16 *)&ie->key.file_name.file_name, - ie->key.file_name.file_name_length, 1, + ie->key.file_name.file_name_length, false, IGNORE_CASE, vol->upcase, vol->upcase_len); /* * If uname collates before the name of the current entry, there @@ -493,7 +493,7 @@ found_it2: */ rc = ntfs_collate_names(uname, uname_len, (__le16 *)&ie->key.file_name.file_name, - ie->key.file_name.file_name_length, 1, + ie->key.file_name.file_name_length, false, CASE_SENSITIVE, vol->upcase, vol->upcase_len); if (rc == -1) break; diff --git a/fs/ntfs/ntfs.h b/fs/ntfs/ntfs.h index 45f77848a9cf..1bee1626abdf 100644 --- a/fs/ntfs/ntfs.h +++ b/fs/ntfs/ntfs.h @@ -234,14 +234,14 @@ bool ntfs_are_names_equal(const __le16 *s1, size_t s1_len, const __le16 *upcase, const u32 upcase_size); int ntfs_collate_names(const __le16 *name1, const u32 name1_len, const __le16 *name2, const u32 name2_len, - const int err_val, const u32 ic, + const bool check_invalid, const u32 ic, const __le16 *upcase, const u32 upcase_len); int ntfs_ucsncmp(const __le16 *s1, const __le16 *s2, size_t n); int ntfs_ucsncasecmp(const __le16 *s1, const __le16 *s2, size_t n, const __le16 *upcase, const u32 upcase_size); int ntfs_file_compare_values(const struct file_name_attr *file_name_attr1, const struct file_name_attr *file_name_attr2, - const int err_val, const u32 ic, + const bool check_invalid, const u32 ic, const __le16 *upcase, const u32 upcase_len); int ntfs_nlstoucs(const struct ntfs_volume *vol, const char *ins, const int ins_len, __le16 **outs, int max_name_len); diff --git a/fs/ntfs/unistr.c b/fs/ntfs/unistr.c index 7f11a2825527..733bd6fe8599 100644 --- a/fs/ntfs/unistr.c +++ b/fs/ntfs/unistr.c @@ -64,7 +64,8 @@ bool ntfs_are_names_equal(const __le16 *s1, size_t s1_len, * @name1_len: first Unicode name length * @name2: second Unicode name to compare * @name2_len: second Unicode name length - * @err_val: if @name1 contains an invalid character return this value + * @check_invalid: if true and @name1 contains an invalid character, + * return -EINVAL * @ic: either CASE_SENSITIVE or IGNORE_CASE * @upcase: upcase table (ignored if @ic is CASE_SENSITIVE) * @upcase_len: upcase table size (ignored if @ic is CASE_SENSITIVE) @@ -74,13 +75,14 @@ bool ntfs_are_names_equal(const __le16 *s1, size_t s1_len, * -1 if the first name collates before the second one, * 0 if the names match, * 1 if the second name collates before the first one, or - * @err_val if an invalid character is found in @name1 during the comparison. + * -EINVAL if @check_invalid is true and an invalid character is found in + * @name1 during the comparison. * * The following characters are considered invalid: '"', '*', '<', '>' and '?'. */ int ntfs_collate_names(const __le16 *name1, const u32 name1_len, const __le16 *name2, const u32 name2_len, - const int err_val, const u32 ic, + const bool check_invalid, const u32 ic, const __le16 *upcase, const u32 upcase_len) { u32 cnt, min_len; @@ -98,8 +100,8 @@ int ntfs_collate_names(const __le16 *name1, const u32 name1_len, if (c2 < upcase_len) c2 = le16_to_cpu(upcase[c2]); } - if (c1 < 64 && legal_ansi_char_array[c1] & 8) - return err_val; + if (check_invalid && c1 < 64 && legal_ansi_char_array[c1] & 8) + return -EINVAL; if (c1 < c2) return -1; if (c1 > c2) @@ -111,8 +113,8 @@ int ntfs_collate_names(const __le16 *name1, const u32 name1_len, return 0; /* name1_len > name2_len */ c1 = le16_to_cpu(*name1); - if (c1 < 64 && legal_ansi_char_array[c1] & 8) - return err_val; + if (check_invalid && c1 < 64 && legal_ansi_char_array[c1] & 8) + return -EINVAL; return 1; } @@ -191,14 +193,23 @@ int ntfs_ucsncasecmp(const __le16 *s1, const __le16 *s2, size_t n, int ntfs_file_compare_values(const struct file_name_attr *file_name_attr1, const struct file_name_attr *file_name_attr2, - const int err_val, const u32 ic, + const bool check_invalid, const u32 ic, const __le16 *upcase, const u32 upcase_len) { + bool compare_check = check_invalid; + + /* + * POSIX file names may contain characters that are invalid in the + * Windows namespace, so compare them without treating them as errors. + */ + if (file_name_attr1->file_name_type == FILE_NAME_POSIX) + compare_check = false; + return ntfs_collate_names((__le16 *)&file_name_attr1->file_name, file_name_attr1->file_name_length, (__le16 *)&file_name_attr2->file_name, file_name_attr2->file_name_length, - err_val, ic, upcase, upcase_len); + compare_check, ic, upcase, upcase_len); } /* -- cgit v1.2.3 From 111de0d5a18c991bcfe507194da7a809f4276a73 Mon Sep 17 00:00:00 2001 From: Marek Wesołowski Date: Tue, 15 Sep 2026 09:47:22 +0200 Subject: ntfs: fix directory lookup on large-page systems MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ntfs_lookup_inode_by_name() copies PAGE_SIZE bytes from the index allocation mapping and applies MST fixups to that entire buffer before selecting the requested INDX block. When PAGE_SIZE is larger than the NTFS index block size, the USA count belongs to the smaller INDX block. post_read_mst_fixup() therefore treats the PAGE_SIZE call as not applicable and leaves the update sequence words in place. A directory entry crossing a sector boundary can then be returned by readdir but missed by lookup. Select the INDX block first, apply the fixup using its actual block size, and propagate fixup failures before validating the block. Report fixup failures through ntfs_error() so the configured errors= policy is applied. Return -EIO when post_read_mst_fixup() detects an incomplete multi sector transfer, since this indicates an on-disk metadata integrity error. Reproduced on arm64 with 16 KiB pages and 4 KiB NTFS index blocks. The affected entry was visible in readdir but stat returned ENOENT. Both exact and case-insensitive lookup succeed after this change. Fixes: 0a8ac0c1fa0b ("ntfs: update directory operations") Signed-off-by: Marek Wesołowski Reviewed-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/dir.c | 9 ++++++++- fs/ntfs/mst.c | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/dir.c b/fs/ntfs/dir.c index 54b706d807b4..c2b48bc696bf 100644 --- a/fs/ntfs/dir.c +++ b/fs/ntfs/dir.c @@ -331,7 +331,6 @@ descend_into_child_node: } memcpy_from_folio(kaddr, folio, 0, PAGE_SIZE); - post_read_mst_fixup((struct ntfs_record *)kaddr, PAGE_SIZE); folio_unlock(folio); folio_put(folio); fast_descend_into_child_node: @@ -352,6 +351,14 @@ fast_descend_into_child_node: vcn, dir_ni->mft_no); goto unm_err_out; } + err = post_read_mst_fixup((struct ntfs_record *)ia, + dir_ni->itype.index.block_size); + if (err) { + ntfs_error(sb, + "MST fixup failed for index block vcn %lld in directory inode 0x%llx.", + vcn, dir_ni->mft_no); + goto unm_err_out; + } err = ntfs_index_block_inconsistent(vol, ia, dir_ni->itype.index.block_size, vcn, COLLATION_FILE_NAME, diff --git a/fs/ntfs/mst.c b/fs/ntfs/mst.c index 7f9faad924ad..cc6363413449 100644 --- a/fs/ntfs/mst.c +++ b/fs/ntfs/mst.c @@ -19,7 +19,7 @@ * magic of the ntfs record header being processed with "BAAD" (in memory only!) * and abort processing. * - * Return 0 on success and -EINVAL on error ("BAAD" magic will be present). + * Return 0 on success and -EIO on error ("BAAD" magic will be present). * * NOTE: We consider the absence / invalidity of an update sequence array to * mean that the structure is not protected at all and hence doesn't need to @@ -71,7 +71,7 @@ int post_read_mst_fixup(struct ntfs_record *b, const u32 size) * Note that magic_BAAD is already converted to le32. */ b->magic = magic_BAAD; - return -EINVAL; + return -EIO; } data_pos += NTFS_BLOCK_SIZE / sizeof(u16); } -- cgit v1.2.3 From cc24e2acac48956643f33486491da703b854633b Mon Sep 17 00:00:00 2001 From: Hyunchul Lee Date: Fri, 18 Sep 2026 09:18:53 +0900 Subject: ntfs: fix incorrect VCN units for index blocks in large clusters NTFS stores VCNs for index blocks smaller than their cluster in fixed 512-byte units, independent of logical sector size. Use correct block units when initializing and traversing these indexes so directory indexes are located correctly. Verified on a Windows-created 4Kn/32KB-cluster NTFS volume that when the index block is smaller than the cluster, the VCN unit is 512 bytes. Signed-off-by: Hyunchul Lee Reviewed-by: Baolin Liu Signed-off-by: Namjae Jeon --- fs/ntfs/index.c | 2 +- fs/ntfs/index.h | 2 +- fs/ntfs/inode.c | 8 ++++---- fs/ntfs/layout.h | 6 +++--- fs/ntfs/namei.c | 7 +++---- 5 files changed, 12 insertions(+), 13 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/index.c b/fs/ntfs/index.c index 580998990bc9..4410df0a8f31 100644 --- a/fs/ntfs/index.c +++ b/fs/ntfs/index.c @@ -852,7 +852,7 @@ int ntfs_index_lookup(const void *key, const u32 key_len, struct ntfs_index_cont if (ni->vol->cluster_size <= icx->block_size) icx->vcn_size_bits = ni->vol->cluster_size_bits; else - icx->vcn_size_bits = ni->vol->sector_size_bits; + icx->vcn_size_bits = NTFS_BLOCK_SIZE_BITS; icx->cr = ir->collation_rule; if (!ntfs_is_collation_rule_supported(icx->cr)) { diff --git a/fs/ntfs/index.h b/fs/ntfs/index.h index 9a03f53bba47..a898e348f2c9 100644 --- a/fs/ntfs/index.h +++ b/fs/ntfs/index.h @@ -37,7 +37,7 @@ * (maximum is MAX_PARENT_VCN) * @ib_dirty: true if the current index block (@ia/@ib) was modified * @block_size: size of index blocks in bytes (from $INDEX_ROOT or $Boot) - * @vcn_size_bits: log2(cluster size) + * @vcn_size_bits: log2 of the VCN unit size * @sync_write: true if synchronous writeback is requested for this context * * @idx_ni is the index inode this context belongs to. diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c index b3c5219c7b77..111eff42870b 100644 --- a/fs/ntfs/inode.c +++ b/fs/ntfs/inode.c @@ -1021,8 +1021,8 @@ view_index_meta: ni->itype.index.vcn_size = vol->cluster_size; ni->itype.index.vcn_size_bits = vol->cluster_size_bits; } else { - ni->itype.index.vcn_size = vol->sector_size; - ni->itype.index.vcn_size_bits = vol->sector_size_bits; + ni->itype.index.vcn_size = NTFS_BLOCK_SIZE; + ni->itype.index.vcn_size_bits = NTFS_BLOCK_SIZE_BITS; } /* Setup the index allocation attribute, even if not present. */ @@ -1612,8 +1612,8 @@ static int ntfs_read_locked_index_inode(struct inode *base_vi, struct inode *vi) ni->itype.index.vcn_size = vol->cluster_size; ni->itype.index.vcn_size_bits = vol->cluster_size_bits; } else { - ni->itype.index.vcn_size = vol->sector_size; - ni->itype.index.vcn_size_bits = vol->sector_size_bits; + ni->itype.index.vcn_size = NTFS_BLOCK_SIZE; + ni->itype.index.vcn_size_bits = NTFS_BLOCK_SIZE_BITS; } /* Find index allocation attribute. */ diff --git a/fs/ntfs/layout.h b/fs/ntfs/layout.h index 9438fd9b668e..8f5792139d71 100644 --- a/fs/ntfs/layout.h +++ b/fs/ntfs/layout.h @@ -1880,8 +1880,8 @@ struct index_header { * @index_block_size: Size of each index block in bytes * (in $INDEX_ALLOCATION). * @clusters_per_index_block: - * Clusters per index block (or log2(bytes) - * if < cluster). + * Clusters per index block, or 512-byte units when + * the index block is smaller than a cluster. * Power of 2; used for encoding block size. * @reserved: 3 bytes reserved/alignment (zero). * @index: Index header for root entries (entries follow @@ -1921,7 +1921,7 @@ struct index_root { * @lsn: Log sequence number of last modification. * @index_block_vcn: VCN of this index block. * Units: clusters if cluster_size <= index_block_size; - * sectors otherwise. + * 512-byte blocks otherwise. * @index: Index header describing entries in this block. * * When creating the index block, we place the update sequence array at this diff --git a/fs/ntfs/namei.c b/fs/ntfs/namei.c index fdf52fac4329..a8ad26f69f89 100644 --- a/fs/ntfs/namei.c +++ b/fs/ntfs/namei.c @@ -433,9 +433,8 @@ static struct ntfs_inode *__ntfs_create(struct mnt_idmap *idmap, struct inode *d ni->itype.index.vcn_size_bits = vol->cluster_size_bits; } else { - ni->itype.index.vcn_size = vol->sector_size; - ni->itype.index.vcn_size_bits = - vol->sector_size_bits; + ni->itype.index.vcn_size = NTFS_BLOCK_SIZE; + ni->itype.index.vcn_size_bits = NTFS_BLOCK_SIZE_BITS; } } @@ -569,7 +568,7 @@ static struct ntfs_inode *__ntfs_create(struct mnt_idmap *idmap, struct inode *d NTFS_B_TO_CLU(vol, ni->vol->index_record_size); else ir->clusters_per_index_block = - ni->vol->index_record_size >> ni->vol->sector_size_bits; + ni->vol->index_record_size >> NTFS_BLOCK_SIZE_BITS; ir->index.entries_offset = cpu_to_le32(sizeof(struct index_header)); ir->index.index_length = cpu_to_le32(index_len); ir->index.allocated_size = cpu_to_le32(index_len); -- cgit v1.2.3 From 18ea1beed901ebe0304dcb2baa85257fd9c71087 Mon Sep 17 00:00:00 2001 From: Hyunchul Lee Date: Mon, 21 Sep 2026 10:41:15 +0900 Subject: ntfs: fix FITRIM bitmap indexing for large pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The LCN bitmap has one bit per cluster, so a bitmap page represents PAGE_SIZE * 8 clusters. ntfs_trim_fs() still used a 15-bit shift when selecting the page. On arm64 with 16 KiB pages this selects the wrong bitmap page and can test or discard the wrong clusters. Compute bitmap indices from buf_clusters, use the page cluster base as the bit-search origin, and keep the requested range as the clipping boundary. Fixes: 11ccc9107dc4 ("ntfs: update runlist handling and cluster allocator") Signed-off-by: Hyunchul Lee Cc: Marek Wesołowski Reviewed-by: Baolin Liu Signed-off-by: Namjae Jeon --- fs/ntfs/bitmap.c | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/bitmap.c b/fs/ntfs/bitmap.c index 5a4457551306..912fdcca8e01 100644 --- a/fs/ntfs/bitmap.c +++ b/fs/ntfs/bitmap.c @@ -20,7 +20,7 @@ int ntfs_trim_fs(struct ntfs_volume *vol, struct fstrim_range *range) struct folio *folio; unsigned long *bitmap; char *kaddr; - u64 end, trimmed = 0, start_buf, end_buf, end_cluster; + u64 end, trimmed = 0, start_buf, end_buf, end_cluster, page_cluster; u64 start_cluster = ntfs_bytes_to_cluster(vol, range->start); u32 dq = bdev_discard_granularity(vol->sb->s_bdev); int ret = 0; @@ -45,8 +45,8 @@ int ntfs_trim_fs(struct ntfs_volume *vol, struct fstrim_range *range) return -ENOMEM; buf_clusters = PAGE_SIZE * 8; - start_index = start_cluster >> 15; - end_index = (end_cluster + buf_clusters - 1) >> 15; + start_index = start_cluster / buf_clusters; + end_index = DIV_ROUND_UP_ULL(end_cluster, buf_clusters); for (index = start_index; index < end_index; index++) { folio = ntfs_get_locked_folio(vol->lcnbmp_ino->i_mapping, @@ -59,19 +59,20 @@ int ntfs_trim_fs(struct ntfs_volume *vol, struct fstrim_range *range) kaddr = kmap_local_folio(folio, 0); bitmap = (unsigned long *)kaddr; - start_buf = max_t(u64, index * buf_clusters, start_cluster); - end_buf = min_t(u64, (index + 1) * buf_clusters, end_cluster); + page_cluster = (u64)index * buf_clusters; + start_buf = max_t(u64, page_cluster, start_cluster); + end_buf = min_t(u64, page_cluster + buf_clusters, end_cluster); end = start_buf; while (end < end_buf) { u64 aligned_start, aligned_end, aligned_count; - u64 start = find_next_zero_bit(bitmap, end_buf - start_buf, - end - start_buf) + start_buf; + u64 start = find_next_zero_bit(bitmap, end_buf - page_cluster, + end - page_cluster) + page_cluster; if (start >= end_buf) break; - end = find_next_bit(bitmap, end_buf - start_buf, - start - start_buf) + start_buf; + end = find_next_bit(bitmap, end_buf - page_cluster, + start - page_cluster) + page_cluster; aligned_start = ALIGN(ntfs_cluster_to_bytes(vol, start), dq); aligned_end = ALIGN_DOWN(ntfs_cluster_to_bytes(vol, end), dq); -- cgit v1.2.3 From 7de32a72b75b77a5c6c84923524d02be9753ff5c Mon Sep 17 00:00:00 2001 From: Hyunchul Lee Date: Mon, 21 Sep 2026 10:42:57 +0900 Subject: ntfs: require page-aligned full-page attribute writes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit __ntfs_inode_non_resident_attr_pwrite() treats count == PAGE_SIZE as proof that the request covers the complete folio. it allocates the folio without reading it and copies at offset zero. That proof is valid only when pos is page aligned. Fixes: af0db57d4293 ("ntfs: update inode operations") Signed-off-by: Hyunchul Lee Cc: Marek Wesołowski Reviewed-by: Baolin Liu Signed-off-by: Namjae Jeon --- fs/ntfs/inode.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c index 111eff42870b..5a39ff655528 100644 --- a/fs/ntfs/inode.c +++ b/fs/ntfs/inode.c @@ -3719,7 +3719,7 @@ static s64 __ntfs_inode_non_resident_attr_pwrite(struct inode *vi, index = pos >> PAGE_SHIFT; while (count) { - if (count == PAGE_SIZE) { + if (count == PAGE_SIZE && !offset_in_page(pos)) { folio = __filemap_get_folio(vi->i_mapping, index, FGP_CREAT | FGP_LOCK, mapping_gfp_mask(mapping)); @@ -3741,7 +3741,7 @@ static s64 __ntfs_inode_non_resident_attr_pwrite(struct inode *vi, folio_wait_writeback(folio); - if (count == PAGE_SIZE) { + if (count == PAGE_SIZE && !offset_in_page(pos)) { offset = 0; attr_len = count; } else { -- cgit v1.2.3 From d526960f302e1b34b52a09d6ccdb5d199db6dff8 Mon Sep 17 00:00:00 2001 From: Xuewen Wang Date: Sun, 20 Sep 2026 14:31:53 +0800 Subject: ntfs: preserve the 64-bit FITRIM minimum length The minlen member of struct fstrim_range is a 64-bit byte count, but ntfs_ioctl_fitrim() uses max_t(u32, ...) to compare it with the device's discard granularity. This truncates the requested minimum length before passing it to ntfs_trim_fs(). For example, a minimum length of 4 GiB is reduced to the discard granularity, allowing smaller free extents to be discarded. The truncated minimum length is also copied back to userspace. Use max_t(u64, ...) to preserve the requested minimum length. The bitmap scan already compares the aligned extent length against range->minlen using 64-bit values. Fixes: 9c87959601e8 ("ntfs: update file operations") Signed-off-by: Xuewen Wang Reviewed-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/file.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'fs') diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c index 7c7cbd16e944..8fe753fcdcf6 100644 --- a/fs/ntfs/file.c +++ b/fs/ntfs/file.c @@ -883,7 +883,7 @@ static int ntfs_ioctl_fitrim(struct ntfs_volume *vol, unsigned long arg) if (range.len < vol->cluster_size) return -EINVAL; - range.minlen = max_t(u32, range.minlen, bdev_discard_granularity(dev)); + range.minlen = max_t(u64, range.minlen, bdev_discard_granularity(dev)); err = ntfs_trim_fs(vol, &range); if (err < 0) -- cgit v1.2.3 From 381f939d17df7f2f45aaa0cf2d3b7cee09c74008 Mon Sep 17 00:00:00 2001 From: Hongling Zeng Date: Mon, 7 Sep 2026 15:08:52 +0800 Subject: ntfs: NULL vol->vol_ino in the load_system_files() error teardown The error unwind of load_system_files() drops vol->vol_ino on two paths but leaves the stale pointer in place while it keeps iput()ing the remaining system inodes ($Bitmap, $MFT bitmap, $MFTMirr; $MFT itself is dropped by the caller, ntfs_fill_super()). A third path carried an iput() that can never execute: it sits inside the IS_ERR(vol->vol_ino) branch guarded by !IS_ERR(vol->vol_ino), and is removed along with the stale pointers. Nothing in the teardown dereferences vol_ino today, so this is pure hygiene, but a stale pointer to a freed inode surviving the unwind is a trap for any future code walking the volume during teardown. ntfs_put_super() and the ntfs_fill_super() error path already NULL it after their iput(); do the same here. Cc: stable@vger.kernel.org Signed-off-by: Hongling Zeng Reviewed-by: Baolin Liu Signed-off-by: Namjae Jeon --- fs/ntfs/super.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c index 49ec1eb1520c..9f5cf65fd3a5 100644 --- a/fs/ntfs/super.c +++ b/fs/ntfs/super.c @@ -1536,8 +1536,7 @@ bitmap_failed: */ vol->vol_ino = ntfs_iget(sb, FILE_Volume); if (IS_ERR(vol->vol_ino)) { - if (!IS_ERR(vol->vol_ino)) - iput(vol->vol_ino); + vol->vol_ino = NULL; volume_failed: ntfs_error(sb, "Failed to load $Volume."); goto iput_lcnbmp_err_out; @@ -1546,6 +1545,7 @@ volume_failed: if (IS_ERR(m)) { iput_volume_failed: iput(vol->vol_ino); + vol->vol_ino = NULL; goto volume_failed; } @@ -1706,6 +1706,8 @@ iput_logfile_err_out: if (vol->logfile_ino) iput(vol->logfile_ino); iput(vol->vol_ino); + /* Do not leave a stale pointer behind for the rest of the teardown. */ + vol->vol_ino = NULL; iput_lcnbmp_err_out: iput(vol->lcnbmp_ino); iput_attrdef_err_out: -- cgit v1.2.3 From 02a5cbd2b097216529e829deb9e11496e574e8b3 Mon Sep 17 00:00:00 2001 From: Hongling Zeng Date: Mon, 7 Sep 2026 11:57:26 +0800 Subject: ntfs: use fatal_signal_pending() for fallocate interruption checks The fallocate implementation uses signal checks to let a long allocation loop abort early. However, signal_pending() also returns true for TIF_NOTIFY_SIGNAL, which io_uring uses to deliver task_work completions to the submitting task. generic/616 is an io_uring fsx soak test that issues fsx-style read/write requests through io_uring and calls fallocate() directly in between. A pending io_uring task_work notification can therefore make signal_pending() true inside ntfs_attr_fallocate() even though no real signal was delivered. That caused the previous signal-interruption patch to return -EINTR for an io_uring notification, which fsx treats as a fatal failure. Switch both checks to fatal_signal_pending() so only fatal signals interrupt the allocation loop. A real fatal signal still returns -EINTR, and any other error still takes precedence. Fixes: 4dc8f4ee2d46 ("ntfs: handle signal interruption in fallocate") Cc: stable@vger.kernel.org Signed-off-by: Hongling Zeng Reviewed-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/attrib.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c index eb6a166cfcc7..808b44671fff 100644 --- a/fs/ntfs/attrib.c +++ b/fs/ntfs/attrib.c @@ -5855,7 +5855,7 @@ int ntfs_attr_fallocate(struct ntfs_inode *ni, loff_t start, loff_t byte_len, bo goto out; } - if (signal_pending(current)) + if (fatal_signal_pending(current)) goto signal_out; vcn += alloc_cnt; @@ -5876,7 +5876,7 @@ int ntfs_attr_fallocate(struct ntfs_inode *ni, loff_t start, loff_t byte_len, bo try_alloc_cnt, &balloc, false, false); up_write(&ni->runlist.lock); mutex_unlock(&ni->mrec_lock); - if (err || signal_pending(current)) + if (err || fatal_signal_pending(current)) goto signal_out; vcn += alloc_cnt; -- cgit v1.2.3 From 4aeb34cb8de70009c29063806bc958b55fbf970b Mon Sep 17 00:00:00 2001 From: Jiangshan Yi Date: Mon, 7 Sep 2026 18:16:23 +0800 Subject: ntfs: remove unreachable code in load_and_init_attrdef/upcase Both load_and_init_attrdef() and load_and_init_upcase() open a system inode with ntfs_iget() and, on error, run: if (IS_ERR(ino)) { if (!IS_ERR(ino)) iput(ino); goto failed; } The inner guard is the exact negation of the outer one, so iput(ino) is dead code. Drop the unreachable branch and collapse the error check to a single statement. No functional change. Signed-off-by: Jiangshan Yi Reviewed-by: Baolin Liu Signed-off-by: Namjae Jeon --- fs/ntfs/super.c | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c index 9f5cf65fd3a5..f7b3140049c4 100644 --- a/fs/ntfs/super.c +++ b/fs/ntfs/super.c @@ -1303,11 +1303,8 @@ static bool load_and_init_attrdef(struct ntfs_volume *vol) ntfs_debug("Entering."); /* Read attrdef table and setup vol->attrdef and vol->attrdef_size. */ ino = ntfs_iget(sb, FILE_AttrDef); - if (IS_ERR(ino)) { - if (!IS_ERR(ino)) - iput(ino); + if (IS_ERR(ino)) goto failed; - } NInoSetSparseDisabled(NTFS_I(ino)); /* FILE_AttrDef must hold at least one entry and fit inside 31 bits. */ i_size = i_size_read(ino); @@ -1369,11 +1366,8 @@ static bool load_and_init_upcase(struct ntfs_volume *vol) ntfs_debug("Entering."); /* Read upcase table and setup vol->upcase and vol->upcase_len. */ ino = ntfs_iget(sb, FILE_UpCase); - if (IS_ERR(ino)) { - if (!IS_ERR(ino)) - iput(ino); + if (IS_ERR(ino)) goto upcase_failed; - } /* * The upcase size must not be above 64k Unicode characters, must not * be zero and must be a multiple of sizeof(__le16). -- cgit v1.2.3 From 95fdd2010d0d21dd7e4506992f3e948314504725 Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Fri, 4 Sep 2026 17:00:50 +0800 Subject: ntfs: return -EINVAL from ntfs_collate_ntofs_ulongs() on bad length ntfs_collate_ntofs_ulongs() returns -1 when the key lengths differ or are not a multiple of 4. But -1 is also what cmp_int() returns for "collates before", so the error is indistinguishable from an ordinary result. ntfs_ie_lookup() acts on that difference: it maps -EINVAL to -ERANGE, but treats -1 as a tree-descent hint and breaks out of the loop. A malformed entry in $Reparse/$R or $ObjId/$O is therefore silently taken as a lookup miss. Return -EINVAL, as the sibling ntfs_collate_ntofs_ulong() already does. Well-formed keys are unaffected; both indexes use key sizes that are multiples of 4. Fixes: 5218cd102aec ("ntfs: update misc operations") Signed-off-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/collate.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'fs') diff --git a/fs/ntfs/collate.c b/fs/ntfs/collate.c index 885efebe8959..5417288c5825 100644 --- a/fs/ntfs/collate.c +++ b/fs/ntfs/collate.c @@ -73,7 +73,7 @@ static int ntfs_collate_ntofs_ulongs(struct ntfs_volume *vol, if (data1_len != data2_len || data1_len & 3) { ntfs_error(vol->sb, "data1_len or data2_len not valid\n"); - return -1; + return -EINVAL; } len = data1_len; -- cgit v1.2.3 From 7b1f1031450c06657d6b561a048858439cabf202 Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Fri, 4 Sep 2026 17:00:52 +0800 Subject: ntfs: preserve the truncate error in ntfs_enlarge_attribute() ntfs_enlarge_attribute() discards the error from ntfs_attr_truncate() and returns -1, which reaches userspace as EPERM. The discarded codes are meaningful: -ENOSPC when the attribute cannot grow, -EACCES when it is encrypted, -EOPNOTSUPP when compressed, plus -EINVAL and -ENOMEM. Most callers of ntfs_inode_attr_pwrite() compare the result against the requested length and substitute -EIO, masking this. ntfs_ib_write() does not; it returns the value verbatim, which propagates through ntfs_index_add_filename() to __ntfs_create() and __ntfs_link(), so a full volume reports EPERM instead of ENOSPC from create(), mkdir() and link(). Fixes: af0db57d4293 ("ntfs: update inode operations") Signed-off-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/inode.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c index 5a39ff655528..8ea0c5f65aa3 100644 --- a/fs/ntfs/inode.c +++ b/fs/ntfs/inode.c @@ -3629,9 +3629,10 @@ static inline int ntfs_enlarge_attribute(struct inode *vi, s64 pos, s64 count, return -EOPNOTSUPP; if (pos + count > ni->data_size) { - if (ntfs_attr_truncate(ni, pos + count)) { + ret = ntfs_attr_truncate(ni, pos + count); + if (ret) { ntfs_debug("Failed to truncate attribute"); - return -1; + return ret; } ntfs_attr_reinit_search_ctx(ctx); -- cgit v1.2.3 From 7b4d18016ab530b432960da10e6343fb48da6dfc Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Fri, 4 Sep 2026 17:00:53 +0800 Subject: ntfs: propagate the map_mft_record() error in ntfs_attrlist_entry_add() ntfs_attrlist_entry_add() replaces the map_mft_record() error with -EIO, so an -ENOMEM allocation failure is reported as an I/O error. Both callers pass the value on to ntfs_attr_add(), which no longer collapses it since commit 8efe00b098b5 ("ntfs: preserve error code in ntfs_resident_attr_record_add()"). Return PTR_ERR(ni_mrec) instead. Also fix the debug message. Fixes: 495e90fa3348 ("ntfs: update attrib operations") Signed-off-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/attrlist.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/attrlist.c b/fs/ntfs/attrlist.c index bb191953dcb1..7c09d02fd941 100644 --- a/fs/ntfs/attrlist.c +++ b/fs/ntfs/attrlist.c @@ -335,8 +335,8 @@ int ntfs_attrlist_entry_add(struct ntfs_inode *ni, struct attr_record *attr) ni_mrec = map_mft_record(ni); if (IS_ERR(ni_mrec)) { - ntfs_debug("Invalid arguments.\n"); - return -EIO; + ntfs_debug("Failed to map mft record.\n"); + return PTR_ERR(ni_mrec); } mref = MK_LE_MREF(ni->mft_no, le16_to_cpu(ni_mrec->sequence_number)); -- cgit v1.2.3 From d06fb2f764eb792f96c0d0668177edc2d5e9d5dd Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Fri, 4 Sep 2026 17:00:54 +0800 Subject: ntfs: propagate the ntfs_attr_iget() error in update_reparse_data() update_reparse_data() replaces the ntfs_attr_iget() error with -EINVAL, so an -ENOMEM from iget5_locked() or a read failure from ntfs_read_locked_attr_inode() is reported to userspace as EINVAL. The path is reached from __ntfs_create() via the three ntfs_reparse_set_*() helpers, so symlink() and mknod() are affected. Return PTR_ERR(rp_inode) 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(-) (limited to 'fs') diff --git a/fs/ntfs/reparse.c b/fs/ntfs/reparse.c index 1a6073e22677..2a6480211fe3 100644 --- a/fs/ntfs/reparse.c +++ b/fs/ntfs/reparse.c @@ -676,7 +676,7 @@ static int update_reparse_data(struct ntfs_inode *ni, struct ntfs_index_context rp_inode = ntfs_attr_iget(VFS_I(ni), AT_REPARSE_POINT, AT_UNNAMED, 0); if (IS_ERR(rp_inode)) - return -EINVAL; + return PTR_ERR(rp_inode); rp_ni = NTFS_I(rp_inode); /* remove the existing reparse data */ -- cgit v1.2.3 From 23824b751c49f2484582d221b9983846b8198619 Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Mon, 7 Sep 2026 11:00:43 +0800 Subject: ntfs: fix error handling in ntfs_extent_inode_open and propagate errors ntfs_extent_inode_open() has two issues: 1. When map_mft_record() fails or a stale extent reference is found, it returns the non-NULL 'ni' pointer instead of an error, causing the caller to treat the failure as success. 2. For allocation failures, it returns NULL, losing information about what actually went wrong (-ENOMEM). Fix both by converting the function to return ERR_PTR() on error, and update the single caller (ntfs_inode_attach_all_extents) to check with IS_ERR() and propagate the actual error code with PTR_ERR(). Fixes: af0db57d4293 ("ntfs: update inode operations") Signed-off-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/inode.c | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c index 8ea0c5f65aa3..cadf623d54d4 100644 --- a/fs/ntfs/inode.c +++ b/fs/ntfs/inode.c @@ -2915,8 +2915,7 @@ err_out: * abort if deprotection or checks fail. * * Finally attach the ntfs inode to its base inode @base_ni and return a - * pointer to the ntfs_inode structure on success or NULL on error, with errno - * set to the error code. + * pointer to the ntfs_inode structure on success or ERR_PTR() on error. * * Note, extent inodes are never closed directly. They are automatically * disposed off by the closing of the base inode. @@ -2932,7 +2931,7 @@ static struct ntfs_inode *ntfs_extent_inode_open(struct ntfs_inode *base_ni, struct super_block *sb; if (!base_ni) - return NULL; + return ERR_PTR(-EINVAL); sb = base_ni->vol->sb; ntfs_debug("Opening extent inode %llu (base mft record %llu).\n", @@ -2951,7 +2950,7 @@ static struct ntfs_inode *ntfs_extent_inode_open(struct ntfs_inode *base_ni, if (IS_ERR(ni_mrec)) { ntfs_error(sb, "failed to map mft record for %llu", ni->mft_no); - goto out; + return ERR_CAST(ni_mrec); } /* Verify the sequence number if given. */ seq_no = MSEQNO_LE(mref); @@ -2960,7 +2959,7 @@ static struct ntfs_inode *ntfs_extent_inode_open(struct ntfs_inode *base_ni, ntfs_error(sb, "Found stale extent mft reference mft=%llu", ni->mft_no); unmap_mft_record(ni); - goto out; + return ERR_PTR(-EIO); } unmap_mft_record(ni); goto out; @@ -2969,7 +2968,7 @@ static struct ntfs_inode *ntfs_extent_inode_open(struct ntfs_inode *base_ni, /* Wasn't there, we need to load the extent inode. */ ni = ntfs_new_extent_inode(base_ni->vol->sb, mft_no); if (!ni) - goto out; + return ERR_PTR(-ENOMEM); ni->seq_no = (u16)MSEQNO_LE(mref); ni->nr_extents = -1; @@ -2979,8 +2978,10 @@ static struct ntfs_inode *ntfs_extent_inode_open(struct ntfs_inode *base_ni, i = (base_ni->nr_extents + 4) * sizeof(struct ntfs_inode *); extent_nis = kvzalloc(i, GFP_NOFS); - if (!extent_nis) - goto err_out; + if (!extent_nis) { + ntfs_destroy_ext_inode(ni); + return ERR_PTR(-ENOMEM); + } if (base_ni->nr_extents) { memcpy(extent_nis, base_ni->ext.extent_ntfs_inos, i - 4 * sizeof(struct ntfs_inode *)); @@ -2993,10 +2994,6 @@ static struct ntfs_inode *ntfs_extent_inode_open(struct ntfs_inode *base_ni, out: ntfs_debug("\n"); return ni; -err_out: - ntfs_destroy_ext_inode(ni); - ni = NULL; - goto out; } /* @@ -3034,9 +3031,12 @@ int ntfs_inode_attach_all_extents(struct ntfs_inode *ni) while ((u8 *)ale < ni->attr_list + ni->attr_list_size) { if (ni->mft_no != MREF_LE(ale->mft_reference) && prev_attached != MREF_LE(ale->mft_reference)) { - if (!ntfs_extent_inode_open(ni, ale->mft_reference)) { + struct ntfs_inode *ext_ni; + + ext_ni = ntfs_extent_inode_open(ni, ale->mft_reference); + if (IS_ERR(ext_ni)) { ntfs_debug("Couldn't attach extent inode.\n"); - return -1; + return PTR_ERR(ext_ni); } prev_attached = MREF_LE(ale->mft_reference); } -- cgit v1.2.3 From fa72bf630e6817dec879a6c8ee0e5595f1a1125f Mon Sep 17 00:00:00 2001 From: Hyunchul Lee Date: Tue, 15 Sep 2026 00:19:15 +0900 Subject: ntfs: avoid truncate and writeback deadlock ntfs_non_resident_attr_shrink() drops page-cache folios after freeing clusters and truncating the runlist. The VFS truncate path calls it with mrec_lock held. A folio writeback can need the same lock to resolve its iomap mapping, so truncate can wait for writeback while writeback waits for mrec_lock. A hung-task call stack: truncate: mrec_lock -> truncate_inode_pages() -> folio lock/writeback writeback: folio lock/writeback -> ntfs_write_iomap_begin() -> mrec_lock ntfs_setattr_size() already calls truncate_setsize() before ntfs_truncate_vfs() acquires mrec_lock. Reuse that page-cache truncation in the VFS path and skip the duplicate truncation under mrec_lock. Keep the cleanup for internal attribute callers, which do not call truncate_setsize(). This avoids synchronously flushing the whole discarded tail while preserving page-cache correctness. Fixes: 4e646ecd4475 ("ntfs: drop stale page-cache when shrinking a non-resident attr") Cc: stable@vger.kernel.org Signed-off-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/attrib.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c index 808b44671fff..d8dd40526c3b 100644 --- a/fs/ntfs/attrib.c +++ b/fs/ntfs/attrib.c @@ -4234,12 +4234,14 @@ static int ntfs_attr_make_resident(struct ntfs_inode *ni, struct ntfs_attr_searc * ntfs_non_resident_attr_shrink - shrink a non-resident, open ntfs attribute * @ni: non-resident ntfs attribute to shrink * @newsize: new size (in bytes) to which to shrink the attribute + * @pagecache_truncated: page cache was already truncated to @newsize * * Reduce the size of a non-resident, open ntfs attribute @na to @newsize bytes. */ static int ntfs_non_resident_attr_shrink(struct ntfs_inode *ni, const s64 newsize, - struct ntfs_inode *locked_ni) + struct ntfs_inode *locked_ni, + bool pagecache_truncated) { struct ntfs_volume *vol; struct ntfs_attr_search_ctx *ctx; @@ -4389,7 +4391,8 @@ static int ntfs_non_resident_attr_shrink(struct ntfs_inode *ni, * 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); + if (!pagecache_truncated) + truncate_inode_pages(VFS_I(ni)->i_mapping, newsize); /* Update data size in the index. */ if (ni->type == AT_DATA && ni->name == AT_UNNAMED) @@ -4991,7 +4994,7 @@ int __ntfs_attr_truncate_vfs(struct ntfs_inode *ni, const s64 newsize, up_write(&ni->runlist.lock); } else err = ntfs_non_resident_attr_shrink( - ni, newsize, NULL); + ni, newsize, NULL, true); } else err = ntfs_resident_attr_resize(ni, newsize, 0, NVolDisableSparse(ni->vol) ? @@ -5104,7 +5107,7 @@ int ntfs_attr_truncate_i_locked(struct ntfs_inode *ni, const s64 newsize, ni, newsize, 0, holes, locked_ni); else err = ntfs_non_resident_attr_shrink( - ni, newsize, locked_ni); + ni, newsize, locked_ni, false); } else err = ntfs_resident_attr_resize(ni, newsize, 0, holes); ntfs_debug("Return status %d\n", err); -- cgit v1.2.3 From b6f7bdcaecb3a67c41beda81b26613f1fc533cf7 Mon Sep 17 00:00:00 2001 From: Hongling Zeng Date: Tue, 15 Sep 2026 16:32:46 +0800 Subject: ntfs: fix volume flag update races ntfs_set_volume_flags() and ntfs_clear_volume_flags() both read vol->vol_flags outside any lock to compute the new value before handing it to ntfs_write_volume_flags(), which only takes ni->mrec_lock around the actual write. The read-modify-write is therefore not atomic, and two concurrent callers can lose an update: ntfs_sync_fs() may derive a "clean" value from vol->vol_flags while a writer concurrently records an error and sets VOLUME_IS_DIRTY; the locked write then silently overwrites the freshly-set dirty bit. The on-disk volume looks clean despite the recorded errors, so chkdsk will not run on the next mount and corrupted metadata can persist. Fix by moving the read-modify-write inside the mrec_lock: pass the bits to set and to clear separately, and combine them with the current flag state under the lock inside ntfs_write_volume_flags(). The set/clear helpers pass only the bits to modify, not the complete flag state. The bit manipulation is done on CPU-endian values, and the result is converted back to little-endian before storing it. The wrappers keep their signatures so callers are unchanged. Cc: stable@vger.kernel.org Signed-off-by: Hongling Zeng Reviewed-by: Hyunchul Lee Reviewed-by: Baolin Liu Signed-off-by: Namjae Jeon --- fs/ntfs/super.c | 63 ++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 18 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c index f7b3140049c4..5c71aaa0e25d 100644 --- a/fs/ntfs/super.c +++ b/fs/ntfs/super.c @@ -360,31 +360,45 @@ void ntfs_handle_error(struct super_block *sb) } /* - * ntfs_write_volume_flags - write new flags to the volume information flags + * ntfs_write_volume_flags - apply flag changes to the volume information flags * @vol: ntfs volume on which to modify the flags - * @flags: new flags value for the volume information flags + * @set_bits: bits to set in the volume information flags + * @clear_bits: bits to clear in the volume information flags * * Internal function. You probably want to use ntfs_{set,clear}_volume_flags() * instead (see below). * - * Replace the volume information flags on the volume @vol with the value - * supplied in @flags. Note, this overwrites the volume information flags, so - * make sure to combine the flags you want to modify with the old flags and use - * the result when calling ntfs_write_volume_flags(). + * Combine @set_bits and @clear_bits with the current in-memory flag state and + * write the result back. The set/clear helpers pass only the bits to modify, + * not the complete flag state. The read-modify-write happens under + * ni->mrec_lock so that concurrent set/clear operations cannot lose updates. + * All bit manipulation is done on CPU-endian values, and the result is + * converted back to little-endian before storing it. * * Return 0 on success and -errno on error. */ -static int ntfs_write_volume_flags(struct ntfs_volume *vol, const __le16 flags) +static int ntfs_write_volume_flags(struct ntfs_volume *vol, + const __le16 set_bits, const __le16 clear_bits, + const bool skip_if_errors) { struct ntfs_inode *ni = NTFS_I(vol->vol_ino); struct volume_information *vi; struct ntfs_attr_search_ctx *ctx; + u16 flags; int err; - ntfs_debug("Entering, old flags = 0x%x, new flags = 0x%x.", - le16_to_cpu(vol->vol_flags), le16_to_cpu(flags)); mutex_lock(&ni->mrec_lock); - if (vol->vol_flags == flags) + + if (skip_if_errors && NVolErrors(vol)) + goto done; + + flags = le16_to_cpu(vol->vol_flags); + flags |= le16_to_cpu(set_bits) & le16_to_cpu(VOLUME_FLAGS_MASK); + flags &= ~(le16_to_cpu(clear_bits) & le16_to_cpu(VOLUME_FLAGS_MASK)); + ntfs_debug("Entering, old flags = 0x%x, new flags = 0x%x.", + le16_to_cpu(vol->vol_flags), flags); + + if (le16_to_cpu(vol->vol_flags) == flags) goto done; ctx = ntfs_attr_get_search_ctx(ni, NULL); @@ -400,7 +414,7 @@ static int ntfs_write_volume_flags(struct ntfs_volume *vol, const __le16 flags) vi = (struct volume_information *)((u8 *)ctx->attr + le16_to_cpu(ctx->attr->data.resident.value_offset)); - vol->vol_flags = vi->flags = flags; + vol->vol_flags = vi->flags = cpu_to_le16(flags); mark_mft_record_dirty(ctx->ntfs_ino); ntfs_attr_put_search_ctx(ctx); done: @@ -421,13 +435,14 @@ put_unm_err_out: * @flags: flags to set on the volume * * Set the bits in @flags in the volume information flags on the volume @vol. + * The bits are combined with the current flag state under the lock in + * ntfs_write_volume_flags(), so concurrent updates are not lost. * * Return 0 on success and -errno on error. */ int ntfs_set_volume_flags(struct ntfs_volume *vol, __le16 flags) { - flags &= VOLUME_FLAGS_MASK; - return ntfs_write_volume_flags(vol, vol->vol_flags | flags); + return ntfs_write_volume_flags(vol, flags, 0, false); } /* @@ -436,14 +451,27 @@ int ntfs_set_volume_flags(struct ntfs_volume *vol, __le16 flags) * @flags: flags to clear on the volume * * Clear the bits in @flags in the volume information flags on the volume @vol. + * The bits are combined with the current flag state under the lock in + * ntfs_write_volume_flags(), so concurrent updates are not lost. * * Return 0 on success and -errno on error. */ int ntfs_clear_volume_flags(struct ntfs_volume *vol, __le16 flags) { - flags &= VOLUME_FLAGS_MASK; - flags = vol->vol_flags & cpu_to_le16(~le16_to_cpu(flags)); - return ntfs_write_volume_flags(vol, flags); + return ntfs_write_volume_flags(vol, 0, flags, false); +} + +/* + * ntfs_clear_volume_dirty_if_no_errors - clear dirty bit if no errors exist + * @vol: ntfs volume whose dirty bit should be cleared + * + * Check NVolErrors() and clear VOLUME_IS_DIRTY under the same mrec_lock so + * ntfs_sync_fs() cannot clear the dirty bit after a concurrent error has been + * recorded. + */ +static int ntfs_clear_volume_dirty_if_no_errors(struct ntfs_volume *vol) +{ + return ntfs_write_volume_flags(vol, 0, VOLUME_IS_DIRTY, true); } int ntfs_write_volume_label(struct ntfs_volume *vol, char *label) @@ -1926,8 +1954,7 @@ static int ntfs_sync_fs(struct super_block *sb, int wait) return 0; /* If there are some dirty buffers in the bdev inode */ - if (!NVolErrors(vol) && - ntfs_clear_volume_flags(vol, VOLUME_IS_DIRTY)) { + if (ntfs_clear_volume_dirty_if_no_errors(vol)) { ntfs_warning(sb, "Failed to clear dirty bit in volume information flags. Run chkdsk."); err = -EIO; } -- cgit v1.2.3 From b2aa92678f7a884c65b158f7b06728af95be7ecb Mon Sep 17 00:00:00 2001 From: Hongling Zeng Date: Mon, 21 Sep 2026 23:13:19 +0900 Subject: ntfs: set the volume dirty bit unconditionally on metadata changes The callers in file.c and namei.c skip ntfs_set_volume_flags() when the in-memory vol_flags already show VOLUME_IS_DIRTY, but that check runs without any lock: if it observes the bit set and ntfs_sync_fs() clears it under the mrec_lock before the caller's metadata update completes, the set is skipped and the volume can end up clean on disk despite the modification, so chkdsk will not run on the next mount. Drop the caller-side checks and call ntfs_set_volume_flags() unconditionally: ntfs_write_volume_flags() already skips the write under the mrec_lock when the combined value is unchanged. That unconditional call costs one mrec_lock acquisition per metadata operation even in the already-dirty steady state; it cannot be avoided, because deciding to skip the call without the lock is itself what allows a concurrent ntfs_sync_fs() clear to lose the set. The IOCB_NOWAIT path in ntfs_file_write_iter() goes through the same sleeping call: a RWF_NOWAIT write can block in the marking, as it already could before this change whenever the volume appeared clean. Giving that path a non-blocking variant is left as follow-up work. The callers keep the pre-existing behavior of proceeding when the marking fails, so the dirty bit remains best-effort. This closes the variant where the set is skipped outright. A clear for a concurrent, error-free sync can still land between the set and the end of the metadata operation; that mark-at-start lifecycle is pre-existing and is not changed by this patch. Reported-by: Baolin Liu Cc: stable@vger.kernel.org Signed-off-by: Hongling Zeng Reviewed-by: Hyunchul Lee Reviewed-by: Baolin Liu Signed-off-by: Namjae Jeon --- fs/ntfs/file.c | 20 +++++++++++--------- fs/ntfs/namei.c | 24 ++++++++---------------- 2 files changed, 19 insertions(+), 25 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c index 8fe753fcdcf6..5a82c564876c 100644 --- a/fs/ntfs/file.c +++ b/fs/ntfs/file.c @@ -341,8 +341,7 @@ int ntfs_setattr(struct mnt_idmap *idmap, struct dentry *dentry, goto out; } - if (!(vol->vol_flags & VOLUME_IS_DIRTY)) - ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); + ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); if (ia_valid & ATTR_SIZE) { err = ntfs_setattr_size(vi, attr); @@ -636,8 +635,13 @@ static ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from) goto out_lock; } - if (!(vol->vol_flags & VOLUME_IS_DIRTY)) - ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); + /* + * The volume must be marked dirty before the modification is made, + * without an unlocked check of the in-memory flag: ntfs_sync_fs() + * can clear the bit concurrently and the modification would then + * land on a volume that is clean on disk. + */ + ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); pos = iocb->ki_pos; count = ret; @@ -1169,11 +1173,9 @@ static long ntfs_fallocate(struct file *file, int mode, loff_t offset, loff_t le return err; } - if (!(vol->vol_flags & VOLUME_IS_DIRTY)) { - err = ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); - if (err) - return err; - } + err = ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); + if (err) + return err; inode_lock(vi); if (NInoCompressed(ni) || NInoEncrypted(ni) || NInoWofCompressed(ni)) { diff --git a/fs/ntfs/namei.c b/fs/ntfs/namei.c index a8ad26f69f89..75e201096525 100644 --- a/fs/ntfs/namei.c +++ b/fs/ntfs/namei.c @@ -756,8 +756,7 @@ static int ntfs_create(struct mnt_idmap *idmap, struct inode *dir, return err; } - if (!(vol->vol_flags & VOLUME_IS_DIRTY)) - ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); + ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); ni = __ntfs_create(idmap, dir, uname, uname_len, S_IFREG | mode, 0, NULL, 0); kmem_cache_free(ntfs_name_cache, uname); @@ -1031,8 +1030,7 @@ static int ntfs_unlink(struct inode *dir, struct dentry *dentry) return err; } - if (!(vol->vol_flags & VOLUME_IS_DIRTY)) - ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); + ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); err = ntfs_delete(ni, NTFS_I(dir), uname, uname_len, true); if (err) @@ -1075,8 +1073,7 @@ static struct dentry *ntfs_mkdir(struct mnt_idmap *idmap, struct inode *dir, return ERR_PTR(err); } - if (!(vol->vol_flags & VOLUME_IS_DIRTY)) - ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); + ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); ni = __ntfs_create(idmap, dir, uname, uname_len, mode, 0, NULL, 0); kmem_cache_free(ntfs_name_cache, uname); @@ -1117,8 +1114,7 @@ static int ntfs_rmdir(struct inode *dir, struct dentry *dentry) return err; } - if (!(vol->vol_flags & VOLUME_IS_DIRTY)) - ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); + ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); err = ntfs_delete(ni, NTFS_I(dir), uname, uname_len, true); if (err) @@ -1304,8 +1300,7 @@ static int ntfs_rename(struct mnt_idmap *idmap, struct inode *old_dir, new_dir_first = is_subdir(new_dentry->d_parent, old_dentry->d_parent); - if (!(vol->vol_flags & VOLUME_IS_DIRTY)) - ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); + ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); mutex_lock_nested(&old_ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL); if (new_ni) @@ -1428,8 +1423,7 @@ static int ntfs_symlink(struct mnt_idmap *idmap, struct inode *dir, goto out; } - if (!(vol->vol_flags & VOLUME_IS_DIRTY)) - ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); + ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); ni = __ntfs_create(idmap, dir, usrc, usrc_len, S_IFLNK | 0777, 0, symname, symlen); @@ -1473,8 +1467,7 @@ static int ntfs_mknod(struct mnt_idmap *idmap, struct inode *dir, return err; } - if (!(vol->vol_flags & VOLUME_IS_DIRTY)) - ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); + ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); switch (mode & S_IFMT) { case S_IFCHR: @@ -1520,8 +1513,7 @@ static int ntfs_link(struct dentry *old_dentry, struct inode *dir, return -ENOMEM; } - if (!(vol->vol_flags & VOLUME_IS_DIRTY)) - ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); + ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); ihold(vi); mutex_lock_nested(&ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL); -- cgit v1.2.3 From 86e6932550ba0dd5cd6da5674f8f3e9d0365854b Mon Sep 17 00:00:00 2001 From: Hongling Zeng Date: Tue, 15 Sep 2026 16:32:48 +0800 Subject: ntfs: sync the volume dirty bit with the recorded error state The runtime metadata-corruption paths in fs/ntfs only record the in-memory NVolErrors() flag; whether VOLUME_IS_DIRTY ever reaches disk depends on ntfs_set_volume_flags() being called by some other path, which for most error sites never happens. A volume can therefore unmount with a clean on-disk flag despite recorded corruption, and chkdsk will not run on the next mount. Persisting the dirty bit from the error paths themselves does not work: they run under a wide variety of ntfs locks, and the dirty-bit write takes the $Volume mrec_lock and maps the $Volume mft record, which on an $MFT page-cache miss takes the $MFT runlist lock for writing. That is enough to self-deadlock or form ABBA cycles from several of them: the $MFT extend undo paths hold the $MFT runlist lock and then take vol->lcnbmp_lock inside ntfs_cluster_free(); the cluster allocation and free rollback paths hold vol->lcnbmp_lock; and the whole mft record allocation tree is reachable from ntfs_write_volume_label()'s attribute-list maintenance while it holds the $Volume mrec_lock itself. Instead, make the persistence a property of the sync paths, which run without ntfs locks held. The new ntfs_sync_volume_dirty_state() sets VOLUME_IS_DIRTY when NVolErrors() is recorded and clears it otherwise, evaluating the error flag under the $Volume mrec_lock. It is called from ntfs_sync_fs(), from the remount-to-read-only path of ntfs_reconfigure(), and from ntfs_put_super(), which previously evaluated NVolErrors() outside the lock before clearing the dirty bit unconditionally, and which now also persists the dirty bit for volumes with recorded errors so they unmount with chkdsk scheduled. The ntfs_clear_volume_flags() wrapper, whose last callers this patch replaces, has no users left and is removed. The guarantee this provides is eventual, not instantaneous: the error paths record NVolErrors() with a lock-free set_bit(), so a persistence point that evaluates the flag just before an error is recorded can still leave the on-disk bit clean until the next one. This is sound because NVolErrors() is sticky for the lifetime of the mount and every persistence point re-derives the on-disk bit from it; the last one, ntfs_put_super(), runs after evict_inodes() on a quiesced filesystem, so a volume that is read-write at unmount time cannot unmount clean. A volume that is already read-only when the error is recorded (errors=remount-ro flips the superblock on the first error, as does an earlier remount-ro) has no persistence point left and keeps whatever on-disk bit it had; that behaviour is unchanged. The residual window is a crash between the error and the next persistence point. The persistence paths never write a hibernated volume: resuming Windows from a modified image corrupts it. Record the mount-time hibernation verdict in the new NV_Hibernated volume flag and make ntfs_sync_volume_dirty_state() a no-op while it is set, so the dirty bit is left exactly as it is on disk and only the in-memory error state is kept. Without this, an rw mount of a hibernated volume with the default errors=continue would gain a filesystem-internal write on the first sync, remount or unmount. Other writes to such a mount, like the mount-time logfile emptying, are pre-existing and unchanged. Cc: stable@vger.kernel.org Signed-off-by: Hongling Zeng Reviewed-by: Hyunchul Lee Reviewed-by: Baolin Liu Signed-off-by: Namjae Jeon --- fs/ntfs/ntfs.h | 1 - fs/ntfs/super.c | 129 ++++++++++++++++++++++++++++++++++++++----------------- fs/ntfs/volume.h | 4 ++ 3 files changed, 93 insertions(+), 41 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/ntfs.h b/fs/ntfs/ntfs.h index 1bee1626abdf..dd56894762a9 100644 --- a/fs/ntfs/ntfs.h +++ b/fs/ntfs/ntfs.h @@ -219,7 +219,6 @@ struct option_t { }; extern const struct option_t on_errors_arr[]; int ntfs_set_volume_flags(struct ntfs_volume *vol, __le16 flags); -int ntfs_clear_volume_flags(struct ntfs_volume *vol, __le16 flags); int ntfs_write_volume_label(struct ntfs_volume *vol, char *label); /* From fs/ntfs/mst.c */ diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c index 5c71aaa0e25d..785f5b222293 100644 --- a/fs/ntfs/super.c +++ b/fs/ntfs/super.c @@ -263,6 +263,8 @@ static int ntfs_parse_param(struct fs_context *fc, struct fs_parameter *param) return 0; } +static int ntfs_sync_volume_dirty_state(struct ntfs_volume *vol); + static int ntfs_reconfigure(struct fs_context *fc) { struct super_block *sb = fc->root->d_sb; @@ -319,10 +321,24 @@ static int ntfs_reconfigure(struct fs_context *fc) } } else if (!sb_rdonly(sb) && (fc->sb_flags & SB_RDONLY)) { /* Remounting read-only. */ - if (!NVolErrors(vol)) { - if (ntfs_clear_volume_flags(vol, VOLUME_IS_DIRTY)) - ntfs_warning(sb, - "Failed to clear dirty bit in volume information flags. Run chkdsk."); + /* + * With errors recorded the dirty bit is set rather than + * cleared, and it is committed right away: the VFS does + * not sync the filesystem during a remount, and once the + * remount succeeds no further persistence point exists - + * ntfs_sync_fs() is only ever invoked for read-write + * superblocks (all its VFS callers skip read-only ones) + * and ntfs_put_super() skips them, so the only remaining + * write would be the evict-time commit at unmount, which + * a crash never reaches. An error recorded only after + * the remount is still never persisted. + */ + if (ntfs_sync_volume_dirty_state(vol)) { + ntfs_warning(sb, + "Failed to update dirty bit in volume information flags. Run chkdsk."); + } else if (NInoDirty(NTFS_I(vol->vol_ino))) { + ntfs_commit_inode(vol->vol_ino); + blkdev_issue_flush(sb->s_bdev); } } @@ -364,9 +380,10 @@ void ntfs_handle_error(struct super_block *sb) * @vol: ntfs volume on which to modify the flags * @set_bits: bits to set in the volume information flags * @clear_bits: bits to clear in the volume information flags + * @dirty_if_errors: force VOLUME_IS_DIRTY on when NVolErrors() is set * * Internal function. You probably want to use ntfs_{set,clear}_volume_flags() - * instead (see below). + * or ntfs_sync_volume_dirty_state() instead (see below). * * Combine @set_bits and @clear_bits with the current in-memory flag state and * write the result back. The set/clear helpers pass only the bits to modify, @@ -375,11 +392,18 @@ void ntfs_handle_error(struct super_block *sb) * All bit manipulation is done on CPU-endian values, and the result is * converted back to little-endian before storing it. * + * When @dirty_if_errors is true and errors have been recorded on @vol, + * VOLUME_IS_DIRTY is forced on after the requested changes. NVolErrors() is + * evaluated under the same mrec_lock, which orders this against other + * locked flag updates; the runtime error paths themselves record the flag + * lock-free, so see ntfs_sync_volume_dirty_state() for the guarantee this + * provides against them. + * * Return 0 on success and -errno on error. */ static int ntfs_write_volume_flags(struct ntfs_volume *vol, const __le16 set_bits, const __le16 clear_bits, - const bool skip_if_errors) + const bool dirty_if_errors) { struct ntfs_inode *ni = NTFS_I(vol->vol_ino); struct volume_information *vi; @@ -389,12 +413,11 @@ static int ntfs_write_volume_flags(struct ntfs_volume *vol, mutex_lock(&ni->mrec_lock); - if (skip_if_errors && NVolErrors(vol)) - goto done; - flags = le16_to_cpu(vol->vol_flags); flags |= le16_to_cpu(set_bits) & le16_to_cpu(VOLUME_FLAGS_MASK); flags &= ~(le16_to_cpu(clear_bits) & le16_to_cpu(VOLUME_FLAGS_MASK)); + if (dirty_if_errors && NVolErrors(vol)) + flags |= le16_to_cpu(VOLUME_IS_DIRTY); ntfs_debug("Entering, old flags = 0x%x, new flags = 0x%x.", le16_to_cpu(vol->vol_flags), flags); @@ -446,31 +469,43 @@ int ntfs_set_volume_flags(struct ntfs_volume *vol, __le16 flags) } /* - * ntfs_clear_volume_flags - clear bits in the volume information flags - * @vol: ntfs volume on which to modify the flags - * @flags: flags to clear on the volume + * ntfs_sync_volume_dirty_state - persist the dirty bit per the error state + * @vol: ntfs volume whose dirty bit to persist * - * Clear the bits in @flags in the volume information flags on the volume @vol. - * The bits are combined with the current flag state under the lock in - * ntfs_write_volume_flags(), so concurrent updates are not lost. + * Set VOLUME_IS_DIRTY if errors have been recorded on @vol and clear it + * otherwise, under the $Volume mrec_lock. * - * Return 0 on success and -errno on error. - */ -int ntfs_clear_volume_flags(struct ntfs_volume *vol, __le16 flags) -{ - return ntfs_write_volume_flags(vol, 0, flags, false); -} - -/* - * ntfs_clear_volume_dirty_if_no_errors - clear dirty bit if no errors exist - * @vol: ntfs volume whose dirty bit should be cleared + * The guarantee this provides is eventual, not instantaneous: the runtime + * error paths record NVolErrors() with a lock-free set_bit(), so a + * persistence point that evaluates the flag just before an error is + * recorded can still leave the on-disk bit clean. This is sound because + * NVolErrors() is sticky (nothing clears it for the lifetime of the mount) + * and every persistence point re-derives the on-disk bit from it; the + * last one, ntfs_put_super(), runs after evict_inodes() on a quiesced + * filesystem, so a volume that is read-write at unmount time cannot + * unmount clean. A volume that is already read-only when the error is + * recorded (errors=remount-ro flips the superblock on the first error, + * as does an earlier remount-ro) has no persistence point left and + * keeps whatever on-disk bit it had; that behaviour is unchanged. The + * residual window is a crash between the error and the next + * persistence point. + * + * This is the single point that persists the in-memory error state to disk. + * The runtime error paths only record NVolErrors() because they run under a + * variety of ntfs locks the dirty-bit write cannot be taken under (runlist + * locks, vol->lcnbmp_lock, vol->mftbmp_lock, mrec_locks); the first + * ntfs_sync_fs(), a remount, or the unmount then persists the flag here. + * + * A hibernated volume is not written from these persistence paths: + * resuming Windows from a modified image corrupts it, so the dirty bit + * is left as it is on disk and only the in-memory error state is kept. * - * Check NVolErrors() and clear VOLUME_IS_DIRTY under the same mrec_lock so - * ntfs_sync_fs() cannot clear the dirty bit after a concurrent error has been - * recorded. + * Return 0 on success and -errno on error. */ -static int ntfs_clear_volume_dirty_if_no_errors(struct ntfs_volume *vol) +static int ntfs_sync_volume_dirty_state(struct ntfs_volume *vol) { + if (NVolHibernated(vol)) + return 0; return ntfs_write_volume_flags(vol, 0, VOLUME_IS_DIRTY, true); } @@ -1683,6 +1718,11 @@ get_ctx_vol_failed: ntfs_error(sb, "%s. Mounting read-only%s", es1, es2); } NVolSetErrors(vol); + /* + * Remember it for the lifetime of the mount: see + * ntfs_sync_volume_dirty_state(). + */ + NVolSetHibernated(vol); } /* If (still) a read-write mount, empty the logfile. */ @@ -1840,22 +1880,31 @@ static void ntfs_put_super(struct super_block *sb) ntfs_commit_inode(vol->mft_ino); /* - * If a read-write mount and no volume errors have occurred, mark the - * volume clean. Also, re-commit all affected inodes. + * If a read-write mount, persist the error state in the volume flags: + * mark the volume clean if no volume errors have occurred, and make + * sure VOLUME_IS_DIRTY is on disk if any have, so chkdsk runs on the + * next mount. Also, re-commit all affected inodes. */ if (!sb_rdonly(sb)) { + if (ntfs_sync_volume_dirty_state(vol)) { + ntfs_warning(sb, + "Failed to sync dirty bit in volume information flags. Run chkdsk."); + } else if (NVolErrors(vol)) { + /* + * The dirty bit is on disk now; only warn when the + * sync actually succeeded, or this message would + * contradict the one above. + */ + ntfs_warning(sb, + "Volume has errors. Leaving volume marked dirty. Run chkdsk."); + } + /* Commits the updated volume flags if they were written. */ + ntfs_commit_inode(vol->vol_ino); if (!NVolErrors(vol)) { - if (ntfs_clear_volume_flags(vol, VOLUME_IS_DIRTY)) - ntfs_warning(sb, - "Failed to clear dirty bit in volume information flags. Run chkdsk."); - ntfs_commit_inode(vol->vol_ino); ntfs_commit_inode(vol->root_ino); if (vol->mftmirr_ino) ntfs_commit_inode(vol->mftmirr_ino); ntfs_commit_inode(vol->mft_ino); - } else { - ntfs_warning(sb, - "Volume has errors. Leaving volume marked dirty. Run chkdsk."); } } @@ -1954,8 +2003,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_dirty_if_no_errors(vol)) { - ntfs_warning(sb, "Failed to clear dirty bit in volume information flags. Run chkdsk."); + if (ntfs_sync_volume_dirty_state(vol)) { + ntfs_warning(sb, "Failed to sync dirty bit in volume information flags. Run chkdsk."); err = -EIO; } sync_inodes_sb(sb); diff --git a/fs/ntfs/volume.h b/fs/ntfs/volume.h index 8c17a66b4f22..fdb57279de84 100644 --- a/fs/ntfs/volume.h +++ b/fs/ntfs/volume.h @@ -189,6 +189,8 @@ struct ntfs_volume { * Windows-reserved names (CON, AUX, NUL, COM1, * LPT1, etc.) or invalid characters. * + * NV_Hibernated Windows is hibernated on the volume; the sync + * paths must not write the volume flags. * NV_Discard Issue discard/TRIM commands for freed clusters. * NV_DisableSparse Disable creation of sparse regions. * NV_NativeSymlinkRel Translate absolute Windows reparse targets (native_symlink=rel). @@ -207,6 +209,7 @@ enum { NV_ShowHiddenFiles, NV_HideDotFiles, NV_CheckWindowsNames, + NV_Hibernated, NV_Discard, NV_DisableSparse, NV_NativeSymlinkRel, @@ -245,6 +248,7 @@ DEFINE_NVOL_BIT_OPS(SysImmutable) DEFINE_NVOL_BIT_OPS(ShowHiddenFiles) DEFINE_NVOL_BIT_OPS(HideDotFiles) DEFINE_NVOL_BIT_OPS(CheckWindowsNames) +DEFINE_NVOL_BIT_OPS(Hibernated) DEFINE_NVOL_BIT_OPS(Discard) DEFINE_NVOL_BIT_OPS(DisableSparse) DEFINE_NVOL_BIT_OPS(NativeSymlinkRel) -- cgit v1.2.3 From f9d6eb76f5cbe7cb2e57871e16fdad45abd31c0e Mon Sep 17 00:00:00 2001 From: Hongling Zeng Date: Tue, 15 Sep 2026 16:32:49 +0800 Subject: ntfs: persist the dirty state after the final put_super() commits The just-in-case mftmirr/mft commits and the final write_inode_now() in ntfs_put_super() can record NVolErrors() after the dirty state has been persisted, so errors from those points would leave the volume unmounted with a clean on-disk dirty bit - contradicting the "cannot unmount clean" guarantee ntfs_sync_volume_dirty_state() is meant to provide. Move the persistence to the end of ntfs_put_super(): keep the gated re-commits and the tail commits where they are, run ntfs_sync_volume_dirty_state() and the $Volume commit after the last write_inode_now(), and release vol->vol_ino only after the sync. The release order of the special inodes matters for the $Volume commit: writing the $Volume record mirrors it through ntfs_sync_mft_mirror() (record number 3 is below vol->mftmirr_size), which fails with -EIO and leaves the mirror stale once vol->mftmirr_ino is gone, so the mirror inode is released only after that commit. vol->vol_ino is then put before vol->mft_ino is dropped: if the commit failed before it could clear the dirty flag, ntfs_evict_big_inode() commits the inode again on its way out, and __ntfs_write_inode() resolves the runlist through vol->mft_ino. Reported-by: Baolin Liu Cc: stable@vger.kernel.org Signed-off-by: Hongling Zeng Reviewed-by: Hyunchul Lee Reviewed-by: Baolin Liu Signed-off-by: Namjae Jeon --- fs/ntfs/super.c | 75 +++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 52 insertions(+), 23 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c index 785f5b222293..42d895776f32 100644 --- a/fs/ntfs/super.c +++ b/fs/ntfs/super.c @@ -1880,26 +1880,13 @@ static void ntfs_put_super(struct super_block *sb) ntfs_commit_inode(vol->mft_ino); /* - * If a read-write mount, persist the error state in the volume flags: - * mark the volume clean if no volume errors have occurred, and make - * sure VOLUME_IS_DIRTY is on disk if any have, so chkdsk runs on the - * next mount. Also, re-commit all affected inodes. + * If a read-write mount, re-commit all affected inodes once more. + * The dirty state itself is persisted at the end of ntfs_put_super(), + * after the last commits and the final write_inode_now(): those can + * still record errors via __ntfs_write_inode(), and the sync must + * evaluate NVolErrors() with the last setter already run. */ if (!sb_rdonly(sb)) { - if (ntfs_sync_volume_dirty_state(vol)) { - ntfs_warning(sb, - "Failed to sync dirty bit in volume information flags. Run chkdsk."); - } else if (NVolErrors(vol)) { - /* - * The dirty bit is on disk now; only warn when the - * sync actually succeeded, or this message would - * contradict the one above. - */ - ntfs_warning(sb, - "Volume has errors. Leaving volume marked dirty. Run chkdsk."); - } - /* Commits the updated volume flags if they were written. */ - ntfs_commit_inode(vol->vol_ino); if (!NVolErrors(vol)) { ntfs_commit_inode(vol->root_ino); if (vol->mftmirr_ino) @@ -1908,9 +1895,6 @@ static void ntfs_put_super(struct super_block *sb) } } - iput(vol->vol_ino); - vol->vol_ino = NULL; - /* NTFS 3.0+ specific clean up. */ if (vol->major_ver >= 3) { if (vol->extend_ino) { @@ -1940,8 +1924,6 @@ static void ntfs_put_super(struct super_block *sb) /* Re-commit the mft mirror and mft just in case. */ ntfs_commit_inode(vol->mftmirr_ino); ntfs_commit_inode(vol->mft_ino); - iput(vol->mftmirr_ino); - vol->mftmirr_ino = NULL; } /* * We should have no dirty inodes left, due to @@ -1951,6 +1933,53 @@ static void ntfs_put_super(struct super_block *sb) ntfs_commit_inode(vol->mft_ino); write_inode_now(vol->mft_ino, 1); + /* + * If a read-write mount, persist the error state in the volume flags: + * mark the volume clean if no volume errors have occurred, and make + * sure VOLUME_IS_DIRTY is on disk if any have, so chkdsk runs on the + * next mount. + */ + if (!sb_rdonly(sb)) { + if (ntfs_sync_volume_dirty_state(vol)) { + ntfs_warning(sb, + "Failed to sync dirty bit in volume information flags. Run chkdsk."); + } else if (NVolErrors(vol)) { + /* + * The dirty bit is on disk now; only warn when the + * sync actually succeeded, or this message would + * contradict the one above. + */ + ntfs_warning(sb, + "Volume has errors. Leaving volume marked dirty. Run chkdsk."); + } + /* + * Commits the updated volume flags if they were written. + * The mft mirror must still be around for this: the + * $Volume record (mft record number 3, below + * vol->mftmirr_size) is mirrored by write_mft_record() + * through ntfs_sync_mft_mirror(), which fails with -EIO + * and leaves the mirror stale once vol->mftmirr_ino is + * gone, so the mirror inode is only released after this + * commit. + */ + ntfs_commit_inode(vol->vol_ino); + } + + /* + * Release $Volume while the mft inode is still available: if the + * commit above failed before it could clear the dirty flag, + * ntfs_evict_big_inode() commits the inode again on its way out, + * and __ntfs_write_inode() needs vol->mft_ino to look up the + * runlist of the record to write. + */ + iput(vol->vol_ino); + vol->vol_ino = NULL; + + if (vol->mftmirr_ino) { + iput(vol->mftmirr_ino); + vol->mftmirr_ino = NULL; + } + iput(vol->mft_ino); vol->mft_ino = NULL; blkdev_issue_flush(sb->s_bdev); -- cgit v1.2.3 From 711dcf2007c794e357f35b75401f2040c100843e Mon Sep 17 00:00:00 2001 From: Hongling Zeng Date: Tue, 15 Sep 2026 16:32:50 +0800 Subject: ntfs: do not clear the volume dirty bit during sync ntfs_sync_fs() clears VOLUME_IS_DIRTY while the volume is still mounted read-write, so a sync running concurrently with an in-flight metadata modification can clear and persist a bit that was just set: the modification then lands on a volume that is clean on disk, and a crash does not run chkdsk. Stop clearing the bit in ntfs_sync_fs() and leave the clearing to the remount-to-read-only path, which the VFS reaches only after sb_prepare_remount_readonly() has drained in-flight writers, and to ntfs_put_super(), which runs after evict_inodes() on a quiesced filesystem. A mounted read-write volume now keeps the dirty bit until it is dismounted cleanly, which matches the NTFS semantics; the cost is a needless chkdsk if the machine crashes between a sync and the unmount. A recorded error state is still persisted right away when the filesystem is synced: with NVolErrors() set, ntfs_sync_volume_dirty_state() can only set the bit, never clear it, so it cannot lose a modification the way the old unconditional call did. This keeps the error report from being lost to a crash on a volume that has seen no modification; an error recorded after the last sync is still only persisted at the next quiescent transition. sync_blockdev() and blkdev_issue_flush() are both called and the first error is returned, so a writeback failure neither hides a flush failure nor skips it. A RWF_NOWAIT write still blocks in the marking when the volume looks clean, as it already did on the base; a non-blocking marking is follow-up work. Reported-by: Baolin Liu Cc: stable@vger.kernel.org Signed-off-by: Hongling Zeng Reviewed-by: Hyunchul Lee Reviewed-by: Baolin Liu Signed-off-by: Namjae Jeon --- fs/ntfs/file.c | 7 ++++--- fs/ntfs/super.c | 31 ++++++++++++++++++++++++------- 2 files changed, 28 insertions(+), 10 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c index 5a82c564876c..3ec82715a588 100644 --- a/fs/ntfs/file.c +++ b/fs/ntfs/file.c @@ -637,9 +637,10 @@ static ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from) /* * The volume must be marked dirty before the modification is made, - * without an unlocked check of the in-memory flag: ntfs_sync_fs() - * can clear the bit concurrently and the modification would then - * land on a volume that is clean on disk. + * without an unlocked check of the in-memory flag: the dirty bit + * is only cleared at the quiescent transitions, under the same + * $Volume mrec_lock this call takes, so an unlocked skip could + * lose the set to one of them. */ ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c index 42d895776f32..a08523457fe0 100644 --- a/fs/ntfs/super.c +++ b/fs/ntfs/super.c @@ -493,8 +493,9 @@ int ntfs_set_volume_flags(struct ntfs_volume *vol, __le16 flags) * This is the single point that persists the in-memory error state to disk. * The runtime error paths only record NVolErrors() because they run under a * variety of ntfs locks the dirty-bit write cannot be taken under (runlist - * locks, vol->lcnbmp_lock, vol->mftbmp_lock, mrec_locks); the first - * ntfs_sync_fs(), a remount, or the unmount then persists the flag here. + * locks, vol->lcnbmp_lock, vol->mftbmp_lock, mrec_locks); a sync of a + * volume with recorded errors, a remount to read-only, or the unmount, + * then persists the flag here. * * A hibernated volume is not written from these persistence paths: * resuming Windows from a modified image corrupts it, so the dirty bit @@ -2023,7 +2024,7 @@ static void ntfs_shutdown(struct super_block *sb) static int ntfs_sync_fs(struct super_block *sb, int wait) { struct ntfs_volume *vol = NTFS_SB(sb); - int err = 0; + int ret, err = 0; if (NVolShutdown(vol)) return -EIO; @@ -2031,14 +2032,30 @@ static int ntfs_sync_fs(struct super_block *sb, int wait) if (!wait) return 0; - /* If there are some dirty buffers in the bdev inode */ - if (ntfs_sync_volume_dirty_state(vol)) { + /* + * The volume dirty bit is deliberately not cleared here: a sync + * running concurrently with an in-flight modification could clear + * and persist a bit that was just set, leaving the modification + * on a volume that is clean on disk. The bit is only cleared at + * the quiescent state transitions, remounting read-only and clean + * unmount. A recorded error state, however, is persisted right + * away so that it is not lost to a crash on a volume that has + * seen no modification; with NVolErrors() set this can only set + * the bit, never clear it. + */ + if (NVolErrors(vol) && + ntfs_sync_volume_dirty_state(vol)) { ntfs_warning(sb, "Failed to sync dirty bit in volume information flags. Run chkdsk."); err = -EIO; } sync_inodes_sb(sb); - sync_blockdev(sb->s_bdev); - blkdev_issue_flush(sb->s_bdev); + ret = sync_blockdev(sb->s_bdev); + if (ret && !err) + err = ret; + + ret = blkdev_issue_flush(sb->s_bdev); + if (ret && !err) + err = ret; return err; } -- cgit v1.2.3 From 4f60714aa0ff4c5048eb5a49b6b394af8b48dfdd Mon Sep 17 00:00:00 2001 From: Hongling Zeng Date: Tue, 15 Sep 2026 16:32:51 +0800 Subject: ntfs: check the dirty-state commit on remount and unmount The remount-to-read-only path commits the updated volume flags with ntfs_commit_inode(), a void wrapper around __ntfs_write_inode(), and ignores the blkdev_issue_flush() return value, so a failed commit or flush is reported as success. Once the remount has succeeded no persistence point is ever reached again: ntfs_put_super() skips read-only superblocks and the VFS never syncs one, so fail the remount unless the commit and the flush succeed. The superblock then stays read-write and ntfs_put_super() retries the persistence at unmount. The errors=remount-ro downgrade does not go through ntfs_reconfigure() and is unchanged. A zero-return commit is not trusted blindly: write_mft_record() redirties the record on allocation failure and reports success, so the $Volume inode is required to be clean afterwards. ntfs_put_super() discards the same commit error. Call __ntfs_write_inode() there with the same dirty re-check and warn on failure, as put_super() cannot return an error. The commit is skipped when the dirty-state sync itself failed, as that could write back an inconsistent flag state; a record left dirty by an earlier update is still committed at evict time. NVolErrors() is deliberately not used to detect the failure: it is sticky for the lifetime of the mount, so it cannot distinguish a fresh commit failure from errors recorded before the remount. Hibernated volumes: ntfs_sync_volume_dirty_state() is a no-op for them and never dirties the $Volume inode on such a mount, since the on-disk flags are already dirty and ntfs_set_volume_flags() has nothing to change. The commit only runs if something dirtied the inode independently, as before this patch; mounting hibernated volumes read-only removes even that. Reported-by: Baolin Liu Cc: stable@vger.kernel.org Signed-off-by: Hongling Zeng Reviewed-by: Hyunchul Lee Reviewed-by: Baolin Liu Signed-off-by: Namjae Jeon --- fs/ntfs/super.c | 76 ++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 54 insertions(+), 22 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c index a08523457fe0..0ec4d2ddf621 100644 --- a/fs/ntfs/super.c +++ b/fs/ntfs/super.c @@ -269,6 +269,7 @@ static int ntfs_reconfigure(struct fs_context *fc) { struct super_block *sb = fc->root->d_sb; struct ntfs_volume *vol = NTFS_SB(sb); + int err; ntfs_debug("Entering with remount"); @@ -331,14 +332,39 @@ static int ntfs_reconfigure(struct fs_context *fc) * and ntfs_put_super() skips them, so the only remaining * write would be the evict-time commit at unmount, which * a crash never reaches. An error recorded only after - * the remount is still never persisted. + * the remount is still never persisted; a failed commit + * or flush fails the remount, leaving the superblock + * read-write so ntfs_put_super() retries at unmount. */ - if (ntfs_sync_volume_dirty_state(vol)) { + err = ntfs_sync_volume_dirty_state(vol); + if (err) { ntfs_warning(sb, "Failed to update dirty bit in volume information flags. Run chkdsk."); - } else if (NInoDirty(NTFS_I(vol->vol_ino))) { - ntfs_commit_inode(vol->vol_ino); - blkdev_issue_flush(sb->s_bdev); + return err; + } + if (NInoDirty(NTFS_I(vol->vol_ino))) { + /* ntfs_commit_inode() would discard the error. */ + err = __ntfs_write_inode(vol->vol_ino, 1); + if (err) { + ntfs_warning(sb, + "Failed to commit volume information flags. Run chkdsk."); + return err; + } + /* + * write_mft_record() redirties the record on + * -ENOMEM and still reports success. + */ + if (NInoDirty(NTFS_I(vol->vol_ino))) { + ntfs_warning(sb, + "Volume information flags remain dirty after commit. Run chkdsk."); + return -EIO; + } + err = blkdev_issue_flush(sb->s_bdev); + if (err) { + ntfs_warning(sb, + "Failed to flush volume information flags. Run chkdsk."); + return err; + } } } @@ -1944,26 +1970,32 @@ static void ntfs_put_super(struct super_block *sb) if (ntfs_sync_volume_dirty_state(vol)) { ntfs_warning(sb, "Failed to sync dirty bit in volume information flags. Run chkdsk."); - } else if (NVolErrors(vol)) { + } else { /* - * The dirty bit is on disk now; only warn when the - * sync actually succeeded, or this message would - * contradict the one above. + * __ntfs_write_inode(), not the void + * ntfs_commit_inode() wrapper: the error can only + * be warned about here. The mirror inode is only + * released below: writing the $Volume record (mft + * record number 3, below vol->mftmirr_size) mirrors + * it through ntfs_sync_mft_mirror(), which fails + * with -EIO once vol->mftmirr_ino is gone. */ - ntfs_warning(sb, - "Volume has errors. Leaving volume marked dirty. Run chkdsk."); + if (__ntfs_write_inode(vol->vol_ino, 1)) { + ntfs_warning(sb, + "Failed to commit volume information flags. Run chkdsk."); + } else if (NInoDirty(NTFS_I(vol->vol_ino))) { + ntfs_warning(sb, + "Volume information flags remain dirty after commit. Run chkdsk."); + } else if (NVolErrors(vol)) { + /* + * Only warn once the commit has succeeded, + * or this could contradict a failure + * reported above. + */ + ntfs_warning(sb, + "Volume has errors. Leaving volume marked dirty. Run chkdsk."); + } } - /* - * Commits the updated volume flags if they were written. - * The mft mirror must still be around for this: the - * $Volume record (mft record number 3, below - * vol->mftmirr_size) is mirrored by write_mft_record() - * through ntfs_sync_mft_mirror(), which fails with -EIO - * and leaves the mirror stale once vol->mftmirr_ino is - * gone, so the mirror inode is only released after this - * commit. - */ - ntfs_commit_inode(vol->vol_ino); } /* -- cgit v1.2.3 From 444c7dc82415a353b57e43189f276124cc5fd785 Mon Sep 17 00:00:00 2001 From: Hongling Zeng Date: Tue, 15 Sep 2026 16:32:52 +0800 Subject: ntfs: fail remount on sync errors and keep the dirty bit on SB_FORCE ntfs_reconfigure() currently ignores sync_filesystem() errors, allowing a regular read-only remount to succeed even when dirty data was not synced. Check the error and fail regular remounts, leaving the superblock read-write so ntfs_put_super() can retry at unmount. SB_FORCE does not wait for writers already in progress, so it must not clear the on-disk dirty bit. Warn and continue on sync errors for SB_FORCE. A forced remount updates the dirty state only when errors have been recorded, and never clears the dirty bit. Skip the commit if that state update fails, since the in-memory flags may be inconsistent. Reported-by: Hyunchul Lee Cc: stable@vger.kernel.org Signed-off-by: Hongling Zeng Reviewed-by: Hyunchul Lee Reviewed-by: Baolin Liu Signed-off-by: Namjae Jeon --- fs/ntfs/super.c | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c index 0ec4d2ddf621..6a107ec0d0f6 100644 --- a/fs/ntfs/super.c +++ b/fs/ntfs/super.c @@ -273,7 +273,13 @@ static int ntfs_reconfigure(struct fs_context *fc) ntfs_debug("Entering with remount"); - sync_filesystem(sb); + err = sync_filesystem(sb); + if (err) { + ntfs_warning(sb, "Failed to sync the filesystem."); + /* A forced remount must still turn the superblock read-only. */ + if (!(fc->sb_flags & SB_FORCE)) + return err; + } /* * For the read-write compiled driver, if we are remounting read-write, @@ -336,11 +342,20 @@ static int ntfs_reconfigure(struct fs_context *fc) * or flush fails the remount, leaving the superblock * read-write so ntfs_put_super() retries at unmount. */ - err = ntfs_sync_volume_dirty_state(vol); - if (err) { - ntfs_warning(sb, - "Failed to update dirty bit in volume information flags. Run chkdsk."); - return err; + /* + * A forced remount does not drain writers in progress, + * so one may still be modifying metadata when the flags + * are committed: never clear the dirty bit then; if + * errors have been recorded, the update preserves or + * sets it; otherwise, skip the update entirely. + */ + if (!(fc->sb_flags & SB_FORCE) || NVolErrors(vol)) { + err = ntfs_sync_volume_dirty_state(vol); + if (err) { + ntfs_warning(sb, + "Failed to update dirty bit in volume information flags. Run chkdsk."); + return err; + } } if (NInoDirty(NTFS_I(vol->vol_ino))) { /* ntfs_commit_inode() would discard the error. */ -- cgit v1.2.3 From b56469c26f99c04c2fa0899e161c18dd3c899729 Mon Sep 17 00:00:00 2001 From: Karl Mehltretter Date: Tue, 22 Sep 2026 06:33:01 +0200 Subject: ntfs: include the final partial cluster in sync writes ntfs_attrlist_repack() synchronously writes the replacement attribute list before updating its mapping pairs and freeing the old run. With 512-byte clusters, a 544-byte list submits a 512-byte bio, but the write returns 544 and repack accepts it as complete. If the later buffered update does not reach disk, the replacement list lacks its final 32 bytes. __ntfs_inode_non_resident_attr_pwrite() converts attr_len to clusters with ntfs_bytes_to_cluster(), which rounds down. Before repack, its synchronous callers used either cluster-aligned lengths or lengths smaller than one cluster, where max_t() selects one. Round attr_len up so repack writes the final partial cluster before publishing the new mapping pairs. Fixes: b1d732e62a5b ("ntfs: repack $MFT/$ATTRIBUTE LIST") Cc: stable@vger.kernel.org Assisted-by: LLM Signed-off-by: Karl Mehltretter Signed-off-by: Namjae Jeon --- fs/ntfs/inode.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'fs') diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c index cadf623d54d4..d8d3a7b7b8a9 100644 --- a/fs/ntfs/inode.c +++ b/fs/ntfs/inode.c @@ -3761,7 +3761,9 @@ static s64 __ntfs_inode_non_resident_attr_pwrite(struct inode *vi, struct runlist_element *rl; int bio_err; - lcn_count = max_t(s64, 1, ntfs_bytes_to_cluster(vol, attr_len)); + lcn_count = max_t(s64, 1, + ntfs_bytes_to_cluster(vol, attr_len + + vol->cluster_size - 1)); vcn = ntfs_pidx_to_cluster(vol, folio->index); do { -- cgit v1.2.3 From e9c37627c3de536624827769940c23683d6bdf5a Mon Sep 17 00:00:00 2001 From: Hongling Zeng Date: Tue, 22 Sep 2026 18:11:17 +0800 Subject: ntfs: mount hibernated volumes read-only regardless of errors= The hibernation check in load_system_files() only converts the superblock to read-only under errors=remount-ro. With the default errors=continue (and with errors=panic), a hibernated volume is mounted read-write and the mount-time $LogFile emptying writes to it, although a hibernated volume must not be written to at all. Drop the on_errors term so that a hibernated volume, or a volume whose hibernation state cannot be determined, always mounts read-only. NVolErrors() is still recorded, so ntfs_reconfigure() keeps refusing remounts to read-write, and the $LogFile emptying is skipped by its !sb_rdonly() check. The check itself must not apply the errors= policy either: it runs before SB_RDONLY is set, and ntfs_lookup_inode_by_name() and ntfs_iget() call ntfs_error() internally on corruption or I/O errors, which under errors=panic would panic the machine before the read-only fallback has made its decision. Temporarily substitute errors=remount-ro for errors=panic around the check: nested ntfs_error() calls then fall back to read-only, and any read-only transition they trigger is preserved even when the error is not propagated to the check's return value or recorded in NVolErrors(). This is safe during initial mount, before the super block is published. If the check reports hibernation or fails, set SB_RDONLY and record NVolErrors() unconditionally: hibernation safety takes precedence over the errors= policy. A volume with errors recorded during the check or earlier in the mount, e.g. when loading the LogFile, also stays read-only, matching what ntfs_reconfigure() enforces for remounts. Fixes: 6251f0b0de7d ("ntfs: update super block operations") Cc: stable@vger.kernel.org Suggested-by: Baolin Liu Reviewed-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Hongling Zeng Signed-off-by: Namjae Jeon --- fs/ntfs/super.c | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c index 6a107ec0d0f6..716ba775f1f3 100644 --- a/fs/ntfs/super.c +++ b/fs/ntfs/super.c @@ -1571,6 +1571,7 @@ static bool load_system_files(struct ntfs_volume *vol) struct ntfs_attr_search_ctx *ctx; struct restart_page_header *rp; int err; + u8 saved_on_errors; ntfs_debug("Entering."); /* Get mft mirror inode compare the contents of $MFT and $MFTMirr. */ @@ -1745,8 +1746,19 @@ get_ctx_vol_failed: * NVolErrors() without setting the dirty volume flag and mount * read-only. This will prevent read-write remounting and it will also * prevent all writes. + * + * Nested lookup and inode-loading errors must not panic before the + * read-only fallback has run. Temporarily use errors=remount-ro + * instead of errors=panic, preserving any read-only transition even + * if an error is not propagated to the check's return value or + * recorded in NVolErrors(). This is safe during initial mount, + * before the super block is published. */ + saved_on_errors = vol->on_errors; + if (saved_on_errors == ON_ERRORS_PANIC) + vol->on_errors = ON_ERRORS_REMOUNT_RO; err = check_windows_hibernation_status(vol); + vol->on_errors = saved_on_errors; if (unlikely(err)) { static const char *es1a = "Failed to determine if Windows is hibernated"; static const char *es1b = "Windows is hibernated"; @@ -1754,17 +1766,21 @@ get_ctx_vol_failed: const char *es1; es1 = err < 0 ? es1a : es1b; - /* If a read-write mount, convert it to a read-only mount. */ - if (!sb_rdonly(sb) && vol->on_errors == ON_ERRORS_REMOUNT_RO) { - sb->s_flags |= SB_RDONLY; - ntfs_error(sb, "%s. Mounting read-only%s", es1, es2); - } + /* Hibernation safety takes precedence over the errors= policy. */ + sb->s_flags |= SB_RDONLY; NVolSetErrors(vol); + ntfs_error(sb, "%s. Mounting read-only%s", es1, es2); + /* * Remember it for the lifetime of the mount: see * ntfs_sync_volume_dirty_state(). */ NVolSetHibernated(vol); + } else if (!sb_rdonly(sb) && NVolErrors(vol)) { + /* Match the read-write remount restriction for recorded errors. */ + sb->s_flags |= SB_RDONLY; + ntfs_error(sb, + "Errors were recorded during mount. Mounting read-only. Run chkdsk."); } /* If (still) a read-write mount, empty the logfile. */ -- cgit v1.2.3 From 401898d748fcc8e19ceea1a37ed0de9075fe9ff2 Mon Sep 17 00:00:00 2001 From: Hyunchul Lee Date: Tue, 22 Sep 2026 15:18:33 +0900 Subject: ntfs: validate attribute-list sizes before allocation The NULL point dereference could occur when an on-disk $ATTRIBUTE_LIST size was truncated before it was validated and allocated. To fix this issue and prevent excessive memory allocation, limit the $ATTRIBUTE_LIST size to 256 KiB. The following existing implementation and Windows test provide evidence for this 256 KiB compatibility limit: NTFS already treats 256 KiB as the $ATTRIBUTE_LIST limit in its attribute-size checks and $MFT attribute list repack/expand paths. For additional Windows testing, when $MFT's attribute-list data_size was above 256 KiB, Windows attached the disk partition but did not mount a NTFS volume and target access failed. Fixes: af0db57d4293 ("ntfs: update inode operations") Link: https://lore.kernel.org/ntfs/88468.1790006025@30-10-12.wireless.csail.mit.edu/T/#u Cc: stable@vger.kernel.org Cc: Robert Morris Signed-off-by: Hyunchul Lee Reviewed-by: Baolin Liu Signed-off-by: Namjae Jeon --- fs/ntfs/attrib.c | 2 +- fs/ntfs/attrlist.c | 2 -- fs/ntfs/inode.c | 21 +++++++++++++++------ fs/ntfs/ntfs.h | 2 ++ 4 files changed, 18 insertions(+), 9 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c index d8dd40526c3b..333b3371acb4 100644 --- a/fs/ntfs/attrib.c +++ b/fs/ntfs/attrib.c @@ -1775,7 +1775,7 @@ int ntfs_attr_size_bounds_check(const struct ntfs_volume *vol, const __le32 type * $ATTRIBUTE_LIST has a maximum size of 256kiB, but this is not * listed in $AttrDef. */ - if (unlikely(type == AT_ATTRIBUTE_LIST && size > 256 * 1024)) + if (unlikely(type == AT_ATTRIBUTE_LIST && size > NTFS_MAX_ATTR_LIST_SIZE)) return -ERANGE; /* Get the $AttrDef entry for the attribute @type. */ ad = ntfs_attr_find_in_attrdef(vol, type); diff --git a/fs/ntfs/attrlist.c b/fs/ntfs/attrlist.c index 7c09d02fd941..1bbd2bc62c58 100644 --- a/fs/ntfs/attrlist.c +++ b/fs/ntfs/attrlist.c @@ -14,8 +14,6 @@ #include "attrlist.h" #include "lcnalloc.h" -#define NTFS_MAX_ATTR_LIST_SIZE (256 * 1024) - /* * ntfs_attrlist_need - check whether inode need attribute list * @ni: opened ntfs inode for which perform check diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c index d8d3a7b7b8a9..9583b2c6c7a2 100644 --- a/fs/ntfs/inode.c +++ b/fs/ntfs/inode.c @@ -805,6 +805,8 @@ static int ntfs_read_locked_inode(struct inode *vi) goto unm_err_out; } } else { + s64 attr_list_size; + if (vi->i_ino == FILE_MFT) goto skip_attr_list_load; ntfs_debug("Attribute list found in inode 0x%llx.", ni->mft_no); @@ -827,11 +829,14 @@ static int ntfs_read_locked_inode(struct inode *vi) ni->mft_no); } /* Now allocate memory for the attribute list. */ - ni->attr_list_size = (u32)ntfs_attr_size(a); - if (!ni->attr_list_size) { - ntfs_error(vi->i_sb, "Attr_list_size is zero"); + attr_list_size = ntfs_attr_size(a); + if (attr_list_size <= 0 || + attr_list_size > NTFS_MAX_ATTR_LIST_SIZE) { + ntfs_error(vi->i_sb, "Invalid attribute list size %lld (mft_no 0x%llx).", + (long long)attr_list_size, ni->mft_no); goto unm_err_out; } + ni->attr_list_size = (u32)attr_list_size; ni->attr_list = kvzalloc(ni->attr_list_size, GFP_NOFS); if (!ni->attr_list) { ntfs_error(vi->i_sb, @@ -1955,6 +1960,7 @@ int ntfs_read_inode_mount(struct inode *vi) } else /* if (!err) */ { struct attr_list_entry *al_entry, *next_al_entry; u8 *al_end; + s64 attr_list_size; static const char *es = " Not allowed. $MFT is corrupt. You should run chkdsk."; ntfs_debug("Attribute list attribute found in $MFT."); @@ -1978,11 +1984,14 @@ int ntfs_read_inode_mount(struct inode *vi) "Resident attribute list attribute in $MFT system file is marked encrypted/sparse which is not true. However, Windows allows this and chkdsk does not detect or correct it so we will just ignore the invalid flags and pretend they are not set."); } /* Now allocate memory for the attribute list. */ - ni->attr_list_size = (u32)ntfs_attr_size(a); - if (!ni->attr_list_size) { - ntfs_error(sb, "Attr_list_size is zero"); + attr_list_size = ntfs_attr_size(a); + if (attr_list_size <= 0 || + attr_list_size > NTFS_MAX_ATTR_LIST_SIZE) { + ntfs_error(sb, "Invalid attribute list size %lld (mft_no 0x%llx).%s", + (long long)attr_list_size, ni->mft_no, es); goto put_err_out; } + ni->attr_list_size = (u32)attr_list_size; ni->attr_list = kvzalloc(round_up(ni->attr_list_size, SECTOR_SIZE), GFP_NOFS); if (!ni->attr_list) { diff --git a/fs/ntfs/ntfs.h b/fs/ntfs/ntfs.h index dd56894762a9..9557a5c36b81 100644 --- a/fs/ntfs/ntfs.h +++ b/fs/ntfs/ntfs.h @@ -80,6 +80,8 @@ enum { NTFS_MAX_LABEL_LEN = 128, }; +#define NTFS_MAX_ATTR_LIST_SIZE (256 * 1024) + enum { CASE_SENSITIVE = 0, IGNORE_CASE = 1, -- cgit v1.2.3 From bab74ffcb44653cf38b4df06ed01d1d85d019887 Mon Sep 17 00:00:00 2001 From: pwngets <2605452642@qq.com> Date: Wed, 23 Sep 2026 13:41:06 +0800 Subject: ntfs: fix runlist marker allocation and count The marker path can append an unmapped element and an ENOENT terminator after the merge helpers have already computed the resulting runlist count. In that case, allocating ds + 2 elements leaves no room for the terminator at ds + 2. The path also increments new_rl_count even though the merge helper already accounted for the final runlist size, causing a later merge to start past the terminator. Allocate space through ds + 2, and derive new_rl_count from the final terminator index instead of incrementing the count a second time. A crafted NTFS image can reach this path through a normal read after the image has been mounted and trigger out-of-bounds kernel heap accesses. Fixes: 11ccc9107dc4 ("ntfs: update runlist handling and cluster allocator") Assisted-by: OpenAI Codex:GPT-5 Signed-off-by: pwngets <2605452642@qq.com> Reviewed-by: Baolin Liu Signed-off-by: Namjae Jeon --- fs/ntfs/runlist.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/runlist.c b/fs/ntfs/runlist.c index 3a61f19bcbee..f13d28c15887 100644 --- a/fs/ntfs/runlist.c +++ b/fs/ntfs/runlist.c @@ -680,9 +680,8 @@ struct runlist_element *ntfs_runlists_merge(struct runlist *d_runlist, /* Add an unmapped runlist element. */ if (!slots) { drl = ntfs_rl_realloc_nofail(drl, ds, - ds + 2); + ds + 3); slots = 2; - *new_rl_count += 2; } ds++; /* Need to set vcn if it isn't set already. */ @@ -698,11 +697,11 @@ struct runlist_element *ntfs_runlists_merge(struct runlist *d_runlist, ds++; if (!slots) { drl = ntfs_rl_realloc_nofail(drl, ds, ds + 1); - *new_rl_count += 1; } drl[ds].vcn = marker_vcn; drl[ds].lcn = LCN_ENOENT; drl[ds].length = (s64)0; + *new_rl_count = ds + 1; } } } -- cgit v1.2.3 From fd2b3321e0cb9fbab7a5529d89d3a9d8e304937f Mon Sep 17 00:00:00 2001 From: Xuewen Wang Date: Thu, 24 Sep 2026 09:45:11 +0800 Subject: ntfs: bound the adjacent cluster in MFT bitmap extension When the last MFT bitmap run reaches the volume boundary, the direct allocation path tests the bit for nr_clusters, an invalid LCN. A clear bit allows an out-of-volume allocation; a bitmap read failure aborts extension even if valid clusters remain available. Check the candidate LCN before accessing $Bitmap and fall back to ntfs_cluster_alloc() with no locality hint when it is outside the volume. Reviewed-by: Baolin Liu Signed-off-by: Xuewen Wang Reviewed-by: Baolin Liu Signed-off-by: Namjae Jeon --- fs/ntfs/mft.c | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'fs') diff --git a/fs/ntfs/mft.c b/fs/ntfs/mft.c index 8bb8b4085c8b..88bac85d22bc 100644 --- a/fs/ntfs/mft.c +++ b/fs/ntfs/mft.c @@ -1377,6 +1377,11 @@ static int ntfs_mft_bitmap_extend_allocation_nolock(struct ntfs_volume *vol) lcn = rl->lcn + rl->length; ntfs_debug("Last lcn of mft bitmap attribute is 0x%llx.", (long long)lcn); + /* There is no adjacent cluster if the last run ends at the volume end. */ + if (lcn >= vol->nr_clusters) { + lcn = -1; + goto alloc_cluster; + } /* * Attempt to get the cluster following the last allocated cluster by * hand as it may be in the MFT zone so the allocator would not give it @@ -1413,6 +1418,7 @@ static int ntfs_mft_bitmap_extend_allocation_nolock(struct ntfs_volume *vol) kunmap_local(b); folio_put(folio); up_write(&vol->lcnbmp_lock); +alloc_cluster: /* Allocate a cluster from the DATA_ZONE. */ rl2 = ntfs_cluster_alloc(vol, rl[1].vcn, 1, lcn, DATA_ZONE, true, false, false); -- cgit v1.2.3 From ab0c67226dfe860afb68ca6c6ab13b4f56bb409a Mon Sep 17 00:00:00 2001 From: Xuewen Wang Date: Thu, 24 Sep 2026 09:45:12 +0800 Subject: ntfs: fix rollback after MFT bitmap run coalescing After the direct allocation attempt drops lcnbmp_lock, a concurrent free can let ntfs_cluster_alloc() return the adjacent cluster. The merged runlist then extends the existing last run, but rollback assumes a new run was added. A later metadata failure removes the old mapping and frees its first cluster. Record whether the allocated cluster is adjacent before merging the runlists. On rollback, shorten a coalesced run by one cluster and free only that cluster; remove the new run for a non-adjacent allocation. Suggested-by: Baolin Liu Reviewed-by: Baolin Liu Signed-off-by: Xuewen Wang Reviewed-by: Baolin Liu Signed-off-by: Namjae Jeon --- fs/ntfs/mft.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/mft.c b/fs/ntfs/mft.c index 88bac85d22bc..12666668a80e 100644 --- a/fs/ntfs/mft.c +++ b/fs/ntfs/mft.c @@ -1428,6 +1428,8 @@ alloc_cluster: "Failed to allocate a cluster for the mft bitmap."); return PTR_ERR(rl2); } + /* The adjacent cluster may have become available while unlocked. */ + status.added_cluster = rl2->lcn == lcn; rl = ntfs_runlists_merge(&mftbmp_ni->runlist, rl2, 0, &new_rl_count); if (IS_ERR(rl)) { up_write(&mftbmp_ni->runlist.lock); @@ -1442,8 +1444,8 @@ alloc_cluster: } mftbmp_ni->runlist.rl = rl; mftbmp_ni->runlist.count = new_rl_count; - status.added_run = 1; - ntfs_debug("Adding one run to mft bitmap."); + status.added_run = !status.added_cluster; + ntfs_debug("Allocated one cluster for mft bitmap."); /* Find the last run in the new runlist. */ for (; rl[1].length; rl++) ; -- cgit v1.2.3 From ea960344bb3940fb5bd0fdbdc9e66b7b78913099 Mon Sep 17 00:00:00 2001 From: Xuewen Wang Date: Thu, 24 Sep 2026 09:45:13 +0800 Subject: ntfs: wait for the bitmap scan before MFT bitmap extension If MFT bitmap extension fails before the initial $Bitmap scan completes, rollback calls ntfs_bitmap_clear_bit(), which holds a folio lock while ntfs_set_lcn_empty_bits() waits for the scan. The scanner deadlocks if it still needs that folio. Wait for the initial scan before taking the runlist or bitmap folio locks and attempting allocation. Fixes: 11ccc9107dc4 ("ntfs: update runlist handling and cluster allocator") Reviewed-by: Baolin Liu Signed-off-by: Xuewen Wang Reviewed-by: Baolin Liu Signed-off-by: Namjae Jeon --- fs/ntfs/mft.c | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'fs') diff --git a/fs/ntfs/mft.c b/fs/ntfs/mft.c index 12666668a80e..489a5935202c 100644 --- a/fs/ntfs/mft.c +++ b/fs/ntfs/mft.c @@ -1352,6 +1352,10 @@ static int ntfs_mft_bitmap_extend_allocation_nolock(struct ntfs_volume *vol) size_t new_rl_count; ntfs_debug("Extending mft bitmap allocation."); + /* The initial bitmap scan must finish before we lock or change a folio. */ + if (!NVolFreeClusterKnown(vol)) + wait_event(vol->free_waitq, NVolFreeClusterKnown(vol)); + mft_ni = NTFS_I(vol->mft_ino); mftbmp_ni = NTFS_I(vol->mftbmp_ino); /* -- cgit v1.2.3 From 259abb551e2944998cad4214c201954ab1ac5c8d Mon Sep 17 00:00:00 2001 From: Xuewen Wang Date: Thu, 24 Sep 2026 09:45:14 +0800 Subject: ntfs: account for direct MFT bitmap cluster allocation The MFT bitmap extension fast path sets $Bitmap directly, bypassing ntfs_cluster_alloc() and its free_clusters and lcn_empty_bits_per_page updates. Successful extensions overreport free space, and rollback adds back counts that were never deducted. Update both counts under lcnbmp_lock and the bitmap folio lock. Require a nonzero page count and space remaining after delayed-allocation reservations; otherwise use the regular allocator. The page check also prevents underflow when the initial scan recorded zero after a read error. Fixes: 6251f0b0de7d ("ntfs: update super block operations") Suggested-by: Baolin Liu Reviewed-by: Baolin Liu Signed-off-by: Xuewen Wang Reviewed-by: Baolin Liu Signed-off-by: Namjae Jeon --- fs/ntfs/mft.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'fs') diff --git a/fs/ntfs/mft.c b/fs/ntfs/mft.c index 489a5935202c..e01e367a588d 100644 --- a/fs/ntfs/mft.c +++ b/fs/ntfs/mft.c @@ -1404,10 +1404,18 @@ static int ntfs_mft_bitmap_extend_allocation_nolock(struct ntfs_volume *vol) folio_lock(folio); b = (u8 *)kmap_local_folio(folio, 0) + (ll & ~PAGE_MASK); tb = 1 << (lcn & 7ull); - if (*b != 0xff && !(*b & tb)) { + /* + * A page skipped by the initial scan has no free bits recorded. + * Honor that and the space reserved for delayed allocation. + */ + if (*b != 0xff && !(*b & tb) && + vol->lcn_empty_bits_per_page[ll >> PAGE_SHIFT] && + ntfs_available_clusters_count(vol, 1) > 0) { /* Next cluster is free, allocate it. */ *b |= tb; folio_mark_dirty(folio); + ntfs_dec_free_clusters(vol, 1); + ntfs_set_lcn_empty_bits(vol, ll >> PAGE_SHIFT, 1, 1); folio_unlock(folio); kunmap_local(b); folio_put(folio); -- cgit v1.2.3 From ecb78f8f8039d145045f566a87a3cd6580fb6143 Mon Sep 17 00:00:00 2001 From: Hyunchul Lee Date: Mon, 28 Sep 2026 16:24:51 +0900 Subject: ntfs: avoid misclassifying directory reparse points Windows directory symlinks and junctions can carry FILE_ATTR_DUP_FILE_NAME_INDEX_PRESENT in their entry while the reparse flag is stored only in. The old directory enumeration path treated such entries as DT_DIR, allowing no-follow userspace scanners to recurse through symlink targets. Fixes: 0a8ac0c1fa0b ("ntfs: update directory operations") Signed-off-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/dir.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/dir.c b/fs/ntfs/dir.c index c2b48bc696bf..b95173f068cb 100644 --- a/fs/ntfs/dir.c +++ b/fs/ntfs/dir.c @@ -635,11 +635,11 @@ static inline int ntfs_filldir(struct ntfs_volume *vol, } mref = MREF_LE(ie->data.dir.indexed_file); - if (ie->key.file_name.file_attributes & + if (ie->key.file_name.file_attributes & FILE_ATTR_REPARSE_POINT) + dt_type = ntfs_reparse_tag_dt_types(vol, mref); + else if (ie->key.file_name.file_attributes & FILE_ATTR_DUP_FILE_NAME_INDEX_PRESENT) dt_type = DT_DIR; - else if (ie->key.file_name.file_attributes & FILE_ATTR_REPARSE_POINT) - dt_type = ntfs_reparse_tag_dt_types(vol, mref); else dt_type = DT_REG; -- cgit v1.2.3 From 6c81ca00644eed0a9d1986475a50131b37483b60 Mon Sep 17 00:00:00 2001 From: Hyunchul Lee Date: Mon, 28 Sep 2026 16:25:13 +0900 Subject: ntfs: accept standard sparse compression unit Windows native sparse-file inspection shows that a non-resident $DATA attribute can be marked sparse and record the standard compression unit (4) even when the file is not compressed. Fixes: af0db57d4293 ("ntfs: update inode operations") Reviewed-by: Baolin Liu Signed-off-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/inode.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c index 9583b2c6c7a2..de848180749d 100644 --- a/fs/ntfs/inode.c +++ b/fs/ntfs/inode.c @@ -1148,14 +1148,21 @@ view_index_meta: goto unm_err_out; } + /* + * Windows stores the standard compression unit in + * sparse attributes even when the data is not compressed. + */ if (NInoSparse(ni) && a->data.non_resident.compression_unit && a->data.non_resident.compression_unit != - vol->sparse_compression_unit) { + vol->sparse_compression_unit && + a->data.non_resident.compression_unit != + STANDARD_COMPRESSION_UNIT) { ntfs_error(vi->i_sb, - "Found non-standard compression unit (%u instead of 0 or %d). Cannot handle this.", + "Found non-standard compression unit (%u instead of 0, %d, or %d). Cannot handle this.", a->data.non_resident.compression_unit, - vol->sparse_compression_unit); + vol->sparse_compression_unit, + STANDARD_COMPRESSION_UNIT); err = -EOPNOTSUPP; goto unm_err_out; } -- cgit v1.2.3 From ec044bad2a60e81323d83c0337077f3514af98bd Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Wed, 30 Sep 2026 11:48:30 +0800 Subject: ntfs: do not map an unmappable runlist fragment as a hole When the extent mft record holding part of a file's runlist cannot be read, ntfs_attr_vcn_to_rl() ignores the failed ntfs_map_runlist_nolock() retry and returns with *lcn == LCN_RL_NOT_MAPPED. The iomap read path only rejects lcn < LCN_ENOENT, so it maps the range as a hole and read() returns zeros with no error. The first read already does this: ntfs_attr_map_whole_runlist() keeps the fragments it could read, readahead drops its error, and the next lookup finds the unmapped tail. On a file whose runlist is split between its base record (vcn 0-214) and one extent record (vcn 215-1499), breaking only the extent record's FILE magic makes the kernel log "Failed to map extent mft record", yet read() returns 1285 clusters of zeros for vcn 215-1499. A damaged attribute list entry, which makes the retry fail with -ENOENT, gives the same zeros. Return -ENOMEM if the retry ran out of memory and -EIO otherwise. Both callers already handle an ERR_PTR; other negative lcns, including LCN_ENOENT, are returned as before, and so is LCN_RL_NOT_MAPPED at or beyond allocated_size, where nothing is mapped: the runlist ends there with LCN_RL_NOT_MAPPED when only the last extent is mapped, as after a write into it, and with clusters smaller than a page every read of a file's last folio looks up such vcns. Reads of vcn 215-1499 now fail with -EIO. An intact volume exercised with buffered, mmap and O_DIRECT I/O, fallocate, truncate, sparse and compressed files behaves as before. Fixes: 495e90fa3348 ("ntfs: update attrib operations") Cc: stable@vger.kernel.org Signed-off-by: Matthias Goergens Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/attrib.c | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c index 333b3371acb4..5f5a91265af5 100644 --- a/fs/ntfs/attrib.c +++ b/fs/ntfs/attrib.c @@ -317,7 +317,7 @@ int ntfs_map_runlist(struct ntfs_inode *ni, s64 vcn) struct runlist_element *ntfs_attr_vcn_to_rl(struct ntfs_inode *ni, s64 vcn, s64 *lcn) { struct runlist_element *rl = ni->runlist.rl; - int err; + int err = 0; bool is_retry = false; if (!rl) { @@ -335,12 +335,32 @@ remap_rl: if (*lcn <= LCN_RL_NOT_MAPPED && is_retry == false) { is_retry = true; - if (!ntfs_map_runlist_nolock(ni, vcn, NULL)) { + err = ntfs_map_runlist_nolock(ni, vcn, NULL); + if (!err) { rl = ni->runlist.rl; goto remap_rl; } } + /* + * The runlist fragment containing @vcn could not be mapped, e.g. + * because the extent mft record holding it is corrupt. Do not hand + * LCN_RL_NOT_MAPPED back to callers, which would treat it as a hole. + * At or beyond the allocated size nothing is mapped, and the runlist + * ends there with LCN_RL_NOT_MAPPED if only a later extent has been + * mapped, so return that end as it is. + */ + if (*lcn == LCN_RL_NOT_MAPPED) { + unsigned long flags; + s64 allocated_size; + + read_lock_irqsave(&ni->size_lock, flags); + allocated_size = ni->allocated_size; + read_unlock_irqrestore(&ni->size_lock, flags); + if ((s64)ntfs_cluster_to_bytes(ni->vol, vcn) < allocated_size) + return ERR_PTR(err == -ENOMEM ? -ENOMEM : -EIO); + } + return rl; } -- cgit v1.2.3 From 3820b9a512b019144f26f9896693fc1aa5d91d6f Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Wed, 30 Sep 2026 11:48:31 +0800 Subject: ntfs: do not turn an unmappable runlist fragment into delalloc on write ntfs_write_simple_iomap_begin_non_resident() has the same unchecked retry. LCN_RL_NOT_MAPPED passes its "lcn <= LCN_HOLE" test, so a buffered write into a range whose extent record cannot be read is merged into the runlist as LCN_DELALLOC over clusters that are already allocated on disk, and write() succeeds. If the record stays unreadable, writeback fails too and the data never reaches the disk; the error only shows up at a later fsync() or a synchronous write. On the volume from the previous patch, after a read of the file, an 8 KiB pwrite() at vcn 1000 returns 8192, fsync() returns -EIO, and after remount the range is unchanged. Fail the lookup here too, with -ENOMEM or -EIO. That pwrite() now fails with -EIO. Writes whose range can be mapped are unaffected. Fixes: b041ca562526 ("ntfs: update iomap and address space operations") Cc: stable@vger.kernel.org Signed-off-by: Matthias Goergens Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/iomap.c | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/iomap.c b/fs/ntfs/iomap.c index c812d7f19b36..b4475963e57a 100644 --- a/fs/ntfs/iomap.c +++ b/fs/ntfs/iomap.c @@ -394,7 +394,7 @@ static int ntfs_write_simple_iomap_begin_non_resident(struct inode *inode, loff_ loff_t vcn_ofs, rl_length; struct runlist_element *rl, *rlc; bool is_retry = false; - int err = 0; + int err = 0, map_err = 0; s64 vcn, lcn; s64 max_clu_count = ntfs_bytes_to_cluster(vol, round_up(length, vol->cluster_size)); @@ -429,12 +429,24 @@ remap_rl: if (lcn <= LCN_RL_NOT_MAPPED && is_retry == false) { is_retry = true; - if (!ntfs_map_runlist_nolock(ni, vcn, NULL)) { + map_err = ntfs_map_runlist_nolock(ni, vcn, NULL); + if (!map_err) { rl = ni->runlist.rl; goto remap_rl; } } + /* + * As in ntfs_attr_vcn_to_rl(): a runlist fragment that could not be + * mapped is not a hole. Treating it as one would put a delalloc + * extent over clusters that are allocated on disk but unknown to us. + */ + if (lcn == LCN_RL_NOT_MAPPED) { + up_write(&ni->runlist.lock); + mutex_unlock(&ni->mrec_lock); + return map_err == -ENOMEM ? -ENOMEM : -EIO; + } + max_clu_count = min(max_clu_count, rl->length - (vcn - rl->vcn)); if (max_clu_count == 0) { ntfs_error(inode->i_sb, -- cgit v1.2.3 From 366e8e30f73e44407cc9eb91c86186f3a19b62a9 Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Wed, 30 Sep 2026 11:48:32 +0800 Subject: ntfs: fail the mount when $MFT needs its own extent records Mounting a crafted image hangs the mount process forever, unkillable at 0% CPU. Only the hung-task detector reports it: INFO: task mount:74 blocked in I/O wait for more than 30 seconds. folio_wait_bit_common <- waits forever filemap_read_folio map_mft_record_folio map_mft_record ntfs_map_runlist_nolock ntfs_attr_vcn_to_rl __ntfs_read_iomap_begin iomap_read_folio ntfs_read_folio <- already holds that folio's lock ntfs_read_inode_mount ntfs_fill_super ntfs_read_inode_mount() assembles $MFT's runlist one $DATA extent at a time, hoping, as its comment says, that it never needs a part of $MFT it has not decoded yet. If $MFT's attribute list puts one of its attributes or later $DATA extents in an extent record outside the runlist decoded so far, reading that record re-enters ntfs_map_runlist_nolock() for $MFT. Depending on the layout, that waits on the folio lock it already holds, as above, blocks on $MFT's runlist lock, or dereferences NULL in map_extent_mft_record(). Mark the volume for the whole $DATA enumeration and have ntfs_map_runlist_nolock() refuse $MFT with -EIO while the mark is set. The enumeration decodes each extent itself with ntfs_mapping_pairs_decompress(), so it does not need that path. A validly placed record can trip the check too. The driver puts an extent record for $MFT's own mapping pairs before the first vcn it describes, but with clusters smaller than a page, the folio holding it can still run past the decoded runlist. An unpatched kernel hangs on that layout as well; with the check the mount fails. The check then refuses only the folio's tail, so this patch needs "ntfs: do not map an unmappable runlist fragment as a hole": without it the tail is zero-filled and the mount serves an all-zero mft record. The mount now fails instead of hanging: ntfs: (device vda): ntfs_map_runlist_nolock(): $MFT needs its own extent records to describe itself; cannot mount. Tested under qemu with KASAN, PROVE_LOCKING and the hung-task detector on ntfs-next, with and without the whole series: six crafted images that hang or crash an unpatched kernel fail to mount with the series, and nine images that mount without it still mount, three of them with the same file listing and contents, among them a volume written by ntfs-3g whose $MFT has its $DATA in three extent records. Fixes: b041ca562526 ("ntfs: update iomap and address space operations") Suggested-by: Hyunchul Lee Cc: stable@vger.kernel.org Signed-off-by: Matthias Goergens Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/attrib.c | 11 +++++++++++ fs/ntfs/inode.c | 9 +++++++++ fs/ntfs/volume.h | 3 +++ 3 files changed, 23 insertions(+) (limited to 'fs') diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c index 5f5a91265af5..2e72cd816d04 100644 --- a/fs/ntfs/attrib.c +++ b/fs/ntfs/attrib.c @@ -103,6 +103,17 @@ int ntfs_map_runlist_nolock(struct ntfs_inode *ni, s64 vcn, struct ntfs_attr_sea base_ni = ni; else base_ni = ni->ext.base_ntfs_ino; + /* + * ntfs_read_inode_mount() builds $MFT's runlist itself, so nothing + * should reach here for $MFT. A crafted image can: the read that + * gets here already holds the $MFT folio lock it would wait on. + */ + if (unlikely(NVolMftBootstrap(ni->vol) && + base_ni == NTFS_I(ni->vol->mft_ino))) { + ntfs_error(ni->vol->sb, + "$MFT needs its own extent records to describe itself; cannot mount."); + return -EIO; + } if (!ctx) { ctx_is_temporary = ctx_needs_reset = true; m = map_mft_record(base_ni); diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c index de848180749d..6356026f732d 100644 --- a/fs/ntfs/inode.c +++ b/fs/ntfs/inode.c @@ -2087,6 +2087,11 @@ int ntfs_read_inode_mount(struct inode *vi) /* Now load all attribute extents. */ a = NULL; next_vcn = last_vcn = highest_vcn = 0; + /* + * Reading one of $MFT's own extent records in this loop can re-enter + * ntfs_map_runlist_nolock() for $MFT; see the check there. + */ + NVolSetMftBootstrap(vol); while (!(err = ntfs_attr_lookup(AT_DATA, NULL, 0, 0, next_vcn, NULL, 0, ctx))) { struct runlist_element *nrl; @@ -2169,6 +2174,7 @@ int ntfs_read_inode_mount(struct inode *vi) err = ntfs_read_locked_inode(vi); if (err) { ntfs_error(sb, "ntfs_read_inode() of $MFT failed.\n"); + NVolClearMftBootstrap(vol); ntfs_attr_put_search_ctx(ctx); /* Revert to the safe super operations. */ kfree(m); @@ -2202,6 +2208,7 @@ int ntfs_read_inode_mount(struct inode *vi) goto put_err_out; } } + NVolClearMftBootstrap(vol); if (err != -ENOENT) { ntfs_error(sb, "Failed to lookup $MFT/$DATA attribute extent. Run chkdsk.\n"); goto put_err_out; @@ -2236,6 +2243,8 @@ em_put_err_out: put_err_out: ntfs_attr_put_search_ctx(ctx); err_out: + /* Also reached from inside the $DATA loop. */ + NVolClearMftBootstrap(vol); ntfs_error(sb, "Failed. Marking inode as bad."); kfree(m); return -1; diff --git a/fs/ntfs/volume.h b/fs/ntfs/volume.h index fdb57279de84..0473b602084c 100644 --- a/fs/ntfs/volume.h +++ b/fs/ntfs/volume.h @@ -194,6 +194,7 @@ struct ntfs_volume { * NV_Discard Issue discard/TRIM commands for freed clusters. * NV_DisableSparse Disable creation of sparse regions. * NV_NativeSymlinkRel Translate absolute Windows reparse targets (native_symlink=rel). + * NV_MftBootstrap Mount is still assembling $MFT's own runlist. */ enum { NV_Errors, @@ -214,6 +215,7 @@ enum { NV_DisableSparse, NV_NativeSymlinkRel, NV_SymlinkNative, + NV_MftBootstrap, }; /* @@ -253,6 +255,7 @@ DEFINE_NVOL_BIT_OPS(Discard) DEFINE_NVOL_BIT_OPS(DisableSparse) DEFINE_NVOL_BIT_OPS(NativeSymlinkRel) DEFINE_NVOL_BIT_OPS(SymlinkNative) +DEFINE_NVOL_BIT_OPS(MftBootstrap) static inline void ntfs_inc_free_clusters(struct ntfs_volume *vol, s64 nr) { -- cgit v1.2.3 From e43715851cf184a3e1b64c8a484cf18563080700 Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Wed, 30 Sep 2026 11:48:33 +0800 Subject: ntfs: do not map a vcn as a hole when its runlist lookup failed ntfs_attr_vcn_to_rl() retries ntfs_map_runlist_nolock() for any lcn up to LCN_RL_NOT_MAPPED, which includes LCN_ENOENT, but turns a failed retry into an error only for LCN_RL_NOT_MAPPED. For LCN_ENOENT the error is dropped and the read path maps the range as a hole. An LCN_ENOENT below allocated_size comes from a base extent with a highest_vcn of 0, which ntfs_mapping_pairs_decompress() takes to map the whole attribute, so the runlist ends after its last mapping pair. If the pairs end early, the retry finds the same extent and fails with -ENOENT. On a crafted volume with 4 KiB clusters, a 64-cluster file whose mapping pairs stop after 16 clusters reads 48 clusters of zeros, with no error. The same layout gets a crafted $MFT past the check from "ntfs: fail the mount when $MFT needs its own extent records". With 512-byte clusters and $MFT's mapping pairs ending at vcn 4, an unpatched kernel hangs on the folio lock reading records 0-3. With the check alone, the -EIO is dropped, records 2 and 3 read as zeros and the mount carries on until check_mft_mirror() finds the zeroed record 2. Fail the lookup whenever the retry leaves @vcn unmapped below allocated_size, -ENOENT included. At or beyond allocated_size the end of the runlist is still returned as it is. A failed expansion in ntfs_non_resident_attr_expand() or ntfs_attrlist_repack() truncates the runlist under the runlist lock but restores allocated_size only after dropping it. A lookup in between would now fail, so restore allocated_size before dropping the lock in both, under size_lock, which ntfs_attr_vcn_to_rl() takes to read it. The crafted file now fails from vcn 16 on with -EIO, and the crafted volume fails to mount with the check's message. Fixes: 495e90fa3348 ("ntfs: update attrib operations") Cc: stable@vger.kernel.org Signed-off-by: Matthias Goergens Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/attrib.c | 30 ++++++++++++++++++++---------- fs/ntfs/attrlist.c | 8 ++++++-- 2 files changed, 26 insertions(+), 12 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c index 2e72cd816d04..5439c12f9280 100644 --- a/fs/ntfs/attrib.c +++ b/fs/ntfs/attrib.c @@ -354,14 +354,16 @@ remap_rl: } /* - * The runlist fragment containing @vcn could not be mapped, e.g. - * because the extent mft record holding it is corrupt. Do not hand - * LCN_RL_NOT_MAPPED back to callers, which would treat it as a hole. - * At or beyond the allocated size nothing is mapped, and the runlist - * ends there with LCN_RL_NOT_MAPPED if only a later extent has been - * mapped, so return that end as it is. + * Neither the runlist nor the retry mapped @vcn, e.g. because the + * extent mft record holding it is corrupt or because the mapping + * pairs end too soon. ntfs_map_runlist_nolock() reports the latter + * as -ENOENT, as @vcn lies past the extent it found. Below the + * allocated size, callers would treat LCN_RL_NOT_MAPPED or LCN_ENOENT + * as a hole, so fail instead. At or beyond it nothing is mapped: the + * runlist ends there with LCN_ENOENT, or with LCN_RL_NOT_MAPPED if + * only a later extent has been mapped, so return that end as it is. */ - if (*lcn == LCN_RL_NOT_MAPPED) { + if (*lcn <= LCN_RL_NOT_MAPPED) { unsigned long flags; s64 allocated_size; @@ -4484,6 +4486,7 @@ static int ntfs_non_resident_attr_expand(struct ntfs_inode *ni, const s64 newsiz struct ntfs_inode *base_ni; struct super_block *sb = ni->vol->sb; size_t new_rl_count; + unsigned long flags; ntfs_debug("Inode 0x%llx, attr 0x%x, new size %lld old size %lld\n", (unsigned long long)ni->mft_no, ni->type, @@ -4714,11 +4717,20 @@ rollback: if (err2) ntfs_debug("Leaking clusters"); - /* Now, truncate the runlist itself. */ + /* + * Now, truncate the runlist itself. Restore allocated_size before + * dropping the lock: ntfs_attr_vcn_to_rl() fails a lookup below the + * allocated size that falls past the end of the runlist. + */ if (ni != locked_ni) down_write(&ni->runlist.lock); err2 = ntfs_rl_truncate_nolock(vol, &ni->runlist, ntfs_bytes_to_cluster(vol, org_alloc_size)); + if (!err2) { + write_lock_irqsave(&ni->size_lock, flags); + ni->allocated_size = org_alloc_size; + write_unlock_irqrestore(&ni->size_lock, flags); + } if (ni != locked_ni) up_write(&ni->runlist.lock); if (err2) { @@ -4730,8 +4742,6 @@ rollback: ni->runlist.rl = NULL; ntfs_error(sb, "Couldn't truncate runlist. Rollback failed"); } else { - /* Prepare to mapping pairs update. */ - ni->allocated_size = org_alloc_size; /* Restore mapping pairs. */ if (ni != locked_ni) down_read(&ni->runlist.lock); diff --git a/fs/ntfs/attrlist.c b/fs/ntfs/attrlist.c index 1bbd2bc62c58..3660e7fd24b1 100644 --- a/fs/ntfs/attrlist.c +++ b/fs/ntfs/attrlist.c @@ -168,14 +168,18 @@ static int ntfs_attrlist_repack(struct inode *attr_vi, return 0; restore_old_runlist: + /* + * Restore allocated_size before dropping the runlist lock: + * ntfs_attr_vcn_to_rl() fails a lookup below the allocated size that + * falls past the end of the runlist. + */ down_write(&attr_ni->runlist.lock); attr_ni->runlist.rl = old_rl; attr_ni->runlist.count = old_rl_count; - up_write(&attr_ni->runlist.lock); - write_lock_irqsave(&attr_ni->size_lock, flags); attr_ni->allocated_size = old_alloc_size; write_unlock_irqrestore(&attr_ni->size_lock, flags); + up_write(&attr_ni->runlist.lock); restore_err = ntfs_attr_update_mapping_pairs_locked( attr_ni, 0, locked_ni); -- cgit v1.2.3 From 99fb277d5873330e2c926be633be0ca0ad7a8c56 Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Wed, 30 Sep 2026 11:48:34 +0800 Subject: ntfs: fail the mount when $MFT's data size exceeds its allocation ntfs_read_inode_mount() takes $MFT's i_size from data_size without checking it against allocated_size, unlike the resident case in ntfs_read_locked_inode(). mft records between the two are not on disk. On a crafted volume with 512-byte clusters whose $MFT is cut to 4 clusters while data_size still says 27 records, reading the folio holding records 0-3 finds the end of the runlist at vcn 4. An unpatched kernel hangs there on the folio lock, as in "ntfs: fail the mount when $MFT needs its own extent records". With the previous patch vcn 4-7 are past the allocation and so read as a hole: records 2 and 3 come back as zeros and the mount carries on until check_mft_mirror() finds the zeroed record 2. Refuse such a $MFT before anything is read through it. The mount now fails with: ntfs: (device vda): ntfs_read_inode_mount(): $MFT data size 27648 exceeds its allocated size 2048. $MFT is corrupt. Run chkdsk. fs/ntfs3 rejects any non-resident attribute whose data_size exceeds its allocated size. This patch checks only $MFT; the next one covers the other non-resident attributes. Fixes: b041ca562526 ("ntfs: update iomap and address space operations") Cc: stable@vger.kernel.org Signed-off-by: Matthias Goergens Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/inode.c | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'fs') diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c index 6356026f732d..926b6676d253 100644 --- a/fs/ntfs/inode.c +++ b/fs/ntfs/inode.c @@ -2143,6 +2143,16 @@ int ntfs_read_inode_mount(struct inode *vi) vi->i_size = le64_to_cpu(a->data.non_resident.data_size); ni->initialized_size = le64_to_cpu(a->data.non_resident.initialized_size); ni->allocated_size = le64_to_cpu(a->data.non_resident.allocated_size); + /* + * Records between allocated_size and data_size are not + * on disk, and would be read as zeros. + */ + if (vi->i_size > ni->allocated_size) { + ntfs_error(sb, + "$MFT data size %lld exceeds its allocated size %lld. $MFT is corrupt. Run chkdsk.", + vi->i_size, ni->allocated_size); + goto put_err_out; + } /* * Verify the number of mft records does not exceed * 2^32 - 1. -- cgit v1.2.3 From 708f9d56cacae21aeee98d16bcdd50a66edc04a0 Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Wed, 30 Sep 2026 11:48:35 +0800 Subject: ntfs: reject non-resident attributes whose sizes exceed their allocation ntfs_read_locked_inode(), ntfs_read_locked_attr_inode() and ntfs_read_locked_index_inode() take a non-resident attribute's data_size and initialized_size from disk without checking them against its allocated_size. The runlist ends at allocated_size and the read path maps what lies beyond it as a hole, so a data_size larger than the allocation makes reads return zeros that are not on disk, with no error. On a crafted volume with 4 KiB clusters where a 6144000-byte file claims a data_size and initialized_size 64 KiB beyond its allocation, reading the file returns 16 clusters of such zeros. A sparse file behaves the same. A compressed file instead fails with -EIO after a burst of "Still have pages left!" errors from ntfs_read_compressed_block(). Require 0 <= initialized_size <= data_size <= allocated_size, with allocated_size a multiple of the cluster size, when the inode is read, as fs/ntfs3 does in mi_enum_attr(), and fail with -EIO otherwise, which marks the inode bad and the volume as having errors. initialized_size above data_size is rejected too because an extending write zeroes the gap it opens only from initialized_size onwards. An unaligned allocated_size ends inside a cluster that the read path treats as past the end: a crafted 66440-byte file with 4 KiB clusters reads its last 904 bytes as zeros. Sparse and compressed attributes need no exception. Every non-resident attribute has a cluster-aligned allocated_size >= data_size, holes included, on eight public test volumes (seven written by Windows, with LZNT1-compressed files, a sparse $UsnJrnl:$J and a OneDrive placeholder whose unnamed $DATA is one hole, and one by mkntfs), on a volume written by ntfs-3g and on one written by this driver, and the patched driver reads every file on them as before. fs/ntfs3 has enforced the same checks since v6.6, also without exceptions. The layout.h comment saying that data_size can exceed allocated_size for compressed and sparse attributes is corrected. This also covers a non-resident attribute list, which load_attribute_list() reads through ntfs_attr_iget(), and $MFT, whose inode goes through ntfs_read_locked_inode() after the previous patch's check. $MFT's own non-resident attribute list goes through neither: ntfs_read_inode_mount() loads it with load_attribute_list_mount() and ntfs_read_locked_inode() skips it for $MFT, so the check is added there too. The check was already missing in the classic driver; the Fixes tag names the commit that brought that code back. Fixes: 1e9ea7e04472 ("Revert "fs: Remove NTFS classic"") Signed-off-by: Matthias Goergens Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/inode.c | 27 +++++++++++++++++++++++++++ fs/ntfs/layout.h | 13 ++++++++----- 2 files changed, 35 insertions(+), 5 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c index 926b6676d253..b666cdb3b102 100644 --- a/fs/ntfs/inode.c +++ b/fs/ntfs/inode.c @@ -651,6 +651,25 @@ void ntfs_set_vfs_operations(struct inode *inode, mode_t mode, dev_t dev) } } +static bool ntfs_non_resident_sizes_inconsistent(struct inode *vi, + const struct attr_record *a) +{ + s64 allocated_size = le64_to_cpu(a->data.non_resident.allocated_size); + s64 data_size = le64_to_cpu(a->data.non_resident.data_size); + s64 initialized_size = le64_to_cpu(a->data.non_resident.initialized_size); + + if (initialized_size >= 0 && initialized_size <= data_size && + data_size <= allocated_size && + !ntfs_bytes_to_cluster_off(NTFS_I(vi)->vol, allocated_size)) + return false; + + ntfs_error(vi->i_sb, + "Attribute 0x%x of inode 0x%llx is corrupt (initialized size %lld, data size %lld, allocated size %lld).", + le32_to_cpu(a->type), NTFS_I(vi)->mft_no, initialized_size, + data_size, allocated_size); + return true; +} + /* * ntfs_read_locked_inode - read an inode from its device * @vi: inode to read @@ -1191,6 +1210,8 @@ view_index_meta: "First extent of $DATA attribute has non zero lowest_vcn."); goto unm_err_out; } + if (ntfs_non_resident_sizes_inconsistent(vi, a)) + goto unm_err_out; vi->i_size = ni->data_size = le64_to_cpu(a->data.non_resident.data_size); ni->initialized_size = le64_to_cpu(a->data.non_resident.initialized_size); ni->allocated_size = le64_to_cpu(a->data.non_resident.allocated_size); @@ -1453,6 +1474,8 @@ static int ntfs_read_locked_attr_inode(struct inode *base_vi, struct inode *vi) ntfs_error(vi->i_sb, "First extent of attribute has non-zero lowest_vcn."); goto unm_err_out; } + if (ntfs_non_resident_sizes_inconsistent(vi, a)) + goto unm_err_out; vi->i_size = ni->data_size = le64_to_cpu(a->data.non_resident.data_size); ni->initialized_size = le64_to_cpu(a->data.non_resident.initialized_size); ni->allocated_size = le64_to_cpu(a->data.non_resident.allocated_size); @@ -1682,6 +1705,8 @@ static int ntfs_read_locked_index_inode(struct inode *base_vi, struct inode *vi) "First extent of $INDEX_ALLOCATION attribute has non zero lowest_vcn."); goto unm_err_out; } + if (ntfs_non_resident_sizes_inconsistent(vi, a)) + goto unm_err_out; vi->i_size = ni->data_size = le64_to_cpu(a->data.non_resident.data_size); ni->initialized_size = le64_to_cpu(a->data.non_resident.initialized_size); ni->allocated_size = le64_to_cpu(a->data.non_resident.allocated_size); @@ -2015,6 +2040,8 @@ int ntfs_read_inode_mount(struct inode *vi) "Attribute list has non zero lowest_vcn. $MFT is corrupt. You should run chkdsk."); goto put_err_out; } + if (ntfs_non_resident_sizes_inconsistent(vi, a)) + goto put_err_out; rl = ntfs_mapping_pairs_decompress(vol, a, NULL, &new_rl_count); if (IS_ERR(rl)) { diff --git a/fs/ntfs/layout.h b/fs/ntfs/layout.h index 8f5792139d71..2de83d4ca40b 100644 --- a/fs/ntfs/layout.h +++ b/fs/ntfs/layout.h @@ -811,14 +811,17 @@ enum { * on XP SP2+. * @data.non_resident.reserved: 5 bytes for 8-byte alignment. * @data.non_resident.allocated_size: - * Allocated disk space in bytes. - * For compressed: logical allocated size. + * Allocated size in bytes, a multiple of + * the cluster size. For compressed and + * sparse attributes holes count as + * allocated; the clusters actually in use + * are in compressed_size. * @data.non_resident.data_size: Logical attribute value size in bytes. - * Can be larger than allocated_size if - * compressed/sparse. + * Never larger than allocated_size, also + * when compressed/sparse. * @data.non_resident.initialized_size: * Initialized portion size in bytes. - * Usually equals data_size. + * Usually equals data_size, never larger. * @data.non_resident.compressed_size: * Compressed on-disk size in bytes. * Only present when compressed or sparse. -- cgit v1.2.3 From e29de9195f6424a3728580e836c32f80e94d2254 Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Thu, 1 Oct 2026 23:05:58 +0800 Subject: ntfs: hold the runlist lock while expanding a non-resident attribute ntfs_non_resident_attr_expand() changes ni->runlist.rl while it allocates clusters: ntfs_attr_map_whole_runlist(), the compressed branch's ntfs_rl_realloc() and ntfs_runlists_merge() can each kvfree() the old array. Unless the caller already holds ni->runlist.lock, none of this happens under it; only the rollback takes it. Callers without it include the write path, fallocate, ntfs_inode_attr_pwrite() and ntfs_resident_attr_resize(). Those callers hold mrec_lock, and the write path also inode_lock, but the iomap read side takes neither: FIEMAP, page faults, readahead and splice read reach ntfs_read_iomap_begin_non_resident(), which takes only ni->runlist.lock. A FIEMAP running while write(2) extends the same file can walk the array that ntfs_runlists_merge() has just freed: BUG: KASAN: slab-use-after-free in ntfs_attr_vcn_to_rl+0x19b/0x200 A reader that finds a vcn unmapped in the stale array also maps it from disk into the runlist the writer is changing, which shows up as "Run lists overlap. Cannot merge!". Take ni->runlist.lock for writing around the part of the expansion that changes the runlist and allocated_size, up to and including the mapping pairs update, as ntfs_non_resident_attr_shrink() does since commit 91709ba5d6d7 ("ntfs: protect runlist updates with the runlist lock"), and hold it throughout the rollback, where ntfs_cluster_free() requires it. The lock nests inside mrec_lock, as on the truncate-up path, which already calls this function with the runlist lock held. Where the caller says it holds the lock (locked_ni == ni), assert that it holds it for writing. For an $ATTRIBUTE_LIST whose lock the caller does not hold, drop the lock again before the mapping pairs update: that update can resize the same list through ntfs_attrlist_update_locked(), which returns -ENOSPC when told that the list's lock is held. Holding it there made punching holes into a large fragmented file fail with -ENOSPC. Fixes: 495e90fa3348 ("ntfs: update attrib operations") Cc: stable@vger.kernel.org Signed-off-by: Matthias Goergens Reviewed-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/attrib.c | 74 ++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 21 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c index 5439c12f9280..565bd468b893 100644 --- a/fs/ntfs/attrib.c +++ b/fs/ntfs/attrib.c @@ -4487,6 +4487,7 @@ static int ntfs_non_resident_attr_expand(struct ntfs_inode *ni, const s64 newsiz struct super_block *sb = ni->vol->sb; size_t new_rl_count; unsigned long flags; + bool runlist_locked = locked_ni == ni; ntfs_debug("Inode 0x%llx, attr 0x%x, new size %lld old size %lld\n", (unsigned long long)ni->mft_no, ni->type, @@ -4528,10 +4529,19 @@ static int ntfs_non_resident_attr_expand(struct ntfs_inode *ni, const s64 newsiz * clusters if there is a change. */ if (ntfs_bytes_to_cluster(vol, ni->allocated_size) < first_free_vcn) { + /* + * The runlist array is replaced below. Readers such as the + * iomap read path hold only the runlist lock, not mrec_lock. + */ + if (runlist_locked) + lockdep_assert_held_write(&ni->runlist.lock); + else + down_write(&ni->runlist.lock); + err = ntfs_attr_map_whole_runlist(ni); if (err) { ntfs_error(sb, "ntfs_attr_map_whole_runlist failed"); - return err; + goto unlock_runlist; } /* @@ -4556,7 +4566,7 @@ static int ntfs_non_resident_attr_expand(struct ntfs_inode *ni, const s64 newsiz last + more_entries + 1); if (IS_ERR(rl)) { err = -ENOMEM; - goto put_err_out; + goto unlock_runlist; } alloc_size = ni->allocated_size; @@ -4581,7 +4591,7 @@ static int ntfs_non_resident_attr_expand(struct ntfs_inode *ni, const s64 newsiz rl = kmalloc(sizeof(struct runlist_element) * 2, GFP_NOFS); if (!rl) { err = -ENOMEM; - goto put_err_out; + goto unlock_runlist; } rl[0].vcn = ntfs_bytes_to_cluster(vol, ni->allocated_size); @@ -4628,7 +4638,8 @@ static int ntfs_non_resident_attr_expand(struct ntfs_inode *ni, const s64 newsiz ntfs_debug("Cluster allocation failed (%lld)", (long long)first_free_vcn - ntfs_bytes_to_cluster(vol, ni->allocated_size)); - return PTR_ERR(rl); + err = PTR_ERR(rl); + goto unlock_runlist; } /* * A contiguous ATTRIBUTE_LIST allocation keeps its mapping @@ -4652,8 +4663,10 @@ static int ntfs_non_resident_attr_expand(struct ntfs_inode *ni, const s64 newsiz ni->allocated_size), lcn_seek_from, DATA_ZONE, false, false, false); - if (IS_ERR(rl)) - return PTR_ERR(rl); + if (IS_ERR(rl)) { + err = PTR_ERR(rl); + goto unlock_runlist; + } } } @@ -4665,7 +4678,8 @@ static int ntfs_non_resident_attr_expand(struct ntfs_inode *ni, const s64 newsiz ntfs_error(sb, "Run list merge failed"); ntfs_cluster_free_from_rl(vol, rl); kvfree(rl); - return -EIO; + err = -EIO; + goto unlock_runlist; } ni->runlist.rl = rln; ni->runlist.count = new_rl_count; @@ -4673,11 +4687,28 @@ static int ntfs_non_resident_attr_expand(struct ntfs_inode *ni, const s64 newsiz /* Prepare to mapping pairs update. */ ni->allocated_size = ntfs_cluster_to_bytes(vol, first_free_vcn); - err = ntfs_attr_update_mapping_pairs_locked( - ni, 0, locked_ni); - if (err) { - ntfs_debug("Mapping pairs update failed"); - goto rollback; + if (ni->type == AT_ATTRIBUTE_LIST && !runlist_locked) { + /* + * Making room for the list's mapping pairs can resize + * this attribute list again through + * ntfs_attrlist_update_locked(), which takes its + * runlist lock. + */ + up_write(&ni->runlist.lock); + err = ntfs_attr_update_mapping_pairs_locked(ni, 0, + locked_ni); + if (err) { + ntfs_debug("Mapping pairs update failed"); + goto rollback; + } + } else { + err = ntfs_attr_update_mapping_pairs_locked(ni, 0, ni); + if (err) { + ntfs_debug("Mapping pairs update failed"); + goto rollback_locked; + } + if (!runlist_locked) + up_write(&ni->runlist.lock); } } @@ -4711,6 +4742,9 @@ static int ntfs_non_resident_attr_expand(struct ntfs_inode *ni, const s64 newsiz ntfs_attr_put_search_ctx(ctx); return 0; rollback: + if (!runlist_locked) + down_write(&ni->runlist.lock); +rollback_locked: /* Free allocated clusters. */ err2 = ntfs_cluster_free(ni, ntfs_bytes_to_cluster(vol, org_alloc_size), -1, ctx); @@ -4722,8 +4756,6 @@ rollback: * dropping the lock: ntfs_attr_vcn_to_rl() fails a lookup below the * allocated size that falls past the end of the runlist. */ - if (ni != locked_ni) - down_write(&ni->runlist.lock); err2 = ntfs_rl_truncate_nolock(vol, &ni->runlist, ntfs_bytes_to_cluster(vol, org_alloc_size)); if (!err2) { @@ -4731,8 +4763,6 @@ rollback: ni->allocated_size = org_alloc_size; write_unlock_irqrestore(&ni->size_lock, flags); } - if (ni != locked_ni) - up_write(&ni->runlist.lock); if (err2) { /* * Failed to truncate the runlist, so just throw it away, it @@ -4743,12 +4773,8 @@ rollback: ntfs_error(sb, "Couldn't truncate runlist. Rollback failed"); } else { /* Restore mapping pairs. */ - if (ni != locked_ni) - down_read(&ni->runlist.lock); - if (__ntfs_attr_update_mapping_pairs(ni, 0, locked_ni, true)) + if (__ntfs_attr_update_mapping_pairs(ni, 0, ni, true)) ntfs_error(sb, "Failed to restore old mapping pairs"); - if (ni != locked_ni) - up_read(&ni->runlist.lock); if (NInoSparse(ni) || NInoCompressed(ni)) { ni->itype.compressed.size = org_compressed_size; @@ -4756,6 +4782,8 @@ rollback: } else VFS_I(base_ni)->i_blocks = ni->allocated_size >> 9; } + if (!runlist_locked) + up_write(&ni->runlist.lock); if (ctx) ntfs_attr_put_search_ctx(ctx); return err; @@ -4763,6 +4791,10 @@ put_err_out: if (ctx) ntfs_attr_put_search_ctx(ctx); return err; +unlock_runlist: + if (!runlist_locked) + up_write(&ni->runlist.lock); + return err; } /* -- cgit v1.2.3 From 4d19a19d2f0201a11987485d5ae7ee664a0f72bb Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Fri, 2 Oct 2026 12:34:57 +0800 Subject: ntfs: do not use a stale runlist pointer when undoing $MFT extension When ntfs_mft_data_extend_allocation_nolock() fails after it has rebuilt the mapping pairs of the last $MFT data extent, undo_alloc truncates the runlist and then rebuilds the old mapping pairs from rl2, a pointer into the runlist taken before the truncation. ntfs_rl_truncate_nolock() can reallocate the runlist, and KASAN then reports a use-after-free in ntfs_mapping_pairs_build(). Pass the start of the runlist instead, and hold the runlist lock for reading while the mapping pairs are built from it. ntfs_mapping_pairs_build() skips to the element containing the first vcn by itself. Fixes: 1e9ea7e04472 ("Revert "fs: Remove NTFS classic"") Cc: stable@vger.kernel.org Reviewed-by: Baolin Liu Signed-off-by: Matthias Goergens Signed-off-by: Namjae Jeon --- fs/ntfs/mft.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/mft.c b/fs/ntfs/mft.c index e01e367a588d..c2339acc6319 100644 --- a/fs/ntfs/mft.c +++ b/fs/ntfs/mft.c @@ -2043,11 +2043,16 @@ undo_alloc: if (ctx) { a = ctx->attr; if (mp_rebuilt && !IS_ERR(ctx->mrec)) { - if (ntfs_mapping_pairs_build(vol, (u8 *)a + le16_to_cpu( + int err; + + down_read(&mft_ni->runlist.lock); + err = ntfs_mapping_pairs_build(vol, (u8 *)a + le16_to_cpu( a->data.non_resident.mapping_pairs_offset), old_alen - le16_to_cpu( a->data.non_resident.mapping_pairs_offset), - rl2, ll, -1, NULL, NULL, NULL)) { + mft_ni->runlist.rl, ll, -1, NULL, NULL, NULL); + up_read(&mft_ni->runlist.lock); + if (err) { ntfs_error(vol->sb, "Failed to restore mapping pairs array.%s", es); NVolSetErrors(vol); } -- cgit v1.2.3 From 8ff561db909c4f7760eaa3c5011e47ce6e457165 Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Fri, 2 Oct 2026 12:34:58 +0800 Subject: ntfs: add a runlist merge that leaves the source runlist to the caller ntfs_runlists_merge() frees the source runlist once its runs are in the destination. A caller that may have to undo the merge then has to work out its runs again from the merged runlist, where the first and the last of them may have been coalesced with neighbouring runs. Add ntfs_runlists_merge_keep_src(), which neither modifies nor frees the source runlist. For that, ntfs_rl_append() and ntfs_rl_replace() now merge the right-hand end into the copied run in the destination instead of into the last source run before copying it, and the merge helpers take the source as const, so the compiler checks that none of them writes to it. The merged runlist is the same as before. For every caller, a source runlist without any mapped run, which the merge already warns about, now fails with -EINVAL instead of carrying on with nothing to insert: with no run copied, the right-hand merge into the destination would index before the insertion point. The next patch uses ntfs_runlists_merge_keep_src() to free exactly the clusters it allocated when it has to undo an $MFT data extension. Suggested-by: Hyunchul Lee Cc: stable@vger.kernel.org # needed by the next patch Signed-off-by: Matthias Goergens Signed-off-by: Namjae Jeon --- fs/ntfs/runlist.c | 98 ++++++++++++++++++++++++++++++++++++++++++++----------- fs/ntfs/runlist.h | 3 ++ 2 files changed, 82 insertions(+), 19 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/runlist.c b/fs/ntfs/runlist.c index f13d28c15887..76ba6ab72b8a 100644 --- a/fs/ntfs/runlist.c +++ b/fs/ntfs/runlist.c @@ -47,7 +47,7 @@ static inline void ntfs_rl_mm(struct runlist_element *base, int dst, int src, in * @srcbase. */ static inline void ntfs_rl_mc(struct runlist_element *dstbase, int dst, - struct runlist_element *srcbase, int src, int size) + const struct runlist_element *srcbase, int src, int size) { if (likely(size > 0)) memcpy(dstbase + dst, srcbase + src, size * sizeof(*dstbase)); @@ -152,8 +152,8 @@ static inline struct runlist_element *ntfs_rl_realloc_nofail(struct runlist_elem * Return: true Success, the runlists can be merged. * false Failure, the runlists cannot be merged. */ -static inline bool ntfs_are_rl_mergeable(struct runlist_element *dst, - struct runlist_element *src) +static inline bool ntfs_are_rl_mergeable(const struct runlist_element *dst, + const struct runlist_element *src) { /* We can merge unmapped regions even if they are misaligned. */ if ((dst->lcn == LCN_RL_NOT_MAPPED) && (src->lcn == LCN_RL_NOT_MAPPED)) @@ -186,7 +186,8 @@ static inline bool ntfs_are_rl_mergeable(struct runlist_element *dst, * * It is up to the caller to serialize access to the runlists @dst and @src. */ -static inline void __ntfs_rl_merge(struct runlist_element *dst, struct runlist_element *src) +static inline void __ntfs_rl_merge(struct runlist_element *dst, + const struct runlist_element *src) { dst->length += src->length; } @@ -214,10 +215,11 @@ static inline void __ntfs_rl_merge(struct runlist_element *dst, struct runlist_e * On error, return -errno. Both runlists are left unmodified. */ static inline struct runlist_element *ntfs_rl_append(struct runlist_element *dst, - int dsize, struct runlist_element *src, int ssize, int loc, + int dsize, const struct runlist_element *src, int ssize, int loc, size_t *new_size) { bool right = false; /* Right end of @src needs merging. */ + s64 right_length = 0; /* Length of the run merged at the right. */ int marker; /* End of the inserted runs. */ /* First, check if the right hand end needs merging. */ @@ -235,9 +237,9 @@ static inline struct runlist_element *ntfs_rl_append(struct runlist_element *dst * original runlists. */ - /* First, merge the right hand end, if necessary. */ + /* Remember the run that the right hand end absorbs, if any. */ if (right) - __ntfs_rl_merge(src + ssize - 1, dst + loc + 1); + right_length = dst[loc + 1].length; /* First run after the @src runs that have been inserted. */ marker = loc + ssize + 1; @@ -246,6 +248,9 @@ static inline struct runlist_element *ntfs_rl_append(struct runlist_element *dst ntfs_rl_mm(dst, marker, loc + 1 + right, dsize - (loc + 1 + right)); ntfs_rl_mc(dst, loc + 1, src, 0, ssize); + /* Merge the right hand end into the copy, leaving @src intact. */ + dst[marker - 1].length += right_length; + /* Adjust the size of the preceding hole. */ dst[loc].length = dst[loc + 1].vcn - dst[loc].vcn; @@ -279,7 +284,7 @@ static inline struct runlist_element *ntfs_rl_append(struct runlist_element *dst * On error, return -errno. Both runlists are left unmodified. */ static inline struct runlist_element *ntfs_rl_insert(struct runlist_element *dst, - int dsize, struct runlist_element *src, int ssize, int loc, + int dsize, const struct runlist_element *src, int ssize, int loc, size_t *new_size) { bool left = false; /* Left end of @src needs merging. */ @@ -374,12 +379,13 @@ static inline struct runlist_element *ntfs_rl_insert(struct runlist_element *dst * On error, return -errno. Both runlists are left unmodified. */ static inline struct runlist_element *ntfs_rl_replace(struct runlist_element *dst, - int dsize, struct runlist_element *src, int ssize, int loc, + int dsize, const struct runlist_element *src, int ssize, int loc, size_t *new_size) { int delta; bool left = false; /* Left end of @src needs merging. */ bool right = false; /* Right end of @src needs merging. */ + s64 right_length = 0; /* Length of the run merged at the right. */ int tail; /* Start of tail of @dst. */ int marker; /* End of the inserted runs. */ @@ -405,9 +411,12 @@ static inline struct runlist_element *ntfs_rl_replace(struct runlist_element *ds * original runlists. */ - /* First, merge the left and right ends, if necessary. */ + /* + * Remember the run that the right hand end absorbs, if any, then + * merge the left hand end. + */ if (right) - __ntfs_rl_merge(src + ssize - 1, dst + loc + 1); + right_length = dst[loc + 1].length; if (left) __ntfs_rl_merge(dst + loc - 1, src); /* @@ -432,6 +441,13 @@ static inline struct runlist_element *ntfs_rl_replace(struct runlist_element *ds ntfs_rl_mm(dst, marker, tail, dsize - tail); ntfs_rl_mc(dst, loc, src, left, ssize - left); + /* + * Merge the right hand end into the last run of @src, now in @dst at + * @marker - 1 (or into @dst[@loc - 1] if that absorbed all of @src), + * leaving @src intact. + */ + dst[marker - 1].length += right_length; + /* We may have changed the length of the file, so fix the end marker. */ if (dsize - tail > 0 && dst[marker].lcn == LCN_ENOENT) dst[marker].vcn = dst[marker - 1].vcn + dst[marker - 1].length; @@ -461,7 +477,7 @@ static inline struct runlist_element *ntfs_rl_replace(struct runlist_element *ds * On error, return -errno. Both runlists are left unmodified. */ static inline struct runlist_element *ntfs_rl_split(struct runlist_element *dst, int dsize, - struct runlist_element *src, int ssize, int loc, + const struct runlist_element *src, int ssize, int loc, size_t *new_size) { /* Space required: @dst size + @src size + one new hole. */ @@ -488,11 +504,12 @@ static inline struct runlist_element *ntfs_rl_split(struct runlist_element *dst, } /* - * ntfs_runlists_merge - merge two runlists into one + * __ntfs_runlists_merge - merge two runlists into one * @d_runlist: destination runlist structure to merge into * @srl: source runlist to merge from * @s_rl_count: number of elements in @srl (0 to auto-detect) * @new_rl_count: on success, set to the new combined runlist size + * @keep_srl: leave @srl to the caller instead of deallocating it * * First we sanity check the two runlists @srl and @drl to make sure that they * are sensible and can be merged. The runlist @srl must be either after the @@ -515,13 +532,14 @@ static inline struct runlist_element *ntfs_rl_split(struct runlist_element *dst, * On success, return a pointer to the new, combined, runlist. Note, both * runlists @drl and @srl are deallocated before returning so you cannot use * the pointers for anything any more. (Strictly speaking the returned runlist - * may be the same as @dst but this is irrelevant.) + * may be the same as @dst but this is irrelevant.) If @keep_srl is true, + * @srl is neither modified nor deallocated, and @drl must not be NULL. * * On error, return -errno. Both runlists are left unmodified. */ -struct runlist_element *ntfs_runlists_merge(struct runlist *d_runlist, - struct runlist_element *srl, size_t s_rl_count, - size_t *new_rl_count) +static struct runlist_element *__ntfs_runlists_merge(struct runlist *d_runlist, + struct runlist_element *srl, size_t s_rl_count, + size_t *new_rl_count, bool keep_srl) { int di, si; /* Current index into @[ds]rl. */ int sstart; /* First index with lcn > LCN_RL_NOT_MAPPED. */ @@ -553,6 +571,8 @@ struct runlist_element *ntfs_runlists_merge(struct runlist *d_runlist, /* Check for the case where the first mapping is being done now. */ if (unlikely(!drl)) { + if (WARN_ON(keep_srl)) + return ERR_PTR(-EINVAL); drl = srl; /* Complete the source runlist if necessary. */ if (unlikely(drl[0].vcn)) { @@ -582,7 +602,8 @@ struct runlist_element *ntfs_runlists_merge(struct runlist *d_runlist, si++; /* Can't have an entirely unmapped source runlist. */ - WARN_ON(!srl[si].length); + if (WARN_ON(!srl[si].length)) + return ERR_PTR(-EINVAL); /* Record the starting points. */ sstart = si; @@ -652,7 +673,8 @@ struct runlist_element *ntfs_runlists_merge(struct runlist *d_runlist, ntfs_error(NULL, "Merge failed."); return drl; } - kvfree(srl); + if (!keep_srl) + kvfree(srl); if (marker) { ntfs_debug("Triggering marker code."); for (ds = dend; drl[ds].length; ds++) @@ -713,6 +735,44 @@ finished: return drl; } +/* + * ntfs_runlists_merge - merge two runlists into one + * @d_runlist: destination runlist structure to merge into + * @srl: source runlist to merge from, deallocated on success + * @s_rl_count: number of elements in @srl (0 to auto-detect) + * @new_rl_count: on success, set to the new combined runlist size + * + * See __ntfs_runlists_merge(). + */ +struct runlist_element *ntfs_runlists_merge(struct runlist *d_runlist, + struct runlist_element *srl, size_t s_rl_count, + size_t *new_rl_count) +{ + return __ntfs_runlists_merge(d_runlist, srl, s_rl_count, new_rl_count, + false); +} + +/* + * ntfs_runlists_merge_keep_src - merge two runlists, keeping the source + * @d_runlist: destination runlist structure to merge into, not empty + * @srl: source runlist to merge from + * @s_rl_count: number of elements in @srl (0 to auto-detect) + * @new_rl_count: on success, set to the new combined runlist size + * + * Like ntfs_runlists_merge(), but @srl is neither modified nor deallocated, + * on success or on error, and remains the caller's to kvfree(). Even when + * runs of @srl are coalesced with runs of @d_runlist, @srl keeps describing + * exactly its own runs, e.g. the clusters a caller has to free to undo the + * merge. + */ +struct runlist_element *ntfs_runlists_merge_keep_src(struct runlist *d_runlist, + struct runlist_element *srl, size_t s_rl_count, + size_t *new_rl_count) +{ + return __ntfs_runlists_merge(d_runlist, srl, s_rl_count, new_rl_count, + true); +} + /* * ntfs_mapping_pairs_decompress - convert mapping pairs array to runlist * @vol: ntfs volume diff --git a/fs/ntfs/runlist.h b/fs/ntfs/runlist.h index cc92e09fbf68..0d4c32b1a338 100644 --- a/fs/ntfs/runlist.h +++ b/fs/ntfs/runlist.h @@ -68,6 +68,9 @@ enum { struct runlist_element *ntfs_runlists_merge(struct runlist *d_runlist, struct runlist_element *srl, size_t s_rl_count, size_t *new_rl_count); +struct runlist_element *ntfs_runlists_merge_keep_src(struct runlist *d_runlist, + struct runlist_element *srl, size_t s_rl_count, + size_t *new_rl_count); struct runlist_element *ntfs_mapping_pairs_decompress(const struct ntfs_volume *vol, const struct attr_record *attr, struct runlist *old_runlist, size_t *new_rl_count); -- cgit v1.2.3 From b2a3b6b6c7b09040e3f44ade6e20a7689e4acd28 Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Fri, 2 Oct 2026 12:34:59 +0800 Subject: ntfs: balance the $MFT runlist lock in data extension error paths ntfs_mft_data_extend_allocation_nolock() drops the $MFT runlist lock before allocating clusters, and every path into undo_alloc arrives without it, except a map_mft_record() failure, which takes it first. undo_alloc never releases it, so the lock leaks and the next $MFT extension and $MFT writeback hang. The restore_undo_alloc failure path, on the other hand, releases the lock without holding it. And undo_alloc frees the clusters with ntfs_cluster_free() and truncates the runlist without the lock, although both require it. Enter undo_alloc without the lock on every path. Merge the new clusters with ntfs_runlists_merge_keep_src() and keep the runlist that ntfs_cluster_alloc() returned until the extension is complete. On failure, undo_alloc truncates $MFT's runlist under the lock and, after dropping it, frees the clusters from the kept runlist with ntfs_cluster_free_from_rl(). That keeps lcnbmp_lock outside the runlist lock, the order given by the comment before the cluster allocation. Unlike ntfs_cluster_free() on a volume mounted with -o discard, this does not issue discards for the clusters, which were never written; the runlist merge failure path already frees them the same way. If the truncation fails, the runlist may still reference the clusters, so they stay allocated and the volume is marked for chkdsk; at this call site the truncation only shrinks the runlist and cannot fail today. A shorter fix would keep calling ntfs_cluster_free() on the live runlist without the lock, relying on $MFT's runlist being fully mapped and changed only by this function under mrec_lock. That breaks ntfs_cluster_free()'s documented locking rule on an argument about the rest of the driver, so this patch does not do that. Fixes: 115380f9a2f9 ("ntfs: update mft operations") Cc: stable@vger.kernel.org Signed-off-by: Matthias Goergens Signed-off-by: Namjae Jeon --- fs/ntfs/mft.c | 50 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 16 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/mft.c b/fs/ntfs/mft.c index c2339acc6319..89288b794348 100644 --- a/fs/ntfs/mft.c +++ b/fs/ntfs/mft.c @@ -1791,7 +1791,7 @@ static int ntfs_mft_data_extend_allocation_nolock(struct ntfs_volume *vol) s64 min_nr, nr, ll; unsigned long flags; struct ntfs_inode *mft_ni; - struct runlist_element *rl, *rl2; + struct runlist_element *rl, *rl2, *alloc_rl; struct ntfs_attr_search_ctx *ctx = NULL; struct mft_record *mrec; struct attr_record *a = NULL; @@ -1859,15 +1859,15 @@ static int ntfs_mft_data_extend_allocation_nolock(struct ntfs_volume *vol) up_write(&mft_ni->runlist.lock); do { - rl2 = ntfs_cluster_alloc(vol, old_last_vcn, nr, lcn, MFT_ZONE, - true, false, false); - if (!IS_ERR(rl2)) + alloc_rl = ntfs_cluster_alloc(vol, old_last_vcn, nr, lcn, + MFT_ZONE, true, false, false); + if (!IS_ERR(alloc_rl)) break; - if (PTR_ERR(rl2) != -ENOSPC || nr == min_nr) { + if (PTR_ERR(alloc_rl) != -ENOSPC || nr == min_nr) { ntfs_error(vol->sb, "Failed to allocate the minimal number of clusters (%lli) for the mft data attribute.", nr); - return PTR_ERR(rl2); + return PTR_ERR(alloc_rl); } /* * There is not enough space to do the allocation, but there @@ -1878,17 +1878,22 @@ static int ntfs_mft_data_extend_allocation_nolock(struct ntfs_volume *vol) ntfs_debug("Retrying mft data allocation with minimal cluster count %lli.", nr); } while (1); + /* + * Keep @alloc_rl until the extension is complete: it describes exactly + * the new clusters, which undo_alloc frees if a later step fails. + */ down_write(&mft_ni->runlist.lock); - rl = ntfs_runlists_merge(&mft_ni->runlist, rl2, 0, &new_rl_count); + rl = ntfs_runlists_merge_keep_src(&mft_ni->runlist, alloc_rl, 0, + &new_rl_count); if (IS_ERR(rl)) { up_write(&mft_ni->runlist.lock); ntfs_error(vol->sb, "Failed to merge runlists for mft data attribute."); - if (ntfs_cluster_free_from_rl(vol, rl2)) { + if (ntfs_cluster_free_from_rl(vol, alloc_rl)) { ntfs_error(vol->sb, "Failed to deallocate clusters from the mft data attribute.%s", es); NVolSetErrors(vol); } - kvfree(rl2); + kvfree(alloc_rl); return PTR_ERR(rl); } mft_ni->runlist.rl = rl; @@ -1904,7 +1909,6 @@ static int ntfs_mft_data_extend_allocation_nolock(struct ntfs_volume *vol) if (IS_ERR(mrec)) { ntfs_error(vol->sb, "Failed to map mft record."); ret = PTR_ERR(mrec); - down_write(&mft_ni->runlist.lock); goto undo_alloc; } ctx = ntfs_attr_get_search_ctx(mft_ni, mrec); @@ -2002,6 +2006,7 @@ extended_ok: mark_mft_record_dirty(ctx->ntfs_ino); ntfs_attr_put_search_ctx(ctx); unmap_mft_record(mft_ni); + kvfree(alloc_rl); ntfs_debug("Done."); return 0; restore_undo_alloc: @@ -2015,7 +2020,7 @@ restore_undo_alloc: write_unlock_irqrestore(&mft_ni->size_lock, flags); ntfs_attr_put_search_ctx(ctx); unmap_mft_record(mft_ni); - up_write(&mft_ni->runlist.lock); + kvfree(alloc_rl); /* * The only thing that is now wrong is ->allocated_size of the * base attribute extent which chkdsk should be able to fix. @@ -2026,15 +2031,28 @@ restore_undo_alloc: ctx->attr->data.non_resident.highest_vcn = cpu_to_le64(old_last_vcn - 1); undo_alloc: - if (ntfs_cluster_free(mft_ni, old_last_vcn, -1, ctx) < 0) { - ntfs_error(vol->sb, "Failed to free clusters from mft data attribute.%s", es); - NVolSetErrors(vol); - } - + /* + * Entered without the runlist lock. Take the new runs off the + * runlist under it, and free their clusters from @alloc_rl once it + * is dropped, as lcnbmp_lock nests outside it (see above). + */ + down_write(&mft_ni->runlist.lock); if (ntfs_rl_truncate_nolock(vol, &mft_ni->runlist, old_last_vcn)) { ntfs_error(vol->sb, "Failed to truncate mft data attribute runlist.%s", es); NVolSetErrors(vol); + /* + * The runlist may still reference the clusters, so leave them + * allocated until chkdsk. + */ + kvfree(alloc_rl); + alloc_rl = NULL; + } + up_write(&mft_ni->runlist.lock); + if (ntfs_cluster_free_from_rl(vol, alloc_rl)) { + ntfs_error(vol->sb, "Failed to free clusters from mft data attribute.%s", es); + NVolSetErrors(vol); } + kvfree(alloc_rl); if (mp_extended && ntfs_attr_update_mapping_pairs(mft_ni, 0)) { ntfs_error(vol->sb, "Failed to restore mapping pairs.%s", es); -- cgit v1.2.3 From 8ec75fe5558857f53903adb5cc7b1da720e32a7d Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Sun, 4 Oct 2026 17:56:05 +0800 Subject: ntfs: set the attribute list size before reserving space for it ntfs_attrlist_update_locked() resizes the attribute list and then, for $MFT, tries to reserve the maximum list size. When that allocation fails with -ENOSPC it falls back to ntfs_attrlist_repack(), which reads the list through ntfs_inode_attr_pread(). The read stops at i_size, but i_size is only updated after the reserve, so when the list has just grown the repack gets a short read and returns -EIO. Only -ENOSPC from the reserve is ignored, so the -EIO fails the $MFT extension and with it the file creation that needed a new mft record. On a 32 MiB volume with 512-byte clusters whose $MFT has a non-resident attribute list, 591 of 1500 creations of empty files succeed and the rest fail with EIO while 742 KiB is still free: ntfs: (device vda): ntfs_attrlist_update_locked(): Failed to reserve attribute list space ntfs: Failed add attr entry to attrlist ntfs: MP update failed ntfs: (device vda): ntfs_mft_record_alloc(): Failed to extend mft data allocation. Update i_size as soon as the list has been resized. File creation then goes on until the volume is full and fails with ENOSPC. Fixes: b1d732e62a5b ("ntfs: repack $MFT/$ATTRIBUTE LIST") Signed-off-by: Matthias Goergens Reviewed-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/attrlist.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/attrlist.c b/fs/ntfs/attrlist.c index 3660e7fd24b1..6edd5d2d9bf2 100644 --- a/fs/ntfs/attrlist.c +++ b/fs/ntfs/attrlist.c @@ -256,6 +256,9 @@ int ntfs_attrlist_update_locked(struct ntfs_inode *base_ni, return err; } + /* ntfs_attrlist_repack() below reads the list up to i_size. */ + i_size_write(attr_vi, base_ni->attr_list_size); + /* * Reserve the maximum legal list size while the MFT metadata area is * still easy to allocate contiguously. This prevents a later list entry @@ -283,8 +286,6 @@ int ntfs_attrlist_update_locked(struct ntfs_inode *base_ni, } } - i_size_write(attr_vi, base_ni->attr_list_size); - if (NInoNonResident(attr_ni) && !NInoAttrListNonResident(base_ni)) NInoSetAttrListNonResident(base_ni); -- cgit v1.2.3 From d91900254dad1212b2fb9fb53883010b307b5793 Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Sun, 4 Oct 2026 17:56:06 +0800 Subject: ntfs: restart the zone search when the allocation hint fails When ntfs_cluster_alloc() is given a start_lcn, it first tries the clusters from there on. If that does not satisfy the request, it moves on to the zone's current position, but keeps the pass, the zone_end and the has_guess state it had. That loses free clusters in two ways. If the search had already moved on to pass 2, whose range ends at the hint, it scans only from the zone position to the hint and misses every free cluster between the start of the zone and the zone position. If no cluster had been tested yet, has_guess is still set, so the cluster at the zone position is tried as if it had been the hint, and when that cluster is in use the rest of the bitmap buffer is skipped. Both happen when the hint lies at or past the end of the volume, which ntfs_attr_map_cluster() produces when it extrapolates from the last allocated run across a hole. The allocator then finds nothing, shrinks the MFT zone to nothing trying to satisfy the request, and fails with -ENOSPC. To reproduce on a 128 MiB volume with 4 KiB clusters, extend two files in turn by one cluster at a time, alternating between fallocate(), write() past EOF and truncate() up, so that the runs of each file are separated by holes. When the volume is full, truncate both files back to 1 MiB and start again. During the second round a one-cluster fallocate() fails with ENOSPC while 40 MiB is free. Start the zone over as if no hint had been given. A contiguous request can get there with the run from the hint already allocated, when that run reached the end of a bitmap buffer or of the zone. Return that run instead, as when a cluster in use ends it; going on at the zone position gives the request a second run that its caller does not expect. That could happen before this patch too, whenever the cluster at the zone position was free. ntfs_attr_map_cluster() reports and zeroes only the first run, so after a fallocate() of 20 clusters over a hole, with the hint 4 clusters before the end of the volume, 16 of them read back the data of a deleted file. Fixes: 11ccc9107dc4 ("ntfs: update runlist handling and cluster allocator") Suggested-by: Baolin Liu Signed-off-by: Matthias Goergens Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/lcnalloc.c | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/lcnalloc.c b/fs/ntfs/lcnalloc.c index 0d6cd08ee2e7..c58e689fb585 100644 --- a/fs/ntfs/lcnalloc.c +++ b/fs/ntfs/lcnalloc.c @@ -506,13 +506,32 @@ done: } if (!used_zone_pos) { + /* + * The run at @start_lcn reached the end of the buffer + * or the zone. A contiguous request gets that run + * alone, not a second one from the zone position. + */ + if (is_contig && rlpos) + goto out; + /* + * Leaving @start_lcn for the zone position starts the + * zone over as if no hint had been given, even if the + * search had already reached pass 2, whose range ends + * at @start_lcn. + */ used_zone_pos = 1; - if (search_zone == 1) + has_guess = 0; + pass = 1; + if (search_zone == 1) { zone_start = vol->mft_zone_pos; - else if (search_zone == 2) + zone_end = vol->mft_zone_end; + } else if (search_zone == 2) { zone_start = vol->data1_zone_pos; - else + zone_end = vol->nr_clusters; + } else { zone_start = vol->data2_zone_pos; + zone_end = vol->mft_zone_start; + } if (!zone_start || zone_start == vol->mft_zone_start || zone_start == vol->mft_zone_end) -- cgit v1.2.3 From 73875f650eda4cdb7da6a30e97a2a476ba89e321 Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Sun, 4 Oct 2026 17:56:07 +0800 Subject: ntfs: do not refuse fallocate() when the MFT zone is empty ntfs_fallocate() fails with -ENOSPC whenever the MFT zone has shrunk to nothing. The cluster allocator gives the MFT zone up when it cannot find free clusters elsewhere, and nothing restores it before the next mount, so after one such failure every fallocate() on the volume fails with ENOSPC, punching holes included, however much space is freed. The allocator failure fixed by the previous patch was enough to get there: afterwards a one-cluster fallocate() failed with ENOSPC while 123 MiB of the 128 MiB volume were free. write() and truncate() do not look at the MFT zone, and ntfs_allocate_range() already fails with -ENOSPC when too few clusters are free. Remove the check. Fixes: 9c87959601e8 ("ntfs: update file operations") Signed-off-by: Matthias Goergens Reviewed-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/file.c | 3 --- 1 file changed, 3 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c index 3ec82715a588..762045d26296 100644 --- a/fs/ntfs/file.c +++ b/fs/ntfs/file.c @@ -1163,9 +1163,6 @@ static long ntfs_fallocate(struct file *file, int mode, loff_t offset, loff_t le if (!NVolFreeClusterKnown(vol)) wait_event(vol->free_waitq, NVolFreeClusterKnown(vol)); - if ((ni->vol->mft_zone_end - ni->vol->mft_zone_start) == 0) - return -ENOSPC; - if (NInoNonResident(ni) && !NInoFullyMapped(ni)) { down_write(&ni->runlist.lock); err = ntfs_attr_map_whole_runlist(ni); -- cgit v1.2.3 From 3eb0cc1707e6fab1300bebbf3debeee79c7bcbb7 Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Sun, 4 Oct 2026 17:56:08 +0800 Subject: ntfs: do not give a contiguous cluster allocation a second run ntfs_attr_map_cluster() asks ntfs_cluster_alloc() for contiguous clusters and merges the whole result into the runlist, but reports only its first run, so its callers zero only that run. Within a bitmap page the allocator keeps such a request to one run, because a cluster in use ends it. The zone search can also move on without testing the clusters after the run, though: it skips a bitmap page that has no free clusters, it goes from the end of the zone to the start of pass 2, and once every zone has been searched it shrinks the MFT zone by half and goes on at its new end. After each of these jumps the first cluster tested is still taken as the next one of the run, and if it is free it starts a second run elsewhere on the volume. fallocate() over a hole then leaves the clusters of that second run holding whatever they held before: on a nearly full volume, 16 of 20 fallocated clusters read back a deleted file's data, in each of the three cases. Delayed allocation gets a second run without punching any holes: filling the same volume with write() reaches the last case when the MFT zone is shrunk. The allocator then takes only the first run off the dirty-cluster count, and after the fill statfs() reported no free space while 939 clusters were still free, until the next mount. Before setting the bit of a cluster for a contiguous request, check that the cluster comes right after the run, and return the run as it is if not; no bit has to be cleared. ntfs_attr_map_cluster() already handles a short run, which a cluster in use produces in the same way: its callers ask again for the rest. Fixes: 11ccc9107dc4 ("ntfs: update runlist handling and cluster allocator") Suggested-by: Namjae Jeon Link: https://lore.kernel.org/all/CAKYAXd-agPFvDj2-HvjP3SLtG7AxkqUeNPqYyrLt-BN7fT8w6g@mail.gmail.com/ Signed-off-by: Matthias Goergens Signed-off-by: Namjae Jeon --- fs/ntfs/lcnalloc.c | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'fs') diff --git a/fs/ntfs/lcnalloc.c b/fs/ntfs/lcnalloc.c index c58e689fb585..3ebcac4c2864 100644 --- a/fs/ntfs/lcnalloc.c +++ b/fs/ntfs/lcnalloc.c @@ -375,6 +375,16 @@ struct runlist_element *ntfs_cluster_alloc(struct ntfs_volume *vol, const s64 st has_guess = 1; continue; } + /* + * A contiguous request gets a single run. The scan can + * move on without testing the clusters after the run + * (past a bitmap page that is full, to pass 2 or to + * another zone), so return the run when this cluster + * does not follow it. + */ + if (is_contig && rlpos && + lcn + bmp_pos != prev_lcn + prev_run_len) + goto out; /* * Allocate more memory if needed, including space for * the terminator element. -- cgit v1.2.3 From 002ae2270e6920c1d30e24ad879f516371c3a361 Mon Sep 17 00:00:00 2001 From: Jiale Yao Date: Sat, 3 Oct 2026 17:30:34 +0800 Subject: ntfs: drain in-flight DIO before buffered write fallback An asynchronous direct write can remain in flight after the inode lock is released. If another direct write falls back to buffered I/O while the first write is still pending, iomap_file_buffered_write() can dirty pages before the first write completes its post-I/O page cache invalidation. The invalidation then finds dirty pages, reports a page cache invalidation failure, and records -EIO in the mapping error sequence. A later fsync() therefore returns -EIO. Commit 15cdefd0c0522f9d5e12d947fa04f4c11649b699 ("ext4: drain in-flight DIO before buffered write fallback") fixed the same race in ext4. NTFS has an equivalent fallback after iomap_dio_rw() returns -ENOTBLK or a short write, but does not drain other in-flight DIO before dirtying the page cache. Wait for in-flight DIO before calling iomap_file_buffered_write() in the fallback path. Since NTFS supports IOCB_NOWAIT, do not enter the blocking fallback for such requests. Return -EAGAIN if no bytes were written, or preserve the positive short-write result if the direct write made partial progress. A reproducer using concurrent AIO direct writes and buffered fallback triggered the following warning and made a subsequent fsync() return -EIO: Page cache invalidation failure on direct I/O. Possible data corruption due to collision with buffered I/O! Fixes: 9c87959601e8 ("ntfs: update file operations") Link: https://lore.kernel.org/r/20260629113827.4074335-3-libaokun@linux.alibaba.com Reviewed-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Jiale Yao Signed-off-by: Namjae Jeon --- fs/ntfs/file.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'fs') diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c index 762045d26296..1e2500a52148 100644 --- a/fs/ntfs/file.c +++ b/fs/ntfs/file.c @@ -540,8 +540,21 @@ static ssize_t ntfs_dio_write_iter(struct kiocb *iocb, struct iov_iter *from) ssize_t written; int ret2; + if (iocb->ki_flags & IOCB_NOWAIT) { + if (!ret) + ret = -EAGAIN; + goto out; + } + offset = iocb->ki_pos; iocb->ki_flags &= ~IOCB_DIRECT; + + /* + * Prevent concurrent direct I/O and buffered I/O to the same file + * range. Wait for in-flight DIO to finish before dirtying pages. + */ + inode_dio_wait(file_inode(iocb->ki_filp)); + written = iomap_file_buffered_write(iocb, from, &ntfs_write_iomap_ops, &ntfs_iomap_folio_ops, NULL); -- cgit v1.2.3 From 53bbed2cb6fc27ce7183ded03204ed6defed8142 Mon Sep 17 00:00:00 2001 From: Karl Mehltretter Date: Tue, 6 Oct 2026 06:51:35 +0200 Subject: ntfs: fix kunmap_local() of advanced pointers in check_mft_mirror() check_mft_mirror() compares the mft records of a page with the mirror records by advancing kmft and kmirr by the record size for every record. It then passes the advanced pointers to kunmap_local(). After the last record of a page they point one page past the mapping. With HIGHMEM kunmap_local() warns that the address does not match the top of the kmap_local stack. What it does next depends on how the architecture finds the page table entry: - 32-bit ARM looks the entry up by that address. It clears the entry of the neighbouring fixmap slot and leaves the real mapping in place. The next kmap_local() on that CPU that reaches the slot hits the BUG_ON(!pte_none()) in __kmap_local_pfn_prot(). - x86-32 takes the entry from the stack index, so the wrong address does not select a wrong entry. In QEMU the mount carried on after the warning. With 4 KiB pages and 1 KiB mft records the four mirror records fill exactly one page, so every mount whose $MFT and $MFTMirr folios are in highmem hits it. A fresh mkntfs volume on 32-bit ARM (multi_v7_defconfig, 256 MiB of highmem) fails on the first mount: WARNING: mm/highmem.c:623 at kunmap_local_indexed+0x254/0x26c, CPU#1: mount/85 kunmap_local_indexed from ntfs_fill_super+0x224c/0x35a0 ntfs_fill_super from get_tree_bdev_flags+0x1d8/0x2a4 ntfs: volume version 3.1, dev loop0, cluster size 4096 kernel BUG at mm/highmem.c:565! PC is at __kmap_local_pfn_prot+0x210/0x214 __kmap_local_page_prot from ntfs_check_logfile+0x1fc/0x1210 Found with syzkaller. Kernels without HIGHMEM do not show it. kmap_local_folio() returns the direct map address there and kunmap_local() has no mapping to remove. Keep the addresses returned by kmap_local_folio() and unmap with those. Fixes: 6251f0b0de7d ("ntfs: update super block operations") Cc: stable@vger.kernel.org Assisted-by: LLM Signed-off-by: Karl Mehltretter Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/super.c | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c index 716ba775f1f3..2c6685342999 100644 --- a/fs/ntfs/super.c +++ b/fs/ntfs/super.c @@ -1132,7 +1132,7 @@ static bool check_mft_mirror(struct ntfs_volume *vol) struct super_block *sb = vol->sb; struct ntfs_inode *mirr_ni; struct folio *mft_folio = NULL, *mirr_folio = NULL; - u8 *kmft = NULL, *kmirr = NULL; + u8 *kmft = NULL, *kmirr = NULL, *kmft_base = NULL, *kmirr_base = NULL; struct runlist_element *rl, rl2[2]; pgoff_t index; int mrecs_per_page, i; @@ -1147,9 +1147,9 @@ static bool check_mft_mirror(struct ntfs_volume *vol) /* Switch pages if necessary. */ if (!(i % mrecs_per_page)) { if (index) { - kunmap_local(kmirr); + kunmap_local(kmirr_base); folio_put(mirr_folio); - kunmap_local(kmft); + kunmap_local(kmft_base); folio_put(mft_folio); } /* Get the $MFT page. */ @@ -1159,7 +1159,8 @@ static bool check_mft_mirror(struct ntfs_volume *vol) ntfs_error(sb, "Failed to read $MFT."); return false; } - kmft = kmap_local_folio(mft_folio, 0); + kmft_base = kmap_local_folio(mft_folio, 0); + kmft = kmft_base; /* Get the $MFTMirr page. */ mirr_folio = read_mapping_folio(vol->mftmirr_ino->i_mapping, index, NULL); @@ -1167,7 +1168,8 @@ static bool check_mft_mirror(struct ntfs_volume *vol) ntfs_error(sb, "Failed to read $MFTMirr."); goto mft_unmap_out; } - kmirr = kmap_local_folio(mirr_folio, 0); + kmirr_base = kmap_local_folio(mirr_folio, 0); + kmirr = kmirr_base; ++index; } @@ -1179,10 +1181,10 @@ static bool check_mft_mirror(struct ntfs_volume *vol) "Incomplete multi sector transfer detected in mft record %i.", i); mm_unmap_out: - kunmap_local(kmirr); + kunmap_local(kmirr_base); folio_put(mirr_folio); mft_unmap_out: - kunmap_local(kmft); + kunmap_local(kmft_base); folio_put(mft_folio); return false; } @@ -1218,9 +1220,9 @@ mft_unmap_out: kmirr += vol->mft_record_size; } while (++i < vol->mftmirr_size); /* Release the last folios. */ - kunmap_local(kmirr); + kunmap_local(kmirr_base); folio_put(mirr_folio); - kunmap_local(kmft); + kunmap_local(kmft_base); folio_put(mft_folio); /* Construct the mft mirror runlist by hand. */ -- cgit v1.2.3 From 1de445b463d6494041b7d6c4659c8545a7f73f3c Mon Sep 17 00:00:00 2001 From: Karl Mehltretter Date: Tue, 6 Oct 2026 06:51:36 +0200 Subject: ntfs: fix kmap_local leak in ntfs_check_logfile() ntfs_check_logfile() calls kmap_local_folio() on every iteration of its search loop but kunmap_local() only once per folio. The loop visits positions 0, 512, 1024, 2048, 4096 and so on. With 4 KiB pages the first four are in one page, so a mount whose first LogFile folio is in highmem leaves three entries on the kmap_local stack of the mounting task. Kernels without HIGHMEM are not affected. The leaked entries keep migration disabled. nr_pinned of the runqueue the mount ran on stays raised after the task exits, so taking that CPU offline blocks forever in balance_hotplug_wait(). The fifth mount in one task reaches KM_MAX_IDX (12 leaked entries plus 4 in the loop) and hits the BUG_ON() in kmap_local_idx_push(): kernel BUG at mm/highmem.c:480! On x86-32 the generic entry code also warns on return to user space: DEBUG_LOCKS_WARN_ON(current->kmap_ctrl.idx) WARNING: include/linux/highmem-internal.h:17 at __do_fast_syscall_32+0x21e/0x550 Map each page once, when it is read, and unmap that address. Fixes: 5218cd102aec ("ntfs: update misc operations") Cc: stable@vger.kernel.org Assisted-by: LLM Signed-off-by: Karl Mehltretter Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/logfile.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/logfile.c b/fs/ntfs/logfile.c index 1404664dacc0..ce38ce87f8fc 100644 --- a/fs/ntfs/logfile.c +++ b/fs/ntfs/logfile.c @@ -457,7 +457,7 @@ bool ntfs_check_logfile(struct inode *log_vi, struct restart_page_header **rp) struct ntfs_volume *vol = NTFS_SB(log_vi->i_sb); struct address_space *mapping = log_vi->i_mapping; struct folio *folio = NULL; - u8 *kaddr = NULL; + u8 *kaddr = NULL, *kbase = NULL; struct restart_page_header *rstr1_ph = NULL; struct restart_page_header *rstr2_ph = NULL; int log_page_size, err; @@ -511,7 +511,7 @@ bool ntfs_check_logfile(struct inode *log_vi, struct restart_page_header **rp) if (!folio || folio->index != idx) { if (folio) { - kunmap_local(kaddr); + kunmap_local(kbase); folio_put(folio); } folio = read_mapping_folio(mapping, idx, NULL); @@ -520,8 +520,9 @@ bool ntfs_check_logfile(struct inode *log_vi, struct restart_page_header **rp) idx); goto err_out; } + kbase = kmap_local_folio(folio, 0); } - kaddr = (u8 *)kmap_local_folio(folio, 0) + (pos & ~PAGE_MASK); + kaddr = kbase + (pos & ~PAGE_MASK); /* * A non-empty block means the logfile is not empty while an * empty block after a non-empty block has been encountered @@ -574,7 +575,7 @@ bool ntfs_check_logfile(struct inode *log_vi, struct restart_page_header **rp) * find a valid one further in the file. */ if (err != -EINVAL) { - kunmap_local(kaddr); + kunmap_local(kbase); folio_put(folio); goto err_out; } @@ -583,7 +584,7 @@ bool ntfs_check_logfile(struct inode *log_vi, struct restart_page_header **rp) pos = NTFS_BLOCK_SIZE >> 1; } if (folio) { - kunmap_local(kaddr); + kunmap_local(kbase); folio_put(folio); } if (logfile_is_empty) { -- cgit v1.2.3 From 8f03e90708d36c1442ddea69f7b49b20130b9a72 Mon Sep 17 00:00:00 2001 From: Hyunchul Lee Date: Tue, 6 Oct 2026 11:24:12 +0900 Subject: ntfs: only require compressed size on first extent The attribute validator required the compressed_size field for every sparse or compressed extent. Continuation extents omit that field, so their valid mapping_pairs_offset of 64 was rejected and compressed reads failed. Fixes: ffc755828de1 ("ntfs: Fix min_len for compressed/sparse attributes in ntfs_non_resident_attr_value_is_valid()") Signed-off-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/attrib.c | 8 ++++++-- fs/ntfs/inode.c | 18 +++++++++--------- 2 files changed, 15 insertions(+), 11 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c index 565bd468b893..4df618abc4ef 100644 --- a/fs/ntfs/attrib.c +++ b/fs/ntfs/attrib.c @@ -733,8 +733,12 @@ static bool ntfs_non_resident_attr_value_is_valid(const struct attr_record *a) 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)) + /* + * Only the first extent of sparse and compressed attributes has the + * extra compressed_size field. + */ + if ((a->flags & (ATTR_IS_SPARSE | ATTR_COMPRESSION_MASK)) && + !a->data.non_resident.lowest_vcn) min_len += sizeof(a->data.non_resident.compressed_size); if (attr_len < min_len) diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c index b666cdb3b102..eca0724c4358 100644 --- a/fs/ntfs/inode.c +++ b/fs/ntfs/inode.c @@ -1157,6 +1157,11 @@ view_index_meta: } if (a->non_resident) { NInoSetNonResident(ni); + if (a->data.non_resident.lowest_vcn) { + ntfs_error(vi->i_sb, + "First extent of $DATA attribute has non zero lowest_vcn."); + goto unm_err_out; + } if (NInoCompressed(ni) || (NInoSparse(ni) && !NInoWofCompressed(ni))) { if (NInoCompressed(ni) && a->data.non_resident.compression_unit != 4) { @@ -1205,11 +1210,6 @@ view_index_meta: ni->itype.compressed.size = le64_to_cpu( a->data.non_resident.compressed_size); } - if (a->data.non_resident.lowest_vcn) { - ntfs_error(vi->i_sb, - "First extent of $DATA attribute has non zero lowest_vcn."); - goto unm_err_out; - } if (ntfs_non_resident_sizes_inconsistent(vi, a)) goto unm_err_out; vi->i_size = ni->data_size = le64_to_cpu(a->data.non_resident.data_size); @@ -1435,6 +1435,10 @@ static int ntfs_read_locked_attr_inode(struct inode *base_vi, struct inode *vi) } } else { NInoSetNonResident(ni); + if (a->data.non_resident.lowest_vcn) { + ntfs_error(vi->i_sb, "First extent of attribute has non-zero lowest_vcn."); + goto unm_err_out; + } /* * Ensure the attribute name is placed before the mapping pairs * array. @@ -1470,10 +1474,6 @@ static int ntfs_read_locked_attr_inode(struct inode *base_vi, struct inode *vi) ni->itype.compressed.size = le64_to_cpu( a->data.non_resident.compressed_size); } - if (a->data.non_resident.lowest_vcn) { - ntfs_error(vi->i_sb, "First extent of attribute has non-zero lowest_vcn."); - goto unm_err_out; - } if (ntfs_non_resident_sizes_inconsistent(vi, a)) goto unm_err_out; vi->i_size = ni->data_size = le64_to_cpu(a->data.non_resident.data_size); -- cgit v1.2.3 From 76e0566af8629240d7f403c54d03f5580079856d Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sat, 3 Oct 2026 08:42:49 +0900 Subject: ntfs: validate and reserve bad clusters at mount The cluster allocator only consults $Bitmap and never loads $BadClus:$Bad. A free bitmap bit for a known bad cluster can therefore place that cluster in the free-space cache and make it available for new file allocations. Load the named nonresident $Bad stream and map every attribute extent before validating full-volume coverage and identity mappings for bad runs. Reserve only free bits in $Bitmap on writable mounts and flush the corrections before the initial free-space scan. Keep bad-cluster counts diagnostic so reservations are accounted for exactly once. Force read-only mounts with repair advice when $Bad cannot be loaded or validated, and prevent read-write remounts after such errors. Leave the bitmap unchanged on read-only mounts and fail mounts on memory allocation failure. Suppress errors=panic while loading $BadClus so damaged metadata can fall back to read-only. Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/bitmap.c | 53 ++++++++++++++++++ fs/ntfs/bitmap.h | 2 + fs/ntfs/super.c | 163 +++++++++++++++++++++++++++++++++++++++++++++++++++---- fs/ntfs/volume.h | 2 + 4 files changed, 209 insertions(+), 11 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/bitmap.c b/fs/ntfs/bitmap.c index 912fdcca8e01..af86fb1c199b 100644 --- a/fs/ntfs/bitmap.c +++ b/fs/ntfs/bitmap.c @@ -12,6 +12,59 @@ #include "bitmap.h" #include "ntfs.h" +/* + * Check a $BadClus run against $Bitmap and optionally reserve its free bits. + * This is only used during mount, before the free-space scan starts. The + * scan will account for the repaired bits, so do not use the regular bitmap + * setters, which wait for that scan and assume every bit changes state. + * @free_bits counts only bits that were free, including on read-only mounts. + */ +int ntfs_bitmap_check_used(struct ntfs_volume *vol, s64 start, s64 count, + bool repair, s64 *free_bits) +{ + struct address_space *mapping = vol->lcnbmp_ino->i_mapping; + const unsigned int bits_per_page = PAGE_SIZE * BITS_PER_BYTE; + struct folio *folio; + u8 *bitmap; + unsigned int bit, end, bits; + s64 nr_free; + + if (start < 0 || count <= 0 || start >= vol->nr_clusters || + count > vol->nr_clusters - start || NVolFreeClusterKnown(vol)) + return -EINVAL; + if (repair && sb_rdonly(vol->sb)) + return -EROFS; + + while (count) { + folio = read_mapping_folio(mapping, + start >> (PAGE_SHIFT + 3), NULL); + if (IS_ERR(folio)) + return PTR_ERR(folio); + + folio_lock(folio); + bitmap = kmap_local_folio(folio, 0); + bit = start & (bits_per_page - 1); + bits = min_t(s64, count, bits_per_page - bit); + end = bit + bits; + nr_free = 0; + for (bit = find_next_zero_bit_le(bitmap, end, bit); bit < end; + bit = find_next_zero_bit_le(bitmap, end, bit + 1)) { + nr_free++; + if (repair) + __set_bit_le(bit, bitmap); + } + kunmap_local(bitmap); + if (repair && nr_free) + folio_mark_dirty(folio); + folio_unlock(folio); + folio_put(folio); + *free_bits += nr_free; + start += bits; + count -= bits; + } + return 0; +} + int ntfs_trim_fs(struct ntfs_volume *vol, struct fstrim_range *range) { size_t buf_clusters; diff --git a/fs/ntfs/bitmap.h b/fs/ntfs/bitmap.h index 31177fe61da6..eccd0cf0f852 100644 --- a/fs/ntfs/bitmap.h +++ b/fs/ntfs/bitmap.h @@ -13,6 +13,8 @@ #include "volume.h" int ntfs_trim_fs(struct ntfs_volume *vol, struct fstrim_range *range); +int ntfs_bitmap_check_used(struct ntfs_volume *vol, s64 start, s64 count, + bool repair, s64 *free_bits); int __ntfs_bitmap_set_bits_in_run(struct inode *vi, const s64 start_bit, const s64 count, const u8 value, const bool is_rollback); diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c index 2c6685342999..d174867b9f5f 100644 --- a/fs/ntfs/super.c +++ b/fs/ntfs/super.c @@ -21,6 +21,7 @@ #include "mft.h" #include "ntfs.h" #include "ea.h" +#include "bitmap.h" #include "volume.h" /* A global default upcase table and a corresponding reference count. */ @@ -1548,6 +1549,125 @@ upcase_failed: return false; } +static int ntfs_validate_badclus(struct ntfs_volume *vol, + const struct runlist *runlist, s64 *bad_clusters) +{ + const struct runlist_element *rl = runlist->rl; + s64 next_vcn = 0, bad = 0; + size_t i; + + if (!rl) + return -EIO; + + for (i = 0; i < runlist->count; i++) { + if (!rl[i].length) { + if (i != runlist->count - 1 || + rl[i].vcn != vol->nr_clusters || + rl[i].lcn != LCN_ENOENT || + next_vcn != vol->nr_clusters) + return -EIO; + *bad_clusters = bad; + return 0; + } + if (rl[i].vcn != next_vcn || rl[i].length < 0 || + rl[i].length > vol->nr_clusters - next_vcn) + return -EIO; + if (rl[i].lcn != LCN_HOLE) { + /* Each bad cluster is mapped to its own volume offset. */ + if (rl[i].lcn != rl[i].vcn) + return -EIO; + bad += rl[i].length; + } + next_vcn += rl[i].length; + } + return -EIO; +} + +/* + * Read the allocation of $BadClus:$Bad, not its data. initialized_size may + * be zero even when the runlist contains bad clusters. Validate every run + * before repairing $Bitmap, and let the later free-space scan account for + * the reservations without subtracting the bad-cluster count again. + */ +static int load_and_check_badclus(struct ntfs_volume *vol) +{ + static __le16 bad_name[] = { + cpu_to_le16('$'), cpu_to_le16('B'), + cpu_to_le16('a'), cpu_to_le16('d'), + }; + struct super_block *sb = vol->sb; + struct inode *base_ino, *bad_ino; + struct ntfs_inode *ni; + s64 free_bits = 0; + u64 size = ntfs_cluster_to_bytes(vol, vol->nr_clusters); + size_t i; + int err; + + base_ino = ntfs_iget(sb, FILE_BadClus); + if (IS_ERR(base_ino)) + return PTR_ERR(base_ino); + if (!S_ISREG(base_ino->i_mode)) { + err = -EIO; + goto iput_base; + } + bad_ino = ntfs_attr_iget(base_ino, AT_DATA, bad_name, + ARRAY_SIZE(bad_name)); + if (IS_ERR(bad_ino)) { + err = PTR_ERR(bad_ino); + goto iput_base; + } + ni = NTFS_I(bad_ino); + if (!NInoNonResident(ni) || NInoCompressed(ni) || NInoEncrypted(ni) || + ni->data_size != size || ni->allocated_size != size) { + err = -EIO; + goto iput_bad; + } + + down_write(&ni->runlist.lock); + err = ntfs_attr_map_whole_runlist(ni); + if (err) + goto unlock; + err = ntfs_validate_badclus(vol, &ni->runlist, &vol->bad_clusters); + if (err) + goto unlock; + for (i = 0; i < ni->runlist.count - 1; i++) { + struct runlist_element *rl = &ni->runlist.rl[i]; + + if (rl->lcn == LCN_HOLE) + continue; + err = ntfs_bitmap_check_used(vol, rl->lcn, rl->length, + !sb_rdonly(sb), &free_bits); + if (err) + break; + } +unlock: + up_write(&ni->runlist.lock); +iput_bad: + iput(bad_ino); +iput_base: + iput(base_ino); + if (err) + return err; + if (free_bits) { + /* A read-only mount must not later remount RW without repair. */ + if (sb_rdonly(sb)) + return -EIO; + err = filemap_write_and_wait(vol->lcnbmp_ino->i_mapping); + if (err) + return err; + err = blkdev_issue_flush(sb->s_bdev); + if (err) + return err; + ntfs_warning(sb, + "Marked %lld bad clusters as used in $Bitmap.", + free_bits); + } + if (vol->bad_clusters) + pr_info("volume contains %lld bad clusters, dev %s\n", + vol->bad_clusters, sb->s_id); + return 0; +} + /* * The lcn and mft bitmap inodes are NTFS-internal inodes with * their own special locking rules: @@ -1563,16 +1683,16 @@ static struct lock_class_key * Open the system files with normal access functions and complete setting up * the ntfs super block @vol. * - * Return 'true' on success or 'false' on error. + * Return 0 on success and -errno on error. */ -static bool load_system_files(struct ntfs_volume *vol) +static int load_system_files(struct ntfs_volume *vol) { struct super_block *sb = vol->sb; struct mft_record *m; struct volume_information *vi; struct ntfs_attr_search_ctx *ctx; struct restart_page_header *rp; - int err; + int err, ret = -EINVAL; u8 saved_on_errors; ntfs_debug("Entering."); @@ -1785,6 +1905,25 @@ get_ctx_vol_failed: "Errors were recorded during mount. Mounting read-only. Run chkdsk."); } + /* Check bad clusters after hibernation checks and before any writes. */ + saved_on_errors = vol->on_errors; + if (saved_on_errors == ON_ERRORS_PANIC) + vol->on_errors = ON_ERRORS_REMOUNT_RO; + err = load_and_check_badclus(vol); + vol->on_errors = saved_on_errors; + if (err == -ENOMEM) { + ret = err; + ntfs_warning(sb, "Not enough memory to load $BadClus."); + goto iput_root_err_out; + } + if (err) { + sb->s_flags |= SB_RDONLY; + NVolSetErrors(vol); + ntfs_error(sb, + "Failed to initialize $BadClus (%d). Mounting read-only. Run fsck.ntfs or chkdsk.", + err); + } + /* If (still) a read-write mount, empty the logfile. */ if (!sb_rdonly(sb) && vol->logfile_ino && !ntfs_empty_logfile(vol->logfile_ino) && @@ -1799,7 +1938,7 @@ get_ctx_vol_failed: } /* If on NTFS versions before 3.0, we are done. */ if (unlikely(vol->major_ver < 3)) - return true; + return 0; /* NTFS 3.0+ specific initialization. */ /* Get the security descriptors inode. */ vol->secure_ino = ntfs_iget(sb, FILE_Secure); @@ -1818,7 +1957,7 @@ get_ctx_vol_failed: ntfs_error(sb, "Failed to load $Extend."); goto iput_sec_err_out; } - return true; + return 0; iput_sec_err_out: iput(vol->secure_ino); @@ -1854,7 +1993,7 @@ iput_mftbmp_err_out: iput(vol->mftbmp_ino); iput_mirr_err_out: iput(vol->mftmirr_ino); - return false; + return ret; } static void ntfs_volume_free(struct ntfs_volume *vol) @@ -2460,7 +2599,7 @@ static int ntfs_fill_super(struct super_block *sb, struct fs_context *fc) { char *boot; struct inode *tmp_ino; - int blocksize, result; + int blocksize, result, err = -EINVAL; pgoff_t lcn_bit_pages; struct ntfs_volume *vol = NTFS_SB(sb); int silent = fc->sb_flags & SB_SILENT; @@ -2632,8 +2771,10 @@ static int ntfs_fill_super(struct super_block *sb, struct fs_context *fc) * Open the system files with normal access functions and complete * setting up the ntfs super block. */ - if (!load_system_files(vol)) { - ntfs_error(sb, "Failed to load system files."); + result = load_system_files(vol); + if (result) { + err = result; + ntfs_warning(sb, "Failed to load system files (%d).", err); goto unl_upcase_iput_tmp_ino_err_out_now; } @@ -2742,9 +2883,9 @@ err_out_now: kfree(vol->volume_label); unload_nls(vol->nls_map); kfree(vol); - ntfs_debug("Failed, returning -EINVAL."); + ntfs_debug("Failed, returning %d.", err); lockdep_on(); - return -EINVAL; + return err; } /* diff --git a/fs/ntfs/volume.h b/fs/ntfs/volume.h index 0473b602084c..6d9d6eb79a7c 100644 --- a/fs/ntfs/volume.h +++ b/fs/ntfs/volume.h @@ -86,6 +86,7 @@ * @nls_utf8: NLS table for UTF-8. * @free_waitq: Wait queue for threads waiting for free clusters or MFT records. * @free_clusters: Track the number of free clusters. + * @bad_clusters: Number of clusters recorded in $BadClus:$Bad. * @free_mft_records: Track the free mft records. * @dirty_clusters: Number of clusters that are dirty. * @sparse_compression_unit: Size of compression/sparse unit in clusters. @@ -123,6 +124,7 @@ struct ntfs_volume { u32 index_record_size_mask; u8 index_record_size_bits; s64 nr_clusters; + s64 bad_clusters; s64 mft_lcn; s64 mftmirr_lcn; u64 serial_no; -- cgit v1.2.3 From 164709280df380f125f8393670c54c2f0729b25f Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Wed, 7 Oct 2026 16:43:28 +0900 Subject: ntfs: sync attribute inode size after resize Internal attribute truncation bypasses truncate_setsize(), so successful resizes must update the attribute inode's i_size. Restrict the update to attribute inodes to keep KEEP_SIZE fallocate from changing a file's i_size. Fixes: 495e90fa3348 ("ntfs: update attrib operations") Signed-off-by: Namjae Jeon --- fs/ntfs/attrib.c | 2 ++ 1 file changed, 2 insertions(+) (limited to 'fs') diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c index 4df618abc4ef..07414a20b7de 100644 --- a/fs/ntfs/attrib.c +++ b/fs/ntfs/attrib.c @@ -5187,6 +5187,8 @@ int ntfs_attr_truncate_i_locked(struct ntfs_inode *ni, const s64 newsize, ni, newsize, locked_ni, false); } else err = ntfs_resident_attr_resize(ni, newsize, 0, holes); + if (!err && NInoAttr(ni)) + i_size_write(VFS_I(ni), newsize); ntfs_debug("Return status %d\n", err); return err; } -- cgit v1.2.3 From 136249959e67b1c53edcca4f8880fcd251c5e9b6 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Wed, 7 Oct 2026 17:12:45 +0900 Subject: ntfs: clear stale pagecache on initialized-size extension iomap_zero_range() skips holes, so EOF-polluted cache data can survive gap zeroing. Discard the uninitialized cache range before advancing initialized_size so later reads cannot expose stale bytes. Perform the cleanup in ntfs_extend_initialized_size() so both file writes and shared writable mmap preparation use it. Hold the invalidate lock while reading initialized_size, clearing the cache, zeroing the gap, and updating the attribute. Release the lock on every exit path. Fixes: af0db57d4293 ("ntfs: update inode operations") Signed-off-by: Namjae Jeon --- fs/ntfs/inode.c | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c index eca0724c4358..6cfe8f24e4cc 100644 --- a/fs/ntfs/inode.c +++ b/fs/ntfs/inode.c @@ -8,6 +8,7 @@ #include #include +#include #include "lcnalloc.h" #include "time.h" @@ -2484,38 +2485,50 @@ int ntfs_extend_initialized_size(struct inode *vi, const loff_t offset, const loff_t new_size) { struct ntfs_inode *ni = NTFS_I(vi); - loff_t old_init_size; + loff_t old_init_size, clear_size; unsigned long flags; - int err; + int err = 0; + + if (!NInoNonResident(ni)) + return -EINVAL; + filemap_invalidate_lock(vi->i_mapping); read_lock_irqsave(&ni->size_lock, flags); old_init_size = ni->initialized_size; read_unlock_irqrestore(&ni->size_lock, flags); - if (!NInoNonResident(ni)) - return -EINVAL; if (old_init_size >= new_size) - return 0; + goto out; err = ntfs_attr_map_whole_runlist(ni); if (err) - return err; + goto out; - if (!NInoCompressed(ni) && old_init_size < offset) { - err = iomap_zero_range(vi, old_init_size, - offset - old_init_size, - NULL, &ntfs_seek_iomap_ops, - &ntfs_iomap_folio_ops, NULL); - if (err) - return err; + if (!NInoCompressed(ni)) { + /* + * Zeroing skips holes, so discard cached uninitialized bytes + * before making them visible to writes or shared mappings. + */ + clear_size = max_t(loff_t, i_size_read(vi), new_size); + truncate_pagecache_range(vi, old_init_size, clear_size - 1); + + if (old_init_size < offset) { + err = iomap_zero_range(vi, old_init_size, + offset - old_init_size, + NULL, &ntfs_seek_iomap_ops, + &ntfs_iomap_folio_ops, NULL); + if (err) + goto out; + } } - mutex_lock(&ni->mrec_lock); err = ntfs_attr_set_initialized_size(ni, new_size); mutex_unlock(&ni->mrec_lock); if (err) truncate_setsize(vi, old_init_size); +out: + filemap_invalidate_unlock(vi->i_mapping); return err; } -- cgit v1.2.3 From 458de45fa3d8a173ea9cc78debb05caf83e550d3 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Wed, 7 Oct 2026 17:12:54 +0900 Subject: ntfs: propagate EA truncate errors Propagate errors from truncating an EA stream so callers do not report a successful xattr update after a failed resize. Fixes: fc053f05ca28 ("ntfs: add reparse and ea operations") Signed-off-by: Namjae Jeon --- fs/ntfs/ea.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'fs') diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c index b4fcfbe2da4c..ff69fb07f84a 100644 --- a/fs/ntfs/ea.c +++ b/fs/ntfs/ea.c @@ -37,7 +37,7 @@ static int ntfs_write_ea(struct ntfs_inode *ni, __le32 type, char *value, s64 ea struct ntfs_inode *ea_ni = NTFS_I(ea_vi); if (need_truncate && ea_ni->data_size > ea_off + ea_size) - ntfs_attr_truncate(ea_ni, ea_off + ea_size); + err = ntfs_attr_truncate(ea_ni, ea_off + ea_size); mark_mft_record_dirty(ni); } -- cgit v1.2.3 From f427dfa949750f59e4dacb6e282fd4e35b0b3e35 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Mon, 31 Aug 2026 15:16:38 +0900 Subject: ntfs: add named stream ioctls support NTFS supports multiple named $DATA attributes, commonly known as alternate data streams. Add NTFS-specific ioctls to list, read, write, and remove named streams through an opened base file or directory. The UAPI provides separate commands for each operation. - NTFS_IOC_STREAM_READ - NTFS_IOC_STREAM_WRITE - NTFS_IOC_STREAM_REMOVE - NTFS_IOC_LIST_STREAMS Read and write requests use struct ntfs_stream to specify the stream name, byte offset, and transfer length. Writes create the stream if it does not already exist. A write failure after creation may leave the new stream visible. It is not rolled back because another caller may already have opened it. Userspace can remove it with NTFS_IOC_STREAM_REMOVE. The list command returns variable-length entries containing each named stream's data size, allocated size, and name. If the buffer is too small, the command returns -ENOSPC without copying entries. bytes_returned reports the required buffer size, and stream_count reports the number of streams. Stream names use the mounted filesystem NLS by default. Set NTFS_STREAM_FL_UTF16 to pass or receive raw UTF-16LE names. In NLS mode, names that cannot be represented by the mounted character set are omitted from the list. Use UTF-16 mode for lossless enumeration. Read and write transfers are limited to 16 MiB per request. Signed-off-by: Namjae Jeon --- Documentation/userspace-api/ioctl/ioctl-number.rst | 1 + fs/ntfs/Makefile | 2 +- fs/ntfs/attrib.c | 148 ++-- fs/ntfs/attrlist.c | 3 +- fs/ntfs/file.c | 162 +++- fs/ntfs/inode.c | 211 ++++- fs/ntfs/inode.h | 19 + fs/ntfs/named_stream.c | 916 +++++++++++++++++++++ fs/ntfs/namei.c | 43 +- fs/ntfs/stream.h | 43 + include/uapi/linux/ntfs.h | 130 +++ 11 files changed, 1527 insertions(+), 151 deletions(-) create mode 100644 fs/ntfs/named_stream.c create mode 100644 fs/ntfs/stream.h create mode 100644 include/uapi/linux/ntfs.h (limited to 'fs') diff --git a/Documentation/userspace-api/ioctl/ioctl-number.rst b/Documentation/userspace-api/ioctl/ioctl-number.rst index 2fc53093752d..94a421970b85 100644 --- a/Documentation/userspace-api/ioctl/ioctl-number.rst +++ b/Documentation/userspace-api/ioctl/ioctl-number.rst @@ -401,6 +401,7 @@ Code Seq# Include File Comments 0xE5 00-3F linux/fuse.h 0xEC 00-01 drivers/platform/chrome/cros_ec_dev.h ChromeOS EC driver 0xEE 00-09 uapi/linux/pfrut.h Platform Firmware Runtime Update and Telemetry +0xEF 00-0F uapi/linux/ntfs.h NTFS 0xF3 00-3F drivers/usb/misc/sisusbvga/sisusb.h sisfb (in development) 0xF6 all LTTng Linux Trace Toolkit Next Generation diff --git a/fs/ntfs/Makefile b/fs/ntfs/Makefile index ee8987e496a8..3e8549577653 100644 --- a/fs/ntfs/Makefile +++ b/fs/ntfs/Makefile @@ -5,7 +5,7 @@ obj-$(CONFIG_NTFS_FS) += ntfs.o ntfs-y := aops.o attrib.o collate.o dir.o file.o index.o inode.o \ mft.o mst.o namei.o runlist.o super.o unistr.o attrlist.o ea.o \ upcase.o bitmap.o lcnalloc.o logfile.o reparse.o compress.o \ - iomap.o debug.o sysctl.o object_id.o bdev-io.o + iomap.o debug.o sysctl.o object_id.o bdev-io.o named_stream.o ntfs-$(CONFIG_NTFS_FS_WOF_COMPRESSION) += wof.o \ lib/decompress_common.o lib/lzx_decompress.o lib/xpress_decompress.o diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c index 07414a20b7de..15131e9401e2 100644 --- a/fs/ntfs/attrib.c +++ b/fs/ntfs/attrib.c @@ -2352,12 +2352,13 @@ int ntfs_attr_set(struct ntfs_inode *ni, s64 ofs, s64 cnt, const u8 val) int ntfs_attr_set_initialized_size(struct ntfs_inode *ni, loff_t new_size) { struct ntfs_attr_search_ctx *ctx; + struct ntfs_inode *base_ni = ntfs_base_inode(ni); int err = 0; if (!NInoNonResident(ni)) return -EINVAL; - ctx = ntfs_attr_get_search_ctx(ni, NULL); + ctx = ntfs_attr_get_search_ctx(base_ni, NULL); if (!ctx) return -ENOMEM; @@ -2439,6 +2440,7 @@ int ntfs_resident_attr_record_add(struct ntfs_inode *ni, __le32 type, struct mft_record *m; int err, offset; struct ntfs_inode *base_ni; + u32 ic; if (!ni || (!name && name_len)) return -EINVAL; @@ -2468,7 +2470,8 @@ int ntfs_resident_attr_record_add(struct ntfs_inode *ni, __le32 type, * attribute in @ni->mrec, not any extent inode in case if @ni is base * file record. */ - err = ntfs_attr_find(type, name, name_len, CASE_SENSITIVE, val, size, ctx); + ic = type == AT_DATA && name_len ? IGNORE_CASE : CASE_SENSITIVE; + err = ntfs_attr_find(type, name, name_len, ic, val, size, ctx); if (!err) { err = -EEXIST; ntfs_debug("Attribute already present.\n"); @@ -2560,6 +2563,7 @@ static int ntfs_non_resident_attr_record_add(struct ntfs_inode *ni, __le32 type, struct mft_record *m; struct ntfs_inode *base_ni; int err, offset; + u32 ic; if (!ni || dataruns_size <= 0 || (!name && name_len)) return -EINVAL; @@ -2589,7 +2593,8 @@ static int ntfs_non_resident_attr_record_add(struct ntfs_inode *ni, __le32 type, * attribute in @ni->mrec, not any extent inode in case if @ni is base * file record. */ - err = ntfs_attr_find(type, name, name_len, CASE_SENSITIVE, NULL, 0, ctx); + ic = type == AT_DATA && name_len ? IGNORE_CASE : CASE_SENSITIVE; + err = ntfs_attr_find(type, name, name_len, ic, NULL, 0, ctx); if (!err) { err = -EEXIST; pr_err("Attribute 0x%x already present\n", type); @@ -2663,7 +2668,7 @@ static int ntfs_non_resident_attr_record_add(struct ntfs_inode *ni, __le32 type, * update of attribute list. */ ntfs_attr_reinit_search_ctx(ctx); - err = ntfs_attr_lookup(type, name, name_len, CASE_SENSITIVE, + err = ntfs_attr_lookup(type, name, name_len, ic, lowest_vcn, NULL, 0, ctx); if (err) { pr_err("%s: attribute lookup failed\n", __func__); @@ -3132,6 +3137,7 @@ int ntfs_attr_open(struct ntfs_inode *ni, const __le32 type, struct attr_record *a; bool cs; struct ntfs_inode *base_ni; + u32 ic; int err; if (!ni || !ni->vol) @@ -3140,10 +3146,7 @@ int ntfs_attr_open(struct ntfs_inode *ni, const __le32 type, ntfs_debug("Entering for inode %lld, attr 0x%x.\n", ni->mft_no, type); - if (NInoAttr(ni)) - base_ni = ni->ext.base_ntfs_ino; - else - base_ni = ni; + base_ni = ntfs_base_inode(ni); if (name && name != AT_UNNAMED && name != I30) { name = ntfs_ucsndup(name, name_len); @@ -3161,7 +3164,8 @@ int ntfs_attr_open(struct ntfs_inode *ni, const __le32 type, goto err_out; } - err = ntfs_attr_lookup(type, name, name_len, 0, 0, NULL, 0, ctx); + ic = type == AT_DATA && name_len ? IGNORE_CASE : CASE_SENSITIVE; + err = ntfs_attr_lookup(type, name, name_len, ic, 0, NULL, 0, ctx); if (err) goto put_err_out; @@ -3346,7 +3350,8 @@ int ntfs_attr_map_whole_runlist(struct ntfs_inode *ni) not_mapped = 1; err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, - CASE_SENSITIVE, next_vcn, NULL, 0, ctx); + CASE_SENSITIVE, next_vcn, + NULL, 0, ctx); if (err) break; @@ -3431,6 +3436,7 @@ int ntfs_attr_record_move_to(struct ntfs_attr_search_ctx *ctx, struct ntfs_inode struct ntfs_attr_search_ctx *nctx; struct attr_record *a; int err; + u32 ic; struct mft_record *ni_mrec; struct super_block *sb; @@ -3466,9 +3472,11 @@ int ntfs_attr_record_move_to(struct ntfs_attr_search_ctx *ctx, struct ntfs_inode * attribute in @ni->mrec, not any extent inode in case if @ni is base * file record. */ - err = ntfs_attr_find(a->type, (__le16 *)((u8 *)a + le16_to_cpu(a->name_offset)), - a->name_length, CASE_SENSITIVE, NULL, - 0, nctx); + ic = a->type == AT_DATA && a->name_length ? + IGNORE_CASE : CASE_SENSITIVE; + err = ntfs_attr_find(a->type, + (__le16 *)((u8 *)a + le16_to_cpu(a->name_offset)), + a->name_length, ic, NULL, 0, nctx); if (!err) { ntfs_debug("Attribute of such type, with same name already present in this MFT record.\n"); err = -EEXIST; @@ -3800,7 +3808,8 @@ retry: finished_build = false; start_rl = ni->runlist.rl; while (!(err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, - CASE_SENSITIVE, from_vcn, NULL, 0, ctx))) { + CASE_SENSITIVE, from_vcn, NULL, + 0, ctx))) { unsigned int de_cnt = 0; a = ctx->attr; @@ -4011,7 +4020,8 @@ retry: ntfs_attr_reinit_search_ctx(ctx); ntfs_debug("Deallocate marked extents.\n"); while (!(err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, - CASE_SENSITIVE, 0, NULL, 0, ctx))) { + CASE_SENSITIVE, 0, NULL, 0, + ctx))) { if (le64_to_cpu(ctx->attr->data.non_resident.highest_vcn) != NTFS_VCN_DELETE_MARK) continue; @@ -4345,7 +4355,7 @@ static int ntfs_non_resident_attr_shrink(struct ntfs_inode *ni, goto unlock_runlist; } - ctx = ntfs_attr_get_search_ctx(ni, NULL); + ctx = ntfs_attr_get_search_ctx(base_ni, NULL); if (!ctx) { ntfs_error(vol->sb, "%s: Failed to get search context", __func__); err = -ENOMEM; @@ -4404,8 +4414,8 @@ static int ntfs_non_resident_attr_shrink(struct ntfs_inode *ni, return -ENOMEM; } - err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, CASE_SENSITIVE, - 0, NULL, 0, ctx); + err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, + CASE_SENSITIVE, 0, NULL, 0, ctx); if (err) { if (err == -ENOENT) err = -EIO; @@ -4724,8 +4734,8 @@ static int ntfs_non_resident_attr_expand(struct ntfs_inode *ni, const s64 newsiz goto rollback; } - err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, CASE_SENSITIVE, - 0, NULL, 0, ctx); + err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, + CASE_SENSITIVE, 0, NULL, 0, ctx); if (err) { if (err == -ENOENT) err = -EIO; @@ -4837,7 +4847,7 @@ attr_resize_again: } err = ntfs_attr_lookup(attr_ni->type, attr_ni->name, attr_ni->name_len, - 0, 0, NULL, 0, ctx); + CASE_SENSITIVE, 0, NULL, 0, ctx); if (err) { ntfs_error(sb, "ntfs_attr_lookup failed"); goto put_err_out; @@ -5213,6 +5223,7 @@ int ntfs_attr_map_cluster(struct ntfs_inode *ni, s64 vcn_start, s64 *lcn_start, s64 *lcn_count, s64 max_clu_count, bool *balloc, bool update_mp, bool skip_holes) { + struct ntfs_inode *base_ni = ntfs_base_inode(ni); struct ntfs_volume *vol = ni->vol; struct ntfs_attr_search_ctx *ctx; struct runlist_element *rl, *rlc; @@ -5226,10 +5237,7 @@ int ntfs_attr_map_cluster(struct ntfs_inode *ni, s64 vcn_start, s64 *lcn_start, if (err) return err; - if (NInoAttr(ni)) - ctx = ntfs_attr_get_search_ctx(ni->ext.base_ntfs_ino, NULL); - else - ctx = ntfs_attr_get_search_ctx(ni, NULL); + ctx = ntfs_attr_get_search_ctx(base_ni, NULL); if (!ctx) { ntfs_error(vol->sb, "%s: Failed to get search context", __func__); return -ENOMEM; @@ -5379,7 +5387,7 @@ int ntfs_attr_map_cluster(struct ntfs_inode *ni, s64 vcn_start, s64 *lcn_start, } else { VFS_I(ni)->i_blocks += clu_count << (vol->cluster_size_bits - 9); NInoSetRunlistDirty(ni); - mark_mft_record_dirty(ni); + mark_mft_record_dirty(base_ni); } *lcn_start = lcn; @@ -5405,10 +5413,7 @@ int ntfs_attr_rm(struct ntfs_inode *ni) struct ntfs_inode *base_ni; struct super_block *sb = ni->vol->sb; - if (NInoAttr(ni)) - base_ni = ni->ext.base_ntfs_ino; - else - base_ni = ni; + base_ni = ntfs_base_inode(ni); ntfs_debug("Entering for inode 0x%llx, attr 0x%x.\n", (long long) ni->mft_no, ni->type); @@ -5420,7 +5425,7 @@ int ntfs_attr_rm(struct ntfs_inode *ni) err = ntfs_attr_map_whole_runlist(ni); if (err) return err; - ctx = ntfs_attr_get_search_ctx(ni, NULL); + ctx = ntfs_attr_get_search_ctx(base_ni, NULL); if (!ctx) { ntfs_error(sb, "%s: Failed to get search context", __func__); return -ENOMEM; @@ -5462,6 +5467,7 @@ int ntfs_attr_exist(struct ntfs_inode *ni, const __le32 type, __le16 *name, u32 name_len) { struct ntfs_attr_search_ctx *ctx; + u32 ic; int ret; ntfs_debug("Entering\n"); @@ -5473,7 +5479,8 @@ int ntfs_attr_exist(struct ntfs_inode *ni, const __le32 type, __le16 *name, return 0; } - ret = ntfs_attr_lookup(type, name, name_len, CASE_SENSITIVE, + ic = type == AT_DATA && name_len ? IGNORE_CASE : CASE_SENSITIVE; + ret = ntfs_attr_lookup(type, name, name_len, ic, 0, NULL, 0, ctx); ntfs_attr_put_search_ctx(ctx); @@ -5495,16 +5502,18 @@ int ntfs_attr_remove(struct ntfs_inode *ni, const __le32 type, __le16 *name, attr_vi = ntfs_attr_iget(VFS_I(ni), type, name, name_len); if (IS_ERR(attr_vi)) { err = PTR_ERR(attr_vi); - ntfs_error(ni->vol->sb, "Failed to open attribute 0x%02x of inode 0x%llx", - type, (unsigned long long)ni->mft_no); + ntfs_error(ni->vol->sb, + "Failed to open attribute 0x%02x of inode 0x%llx", + type, (unsigned long long)ni->mft_no); return err; } attr_ni = NTFS_I(attr_vi); err = ntfs_attr_rm(attr_ni); if (err) - ntfs_error(ni->vol->sb, "Failed to remove attribute 0x%02x of inode 0x%llx", - type, (unsigned long long)ni->mft_no); + ntfs_error(ni->vol->sb, + "Failed to remove attribute 0x%02x of inode 0x%llx", + type, (unsigned long long)ni->mft_no); iput(attr_vi); return err; } @@ -5580,13 +5589,14 @@ err_exit: int ntfs_non_resident_attr_insert_range(struct ntfs_inode *ni, s64 start_vcn, s64 len) { + struct ntfs_inode *base_ni = ntfs_base_inode(ni); struct ntfs_volume *vol = ni->vol; struct runlist_element *hole_rl, *rl; struct ntfs_attr_search_ctx *ctx; int ret; size_t new_rl_count; - if (NInoAttr(ni) || ni->type != AT_DATA) + if (ni->type != AT_DATA) return -EOPNOTSUPP; if (start_vcn > ntfs_bytes_to_cluster(vol, ni->allocated_size)) return -EINVAL; @@ -5635,14 +5645,14 @@ int ntfs_non_resident_attr_insert_range(struct ntfs_inode *ni, s64 start_vcn, s6 if (ret) return ret; - ctx = ntfs_attr_get_search_ctx(ni, NULL); + ctx = ntfs_attr_get_search_ctx(base_ni, NULL); if (!ctx) { ret = -ENOMEM; return ret; } - ret = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, CASE_SENSITIVE, - 0, NULL, 0, ctx); + ret = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, + CASE_SENSITIVE, 0, NULL, 0, ctx); if (ret) { ntfs_attr_put_search_ctx(ctx); return ret; @@ -5659,6 +5669,7 @@ int ntfs_non_resident_attr_insert_range(struct ntfs_inode *ni, s64 start_vcn, s6 int ntfs_non_resident_attr_collapse_range(struct ntfs_inode *ni, s64 start_vcn, s64 len) { + struct ntfs_inode *base_ni = ntfs_base_inode(ni); struct ntfs_volume *vol = ni->vol; struct runlist_element *punch_rl, *rl; struct ntfs_attr_search_ctx *ctx = NULL; @@ -5667,7 +5678,7 @@ int ntfs_non_resident_attr_collapse_range(struct ntfs_inode *ni, s64 start_vcn, int ret; size_t new_rl_cnt; - if (NInoAttr(ni) || ni->type != AT_DATA) + if (ni->type != AT_DATA) return -EOPNOTSUPP; end_vcn = ntfs_bytes_to_cluster(vol, ni->allocated_size); @@ -5723,14 +5734,14 @@ int ntfs_non_resident_attr_collapse_range(struct ntfs_inode *ni, s64 start_vcn, } up_write(&ni->runlist.lock); - ctx = ntfs_attr_get_search_ctx(ni, NULL); + ctx = ntfs_attr_get_search_ctx(base_ni, NULL); if (!ctx) { ret = -ENOMEM; goto out_rl; } - ret = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, CASE_SENSITIVE, - 0, NULL, 0, ctx); + ret = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, + CASE_SENSITIVE, 0, NULL, 0, ctx); if (ret) goto out_ctx; @@ -5748,12 +5759,13 @@ out_ctx: ntfs_attr_put_search_ctx(ctx); out_rl: kvfree(punch_rl); - mark_mft_record_dirty(ni); + mark_mft_record_dirty(base_ni); return ret; } int ntfs_non_resident_attr_punch_hole(struct ntfs_inode *ni, s64 start_vcn, s64 len) { + struct ntfs_inode *base_ni = ntfs_base_inode(ni); struct ntfs_volume *vol = ni->vol; struct runlist_element *punch_rl, *rl; s64 end_vcn; @@ -5761,7 +5773,7 @@ int ntfs_non_resident_attr_punch_hole(struct ntfs_inode *ni, s64 start_vcn, s64 int ret; size_t new_rl_count; - if (NInoAttr(ni) || ni->type != AT_DATA) + if (ni->type != AT_DATA) return -EOPNOTSUPP; end_vcn = ntfs_bytes_to_cluster(vol, ni->allocated_size); @@ -5805,12 +5817,13 @@ int ntfs_non_resident_attr_punch_hole(struct ntfs_inode *ni, s64 start_vcn, s64 ntfs_error(vol->sb, "Freeing of clusters failed"); kvfree(punch_rl); - mark_mft_record_dirty(ni); + mark_mft_record_dirty(base_ni); return ret; } int ntfs_attr_fallocate(struct ntfs_inode *ni, loff_t start, loff_t byte_len, bool keep_size) { + struct ntfs_inode *base_ni = ntfs_base_inode(ni); struct ntfs_volume *vol = ni->vol; struct mft_record *mrec; struct ntfs_attr_search_ctx *ctx; @@ -5822,7 +5835,7 @@ int ntfs_attr_fallocate(struct ntfs_inode *ni, loff_t start, loff_t byte_len, bo struct runlist_element *rl; bool balloc; - if (NInoAttr(ni) || ni->type != AT_DATA) + if (ni->type != AT_DATA) return -EINVAL; if (NInoNonResident(ni) && !NInoFullyMapped(ni)) { @@ -5833,20 +5846,21 @@ int ntfs_attr_fallocate(struct ntfs_inode *ni, loff_t start, loff_t byte_len, bo return err; } - mutex_lock_nested(&ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL); - mrec = map_mft_record(ni); + mutex_lock_nested(&base_ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL); + mrec = map_mft_record(base_ni); if (IS_ERR(mrec)) { - mutex_unlock(&ni->mrec_lock); + mutex_unlock(&base_ni->mrec_lock); return PTR_ERR(mrec); } - ctx = ntfs_attr_get_search_ctx(ni, mrec); + ctx = ntfs_attr_get_search_ctx(base_ni, mrec); if (!ctx) { err = -ENOMEM; goto out_unmap; } - err = ntfs_attr_lookup(AT_DATA, AT_UNNAMED, 0, 0, 0, NULL, 0, ctx); + err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, + CASE_SENSITIVE, 0, NULL, 0, ctx); if (err) { err = -EIO; goto out_unmap; @@ -5859,25 +5873,28 @@ int ntfs_attr_fallocate(struct ntfs_inode *ni, loff_t start, loff_t byte_len, bo goto out_unmap; if (keep_size) { ntfs_attr_reinit_search_ctx(ctx); - err = ntfs_attr_lookup(AT_DATA, AT_UNNAMED, 0, 0, 0, NULL, 0, ctx); + err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, + CASE_SENSITIVE, 0, NULL, 0, + ctx); if (err) { err = -EIO; goto out_unmap; } ni->data_size = old_data_size; + i_size_write(VFS_I(ni), old_data_size); if (NInoNonResident(ni)) ctx->attr->data.non_resident.data_size = cpu_to_le64(old_data_size); else ctx->attr->data.resident.value_length = cpu_to_le32((u32)old_data_size); - mark_mft_record_dirty(ni); + mark_mft_record_dirty(base_ni); } } ntfs_attr_put_search_ctx(ctx); - unmap_mft_record(ni); - mutex_unlock(&ni->mrec_lock); + unmap_mft_record(base_ni); + mutex_unlock(&base_ni->mrec_lock); if (!NInoNonResident(ni)) goto out; @@ -5919,12 +5936,13 @@ int ntfs_attr_fallocate(struct ntfs_inode *ni, loff_t start, loff_t byte_len, bo } while (try_alloc_cnt > 0) { - mutex_lock_nested(&ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL); + mutex_lock_nested(&base_ni->mrec_lock, + NTFS_INODE_MUTEX_NORMAL); down_write(&ni->runlist.lock); err = ntfs_attr_map_cluster(ni, vcn, &lcn, &alloc_cnt, try_alloc_cnt, &balloc, false, false); up_write(&ni->runlist.lock); - mutex_unlock(&ni->mrec_lock); + mutex_unlock(&base_ni->mrec_lock); if (err) goto out; @@ -5952,12 +5970,12 @@ int ntfs_attr_fallocate(struct ntfs_inode *ni, loff_t start, loff_t byte_len, bo /* allocate clusters outside of initialized_size */ try_alloc_cnt = vcn_end - vcn; while (try_alloc_cnt > 0) { - mutex_lock_nested(&ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL); + mutex_lock_nested(&base_ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL); down_write(&ni->runlist.lock); err = ntfs_attr_map_cluster(ni, vcn, &lcn, &alloc_cnt, try_alloc_cnt, &balloc, false, false); up_write(&ni->runlist.lock); - mutex_unlock(&ni->mrec_lock); + mutex_unlock(&base_ni->mrec_lock); if (err || fatal_signal_pending(current)) goto signal_out; @@ -5967,7 +5985,7 @@ int ntfs_attr_fallocate(struct ntfs_inode *ni, loff_t start, loff_t byte_len, bo } if (NInoRunlistDirty(ni)) { - mutex_lock_nested(&ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL); + mutex_lock_nested(&base_ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL); down_write(&ni->runlist.lock); err = ntfs_attr_update_mapping_pairs_locked(ni, 0, ni); if (err) @@ -5975,14 +5993,14 @@ int ntfs_attr_fallocate(struct ntfs_inode *ni, loff_t start, loff_t byte_len, bo else NInoClearRunlistDirty(ni); up_write(&ni->runlist.lock); - mutex_unlock(&ni->mrec_lock); + mutex_unlock(&base_ni->mrec_lock); } return err; out_unmap: if (ctx) ntfs_attr_put_search_ctx(ctx); - unmap_mft_record(ni); - mutex_unlock(&ni->mrec_lock); + unmap_mft_record(base_ni); + mutex_unlock(&base_ni->mrec_lock); out: return err >= 0 ? 0 : err; signal_out: diff --git a/fs/ntfs/attrlist.c b/fs/ntfs/attrlist.c index 6edd5d2d9bf2..8291c4c95b37 100644 --- a/fs/ntfs/attrlist.c +++ b/fs/ntfs/attrlist.c @@ -374,7 +374,8 @@ int ntfs_attrlist_entry_add(struct ntfs_inode *ni, struct attr_record *attr) err = ntfs_attr_lookup(attr->type, (attr->name_length) ? (__le16 *) ((u8 *)attr + le16_to_cpu(attr->name_offset)) : - AT_UNNAMED, attr->name_length, CASE_SENSITIVE, + AT_UNNAMED, attr->name_length, + CASE_SENSITIVE, le64_to_cpu(lowest_vcn), (attr->non_resident) ? NULL : ((u8 *)attr + le16_to_cpu(attr->data.resident.value_offset)), (attr->non_resident) ? diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c index 1e2500a52148..7818d88b2133 100644 --- a/fs/ntfs/file.c +++ b/fs/ntfs/file.c @@ -15,6 +15,11 @@ #include #include #include +#include +#include +#include +#include +#include #include "lcnalloc.h" #include "ntfs.h" @@ -23,8 +28,8 @@ #include "iomap.h" #include "bitmap.h" #include "volume.h" - -#include +#include "mft.h" +#include "stream.h" /* * ntfs_file_open - called when an inode is about to be opened @@ -44,7 +49,7 @@ * * After the check passes, just call generic_file_open() to do its work. */ -static int ntfs_file_open(struct inode *vi, struct file *filp) +int ntfs_file_open(struct inode *vi, struct file *filp) { struct ntfs_inode *ni = NTFS_I(vi); @@ -78,6 +83,7 @@ static int ntfs_file_open(struct inode *vi, struct file *filp) static int ntfs_trim_prealloc(struct inode *vi) { struct ntfs_inode *ni = NTFS_I(vi); + struct ntfs_inode *mrec_ni = ntfs_base_inode(ni); struct ntfs_volume *vol = ni->vol; struct runlist_element *rl; s64 aligned_data_size; @@ -86,7 +92,7 @@ static int ntfs_trim_prealloc(struct inode *vi) int err = 0; inode_lock(vi); - mutex_lock(&ni->mrec_lock); + mutex_lock(&mrec_ni->mrec_lock); down_write(&ni->runlist.lock); aligned_data_size = round_up(ni->data_size, vol->cluster_size); @@ -121,13 +127,13 @@ static int ntfs_trim_prealloc(struct inode *vi) out_unlock: up_write(&ni->runlist.lock); - mutex_unlock(&ni->mrec_lock); + mutex_unlock(&mrec_ni->mrec_lock); inode_unlock(vi); return err; } -static int ntfs_file_release(struct inode *vi, struct file *filp) +int ntfs_file_release(struct inode *vi, struct file *filp) { if (!NInoCompressed(NTFS_I(vi)) && !NInoWofCompressed(NTFS_I(vi))) @@ -156,7 +162,7 @@ static int ntfs_file_release(struct inode *vi, struct file *filp) * Also, if @datasync is true, we do not wait on the inode to be written out * but we always wait on the page cache pages to be written out. */ -static int ntfs_file_fsync(struct file *filp, loff_t start, loff_t end, +int ntfs_file_fsync(struct file *filp, loff_t start, loff_t end, int datasync) { struct inode *vi = filp->f_mapping->host; @@ -165,6 +171,7 @@ static int ntfs_file_fsync(struct file *filp, loff_t start, loff_t end, int err, ret = 0; struct inode *parent_vi, *ia_vi; struct ntfs_attr_search_ctx *ctx; + bool non_resident, stream; ntfs_debug("Entering for inode 0x%llx.", ni->mft_no); @@ -175,10 +182,25 @@ static int ntfs_file_fsync(struct file *filp, loff_t start, loff_t end, if (err) return err; - if (!datasync || !NInoNonResident(NTFS_I(vi))) + stream = ntfs_inode_is_named_stream(ni); + non_resident = NInoNonResident(ni); + if (stream) { + ni = ni->ext.base_ntfs_ino; + vi = VFS_I(ni); + } + + if (!datasync || !non_resident) ret = __ntfs_write_inode(vi, 1); write_inode_now(vi, !datasync); + /* + * file_write_and_wait_range() already flushed this stream mapping. + * Do not walk sibling attribute mappings while holding the base MFT + * lock; ordinary file fsync retains its existing behavior below. + */ + if (stream) + goto sync_volume; + ctx = ntfs_attr_get_search_ctx(ni, NULL); if (!ctx) return -ENOMEM; @@ -227,6 +249,7 @@ static int ntfs_file_fsync(struct file *filp, loff_t start, loff_t end, mutex_unlock(&ni->mrec_lock); ntfs_attr_put_search_ctx(ctx); +sync_volume: write_inode_now(vol->mftbmp_ino, 1); down_write(&vol->lcnbmp_lock); write_inode_now(vol->lcnbmp_ino, 1); @@ -268,12 +291,18 @@ static void ntfs_pagecache_extend(struct inode *vi, loff_t from, loff_t to) PAGE_SIZE, 0); } -static int ntfs_setattr_size(struct inode *vi, struct iattr *attr) +int ntfs_setattr_size(struct inode *vi, struct iattr *attr) { struct ntfs_inode *ni = NTFS_I(vi); + struct ntfs_inode *base_ni = ntfs_base_inode(ni); + struct inode *time_vi = vi; + bool stream = ntfs_inode_is_named_stream(ni); int err; loff_t old_size = vi->i_size; + if (stream && NVolShutdown(ni->vol)) + return -EIO; + if (NInoCompressed(ni) || NInoEncrypted(ni) || NInoWofCompressed(ni)) { ntfs_warning( vi->i_sb, @@ -293,7 +322,22 @@ static int ntfs_setattr_size(struct inode *vi, struct iattr *attr) * readers cannot observe the size change until the attribute * updates below have completed. */ - filemap_invalidate_lock(vi->i_mapping); + if (stream) { + filemap_invalidate_lock(vi->i_mapping); + err = filemap_write_and_wait(vi->i_mapping); + if (err) + goto out_unlock_mapping; + + mutex_lock(&base_ni->mrec_lock); + err = ntfs_stream_inode_validate(vi); + mutex_unlock(&base_ni->mrec_lock); + if (err) + goto out_unlock_mapping; + time_vi = VFS_I(base_ni); + } else { + filemap_invalidate_lock(vi->i_mapping); + } + if (attr->ia_size > old_size) { truncate_pagecache(vi, old_size); i_size_write(vi, attr->ia_size); @@ -302,9 +346,28 @@ static int ntfs_setattr_size(struct inode *vi, struct iattr *attr) truncate_setsize(vi, attr->ia_size); } - err = ntfs_truncate_vfs(vi, attr->ia_size, old_size); - if (err) + if (stream) { + mutex_lock(&base_ni->mrec_lock); + err = ntfs_stream_inode_validate(vi); + if (!err) + err = __ntfs_attr_truncate_vfs(ni, attr->ia_size, + old_size); + mutex_unlock(&base_ni->mrec_lock); + } else { + err = ntfs_truncate_vfs(vi, attr->ia_size, old_size); + } + if (err) { i_size_write(vi, old_size); + goto out_unlock_mapping; + } + + if (stream) { + inode_set_mtime_to_ts(time_vi, + inode_set_ctime_current(time_vi)); + mark_inode_dirty(time_vi); + } + +out_unlock_mapping: filemap_invalidate_unlock(vi->i_mapping); return err; @@ -437,7 +500,7 @@ int ntfs_getattr(struct mnt_idmap *idmap, const struct path *path, return 0; } -static loff_t ntfs_file_llseek(struct file *file, loff_t offset, int whence) +loff_t ntfs_file_llseek(struct file *file, loff_t offset, int whence) { struct inode *inode = file->f_mapping->host; @@ -466,7 +529,7 @@ static loff_t ntfs_file_llseek(struct file *file, loff_t offset, int whence) return vfs_setpos(file, offset, inode->i_sb->s_maxbytes); } -static ssize_t ntfs_file_read_iter(struct kiocb *iocb, struct iov_iter *to) +ssize_t ntfs_file_read_iter(struct kiocb *iocb, struct iov_iter *to) { struct inode *vi = file_inode(iocb->ki_filp); struct super_block *sb = vi->i_sb; @@ -513,7 +576,11 @@ static int ntfs_file_write_dio_end_io(struct kiocb *iocb, ssize_t size, if (size) { if (i_size_read(inode) < iocb->ki_pos + size) { i_size_write(inode, iocb->ki_pos + size); - mark_inode_dirty(inode); + if (ntfs_inode_is_named_stream(NTFS_I(inode))) + mark_inode_dirty(VFS_I( + NTFS_I(inode)->ext.base_ntfs_ino)); + else + mark_inode_dirty(inode); } } @@ -583,6 +650,7 @@ out: static int ntfs_expand_for_write(struct ntfs_inode *ni, loff_t end) { struct ntfs_volume *vol = ni->vol; + struct ntfs_inode *mrec_ni = ntfs_base_inode(ni); loff_t prealloc_size = 0; int err; @@ -598,14 +666,14 @@ static int ntfs_expand_for_write(struct ntfs_inode *ni, loff_t end) prealloc_size = ni->allocated_size + vol->preallocated_size; } - mutex_lock(&ni->mrec_lock); + mutex_lock(&mrec_ni->mrec_lock); err = ntfs_attr_expand(ni, end, prealloc_size); - mutex_unlock(&ni->mrec_lock); + mutex_unlock(&mrec_ni->mrec_lock); return err; } -static ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from) +ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from) { struct file *file = iocb->ki_filp; struct inode *vi = file->f_mapping->host; @@ -692,13 +760,21 @@ static ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from) out: if (ret < 0 && ret != -EIOCBQUEUED) { if (ni->initialized_size != old_init_size) { - mutex_lock(&ni->mrec_lock); + struct ntfs_inode *mrec_ni = + ntfs_base_inode(ni); + + mutex_lock(&mrec_ni->mrec_lock); ntfs_attr_set_initialized_size(ni, old_init_size); - mutex_unlock(&ni->mrec_lock); + mutex_unlock(&mrec_ni->mrec_lock); } if (ni->data_size != old_data_size) { + struct ntfs_inode *mrec_ni = + ntfs_base_inode(ni); + truncate_setsize(vi, old_data_size); + mutex_lock(&mrec_ni->mrec_lock); ntfs_attr_truncate(ni, old_data_size); + mutex_unlock(&mrec_ni->mrec_lock); } } out_lock: @@ -727,7 +803,6 @@ static vm_fault_t ntfs_filemap_page_mkwrite(struct vm_fault *vmf) 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; } @@ -738,7 +813,7 @@ static const struct vm_operations_struct ntfs_file_vm_ops = { .page_mkwrite = ntfs_filemap_page_mkwrite, }; -static int ntfs_file_mmap_prepare(struct vm_area_desc *desc) +int ntfs_file_mmap_prepare(struct vm_area_desc *desc) { struct file *file = desc->file; struct inode *inode = file_inode(file); @@ -771,7 +846,7 @@ static int ntfs_file_mmap_prepare(struct vm_area_desc *desc) return 0; } -static int ntfs_fiemap(struct inode *inode, struct fiemap_extent_info *fieinfo, +int ntfs_fiemap(struct inode *inode, struct fiemap_extent_info *fieinfo, u64 start, u64 len) { if (NInoWofCompressed(NTFS_I(inode))) @@ -808,7 +883,7 @@ static const char *ntfs_get_link(struct dentry *dentry, struct inode *inode, return ni->target; } -static ssize_t ntfs_file_splice_read(struct file *in, loff_t *ppos, +ssize_t ntfs_file_splice_read(struct file *in, loff_t *ppos, struct pipe_inode_info *pipe, size_t len, unsigned int flags) { if (NVolShutdown(NTFS_SB(in->f_mapping->host->i_sb))) @@ -924,6 +999,14 @@ long ntfs_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) return ntfs_ioctl_set_volume_label(filp, arg); case FITRIM: return ntfs_ioctl_fitrim(NTFS_SB(file_inode(filp)->i_sb), arg); + case NTFS_IOC_STREAM_READ: + return ntfs_ioctl_stream_read(filp, arg); + case NTFS_IOC_STREAM_WRITE: + return ntfs_ioctl_stream_write(filp, arg); + case NTFS_IOC_STREAM_REMOVE: + return ntfs_ioctl_stream_remove(filp, arg); + case NTFS_IOC_LIST_STREAMS: + return ntfs_ioctl_list_streams(filp, arg); default: return -ENOTTY; } @@ -980,6 +1063,7 @@ out: static int ntfs_punch_hole(struct ntfs_inode *ni, int mode, loff_t offset, loff_t len) { + struct ntfs_inode *mrec_ni = ntfs_base_inode(ni); struct ntfs_volume *vol = ni->vol; struct inode *vi = VFS_I(ni); loff_t end_offset; @@ -1044,16 +1128,17 @@ static int ntfs_punch_hole(struct ntfs_inode *ni, int mode, loff_t offset, end_vcn--; } - mutex_lock_nested(&ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL); + mutex_lock_nested(&mrec_ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL); err = ntfs_non_resident_attr_punch_hole(ni, start_vcn, end_vcn - start_vcn); - mutex_unlock(&ni->mrec_lock); + mutex_unlock(&mrec_ni->mrec_lock); out: return err; } static int ntfs_collapse_range(struct ntfs_inode *ni, loff_t offset, loff_t len) { + struct ntfs_inode *mrec_ni = ntfs_base_inode(ni); struct ntfs_volume *vol = ni->vol; struct inode *vi = VFS_I(ni); loff_t old_size, new_size; @@ -1087,10 +1172,10 @@ static int ntfs_collapse_range(struct ntfs_inode *ni, loff_t offset, loff_t len) truncate_pagecache(vi, offset_down); - mutex_lock_nested(&ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL); + mutex_lock_nested(&mrec_ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL); err = ntfs_non_resident_attr_collapse_range(ni, start_vcn, end_vcn - start_vcn); - mutex_unlock(&ni->mrec_lock); + mutex_unlock(&mrec_ni->mrec_lock); if (new_size != old_size) i_size_write(vi, ni->data_size); @@ -1100,6 +1185,7 @@ out: static int ntfs_insert_range(struct ntfs_inode *ni, loff_t offset, loff_t len) { + struct ntfs_inode *mrec_ni = ntfs_base_inode(ni); struct ntfs_volume *vol = ni->vol; struct inode *vi = VFS_I(ni); loff_t offset_down = round_down(offset, @@ -1143,10 +1229,10 @@ static int ntfs_insert_range(struct ntfs_inode *ni, loff_t offset, loff_t len) truncate_pagecache(vi, offset_down); - mutex_lock_nested(&ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL); + mutex_lock_nested(&mrec_ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL); err = ntfs_non_resident_attr_insert_range(ni, start_vcn, end_vcn - start_vcn); - mutex_unlock(&ni->mrec_lock); + mutex_unlock(&mrec_ni->mrec_lock); if (new_size != old_size) i_size_write(vi, ni->data_size); @@ -1159,11 +1245,13 @@ out: FALLOC_FL_INSERT_RANGE | FALLOC_FL_PUNCH_HOLE | \ FALLOC_FL_COLLAPSE_RANGE) -static long ntfs_fallocate(struct file *file, int mode, loff_t offset, loff_t len) +long ntfs_fallocate(struct file *file, int mode, loff_t offset, loff_t len) { struct inode *vi = file_inode(file); struct ntfs_inode *ni = NTFS_I(vi); + struct ntfs_inode *base_ni = ntfs_base_inode(ni); struct ntfs_volume *vol = ni->vol; + bool stream = ntfs_inode_is_named_stream(ni); int err = 0; loff_t old_size, new_size; @@ -1233,9 +1321,15 @@ out: filemap_invalidate_unlock(vi->i_mapping); if (!err) { - NInoSetFileNameDirty(ni); - inode_set_mtime_to_ts(vi, inode_set_ctime_current(vi)); - mark_inode_dirty(vi); + if (stream) { + inode_set_mtime_to_ts(VFS_I(base_ni), + inode_set_ctime_current(VFS_I(base_ni))); + mark_inode_dirty(VFS_I(base_ni)); + } else { + NInoSetFileNameDirty(ni); + inode_set_mtime_to_ts(vi, inode_set_ctime_current(vi)); + mark_inode_dirty(vi); + } } inode_unlock(vi); diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c index 6cfe8f24e4cc..eb79f0cd3323 100644 --- a/fs/ntfs/inode.c +++ b/fs/ntfs/inode.c @@ -20,6 +20,7 @@ #include "attrib.h" #include "iomap.h" #include "object_id.h" +#include "stream.h" /* * ntfs_test_inode - compare two (possibly fake) inodes for equality @@ -56,9 +57,18 @@ int ntfs_test_inode(struct inode *vi, void *data) return 0; if (ni->name_len != na->name_len) return 0; - if (na->name_len && memcmp(ni->name, na->name, - na->name_len * sizeof(__le16))) - return 0; + if (na->name_len) { + if (ntfs_inode_is_named_stream(ni)) { + if (!ntfs_names_are_equal(ni->name, ni->name_len, + na->name, na->name_len, + IGNORE_CASE, ni->vol->upcase, + ni->vol->upcase_len)) + return 0; + } else if (memcmp(ni->name, na->name, + na->name_len * sizeof(__le16))) { + return 0; + } + } if (!ni->ext.base_ntfs_ino) return 0; } @@ -67,6 +77,33 @@ int ntfs_test_inode(struct inode *vi, void *data) return 1; } +/* + * ntfs_test_inode_rcu() - Test an inode during RCU hash lookup + * @vi: inode being tested + * @data: NTFS attribute key to match + * + * Reject inodes being initialized or destroyed, then run the normal NTFS + * inode match while holding @vi's inode lock. + * + * Return: 1 if the inode matches, or 0 otherwise. + */ +int ntfs_test_inode_rcu(struct inode *vi, void *data) +{ + unsigned long state; + int ret; + + spin_lock(&vi->i_lock); + state = inode_state_read_once(vi); + if (state & (I_FREEING | I_WILL_FREE | I_NEW)) { + ret = 0; + goto out; + } + ret = ntfs_test_inode(vi, data); +out: + spin_unlock(&vi->i_lock); + return ret; +} + /* * ntfs_init_locked_inode - initialize an inode * @vi: vfs inode to initialize @@ -468,6 +505,7 @@ void __ntfs_init_inode(struct super_block *sb, struct ntfs_inode *ni) ni->seq_no = 0; atomic_set(&ni->count, 1); ni->vol = NTFS_SB(sb); + atomic_set(&ni->stream_open_count, 0); ntfs_init_runlist(&ni->runlist); mutex_init(&ni->mrec_lock); if (ni->type == AT_ATTRIBUTE_LIST) { @@ -1320,6 +1358,7 @@ static int ntfs_read_locked_attr_inode(struct inode *base_vi, struct inode *vi) struct attr_record *a; struct ntfs_attr_search_ctx *ctx; int err = 0; + u32 ic; ntfs_debug("Entering for i_ino 0x%llx.", ni->mft_no); @@ -1334,8 +1373,12 @@ static int ntfs_read_locked_attr_inode(struct inode *base_vi, struct inode *vi) inode_set_atime_to_ts(vi, inode_get_atime(base_vi)); vi->i_generation = ni->seq_no = base_ni->seq_no; - /* Set inode type to zero but preserve permissions. */ - vi->i_mode = base_vi->i_mode & ~S_IFMT; + /* Named data streams are regular files throughout initialization. */ + vi->i_mode = base_vi->i_mode & ~S_IFMT; + if (ni->type == AT_DATA && ni->name_len) { + vi->i_mode |= S_IFREG; + vi->i_flags = base_vi->i_flags; + } m = map_mft_record(base_ni); if (IS_ERR(m)) { @@ -1348,11 +1391,29 @@ static int ntfs_read_locked_attr_inode(struct inode *base_vi, struct inode *vi) goto unm_err_out; } /* Find the attribute. */ + ic = ntfs_inode_is_named_stream(ni) ? IGNORE_CASE : CASE_SENSITIVE; err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, - CASE_SENSITIVE, 0, NULL, 0, ctx); + ic, 0, NULL, 0, ctx); if (unlikely(err)) goto unm_err_out; a = ctx->attr; + if (ntfs_inode_is_named_stream(ni)) { + __le16 *disk_name = (__le16 *)((u8 *)a + + le16_to_cpu(a->name_offset)); + + /* Subsequent extent and mutation lookups must select this name. */ + if (ni->name == I30) { + ni->name = kmalloc_array(ni->name_len + 1, + sizeof(__le16), GFP_NOFS); + if (!ni->name) { + err = -ENOMEM; + goto unm_err_out; + } + } + memcpy(ni->name, disk_name, + ni->name_len * sizeof(__le16)); + ni->name[ni->name_len] = 0; + } if (a->flags & (ATTR_COMPRESSION_MASK | ATTR_IS_SPARSE)) { if (a->flags & ATTR_COMPRESSION_MASK) { NInoSetCompressed(ni); @@ -2485,6 +2546,7 @@ int ntfs_extend_initialized_size(struct inode *vi, const loff_t offset, const loff_t new_size) { struct ntfs_inode *ni = NTFS_I(vi); + struct ntfs_inode *mrec_ni = ntfs_base_inode(ni); loff_t old_init_size, clear_size; unsigned long flags; int err = 0; @@ -2522,9 +2584,9 @@ int ntfs_extend_initialized_size(struct inode *vi, const loff_t offset, } } - mutex_lock(&ni->mrec_lock); + mutex_lock(&mrec_ni->mrec_lock); err = ntfs_attr_set_initialized_size(ni, new_size); - mutex_unlock(&ni->mrec_lock); + mutex_unlock(&mrec_ni->mrec_lock); if (err) truncate_setsize(vi, old_init_size); out: @@ -3615,19 +3677,30 @@ s64 ntfs_inode_attr_pread(struct inode *vi, s64 pos, s64 count, u8 *buf) struct address_space *mapping = vi->i_mapping; struct folio *folio; struct ntfs_inode *ni = NTFS_I(vi); + struct ntfs_inode *base_ni = ni->ext.base_ntfs_ino; + struct ntfs_inode *mrec_ni = ntfs_inode_is_named_stream(ni) ? + base_ni : ni; s64 isize; u32 attr_len, total = 0, offset; pgoff_t index; int err = 0; + bool claimed = false; WARN_ON(!NInoAttr(ni)); if (!count) return 0; - mutex_lock(&ni->mrec_lock); + mutex_lock(&mrec_ni->mrec_lock); + if (ntfs_inode_is_named_stream(ni)) { + err = ntfs_stream_inode_validate(vi); + if (err) { + mutex_unlock(&mrec_ni->mrec_lock); + return err; + } + } isize = i_size_read(vi); if (pos > isize) { - mutex_unlock(&ni->mrec_lock); + mutex_unlock(&mrec_ni->mrec_lock); return -EINVAL; } if (pos + count > isize) @@ -3637,37 +3710,44 @@ s64 ntfs_inode_attr_pread(struct inode *vi, s64 pos, s64 count, u8 *buf) struct ntfs_attr_search_ctx *ctx; u8 *attr; - ctx = ntfs_attr_get_search_ctx(ni->ext.base_ntfs_ino, NULL); + ctx = ntfs_attr_get_search_ctx(base_ni, NULL); if (!ctx) { ntfs_error(vi->i_sb, "Failed to get attr search ctx"); err = -ENOMEM; - mutex_unlock(&ni->mrec_lock); + mutex_unlock(&mrec_ni->mrec_lock); goto out; } - err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, CASE_SENSITIVE, + err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, + CASE_SENSITIVE, 0, NULL, 0, ctx); if (err) { ntfs_error(vi->i_sb, "Failed to look up attr %#x", ni->type); ntfs_attr_put_search_ctx(ctx); - mutex_unlock(&ni->mrec_lock); + mutex_unlock(&mrec_ni->mrec_lock); goto out; } attr = (u8 *)ctx->attr + le16_to_cpu(ctx->attr->data.resident.value_offset); memcpy(buf, (u8 *)attr + pos, count); ntfs_attr_put_search_ctx(ctx); - mutex_unlock(&ni->mrec_lock); + mutex_unlock(&mrec_ni->mrec_lock); return count; } - mutex_unlock(&ni->mrec_lock); + if (ntfs_inode_is_named_stream(ni)) { + atomic_inc(&ni->stream_open_count); + claimed = true; + } + mutex_unlock(&mrec_ni->mrec_lock); index = pos >> PAGE_SHIFT; do { /* Update @index and get the next folio. */ folio = read_mapping_folio(mapping, index, NULL); - if (IS_ERR(folio)) + if (IS_ERR(folio)) { + err = PTR_ERR(folio); break; + } offset = offset_in_folio(folio, pos); attr_len = min_t(size_t, (size_t)count, folio_size(folio) - offset); @@ -3684,6 +3764,8 @@ s64 ntfs_inode_attr_pread(struct inode *vi, s64 pos, s64 count, u8 *buf) index++; } while (count); out: + if (claimed) + ntfs_stream_put(vi); return err ? (s64)err : total; } @@ -3711,8 +3793,8 @@ static inline int ntfs_enlarge_attribute(struct inode *vi, s64 pos, s64 count, } ntfs_attr_reinit_search_ctx(ctx); - ret = ntfs_attr_lookup(ni->type, - ni->name, ni->name_len, CASE_SENSITIVE, + ret = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, + CASE_SENSITIVE, 0, NULL, 0, ctx); if (ret) { ntfs_error(sb, "Failed to look up attr %#x", ni->type); @@ -3726,6 +3808,13 @@ static inline int ntfs_enlarge_attribute(struct inode *vi, s64 pos, s64 count, return 0; } + /* Named streams initialize gaps after dropping the MFT record lock. */ + if (ntfs_inode_is_named_stream(ni)) { + if (i_size_read(vi) < ni->data_size) + i_size_write(vi, ni->data_size); + return 0; + } + if (pos + count > ni->initialized_size) { ctx->attr->data.non_resident.initialized_size = cpu_to_le64(pos + count); mark_mft_record_dirty(ctx->ntfs_ino); @@ -3738,7 +3827,8 @@ static inline int ntfs_enlarge_attribute(struct inode *vi, s64 pos, s64 count, static s64 __ntfs_inode_resident_attr_pwrite(struct inode *vi, s64 pos, s64 count, u8 *buf, - struct ntfs_attr_search_ctx *ctx) + struct ntfs_attr_search_ctx *ctx, + bool *modified) { struct ntfs_inode *ni = NTFS_I(vi); struct folio *folio; @@ -3756,6 +3846,8 @@ static s64 __ntfs_inode_resident_attr_pwrite(struct inode *vi, addr = (u8 *)ctx->attr + le16_to_cpu(ctx->attr->data.resident.value_offset); memcpy(addr + pos, buf, count); mark_mft_record_dirty(ctx->ntfs_ino); + if (count) + *modified = true; /* Keep the first page clean and uptodate */ folio = __filemap_get_folio(mapping, 0, FGP_WRITEBEGIN | FGP_NOFS, @@ -3780,8 +3872,7 @@ out: static s64 __ntfs_inode_non_resident_attr_pwrite(struct inode *vi, s64 pos, s64 count, u8 *buf, - struct ntfs_attr_search_ctx *ctx, - bool sync) + bool sync, bool *modified) { struct ntfs_inode *ni = NTFS_I(vi); struct address_space *mapping = vi->i_mapping; @@ -3825,6 +3916,7 @@ static s64 __ntfs_inode_non_resident_attr_pwrite(struct inode *vi, attr_len = min_t(size_t, (size_t)count, folio_size(folio) - offset); } memcpy_to_folio(folio, offset, buf, attr_len); + *modified = true; if (sync) { struct ntfs_volume *vol = ni->vol; @@ -3919,37 +4011,88 @@ err_unlock_folio: s64 ntfs_inode_attr_pwrite(struct inode *vi, s64 pos, s64 count, u8 *buf, bool sync) { struct ntfs_inode *ni = NTFS_I(vi); + struct ntfs_inode *base_ni = ni->ext.base_ntfs_ino; + struct ntfs_inode *mrec_ni = ntfs_inode_is_named_stream(ni) ? + base_ni : ni; struct ntfs_attr_search_ctx *ctx; + loff_t old_data_size, old_init_size; s64 ret; + bool modified = false; WARN_ON(!NInoAttr(ni)); - ctx = ntfs_attr_get_search_ctx(ni->ext.base_ntfs_ino, NULL); + if (ntfs_inode_is_named_stream(ni)) { + if (NInoEncrypted(ni)) + return -EACCES; + if (NInoCompressed(ni)) + return -EOPNOTSUPP; + } + + mutex_lock(&mrec_ni->mrec_lock); + if (ntfs_inode_is_named_stream(ni)) { + ret = ntfs_stream_inode_validate(vi); + if (ret) + goto out_unlock; + } + + ctx = ntfs_attr_get_search_ctx(base_ni, NULL); if (!ctx) { ntfs_error(vi->i_sb, "Failed to get attr search ctx"); - return -ENOMEM; + ret = -ENOMEM; + goto out_unlock; } - ret = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, CASE_SENSITIVE, + ret = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, + CASE_SENSITIVE, 0, NULL, 0, ctx); if (ret) { ntfs_attr_put_search_ctx(ctx); ntfs_error(vi->i_sb, "Failed to look up attr %#x", ni->type); - return ret; + goto out_unlock; } - mutex_lock(&ni->mrec_lock); + old_data_size = ni->data_size; + old_init_size = ni->initialized_size; ret = ntfs_enlarge_attribute(vi, pos, count, ctx); - mutex_unlock(&ni->mrec_lock); + modified = ni->data_size != old_data_size || + ni->initialized_size != old_init_size; if (ret) - goto out; + goto out_ctx; - if (NInoNonResident(ni)) - ret = __ntfs_inode_non_resident_attr_pwrite(vi, pos, count, buf, ctx, sync); - else - ret = __ntfs_inode_resident_attr_pwrite(vi, pos, count, buf, ctx); -out: + if (!NInoNonResident(ni)) { + ret = __ntfs_inode_resident_attr_pwrite(vi, pos, count, buf, ctx, + &modified); + goto out_ctx; + } + + if (ntfs_inode_is_named_stream(ni)) + atomic_inc(&ni->stream_open_count); + ntfs_attr_put_search_ctx(ctx); + mutex_unlock(&mrec_ni->mrec_lock); + /* Attribute writes bypass iomap's delayed-allocation preparation. */ + if (ntfs_inode_is_named_stream(ni)) { + /* A failed write must not expose any newly initialized data. */ + ret = ntfs_extend_initialized_size(vi, pos + count, pos + count); + modified |= ni->initialized_size != old_init_size; + if (!ret) + ret = ntfs_attr_fallocate(ni, pos, count, true); + } + if (!ret) + ret = __ntfs_inode_non_resident_attr_pwrite(vi, pos, count, + buf, sync, + &modified); + if (ntfs_inode_is_named_stream(ni)) + ntfs_stream_put(vi); + if (modified && ntfs_inode_is_named_stream(ni)) + ntfs_stream_update_base_time(base_ni); + return ret; + +out_ctx: ntfs_attr_put_search_ctx(ctx); +out_unlock: + mutex_unlock(&mrec_ni->mrec_lock); + if (modified && ntfs_inode_is_named_stream(ni)) + ntfs_stream_update_base_time(base_ni); return ret; } diff --git a/fs/ntfs/inode.h b/fs/ntfs/inode.h index ff61bd402df0..f4e5562d56a7 100644 --- a/fs/ntfs/inode.h +++ b/fs/ntfs/inode.h @@ -107,6 +107,7 @@ struct ntfs_inode { __le32 type; __le16 *name; u32 name_len; + atomic_t stream_open_count; struct runlist runlist; s64 data_size; s64 initialized_size; @@ -178,6 +179,7 @@ struct ntfs_inode { * NI_BeingCreated ntfs inode is being created. * NI_HasEA ntfs inode has EA attribute. * NI_RunlistDirty runlist need to be updated. + * NI_StreamUnlinked Named stream is unlinked but still open. */ enum { NI_Dirty, @@ -199,6 +201,7 @@ enum { NI_BeingCreated, NI_HasEA, NI_RunlistDirty, + NI_StreamUnlinked, }; /* @@ -259,6 +262,7 @@ TAS_NINO_FNS(FileNameDirty) NINO_FNS(BeingDeleted) NINO_FNS(HasEA) NINO_FNS(RunlistDirty) +NINO_FNS(StreamUnlinked) /* * The full structure containing a ntfs_inode and a vfs struct inode. Used for @@ -286,6 +290,20 @@ static inline struct inode *VFS_I(struct ntfs_inode *ni) return &container_of(ni, struct big_ntfs_inode, ntfs_inode)->vfs_inode; } +/* Return true when @ni represents a named $DATA attribute inode. */ +static inline bool ntfs_inode_is_named_stream(struct ntfs_inode *ni) +{ + return NInoAttr(ni) && ni->type == AT_DATA && ni->name_len; +} + +/* Return the base MFT inode for an attribute inode, or @ni itself. */ +static inline struct ntfs_inode *ntfs_base_inode(struct ntfs_inode *ni) +{ + if (NInoAttr(ni) && ni->nr_extents == -1 && ni->ext.base_ntfs_ino) + return ni->ext.base_ntfs_ino; + return ni; +} + /* * ntfs_attr - ntfs in memory attribute structure * @@ -304,6 +322,7 @@ struct ntfs_attr { }; int ntfs_test_inode(struct inode *vi, void *data); +int ntfs_test_inode_rcu(struct inode *vi, void *data); struct inode *ntfs_iget(struct super_block *sb, u64 mft_no); struct inode *ntfs_attr_iget(struct inode *base_vi, __le32 type, __le16 *name, u32 name_len); diff --git a/fs/ntfs/named_stream.c b/fs/ntfs/named_stream.c new file mode 100644 index 000000000000..5c76cae66ed2 --- /dev/null +++ b/fs/ntfs/named_stream.c @@ -0,0 +1,916 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * NTFS named stream handling. + * + * Copyright (c) 2026 LG Electronics Co., Ltd. + */ + +#include +#include + +#include + +#include "attrib.h" +#include "dir.h" +#include "iomap.h" +#include "mft.h" +#include "ntfs.h" +#include "stream.h" +#include "time.h" + +#define NTFS_STREAM_MAX_IO (16 * 1024 * 1024) + +/* + * ntfs_check_stream_name() - Validate a named-stream name + * @name: UTF-16LE stream name + * @name_len: Length of @name in UTF-16 code units + * + * Reject empty, overlong, separator-containing, or malformed UTF-16 names. + * + * Return: 0 if valid, or -EINVAL otherwise. + */ +int ntfs_check_stream_name(const __le16 *name, unsigned int name_len) +{ + unsigned int i; + + if (!name_len || name_len > NTFS_MAX_NAME_LEN) + return -EINVAL; + + for (i = 0; i < name_len; i++) { + u16 c = le16_to_cpu(name[i]); + + if (c == 0 || c == '/' || c == '\\' || c == ':') + return -EINVAL; + if (c >= 0xd800 && c <= 0xdbff) { + if (++i >= name_len) + return -EINVAL; + c = le16_to_cpu(name[i]); + if (c < 0xdc00 || c > 0xdfff) + return -EINVAL; + } else if (c >= 0xdc00 && c <= 0xdfff) { + return -EINVAL; + } + } + + return 0; +} + +/* + * ntfs_stream_inode_refresh() - Refresh base-derived stream inode metadata + * @vi: inode representing a named stream + * + * Synchronize ownership, mode, flags, timestamps, and generation with the + * base inode. Stream size and allocation remain specific to the stream. + */ +void ntfs_stream_inode_refresh(struct inode *vi) +{ + struct ntfs_inode *ni = NTFS_I(vi); + struct inode *base_vi; + + if (!ntfs_inode_is_named_stream(ni) || ni->nr_extents != -1) + return; + + base_vi = VFS_I(ni->ext.base_ntfs_ino); + vi->i_uid = base_vi->i_uid; + vi->i_gid = base_vi->i_gid; + vi->i_mode = (base_vi->i_mode & ~S_IFMT) | S_IFREG; + vi->i_flags = base_vi->i_flags; + inode_set_mtime_to_ts(vi, inode_get_mtime(base_vi)); + inode_set_ctime_to_ts(vi, inode_get_ctime(base_vi)); + inode_set_atime_to_ts(vi, inode_get_atime(base_vi)); + vi->i_generation = base_vi->i_generation; +} + +/* + * ntfs_stream_update_base_time() - Update base timestamps after a stream change + * @base_ni: base inode containing the stream attribute + * + * Update the base inode's mtime and ctime and mark it dirty. The helper takes + * the base inode lock. + */ +void ntfs_stream_update_base_time(struct ntfs_inode *base_ni) +{ + struct inode *base_vi = VFS_I(base_ni); + + inode_lock_nested(base_vi, I_MUTEX_PARENT); + inode_set_mtime_to_ts(base_vi, inode_set_ctime_current(base_vi)); + mark_inode_dirty(base_vi); + inode_unlock(base_vi); +} + +/* + * ntfs_stream_inode_validate() - Validate a named-stream inode + * @vi: stream inode to validate + * + * Verify that @vi still maps to its named DATA attribute and refresh its + * base-derived metadata. The caller must hold the base inode's MFT-record + * lock. + * + * Return: 0 if valid, or a negative errno. + */ +int ntfs_stream_inode_validate(struct inode *vi) +{ + struct ntfs_inode *ni = NTFS_I(vi); + struct ntfs_inode *base_ni; + struct ntfs_attr_search_ctx *ctx; + int err; + + if (!ntfs_inode_is_named_stream(ni) || ni->nr_extents != -1) + return -EINVAL; + + base_ni = ni->ext.base_ntfs_ino; + lockdep_assert_held(&base_ni->mrec_lock); + if (NVolShutdown(base_ni->vol)) + return -EIO; + if (!NInoStreamUnlinked(ni) && + (!vi->i_nlink || !VFS_I(base_ni)->i_nlink || + NInoBeingDeleted(base_ni))) + return -ESTALE; + + ctx = ntfs_attr_get_search_ctx(base_ni, NULL); + if (!ctx) + return -ENOMEM; + err = ntfs_attr_lookup(AT_DATA, ni->name, ni->name_len, + CASE_SENSITIVE, 0, NULL, 0, ctx); + ntfs_attr_put_search_ctx(ctx); + if (err) + return err; + + ntfs_stream_inode_refresh(vi); + return 0; +} + +/* + * ntfs_stream_unlinked() - Check a cached stream's deferred-unlink state + * @base_ni: base inode containing the stream + * @name: UTF-16LE stream name + * @name_len: Length of @name in UTF-16 code units + * + * The caller must hold the base inode's MFT-record lock. + * + * Return: true if the cached stream inode is unlinked, or false if it is not + * cached or is still linked. + */ +bool ntfs_stream_unlinked(struct ntfs_inode *base_ni, + const __le16 *name, u32 name_len) +{ + struct ntfs_attr na = { + .mft_no = base_ni->mft_no, + .type = AT_DATA, + .name = (__le16 *)name, + .name_len = name_len, + }; + struct inode *vi; + bool unlinked; + + lockdep_assert_held(&base_ni->mrec_lock); + rcu_read_lock(); + vi = find_inode_rcu(base_ni->vol->sb, na.mft_no, + ntfs_test_inode_rcu, &na); + if (!vi) { + rcu_read_unlock(); + return false; + } + unlinked = NInoStreamUnlinked(NTFS_I(vi)); + rcu_read_unlock(); + return unlinked; +} + +/* + * Hold a temporary stream reference so unlink cannot remove its attribute + * while an operation is using it. + */ +static int ntfs_stream_claim(struct inode *inode) +{ + struct ntfs_inode *ni = NTFS_I(inode); + struct ntfs_inode *base_ni = ni->ext.base_ntfs_ino; + int err; + + mutex_lock(&base_ni->mrec_lock); + if (NInoStreamUnlinked(ni)) + err = -ESTALE; + else + err = ntfs_stream_inode_validate(inode); + if (!err) { + if (atomic_read(&ni->stream_open_count) < 0) + err = -ESTALE; + else + atomic_inc(&ni->stream_open_count); + } + mutex_unlock(&base_ni->mrec_lock); + return err; +} + +/* + * Remove an unlinked stream's DATA attribute after its final active reference + * is released. Base-inode deletion handles the attribute when the base is + * already being removed. + */ +static int ntfs_stream_finish_remove(struct inode *attr_vi) +{ + struct ntfs_inode *attr_ni = NTFS_I(attr_vi); + struct ntfs_inode *ni = attr_ni->ext.base_ntfs_ino; + int err; + + mutex_lock(&ni->mrec_lock); + if (!NInoStreamUnlinked(attr_ni) || + atomic_cmpxchg(&attr_ni->stream_open_count, 0, -1)) { + err = 0; + goto out_unlock; + } + if (NInoBeingDeleted(ni) || !VFS_I(ni)->i_nlink) { + remove_inode_hash(attr_vi); + err = 0; + goto out_unlock; + } + mutex_unlock(&ni->mrec_lock); + + truncate_inode_pages(attr_vi->i_mapping, 0); + + mutex_lock(&ni->mrec_lock); + if (NVolShutdown(ni->vol)) { + err = -EIO; + goto out_unlock; + } + if (NInoBeingDeleted(ni) || !VFS_I(ni)->i_nlink) { + remove_inode_hash(attr_vi); + err = 0; + goto out_unlock; + } + err = ntfs_attr_rm(attr_ni); + if (!err) { + NInoClearDirty(attr_ni); + remove_inode_hash(attr_vi); + } else { + NInoSetBeingDeleted(attr_ni); + remove_inode_hash(attr_vi); + } + +out_unlock: + mutex_unlock(&ni->mrec_lock); + return err; +} + +/* + * ntfs_stream_put() - Release a stream operation reference + * @vi: stream inode whose reference is being released + * + * Finish a deferred unlink when this is the last reference to an unlinked + * stream. + * + * Return: 0 on success, or a negative errno if deferred removal fails. + */ +int ntfs_stream_put(struct inode *vi) +{ + struct ntfs_inode *ni = NTFS_I(vi); + + if (atomic_dec_and_test(&ni->stream_open_count) && + NInoStreamUnlinked(ni)) + return ntfs_stream_finish_remove(vi); + return 0; +} + +/* + * ntfs_remove_named_stream() - Remove a named DATA stream + * @ni: base inode containing the stream + * @uname: UTF-16LE stream name + * @uname_len: length of @uname in UTF-16 code units + * @expected_vi: expected cached stream inode, or NULL + * + * Refuse removal while the stream is open. If @expected_vi is non-NULL, it + * must be the inode currently associated with the named attribute. + * + * Return: 0 on success, -ENOENT if the stream is absent, or another negative + * errno. + */ +int ntfs_remove_named_stream(struct ntfs_inode *ni, __le16 *uname, + u32 uname_len, struct inode *expected_vi) +{ + struct inode *attr_vi; + struct ntfs_inode *attr_ni; + int err; + + if (!ni || !uname || uname_len == 0) + return -EINVAL; + if (NVolShutdown(ni->vol)) + return -EIO; + if ((ni->flags & FILE_ATTR_READONLY) || + IS_APPEND(VFS_I(ni)) || IS_IMMUTABLE(VFS_I(ni))) + return -EPERM; + if (!(ni->vol->vol_flags & VOLUME_IS_DIRTY)) { + err = ntfs_set_volume_flags(ni->vol, VOLUME_IS_DIRTY); + if (err) + return err; + } + + mutex_lock(&ni->mrec_lock); + if (NInoBeingDeleted(ni) || !VFS_I(ni)->i_nlink) { + err = -ENOENT; + goto out_unlock; + } + attr_vi = ntfs_attr_iget(VFS_I(ni), AT_DATA, uname, uname_len); + if (IS_ERR(attr_vi)) { + err = PTR_ERR(attr_vi); + goto out_unlock; + } + if (expected_vi && attr_vi != expected_vi) { + err = -ESTALE; + goto out_iput; + } + + attr_ni = NTFS_I(attr_vi); + if (NInoStreamUnlinked(attr_ni)) { + err = -ENOENT; + goto out_iput; + } + if (atomic_read(&attr_ni->stream_open_count) > 0) { + err = -EBUSY; + goto out_iput; + } + err = ntfs_stream_inode_validate(attr_vi); + if (err) + goto out_iput; + + NInoSetStreamUnlinked(attr_ni); + clear_nlink(attr_vi); + mutex_unlock(&ni->mrec_lock); + + ntfs_stream_update_base_time(ni); + err = ntfs_stream_finish_remove(attr_vi); + iput(attr_vi); + return err; + +out_iput: + mutex_unlock(&ni->mrec_lock); + iput(attr_vi); + return err; +out_unlock: + mutex_unlock(&ni->mrec_lock); + return err; +} + +enum ntfs_stream_ioctl_op { + NTFS_STREAM_IOCTL_READ, + NTFS_STREAM_IOCTL_WRITE, + NTFS_STREAM_IOCTL_REMOVE, +}; + +/* + * Look up or create an ioctl target stream and return its referenced inode. + */ +static int ntfs_stream_ioctl_get_inode(struct inode *base_vi, __le16 *sname, + int sname_len, bool create, struct inode **stream_vi) +{ + struct ntfs_inode *base_ni = NTFS_I(base_vi); + struct ntfs_attr_search_ctx *ctx; + struct inode *attr_vi = NULL; + bool created = false; + int err; + + *stream_vi = NULL; + + mutex_lock(&base_ni->mrec_lock); + if (NVolShutdown(base_ni->vol)) { + err = -EIO; + goto out_unlock; + } + if (NInoBeingDeleted(base_ni) || !base_vi->i_nlink) { + err = -ENOENT; + goto out_unlock; + } + + ctx = ntfs_attr_get_search_ctx(base_ni, NULL); + if (!ctx) { + err = -ENOMEM; + goto out_unlock; + } + + err = ntfs_attr_lookup(AT_DATA, sname, sname_len, IGNORE_CASE, + 0, NULL, 0, ctx); + if (err == -ENOENT && create) { + err = ntfs_attr_add(base_ni, AT_DATA, sname, sname_len, + NULL, 0); + if (!err) { + created = true; + mark_mft_record_dirty(base_ni); + } + } + ntfs_attr_put_search_ctx(ctx); + if (err) + goto out_unlock; + + attr_vi = ntfs_attr_iget(base_vi, AT_DATA, sname, sname_len); + if (IS_ERR(attr_vi)) { + err = PTR_ERR(attr_vi); + attr_vi = NULL; + goto out_unlock; + } + if (NInoStreamUnlinked(NTFS_I(attr_vi))) + err = create ? -EBUSY : -ENOENT; + else + err = ntfs_stream_inode_validate(attr_vi); +out_unlock: + mutex_unlock(&base_ni->mrec_lock); + if (created) + ntfs_stream_update_base_time(base_ni); + if (err) { + iput(attr_vi); + return err; + } + + *stream_vi = attr_vi; + return 0; +} + +/* Check file mode, base-inode flags, and the requested transfer range. */ +static int ntfs_stream_ioctl_access(struct file *filp, + enum ntfs_stream_ioctl_op op, loff_t pos, size_t len) +{ + struct inode *inode = file_inode(filp); + bool is_dir = S_ISDIR(inode->i_mode); + int err; + + if (op == NTFS_STREAM_IOCTL_READ) { + if (!(filp->f_mode & FMODE_READ)) + return -EBADF; + return rw_verify_area(READ, filp, &pos, len); + } + + if (!(filp->f_mode & FMODE_WRITE) && !is_dir) + return -EBADF; + if (is_dir) { + err = inode_permission(file_mnt_idmap(filp), inode, + MAY_WRITE | MAY_EXEC); + if (err) + return err; + } + if (IS_APPEND(inode) || IS_IMMUTABLE(inode)) + return -EPERM; + if (op == NTFS_STREAM_IOCTL_REMOVE) + return 0; + return rw_verify_area(WRITE, filp, &pos, len); +} + +/* + * Validate and execute a named-stream read, write, or remove ioctl request. + */ +static long ntfs_ioctl_stream(struct file *filp, unsigned long arg, + enum ntfs_stream_ioctl_op op) +{ + struct inode *base_vi = file_inode(filp); + struct ntfs_inode *base_ni = NTFS_I(base_vi); + struct ntfs_stream hdr; + struct ntfs_stream *req; + struct inode *stream_vi = NULL; + __le16 *uname = NULL, *sname; + size_t data_size, total_size; + loff_t pos; + s64 ret; + int sname_len, err; + bool claimed = false, got_write = false, utf16; + + if (NVolShutdown(base_ni->vol)) + return -EIO; + if (NInoAttr(base_ni) || + (!S_ISREG(base_vi->i_mode) && !S_ISDIR(base_vi->i_mode))) + return -EOPNOTSUPP; + if (copy_from_user(&hdr, (void __user *)arg, sizeof(hdr))) + return -EFAULT; + + if (hdr.io_len > NTFS_STREAM_MAX_IO || + (hdr.flags & ~NTFS_STREAM_FL_UTF16) || hdr.reserved) + return -EINVAL; + if (!hdr.name_len) + return -EINVAL; + + utf16 = hdr.flags & NTFS_STREAM_FL_UTF16; + if (utf16) { + if ((hdr.name_len & 1) || + hdr.name_len > NTFS_MAX_NAME_LEN * sizeof(__le16)) + return -EINVAL; + } else if (hdr.name_len > + NTFS_MAX_NAME_LEN * NLS_MAX_CHARSET_SIZE) { + return -EINVAL; + } + + switch (op) { + case NTFS_STREAM_IOCTL_READ: + case NTFS_STREAM_IOCTL_WRITE: + if (!hdr.io_len) + return -EINVAL; + if (hdr.stream_offset > S64_MAX || + hdr.stream_offset > S64_MAX - hdr.io_len) + return -EOVERFLOW; + data_size = hdr.io_len; + break; + case NTFS_STREAM_IOCTL_REMOVE: + if (hdr.io_len || hdr.stream_offset) + return -EINVAL; + data_size = 0; + break; + default: + return -ENOTTY; + } + + if (check_add_overflow(sizeof(hdr), (size_t)hdr.name_len, + &total_size) || + check_add_overflow(total_size, data_size, &total_size)) + return -EOVERFLOW; + + if (op == NTFS_STREAM_IOCTL_READ) { + req = kvmalloc(total_size, GFP_KERNEL); + if (!req) + return -ENOMEM; + if (copy_from_user(req, (void __user *)arg, + sizeof(hdr) + hdr.name_len)) { + kvfree(req); + return -EFAULT; + } + } else { + req = vmemdup_user((void __user *)arg, total_size); + if (IS_ERR(req)) + return PTR_ERR(req); + } + + req->bytes_returned = 0; + if (req->stream_offset != hdr.stream_offset || + req->io_len != hdr.io_len || req->name_len != hdr.name_len || + req->flags != hdr.flags || req->reserved) { + err = -EINVAL; + goto out_free; + } + + if (utf16) { + sname = (__le16 *)req->buffer; + sname_len = req->name_len / sizeof(__le16); + } else { + if (memchr(req->buffer, '\0', req->name_len)) { + err = -EINVAL; + goto out_free; + } + sname_len = ntfs_nlstoucs(base_ni->vol, req->buffer, + req->name_len, &uname, NTFS_MAX_NAME_LEN); + if (sname_len < 0) { + err = sname_len; + goto out_free; + } + sname = uname; + } + err = ntfs_check_stream_name(sname, sname_len); + if (err) + goto out_free; + + pos = hdr.stream_offset; + err = ntfs_stream_ioctl_access(filp, op, pos, data_size); + if (err) + goto out_free; + + if (op == NTFS_STREAM_IOCTL_REMOVE) { + err = mnt_want_write_file(filp); + if (err) + goto out_free; + err = ntfs_remove_named_stream(base_ni, sname, sname_len, NULL); + mnt_drop_write_file(filp); + goto out_free; + } + + if (op == NTFS_STREAM_IOCTL_WRITE) { + err = mnt_want_write_file(filp); + if (err) + goto out_free; + got_write = true; + inode_lock(base_vi); + err = file_remove_privs(filp); + inode_unlock(base_vi); + if (err) + goto out_drop_write; + if (!(base_ni->vol->vol_flags & VOLUME_IS_DIRTY)) { + err = ntfs_set_volume_flags(base_ni->vol, + VOLUME_IS_DIRTY); + if (err) + goto out_drop_write; + } + } + + err = ntfs_stream_ioctl_get_inode(base_vi, sname, sname_len, + op == NTFS_STREAM_IOCTL_WRITE, &stream_vi); + if (err) + goto out_drop_write; + + err = ntfs_stream_claim(stream_vi); + if (err) + goto out_drop_write; + claimed = true; + + if (op == NTFS_STREAM_IOCTL_READ) { + inode_lock_shared(stream_vi); + if (pos < i_size_read(stream_vi)) + ret = ntfs_inode_attr_pread(stream_vi, pos, data_size, + req->buffer + req->name_len); + else + ret = 0; + inode_unlock_shared(stream_vi); + if (ret < 0) + err = ret; + else + req->bytes_returned = ret; + if (!err) + file_accessed(filp); + } else { + inode_lock(stream_vi); + err = inode_newsize_ok(stream_vi, pos + data_size); + if (!err) { + ret = ntfs_inode_attr_pwrite(stream_vi, pos, data_size, + req->buffer + req->name_len, false); + if (ret < 0) + err = ret; + else + req->bytes_returned = ret; + } + inode_unlock(stream_vi); + } + if (claimed) { + int put_err; + + put_err = ntfs_stream_put(stream_vi); + claimed = false; + if (!err && put_err) + err = put_err; + } + iput(stream_vi); + stream_vi = NULL; + if (err) + goto out_drop_write; + + if (op == NTFS_STREAM_IOCTL_READ) { + if (copy_to_user((void __user *)arg, req, + sizeof(*req) + req->name_len + + req->bytes_returned)) + err = -EFAULT; + } else if (copy_to_user((void __user *)arg + + offsetof(struct ntfs_stream, bytes_returned), + &req->bytes_returned, sizeof(req->bytes_returned))) { + err = -EFAULT; + } + goto out_drop_write; + +out_drop_write: + if (claimed) + ntfs_stream_put(stream_vi); + if (stream_vi) + iput(stream_vi); + if (got_write) + mnt_drop_write_file(filp); +out_free: + if (uname) + kmem_cache_free(ntfs_name_cache, uname); + kvfree(req); + return err; +} + +/* + * ntfs_ioctl_stream_read() - Handle a named-stream read ioctl + * @filp: file opened on the base file or directory + * @arg: userspace pointer to the request and data buffer + * + * Return: 0 on success, or a negative errno. + */ +long ntfs_ioctl_stream_read(struct file *filp, unsigned long arg) +{ + return ntfs_ioctl_stream(filp, arg, NTFS_STREAM_IOCTL_READ); +} + +/* + * ntfs_ioctl_stream_write() - Handle a named-stream write ioctl + * @filp: file opened on the base file or directory + * @arg: userspace pointer to the request and data buffer + * + * Create the stream if needed. + * + * Return: 0 on success, or a negative errno. + */ +long ntfs_ioctl_stream_write(struct file *filp, unsigned long arg) +{ + return ntfs_ioctl_stream(filp, arg, NTFS_STREAM_IOCTL_WRITE); +} + +/* + * ntfs_ioctl_stream_remove() - Handle a named-stream remove ioctl + * @filp: file opened on the base file or directory + * @arg: userspace pointer to the request + * + * Return: 0 on success, or a negative errno. + */ +long ntfs_ioctl_stream_remove(struct file *filp, unsigned long arg) +{ + return ntfs_ioctl_stream(filp, arg, NTFS_STREAM_IOCTL_REMOVE); +} + +/* + * ntfs_ioctl_list_streams() - List named DATA streams + * @filp: file opened on the base file or directory + * @arg: userspace pointer to the request and output buffer + * + * On -ENOSPC, return the required buffer size in the request header. + * + * Return: 0 on success, or a negative errno. + */ +int ntfs_ioctl_list_streams(struct file *filp, unsigned long arg) +{ + struct inode *inode = file_inode(filp); + struct ntfs_inode *ni = NTFS_I(inode); + struct ntfs_list_streams hdr; + struct ntfs_stream_entry *entry, *last_entry = NULL; + size_t required = 0; + void *kbuf = NULL; + void __user *ubuf; + struct attr_record *a; + struct ntfs_attr_search_ctx *actx; + int ret = 0, err, count = 0; + size_t entry_size, offset = 0; + const size_t name_offset = offsetof(struct ntfs_stream_entry, name); + int name_len; + unsigned char *sn = NULL; + bool utf16; + + if (NVolShutdown(ni->vol)) + return -EIO; + if (NInoAttr(ni) || + (!S_ISREG(inode->i_mode) && !S_ISDIR(inode->i_mode))) + return -EOPNOTSUPP; + err = inode_permission(file_mnt_idmap(filp), inode, MAY_READ); + if (err) + return err; + + if (copy_from_user(&hdr, (void __user *)arg, sizeof(hdr))) + return -EFAULT; + + if ((hdr.flags & ~NTFS_STREAM_FL_UTF16) || hdr.reserved) + return -EINVAL; + + utf16 = hdr.flags & NTFS_STREAM_FL_UTF16; + + ubuf = (void __user *)arg + sizeof(hdr); + + mutex_lock(&ni->mrec_lock); + actx = ntfs_attr_get_search_ctx(ni, NULL); + if (!actx) { + mutex_unlock(&ni->mrec_lock); + return -ENOMEM; + } + + while ((err = ntfs_attrs_walk(actx)) == 0) { + a = actx->attr; + if (a->type != AT_DATA || !a->name_length) + continue; + if (a->non_resident && + a->data.non_resident.lowest_vcn) + continue; + if (ntfs_stream_unlinked(ni, + (__le16 *)((u8 *)a + + le16_to_cpu(a->name_offset)), + a->name_length)) + continue; + + if (utf16) { + name_len = a->name_length * sizeof(__le16); + } else { + name_len = ntfs_ucstonls(ni->vol, + (__le16 *)((u8 *)a + + le16_to_cpu(a->name_offset)), + a->name_length, &sn, 0); + if (name_len < 0) { + if (name_len == -EILSEQ || + name_len == -ENAMETOOLONG) + continue; + ret = name_len; + goto out; + } + kfree(sn); + sn = NULL; + } + + entry_size = ALIGN(name_offset + name_len, 8); + + if (required > SIZE_MAX - entry_size) { + ret = -EOVERFLOW; + goto out; + } + required += entry_size; + count++; + } + if (err != -ENOENT) { + ret = err; + goto out; + } + + hdr.stream_count = count; + hdr.bytes_returned = required; + + if (!count) + goto out; + + if (hdr.buffer_size < required) { + ret = -ENOSPC; + goto out; + } + + kbuf = kvzalloc(required, GFP_NOFS); + if (!kbuf) { + ret = -ENOMEM; + goto out; + } + + ntfs_attr_reinit_search_ctx(actx); + while ((err = ntfs_attrs_walk(actx)) == 0) { + a = actx->attr; + if (a->type != AT_DATA || !a->name_length) + continue; + if (a->non_resident && + a->data.non_resident.lowest_vcn) + continue; + if (ntfs_stream_unlinked(ni, + (__le16 *)((u8 *)a + + le16_to_cpu(a->name_offset)), + a->name_length)) + continue; + if (utf16) { + sn = NULL; + name_len = a->name_length * sizeof(__le16); + } else { + name_len = ntfs_ucstonls(ni->vol, + (__le16 *)((u8 *)a + + le16_to_cpu(a->name_offset)), + a->name_length, &sn, 0); + if (name_len < 0) { + if (name_len == -EILSEQ || + name_len == -ENAMETOOLONG) + continue; + ret = name_len; + goto out; + } + } + + entry_size = ALIGN(name_offset + name_len, 8); + if (offset > required - entry_size) { + ret = -EOVERFLOW; + kfree(sn); + sn = NULL; + goto out; + } + + entry = (struct ntfs_stream_entry *)(kbuf + offset); + + if (a->non_resident) { + entry->size = le64_to_cpu(a->data.non_resident.data_size); + entry->alloc_size = + le64_to_cpu(a->data.non_resident.allocated_size); + } else { + entry->size = le32_to_cpu(a->data.resident.value_length); + entry->alloc_size = le32_to_cpu(a->length) - + le16_to_cpu(a->data.resident.value_offset); + } + + entry->name_len = name_len; + entry->name_offset = name_offset; + if (utf16) + memcpy(entry->name, + (u8 *)a + le16_to_cpu(a->name_offset), name_len); + else + memcpy(entry->name, sn, name_len); + kfree(sn); + sn = NULL; + + entry->next_entry_off = entry_size; + last_entry = entry; + offset += entry_size; + } + if (err != -ENOENT) { + ret = err; + } else { + /* The chain terminates at the last entry. */ + if (last_entry) + last_entry->next_entry_off = 0; + hdr.bytes_returned = offset; + } + +out: + kfree(sn); + ntfs_attr_put_search_ctx(actx); + mutex_unlock(&ni->mrec_lock); + + if (ret && ret != -ENOSPC) + goto out_free; + + if (!ret && kbuf && copy_to_user(ubuf, kbuf, hdr.bytes_returned)) { + ret = -EFAULT; + goto out_free; + } + + if (copy_to_user((void __user *)arg, &hdr, sizeof(hdr))) + ret = -EFAULT; + +out_free: + kvfree(kbuf); + return ret; +} diff --git a/fs/ntfs/namei.c b/fs/ntfs/namei.c index 75e201096525..3cf58befd77f 100644 --- a/fs/ntfs/namei.c +++ b/fs/ntfs/namei.c @@ -805,8 +805,29 @@ static int ntfs_test_inode_attr(struct inode *vi, void *data) return 0; if (NInoAttr(ni) || ni->nr_extents == -1) return 1; - else - return 0; + return 0; +} + +static void ntfs_cleanup_deleted_inode(struct ntfs_inode *ni) +{ + struct inode *attr_vi; + struct super_block *sb = VFS_I(ni)->i_sb; + + if (!NInoBeingDeleted(ni)) + return; + + while ((attr_vi = ilookup5(sb, ni->mft_no, ntfs_test_inode_attr, + (void *)(uintptr_t)ni->mft_no))) { + struct ntfs_inode *attr_ni = NTFS_I(attr_vi); + + if (ntfs_inode_is_named_stream(attr_ni)) { + if (atomic_read(&attr_ni->stream_open_count) > 0) + NInoSetStreamUnlinked(attr_ni); + remove_inode_hash(attr_vi); + } + clear_nlink(attr_vi); + iput(attr_vi); + } } /* @@ -976,22 +997,10 @@ search: if (need_lock == true) { mutex_unlock(&dir_ni->mrec_lock); mutex_unlock(&ni->mrec_lock); + if (link_count_zero) + ntfs_cleanup_deleted_inode(ni); } - /* - * If hard link count is not equal to zero then we are done. In other - * case there are no reference to this inode left, so we should free all - * non-resident attributes and mark all MFT record as not in use. - */ - if (link_count_zero == true) { - struct inode *attr_vi; - - while ((attr_vi = ilookup5(sb, ni->mft_no, ntfs_test_inode_attr, - (void *)(uintptr_t)ni->mft_no)) != NULL) { - clear_nlink(attr_vi); - iput(attr_vi); - } - } ntfs_debug("Done.\n"); return 0; err_out: @@ -1385,6 +1394,8 @@ err_out: if (new_ni) mutex_unlock(&new_ni->mrec_lock); mutex_unlock(&old_ni->mrec_lock); + if (new_ni) + ntfs_cleanup_deleted_inode(new_ni); if (uname_new) kmem_cache_free(ntfs_name_cache, uname_new); if (uname_old) diff --git a/fs/ntfs/stream.h b/fs/ntfs/stream.h new file mode 100644 index 000000000000..da0096d2b751 --- /dev/null +++ b/fs/ntfs/stream.h @@ -0,0 +1,43 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#ifndef _NTFS_STREAM_H +#define _NTFS_STREAM_H + +#include + +struct ntfs_inode; +struct ntfs_volume; + +int ntfs_check_stream_name(const __le16 *name, unsigned int name_len); + +void ntfs_stream_inode_refresh(struct inode *inode); +int ntfs_stream_inode_validate(struct inode *inode); +void ntfs_stream_update_base_time(struct ntfs_inode *base_ni); +bool ntfs_stream_unlinked(struct ntfs_inode *base_ni, + const __le16 *name, u32 name_len); +int ntfs_stream_put(struct inode *inode); +int ntfs_remove_named_stream(struct ntfs_inode *ni, __le16 *name, + u32 name_len, struct inode *expected_inode); + +long ntfs_ioctl_stream_read(struct file *file, unsigned long arg); +long ntfs_ioctl_stream_write(struct file *file, unsigned long arg); +long ntfs_ioctl_stream_remove(struct file *file, unsigned long arg); +int ntfs_ioctl_list_streams(struct file *file, unsigned long arg); + +int ntfs_file_open(struct inode *inode, struct file *file); +int ntfs_file_release(struct inode *inode, struct file *file); +int ntfs_file_fsync(struct file *file, loff_t start, loff_t end, + int datasync); +int ntfs_setattr_size(struct inode *inode, struct iattr *attr); +loff_t ntfs_file_llseek(struct file *file, loff_t offset, int whence); +ssize_t ntfs_file_read_iter(struct kiocb *iocb, struct iov_iter *to); +ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from); +int ntfs_file_mmap_prepare(struct vm_area_desc *desc); +ssize_t ntfs_file_splice_read(struct file *in, loff_t *ppos, + struct pipe_inode_info *pipe, size_t len, unsigned int flags); +int ntfs_fiemap(struct inode *inode, struct fiemap_extent_info *fieinfo, + u64 start, u64 len); +long ntfs_fallocate(struct file *file, int mode, loff_t offset, loff_t len); + +int ntfs_test_inode_rcu(struct inode *inode, void *data); + +#endif /* _NTFS_STREAM_H */ diff --git a/include/uapi/linux/ntfs.h b/include/uapi/linux/ntfs.h new file mode 100644 index 000000000000..f5b3d4997a56 --- /dev/null +++ b/include/uapi/linux/ntfs.h @@ -0,0 +1,130 @@ +/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */ +/* + * Copyright (c) 2026 LG Electronics Co., Ltd. + */ + +#ifndef _UAPI_LINUX_NTFS_H +#define _UAPI_LINUX_NTFS_H +#include +#include + +#define NTFS_IOC_MAGIC 0xEF + +/* + * Flags for ntfs_stream.flags and ntfs_list_streams.flags. + * + * NTFS_STREAM_FL_UTF16 makes stream names raw UTF-16LE, exactly as stored on + * disk, instead of encoding them with the mounted filesystem NLS. This + * allows lossless round-tripping of stream names whose characters are not + * representable by the mount NLS (e.g. for Wine, which works in UTF-16 + * natively). When set, the name length fields count bytes of UTF-16LE and + * must therefore be even. + */ +#define NTFS_STREAM_FL_UTF16 0x1 + +/* + * ntfs named stream read, write, and remove ioctl structure. + * + * @stream_offset: Offset within the named stream for read/write. + * @io_len: Number of bytes to read/write. Must be zero for remove. + * @bytes_returned: Actual bytes transferred on success (out). + * @name_len: Stream name length in bytes, not including any + * terminating NUL. When NTFS_STREAM_FL_UTF16 is set, + * this counts UTF-16LE bytes and must be even. + * @flags: Bit mask of NTFS_STREAM_FL_* flags. Other bits must + * be zero. + * @reserved: Must be zero. + * @buffer: Bare stream name followed by stream data for read/write. + * + * The stream name is encoded with the mounted filesystem NLS, or as raw + * UTF-16LE when NTFS_STREAM_FL_UTF16 is set. A write creates the stream if + * it does not already exist. A write failure after creation may leave the + * new stream behind; it can be removed separately. + * + * For NTFS_IOC_STREAM_READ, a successful short read indicates EOF. If a read + * fails, the ioctl returns an error and copies no stream data to userspace. + * + * NTFS_IOC_STREAM_WRITE is not atomic. Write errors may leave partial data + * changes and an extended stream size. On error, bytes_returned is not valid. + * The ioctl does not report the completed byte count. + */ +struct ntfs_stream { + __aligned_u64 stream_offset; + __aligned_u64 io_len; + __aligned_u64 bytes_returned; + __u32 name_len; + __u32 flags; + __aligned_u64 reserved; + __u8 buffer[]; +}; + +/* + * Single stream entry returned by NTFS_IOC_LIST_STREAMS. + * + * @next_entry_off: Byte offset from the start of this entry to the next + * entry, or zero for the last entry. Always a multiple + * of 8. Consumers must use this to advance instead of + * computing the stride themselves. + * @size: Stream data size in bytes. + * @alloc_size: Bytes allocated for the stream (cluster aligned for + * non-resident streams). + * @name_len: Stream name length in bytes, not including a NUL + * terminator (none is stored). Counts UTF-16LE bytes + * when NTFS_STREAM_FL_UTF16 was requested. + * @name_offset: Byte offset from the start of this entry to @name. + * @reserved: Must be zero. + * @name: Bare stream name; NLS encoded, or raw UTF-16LE when + * NTFS_STREAM_FL_UTF16 was requested. + * + * New fixed fields may be added after @reserved and before @name. Consumers + * must use @name_offset to locate the name and @next_entry_off to advance to + * the next entry. + */ +struct ntfs_stream_entry { + __aligned_u64 next_entry_off; + __aligned_u64 size; + __aligned_u64 alloc_size; + __u32 name_len; + __u32 name_offset; + __u32 reserved; + __u8 name[]; +}; + +/* + * ntfs list streams ioctl structure. + * + * @buffer_size: user buffer size(in). + * @bytes_returned: actual bytes written or required(out). + * @stream_count: number of streams(out). + * @flags: Bit mask of NTFS_STREAM_FL_* flags controlling the + * encoding of the returned names; other bits must be zero. + * @reserved: Must be zero. + * @buffer: ntfs_stream_entry array. + * + * The variable-length entries follow the header in @buffer, each aligned on + * an 8-byte boundary and chained via ntfs_stream_entry.next_entry_off. If + * @buffer_size is too small, no data is copied, @stream_count and + * @bytes_returned report the required values and the ioctl fails with + * -ENOSPC. In NLS mode, names that cannot be represented by the mounted + * character set are omitted; use NTFS_STREAM_FL_UTF16 to list all + * names without conversion loss. + */ +struct ntfs_list_streams { + __aligned_u64 buffer_size; + __aligned_u64 bytes_returned; + __aligned_u64 stream_count; + __u32 flags; + __u32 reserved; + __u8 buffer[]; +}; + +#define NTFS_IOC_STREAM_READ \ + _IOWR(NTFS_IOC_MAGIC, 1, struct ntfs_stream) +#define NTFS_IOC_STREAM_WRITE \ + _IOWR(NTFS_IOC_MAGIC, 2, struct ntfs_stream) +#define NTFS_IOC_STREAM_REMOVE \ + _IOWR(NTFS_IOC_MAGIC, 3, struct ntfs_stream) +#define NTFS_IOC_LIST_STREAMS \ + _IOWR(NTFS_IOC_MAGIC, 4, struct ntfs_list_streams) + +#endif /* _UAPI_LINUX_NTFS_H */ -- cgit v1.2.3 From fdbb49ac52a83dc08e41709b6d180a19f5bb818f Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Mon, 21 Sep 2026 11:28:44 +0900 Subject: ntfs: add pathname access for named streams Add an optional Windows-style pathname interface for named $DATA streams, enabled with streams_interface=windows. The option defaults to none, and the named-stream ioctls remain available regardless of this setting. Support lookup, creation, and removal of named streams for existing regular files and directories. A stream is opened as a regular file through the base-name and stream-name form in the final path component. A stream can be created only for an existing file or directory. The pathname interface accepts a base file name and stream name only. It rejects paths that also specify an NTFS attribute type such as $DATA. In windows mode, ordinary filenames containing a colon are hidden from directory listings and cannot be accessed through the pathname interface. Match stream names case-insensitively. Streams and their base objects report the same inode number. Removing the base object while a stream is open detaches the stream from the namespace. Since NTFS has no orphan-stream list, a crash can leave the stream data unreachable on disk. Expose named streams as regular file descriptors for the file operations requested by Wine, including read, write, fsync, fdatasync, sync_file_range, file locking, mmap, futimes, fstat, ftruncate, SEEK_DATA, SEEK_HOLE, fallocate, and pathname unlink. Stream timestamps are shared with the base file. fstat reports the base metadata and inode number with the stream's own size and allocation. Signed-off-by: Namjae Jeon --- fs/ntfs/attrib.c | 40 ++- fs/ntfs/attrib.h | 2 +- fs/ntfs/dir.c | 7 +- fs/ntfs/ea.c | 21 +- fs/ntfs/file.c | 117 +++++-- fs/ntfs/inode.c | 18 +- fs/ntfs/iomap.c | 64 ++-- fs/ntfs/named_stream.c | 907 ++++++++++++++++++++++++++++++++++++++++++++++++- fs/ntfs/namei.c | 132 ++++++- fs/ntfs/stream.h | 38 +++ fs/ntfs/super.c | 33 ++ fs/ntfs/volume.h | 3 + fs/ntfs/wof.c | 6 +- 13 files changed, 1300 insertions(+), 88 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c index 15131e9401e2..fce7144d7bef 100644 --- a/fs/ntfs/attrib.c +++ b/fs/ntfs/attrib.c @@ -5487,34 +5487,46 @@ int ntfs_attr_exist(struct ntfs_inode *ni, const __le32 type, __le16 *name, return !ret; } +/* + * If @attr_vi is non-NULL, the attribute inode reference is returned to the + * caller, which must release it after dropping ni->mrec_lock. + */ int ntfs_attr_remove(struct ntfs_inode *ni, const __le32 type, __le16 *name, - u32 name_len) + u32 name_len, struct inode **attr_vi) { int err; - struct inode *attr_vi; + struct inode *vi; struct ntfs_inode *attr_ni; ntfs_debug("Entering\n"); + if (attr_vi) + *attr_vi = NULL; if (!ni) return -EINVAL; - attr_vi = ntfs_attr_iget(VFS_I(ni), type, name, name_len); - if (IS_ERR(attr_vi)) { - err = PTR_ERR(attr_vi); - ntfs_error(ni->vol->sb, - "Failed to open attribute 0x%02x of inode 0x%llx", - type, (unsigned long long)ni->mft_no); + vi = ntfs_attr_iget(VFS_I(ni), type, name, name_len); + if (IS_ERR(vi)) { + err = PTR_ERR(vi); + ntfs_error(ni->vol->sb, "Failed to open attribute 0x%02x of inode 0x%llx", + type, (unsigned long long)ni->mft_no); return err; } - attr_ni = NTFS_I(attr_vi); + attr_ni = NTFS_I(vi); err = ntfs_attr_rm(attr_ni); - if (err) - ntfs_error(ni->vol->sb, - "Failed to remove attribute 0x%02x of inode 0x%llx", - type, (unsigned long long)ni->mft_no); - iput(attr_vi); + if (err) { + ntfs_error(ni->vol->sb, "Failed to remove attribute 0x%02x of inode 0x%llx", + type, (unsigned long long)ni->mft_no); + } else { + NInoClearDirty(attr_ni); + clear_nlink(vi); + remove_inode_hash(vi); + } + if (attr_vi) + *attr_vi = vi; + else + iput(vi); return err; } diff --git a/fs/ntfs/attrib.h b/fs/ntfs/attrib.h index 6b4fa9f57640..bee91c9f6f81 100644 --- a/fs/ntfs/attrib.h +++ b/fs/ntfs/attrib.h @@ -124,7 +124,7 @@ int ntfs_attr_rm(struct ntfs_inode *ni); int ntfs_attr_exist(struct ntfs_inode *ni, const __le32 type, __le16 *name, u32 name_len); int ntfs_attr_remove(struct ntfs_inode *ni, const __le32 type, __le16 *name, - u32 name_len); + u32 name_len, struct inode **attr_vi); int ntfs_attr_record_rm(struct ntfs_attr_search_ctx *ctx); int ntfs_attr_record_move_to(struct ntfs_attr_search_ctx *ctx, struct ntfs_inode *ni); int ntfs_attr_add(struct ntfs_inode *ni, __le32 type, diff --git a/fs/ntfs/dir.c b/fs/ntfs/dir.c index b95173f068cb..5a0e21e7c283 100644 --- a/fs/ntfs/dir.c +++ b/fs/ntfs/dir.c @@ -8,6 +8,7 @@ */ #include +#include #include "dir.h" #include "mft.h" @@ -624,7 +625,6 @@ static inline int ntfs_filldir(struct ntfs_volume *vol, ntfs_debug("Skipping hidden file."); return 0; } - name_len = ntfs_ucstonls(vol, (__le16 *)&ie->key.file_name.file_name, ie->key.file_name.file_name_length, &name, NTFS_MAX_NAME_LEN * NLS_MAX_CHARSET_SIZE + 1); @@ -633,7 +633,10 @@ static inline int ntfs_filldir(struct ntfs_volume *vol, (long long)MREF_LE(ie->data.dir.indexed_file)); return 0; } - + if (NVolStreamsWindows(vol) && strchr((char *)name, ':')) { + ntfs_debug("Skipping file name containing a stream separator."); + return 0; + } mref = MREF_LE(ie->data.dir.indexed_file); if (ie->key.file_name.file_attributes & FILE_ATTR_REPARSE_POINT) dt_type = ntfs_reparse_tag_dt_types(vol, mref); diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c index ff69fb07f84a..1104fd72cd2a 100644 --- a/fs/ntfs/ea.c +++ b/fs/ntfs/ea.c @@ -246,7 +246,7 @@ create_ea_info: goto out; if (ntfs_attr_exist(ni, AT_EA, AT_UNNAMED, 0)) { - err = ntfs_attr_remove(ni, AT_EA, AT_UNNAMED, 0); + err = ntfs_attr_remove(ni, AT_EA, AT_UNNAMED, 0, NULL); if (err) goto out; } @@ -301,11 +301,12 @@ create_ea_info: 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); + err = ntfs_attr_remove(ni, AT_EA, AT_UNNAMED, 0, NULL); if (err) goto out; - err = ntfs_attr_remove(ni, AT_EA_INFORMATION, AT_UNNAMED, 0); + err = ntfs_attr_remove(ni, AT_EA_INFORMATION, AT_UNNAMED, + 0, NULL); if (err) { /* Restore the original $EA if $EA_INFORMATION removal failed. */ ntfs_attr_add(ni, AT_EA, AT_UNNAMED, 0, old_ea_buf, @@ -610,6 +611,14 @@ static int ntfs_getxattr(const struct xattr_handler *handler, struct ntfs_inode *ni = NTFS_I(inode); int err; + /* + * Stream permissions and privileges belong to the base inode. This + * also lets VFS killpriv helpers find the base security attributes. + */ + if (ntfs_inode_is_named_stream(ni)) { + ni = ni->ext.base_ntfs_ino; + inode = VFS_I(ni); + } if (NVolShutdown(ni->vol)) return -EIO; @@ -716,8 +725,8 @@ static int ntfs_new_attr_flags(struct ntfs_inode *ni, __le32 fattr) goto err_out; ntfs_attr_reinit_search_ctx(ctx); - err = ntfs_attr_lookup(ni->type, ni->name, - ni->name_len, CASE_SENSITIVE, + err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, + CASE_SENSITIVE, 0, NULL, 0, ctx); if (err) { err = -EINVAL; @@ -883,6 +892,8 @@ static int ntfs_setxattr(const struct xattr_handler *handler, int err; __le32 fattr; + if (ntfs_inode_is_named_stream(ni)) + return -EOPNOTSUPP; if (NVolShutdown(ni->vol)) return -EIO; diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c index 7818d88b2133..a2e843cf947c 100644 --- a/fs/ntfs/file.c +++ b/fs/ntfs/file.c @@ -17,8 +17,10 @@ #include #include #include +#include #include #include +#include #include #include "lcnalloc.h" @@ -142,6 +144,11 @@ int ntfs_file_release(struct inode *vi, struct file *filp) return 0; } +struct ntfs_fsync_attr { + struct list_head list; + struct inode *inode; +}; + /* * ntfs_file_fsync - sync a file to disk * @filp: file to be synced @@ -171,6 +178,8 @@ int ntfs_file_fsync(struct file *filp, loff_t start, loff_t end, int err, ret = 0; struct inode *parent_vi, *ia_vi; struct ntfs_attr_search_ctx *ctx; + struct ntfs_fsync_attr *attr, *next; + LIST_HEAD(attrs); bool non_resident, stream; ntfs_debug("Entering for inode 0x%llx.", ni->mft_no); @@ -195,8 +204,7 @@ int ntfs_file_fsync(struct file *filp, loff_t start, loff_t end, /* * file_write_and_wait_range() already flushed this stream mapping. - * Do not walk sibling attribute mappings while holding the base MFT - * lock; ordinary file fsync retains its existing behavior below. + * A stream fsync does not need to flush sibling attribute mappings. */ if (stream) goto sync_volume; @@ -232,22 +240,53 @@ int ntfs_file_fsync(struct file *filp, loff_t start, loff_t end, name = (__le16 *)((u8 *)ctx->attr + le16_to_cpu(ctx->attr->name_offset)); if (ctx->attr->type == AT_DATA && ctx->attr->name_length == 0) continue; + if (ctx->attr->type == AT_DATA && + ntfs_stream_unlinked(ni, name, ctx->attr->name_length)) + continue; + attr = kmalloc_obj(*attr, GFP_NOFS); + if (!attr) { + err = -ENOMEM; + break; + } attr_vi = ntfs_attr_iget(vi, ctx->attr->type, name, ctx->attr->name_length); - if (IS_ERR(attr_vi)) + if (IS_ERR(attr_vi)) { + kfree(attr); continue; - spin_lock(&attr_vi->i_lock); - if (inode_state_read_once(attr_vi) & I_DIRTY_PAGES) { - spin_unlock(&attr_vi->i_lock); - filemap_write_and_wait(attr_vi->i_mapping); - } else - spin_unlock(&attr_vi->i_lock); - iput(attr_vi); + } + if (ntfs_inode_is_named_stream(NTFS_I(attr_vi))) + atomic_inc(&NTFS_I(attr_vi)->stream_open_count); + attr->inode = attr_vi; + list_add_tail(&attr->list, &attrs); } } mutex_unlock(&ni->mrec_lock); ntfs_attr_put_search_ctx(ctx); + if (err != -ENOENT && !ret) + ret = err; + + /* Attribute writeback takes the base inode's MFT record lock. */ + list_for_each_entry_safe(attr, next, &attrs, list) { + struct inode *attr_vi = attr->inode; + + err = filemap_write_and_wait(attr_vi->i_mapping); + if (err && !ret) + ret = err; + if (ntfs_inode_is_named_stream(NTFS_I(attr_vi))) { + err = ntfs_stream_put(attr_vi); + if (err && !ret) + ret = err; + } + iput(attr_vi); + list_del(&attr->list); + kfree(attr); + } + + /* Attribute writeback may have updated the base mapping pairs. */ + err = write_inode_now(vi, 1); + if (err && !ret) + ret = err; sync_volume: write_inode_now(vol->mftbmp_ino, 1); @@ -295,7 +334,6 @@ int ntfs_setattr_size(struct inode *vi, struct iattr *attr) { struct ntfs_inode *ni = NTFS_I(vi); struct ntfs_inode *base_ni = ntfs_base_inode(ni); - struct inode *time_vi = vi; bool stream = ntfs_inode_is_named_stream(ni); int err; loff_t old_size = vi->i_size; @@ -333,7 +371,6 @@ int ntfs_setattr_size(struct inode *vi, struct iattr *attr) mutex_unlock(&base_ni->mrec_lock); if (err) goto out_unlock_mapping; - time_vi = VFS_I(base_ni); } else { filemap_invalidate_lock(vi->i_mapping); } @@ -361,14 +398,10 @@ int ntfs_setattr_size(struct inode *vi, struct iattr *attr) goto out_unlock_mapping; } - if (stream) { - inode_set_mtime_to_ts(time_vi, - inode_set_ctime_current(time_vi)); - mark_inode_dirty(time_vi); - } - out_unlock_mapping: filemap_invalidate_unlock(vi->i_mapping); + if (!err && stream) + ntfs_stream_update_base_time(base_ni); return err; } @@ -500,6 +533,24 @@ int ntfs_getattr(struct mnt_idmap *idmap, const struct path *path, return 0; } +/* + * Validate a stream before VFS write handling removes privileges or updates + * timestamps on its base inode. + */ +static int ntfs_file_modified(struct kiocb *iocb) +{ + struct inode *inode = file_inode(iocb->ki_filp); + int err; + + if (ntfs_inode_is_named_stream(NTFS_I(inode))) { + err = ntfs_stream_validate_for_mutation(inode, + iocb->ki_flags & IOCB_NOWAIT); + if (err) + return err; + } + return kiocb_modified(iocb); +} + loff_t ntfs_file_llseek(struct file *file, loff_t offset, int whence) { struct inode *inode = file->f_mapping->host; @@ -710,7 +761,7 @@ ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from) if (ret <= 0) goto out_lock; - err = file_modified(iocb->ki_filp); + err = ntfs_file_modified(iocb); if (err) { ret = err; goto out_lock; @@ -788,13 +839,24 @@ 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; + bool stream = ntfs_inode_is_named_stream(NTFS_I(inode)); + int err; vm_fault_t ret; if (NInoWofCompressed(NTFS_I(inode))) return VM_FAULT_SIGBUS; sb_start_pagefault(inode->i_sb); - file_update_time(vmf->vma->vm_file); + if (stream) { + err = ntfs_stream_validate_for_mutation(inode, false); + if (err) + goto out_error; + err = file_update_time(vmf->vma->vm_file); + if (err) + goto out_error; + } else { + file_update_time(vmf->vma->vm_file); + } /* * Serialize against truncate/fallocate which hold the lock @@ -805,6 +867,10 @@ static vm_fault_t ntfs_filemap_page_mkwrite(struct vm_fault *vmf) filemap_invalidate_unlock_shared(mapping); sb_end_pagefault(inode->i_sb); return ret; + +out_error: + sb_end_pagefault(inode->i_sb); + return vmf_error(err); } static const struct vm_operations_struct ntfs_file_vm_ops = { @@ -1281,6 +1347,13 @@ long ntfs_fallocate(struct file *file, int mode, loff_t offset, loff_t len) inode_unlock(vi); return -EOPNOTSUPP; } + if (stream) { + err = ntfs_stream_validate_for_mutation(vi, false); + if (err) { + inode_unlock(vi); + return err; + } + } old_size = i_size_read(vi); inode_dio_wait(vi); @@ -1322,9 +1395,7 @@ out: if (!err) { if (stream) { - inode_set_mtime_to_ts(VFS_I(base_ni), - inode_set_ctime_current(VFS_I(base_ni))); - mark_inode_dirty(VFS_I(base_ni)); + ntfs_stream_update_base_time(base_ni); } else { NInoSetFileNameDirty(ni); inode_set_mtime_to_ts(vi, inode_set_ctime_current(vi)); diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c index eb79f0cd3323..8d9475788845 100644 --- a/fs/ntfs/inode.c +++ b/fs/ntfs/inode.c @@ -1547,6 +1547,10 @@ static int ntfs_read_locked_attr_inode(struct inode *base_vi, struct inode *vi) vi->i_blocks = ni->itype.compressed.size >> 9; else vi->i_blocks = ni->allocated_size >> 9; + if (ni->type == AT_DATA && ni->name_len) { + vi->i_op = &ntfs_stream_inode_ops; + vi->i_fop = &ntfs_stream_file_ops; + } /* * Make sure the base inode does not go away and attach it to the * attribute inode. @@ -2537,6 +2541,10 @@ int ntfs_show_options(struct seq_file *sf, struct dentry *root) seq_puts(sf, ",symlink=native"); else seq_puts(sf, ",symlink=wsl"); + if (NVolStreamsWindows(vol)) + seq_puts(sf, ",streams_interface=windows"); + else + seq_puts(sf, ",streams_interface=none"); if (vol->sb->s_flags & SB_POSIXACL) seq_puts(sf, ",acl"); return 0; @@ -2597,15 +2605,19 @@ out: int ntfs_truncate_vfs(struct inode *vi, loff_t new_size, loff_t i_size) { struct ntfs_inode *ni = NTFS_I(vi); + struct ntfs_inode *mrec_ni = ntfs_base_inode(ni); int err; - mutex_lock(&ni->mrec_lock); + mutex_lock(&mrec_ni->mrec_lock); err = __ntfs_attr_truncate_vfs(ni, new_size, i_size); - mutex_unlock(&ni->mrec_lock); + mutex_unlock(&mrec_ni->mrec_lock); if (err < 0) return err; - inode_set_mtime_to_ts(vi, inode_set_ctime_current(vi)); + if (ntfs_inode_is_named_stream(ni)) + ntfs_stream_update_base_time(mrec_ni); + else + inode_set_mtime_to_ts(vi, inode_set_ctime_current(vi)); return 0; } diff --git a/fs/ntfs/iomap.c b/fs/ntfs/iomap.c index b4475963e57a..6e51cce3aeba 100644 --- a/fs/ntfs/iomap.c +++ b/fs/ntfs/iomap.c @@ -90,11 +90,7 @@ static int ntfs_read_iomap_begin_resident(struct inode *inode, loff_t offset, lo int err = 0; char *kattr; - if (NInoAttr(ni)) - base_ni = ni->ext.base_ntfs_ino; - else - base_ni = ni; - + base_ni = ntfs_base_inode(ni); mutex_lock(&base_ni->mrec_lock); ctx = ntfs_attr_get_search_ctx(base_ni, NULL); @@ -140,7 +136,8 @@ out: if (ctx) ntfs_attr_put_search_ctx(ctx); - if (!err && keep_mrec_lock && iomap->type == IOMAP_INLINE) { + if (!err && keep_mrec_lock && + iomap->type == IOMAP_INLINE) { iomap->private = base_ni; return 0; } @@ -390,6 +387,8 @@ static int ntfs_write_simple_iomap_begin_non_resident(struct inode *inode, loff_ loff_t length, struct iomap *iomap) { struct ntfs_inode *ni = NTFS_I(inode); + struct ntfs_inode *mrec_ni = ntfs_inode_is_named_stream(ni) ? + ntfs_base_inode(ni) : ni; struct ntfs_volume *vol = ni->vol; loff_t vcn_ofs, rl_length; struct runlist_element *rl, *rlc; @@ -408,7 +407,7 @@ static int ntfs_write_simple_iomap_begin_non_resident(struct inode *inode, loff_ up_read(&ni->runlist.lock); err = ntfs_map_runlist(ni, vcn); if (err) { - mutex_unlock(&ni->mrec_lock); + mutex_unlock(&mrec_ni->mrec_lock); return -ENOENT; } down_read(&ni->runlist.lock); @@ -422,7 +421,7 @@ remap_rl: rl = __ntfs_attr_find_vcn_nolock(&ni->runlist, vcn); if (IS_ERR(rl)) { up_write(&ni->runlist.lock); - mutex_unlock(&ni->mrec_lock); + mutex_unlock(&mrec_ni->mrec_lock); return -EIO; } lcn = ntfs_rl_vcn_to_lcn(rl, vcn); @@ -443,7 +442,7 @@ remap_rl: */ if (lcn == LCN_RL_NOT_MAPPED) { up_write(&ni->runlist.lock); - mutex_unlock(&ni->mrec_lock); + mutex_unlock(&mrec_ni->mrec_lock); return map_err == -ENOMEM ? -ENOMEM : -EIO; } @@ -453,7 +452,7 @@ remap_rl: "runlist(vcn : %lld, length : %lld) is corrupted\n", rl->vcn, rl->length); up_write(&ni->runlist.lock); - mutex_unlock(&ni->mrec_lock); + mutex_unlock(&mrec_ni->mrec_lock); return -EIO; } @@ -467,7 +466,7 @@ remap_rl: if (max_clu_count < 0) { err = max_clu_count; up_write(&ni->runlist.lock); - mutex_unlock(&ni->mrec_lock); + mutex_unlock(&mrec_ni->mrec_lock); return err; } } @@ -482,7 +481,7 @@ remap_rl: GFP_NOFS); if (!rlc) { up_write(&ni->runlist.lock); - mutex_unlock(&ni->mrec_lock); + mutex_unlock(&mrec_ni->mrec_lock); return -ENOMEM; } @@ -499,7 +498,7 @@ remap_rl: if (IS_ERR(rl)) { ntfs_error(vol->sb, "Failed to merge runlists"); up_write(&ni->runlist.lock); - mutex_unlock(&ni->mrec_lock); + mutex_unlock(&mrec_ni->mrec_lock); kvfree(rlc); return PTR_ERR(rl); } @@ -509,7 +508,7 @@ remap_rl: ni->i_dealloc_clusters += max_clu_count; } up_write(&ni->runlist.lock); - mutex_unlock(&ni->mrec_lock); + mutex_unlock(&mrec_ni->mrec_lock); if (lcn < LCN_DELALLOC) ntfs_hold_dirty_clusters(vol, max_clu_count); @@ -561,7 +560,7 @@ remap_rl: } } else { up_write(&ni->runlist.lock); - mutex_unlock(&ni->mrec_lock); + mutex_unlock(&mrec_ni->mrec_lock); iomap->type = IOMAP_MAPPED; iomap->addr = ntfs_cluster_to_bytes(vol, lcn) + vcn_ofs; @@ -586,6 +585,8 @@ static int ntfs_write_da_iomap_begin_non_resident(struct inode *inode, struct iomap *iomap, int ntfs_iomap_flags) { struct ntfs_inode *ni = NTFS_I(inode); + struct ntfs_inode *mrec_ni = ntfs_inode_is_named_stream(ni) ? + ntfs_base_inode(ni) : ni; struct ntfs_volume *vol = ni->vol; loff_t vcn_ofs, rl_length; s64 vcn, start_lcn, lcn_count; @@ -604,7 +605,7 @@ static int ntfs_write_da_iomap_begin_non_resident(struct inode *inode, max_clu_count, &balloc, update_mp, ntfs_iomap_flags & NTFS_IOMAP_FLAGS_WRITEBACK); up_write(&ni->runlist.lock); - mutex_unlock(&ni->mrec_lock); + mutex_unlock(&mrec_ni->mrec_lock); if (err) { ni->i_dealloc_clusters = 0; return err; @@ -658,8 +659,8 @@ static int ntfs_write_da_iomap_begin_non_resident(struct inode *inode, if (ntfs_iomap_flags & NTFS_IOMAP_FLAGS_MKWRITE && iomap->offset + iomap->length > ni->initialized_size) { - err = ntfs_attr_set_initialized_size(ni, iomap->offset + - iomap->length); + err = ntfs_attr_set_initialized_size(ni, + iomap->offset + iomap->length); } return err; @@ -669,13 +670,15 @@ static int ntfs_write_iomap_begin_resident(struct inode *inode, loff_t offset, struct iomap *iomap) { struct ntfs_inode *ni = NTFS_I(inode); + struct ntfs_inode *mrec_ni = ntfs_inode_is_named_stream(ni) ? + ntfs_base_inode(ni) : ni; struct attr_record *a; - struct ntfs_attr_search_ctx *ctx; + struct ntfs_attr_search_ctx *ctx = NULL; u32 attr_len; int err = 0; char *kattr; - ctx = ntfs_attr_get_search_ctx(ni, NULL); + ctx = ntfs_attr_get_search_ctx(mrec_ni, NULL); if (!ctx) { err = -ENOMEM; goto out; @@ -698,13 +701,14 @@ static int ntfs_write_iomap_begin_resident(struct inode *inode, loff_t offset, iomap->inline_data = kattr; iomap->offset = 0; iomap->length = attr_len; + iomap->private = mrec_ni; out: if (ctx) ntfs_attr_put_search_ctx(ctx); if (err) - mutex_unlock(&ni->mrec_lock); + mutex_unlock(&mrec_ni->mrec_lock); return err; } @@ -713,7 +717,12 @@ 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) { - mutex_lock(&NTFS_I(inode)->mrec_lock); + struct ntfs_inode *ni = NTFS_I(inode); + struct ntfs_inode *mrec_ni = ntfs_inode_is_named_stream(ni) ? + ntfs_base_inode(ni) : ni; + + mutex_lock(&mrec_ni->mrec_lock); + if (ntfs_iomap_flags & NTFS_IOMAP_FLAGS_BEGIN) return ntfs_write_simple_iomap_begin_non_resident(inode, offset, length, iomap); @@ -729,12 +738,15 @@ 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); + struct ntfs_inode *mrec_ni; if (NVolShutdown(ni->vol)) return -EIO; if (!NInoNonResident(ni)) { - mutex_lock(&ni->mrec_lock); + mrec_ni = ntfs_inode_is_named_stream(ni) ? + ntfs_base_inode(ni) : ni; + mutex_lock(&mrec_ni->mrec_lock); return ntfs_write_iomap_begin_resident(inode, offset, iomap); } return ntfs_write_iomap_begin_non_resident(inode, offset, length, flags, @@ -753,10 +765,10 @@ static int ntfs_write_iomap_end_resident(struct inode *inode, loff_t pos, loff_t length, ssize_t written, unsigned int flags, struct iomap *iomap) { - struct ntfs_inode *ni = NTFS_I(inode); + struct ntfs_inode *base_ni = iomap->private; - mark_mft_record_dirty(ni); - mutex_unlock(&ni->mrec_lock); + mark_mft_record_dirty(base_ni); + mutex_unlock(&base_ni->mrec_lock); return written; } diff --git a/fs/ntfs/named_stream.c b/fs/ntfs/named_stream.c index 5c76cae66ed2..e44dbf09057b 100644 --- a/fs/ntfs/named_stream.c +++ b/fs/ntfs/named_stream.c @@ -6,6 +6,7 @@ */ #include +#include #include #include @@ -176,6 +177,598 @@ bool ntfs_stream_unlinked(struct ntfs_inode *base_ni, return unlinked; } +/* + * ntfs_stream_path_parse() - Split a pathname stream component + * @vol: NTFS volume + * @qname: final pathname component + * @path: receives slices of @qname on success + * + * Parse the ``file:stream`` form when the Windows pathname interface is + * enabled. This does not convert or validate the component names. + * + * Return: 1 if stream syntax was parsed, 0 if streams are disabled or no + * stream separator is present, or -EINVAL for malformed syntax. + */ +int ntfs_stream_path_parse(struct ntfs_volume *vol, + const struct qstr *qname, struct ntfs_stream_path *path) +{ + const unsigned char *colon; + + if (!NVolStreamsWindows(vol)) + return 0; + + colon = memchr(qname->name, ':', qname->len); + if (!colon) + return 0; + if (colon == qname->name || colon + 1 == qname->name + qname->len) + return -EINVAL; + if (memchr(colon + 1, ':', qname->name + qname->len - colon - 1)) + return -EINVAL; + + path->base_name = (const char *)qname->name; + path->base_len = colon - qname->name; + path->stream_name = (const char *)colon + 1; + path->stream_len = qname->name + qname->len - colon - 1; + return 1; +} + +/* + * ntfs_stream_path_has_colon() - Check for a pathname stream separator + * @vol: NTFS volume + * @qname: final pathname component + * + * Return: true if the Windows pathname interface is enabled and @qname + * contains a colon, or false otherwise. + */ +bool ntfs_stream_path_has_colon(struct ntfs_volume *vol, + const struct qstr *qname) +{ + return NVolStreamsWindows(vol) && + memchr(qname->name, ':', qname->len); +} + +/* + * ntfs_stream_path_check() - Reject stream syntax for unsupported operations + * @vol: NTFS volume + * @qname: final pathname component + * + * Return: 0 for an ordinary name or when streams are disabled, + * -EOPNOTSUPP for parsed stream syntax, or -EINVAL for malformed syntax. + */ +int ntfs_stream_path_check(struct ntfs_volume *vol, + const struct qstr *qname) +{ + struct ntfs_stream_path path; + int ret; + + ret = ntfs_stream_path_parse(vol, qname, &path); + if (ret < 0) + return ret; + return ret ? -EOPNOTSUPP : 0; +} + +/* + * ntfs_stream_lookup_inode_by_name() - Look up a base inode by name + * @dir_ino: directory containing the base file or directory + * @uname: UTF-16LE base name + * @uname_len: Length of @uname in UTF-16 code units + * + * Resolve the directory entry and verify its MFT reference before returning + * the inode. + * + * Return: Referenced inode on success, or ERR_PTR() on failure. + */ +struct inode *ntfs_stream_lookup_inode_by_name(struct inode *dir_ino, + __le16 *uname, int uname_len) +{ + struct ntfs_volume *vol = NTFS_SB(dir_ino->i_sb); + struct ntfs_name *name = NULL; + struct inode *inode; + u64 mref; + + mutex_lock(&NTFS_I(dir_ino)->mrec_lock); + mref = ntfs_lookup_inode_by_name(NTFS_I(dir_ino), uname, uname_len, + &name); + mutex_unlock(&NTFS_I(dir_ino)->mrec_lock); + kfree(name); + + if (IS_ERR_MREF(mref)) + return ERR_PTR(MREF_ERR(mref)); + + inode = ntfs_iget(vol->sb, MREF(mref)); + if (IS_ERR(inode)) + return inode; + if (MSEQNO(mref) != NTFS_I(inode)->seq_no && + MREF(mref) != FILE_MFT) { + iput(inode); + return ERR_PTR(-EIO); + } + return inode; +} + +/* + * Recheck that a stream dentry still names the same base inode and DATA + * attribute. + */ +static int ntfs_stream_d_revalidate(struct inode *dir, + const struct qstr *qname, struct dentry *dentry, + unsigned int flags) +{ + struct inode *inode = d_inode(dentry); + struct ntfs_inode *ni; + struct ntfs_stream_path path; + struct inode *base_vi; + __le16 *base_name = NULL, *stream_name = NULL; + int base_len, stream_len, err, ret = 0; + + if (flags & LOOKUP_RCU) + return -ECHILD; + if (!inode) + return 0; + if (!inode->i_nlink) + return 0; + if (NVolShutdown(NTFS_SB(dir->i_sb))) + return 0; + + ni = NTFS_I(inode); + if (!ntfs_inode_is_named_stream(ni)) + return 0; + + err = ntfs_stream_path_parse(NTFS_SB(dir->i_sb), qname, &path); + if (err <= 0) + return 0; + base_len = ntfs_nlstoucs(NTFS_SB(dir->i_sb), path.base_name, + path.base_len, &base_name, NTFS_MAX_NAME_LEN); + if (base_len < 0) + goto out; + stream_len = ntfs_nlstoucs(NTFS_SB(dir->i_sb), path.stream_name, + path.stream_len, &stream_name, NTFS_MAX_NAME_LEN); + if (stream_len < 0) + goto out; + if (ntfs_check_stream_name(stream_name, stream_len)) + goto out; + if (stream_len != ni->name_len || + !ntfs_names_are_equal(stream_name, stream_len, ni->name, + ni->name_len, IGNORE_CASE, ni->vol->upcase, + ni->vol->upcase_len)) + goto out; + + base_vi = ntfs_stream_lookup_inode_by_name(dir, base_name, base_len); + if (IS_ERR(base_vi)) + goto out; + if (NTFS_I(base_vi) != ni->ext.base_ntfs_ino) { + iput(base_vi); + goto out; + } + + mutex_lock(&NTFS_I(base_vi)->mrec_lock); + err = ntfs_stream_inode_validate(inode); + mutex_unlock(&NTFS_I(base_vi)->mrec_lock); + iput(base_vi); + if (!err) + ret = 1; +out: + if (stream_name) + kmem_cache_free(ntfs_name_cache, stream_name); + if (base_name) + kmem_cache_free(ntfs_name_cache, base_name); + return ret; +} + +/* Revalidate stream paths while leaving ordinary NTFS dentries cacheable. */ +static int ntfs_dentry_revalidate(struct inode *dir, + const struct qstr *qname, struct dentry *dentry, + unsigned int flags) +{ + struct inode *inode = d_inode(dentry); + struct ntfs_volume *vol = NTFS_SB(dir->i_sb); + + if (ntfs_stream_path_has_colon(vol, qname)) + return ntfs_stream_d_revalidate(dir, qname, dentry, flags); + if (inode && ntfs_inode_is_named_stream(NTFS_I(inode))) + return 0; + return 1; +} + +/* Drop stream-path aliases so future lookups recheck their backing attribute. */ +static int ntfs_dentry_delete(const struct dentry *dentry) +{ + struct inode *inode = d_inode(dentry); + struct ntfs_volume *vol = NTFS_SB(dentry->d_sb); + + if (ntfs_stream_path_has_colon(vol, &dentry->d_name) || + (inode && ntfs_inode_is_named_stream(NTFS_I(inode)))) + return always_delete_dentry(dentry); + return 0; +} + +static const struct dentry_operations ntfs_dentry_ops = { + .d_revalidate = ntfs_dentry_revalidate, + .d_delete = ntfs_dentry_delete, +}; + +/* + * ntfs_set_default_dentry_ops() - Install stream-aware dentry operations + * @sb: superblock on which to install the operations + * + * Ensure pathname stream dentries are revalidated and not kept as stale + * aliases. + */ +void ntfs_set_default_dentry_ops(struct super_block *sb) +{ + set_default_d_op(sb, &ntfs_dentry_ops); +} + +/* + * ntfs_lookup_stream() - Look up a named stream by pathname + * @dir_ino: directory containing the base file or directory + * @dent: dentry for the pathname component + * @path: parsed base and stream name slices + * + * Find the existing base inode and its named DATA attribute, then splice the + * stream inode into @dent. + * + * Return: NULL if @dent was instantiated, an alternate dentry if a + * disconnected alias was spliced, or ERR_PTR() on failure. + */ +struct dentry *ntfs_lookup_stream(struct inode *dir_ino, + struct dentry *dent, const struct ntfs_stream_path *path) +{ + struct ntfs_volume *vol = NTFS_SB(dir_ino->i_sb); + struct inode *base_vi, *stream_vi; + __le16 *base_uname = NULL, *stream_uname = NULL; + int base_len, stream_len, err; + + if (NVolShutdown(vol)) + return ERR_PTR(-EIO); + + base_len = ntfs_nlstoucs(vol, path->base_name, path->base_len, + &base_uname, NTFS_MAX_NAME_LEN); + if (base_len < 0) + return ERR_PTR(base_len); + + stream_len = ntfs_nlstoucs(vol, path->stream_name, path->stream_len, + &stream_uname, NTFS_MAX_NAME_LEN); + if (stream_len < 0) { + kmem_cache_free(ntfs_name_cache, base_uname); + return ERR_PTR(stream_len); + } + err = ntfs_check_stream_name(stream_uname, stream_len); + if (err) { + kmem_cache_free(ntfs_name_cache, base_uname); + kmem_cache_free(ntfs_name_cache, stream_uname); + return ERR_PTR(err); + } + + base_vi = ntfs_stream_lookup_inode_by_name(dir_ino, base_uname, + base_len); + kmem_cache_free(ntfs_name_cache, base_uname); + if (IS_ERR(base_vi)) { + err = PTR_ERR(base_vi); + kmem_cache_free(ntfs_name_cache, stream_uname); + if (err == -ENOENT) + return d_splice_alias(NULL, dent); + return ERR_PTR(err); + } + + if (!S_ISREG(base_vi->i_mode) && !S_ISDIR(base_vi->i_mode)) { + iput(base_vi); + kmem_cache_free(ntfs_name_cache, stream_uname); + return ERR_PTR(-ENOTDIR); + } + + mutex_lock(&NTFS_I(base_vi)->mrec_lock); + stream_vi = ntfs_attr_iget(base_vi, AT_DATA, stream_uname, stream_len); + if (!IS_ERR(stream_vi)) { + if (NInoStreamUnlinked(NTFS_I(stream_vi))) + err = -ENOENT; + else + err = ntfs_stream_inode_validate(stream_vi); + } + mutex_unlock(&NTFS_I(base_vi)->mrec_lock); + if (!IS_ERR(stream_vi) && err) { + iput(stream_vi); + stream_vi = ERR_PTR(err); + } + iput(base_vi); + kmem_cache_free(ntfs_name_cache, stream_uname); + + if (IS_ERR(stream_vi)) { + err = PTR_ERR(stream_vi); + if (err == -ENOENT || err == -ESTALE) + return d_splice_alias(NULL, dent); + return ERR_PTR(err); + } + + return d_splice_alias(stream_vi, dent); +} + +/* + * ntfs_stream_path_names() - Convert and validate pathname stream names + * @vol: NTFS volume + * @qname: final pathname component + * @base_name: receives the allocated UTF-16LE base name + * @base_len: receives the base name length in UTF-16 code units + * @stream_name: receives the allocated UTF-16LE stream name + * @stream_len: receives the stream name length in UTF-16 code units + * + * Return: 1 if stream names were produced, 0 if @qname has no stream syntax, + * or a negative errno. The caller owns both name buffers on success. + */ +int ntfs_stream_path_names(struct ntfs_volume *vol, + const struct qstr *qname, __le16 **base_name, int *base_len, + __le16 **stream_name, int *stream_len) +{ + struct ntfs_stream_path path; + int err; + + *base_name = NULL; + *stream_name = NULL; + err = ntfs_stream_path_parse(vol, qname, &path); + if (err <= 0) + return err; + + *base_len = ntfs_nlstoucs(vol, path.base_name, path.base_len, + base_name, NTFS_MAX_NAME_LEN); + if (*base_len < 0) + return *base_len; + + *stream_len = ntfs_nlstoucs(vol, path.stream_name, path.stream_len, + stream_name, NTFS_MAX_NAME_LEN); + if (*stream_len < 0) { + kmem_cache_free(ntfs_name_cache, *base_name); + *base_name = NULL; + return *stream_len; + } + + err = ntfs_check_bad_windows_name(vol, *base_name, *base_len); + if (err) + goto out_free; + err = ntfs_check_stream_name(*stream_name, *stream_len); + if (err) + goto out_free; + err = ntfs_check_bad_windows_name(vol, *stream_name, *stream_len); + if (err) + goto out_free; + return 1; + +out_free: + kmem_cache_free(ntfs_name_cache, *stream_name); + kmem_cache_free(ntfs_name_cache, *base_name); + *stream_name = NULL; + *base_name = NULL; + return err; +} + +/* + * ntfs_create_named_stream() - Create a named DATA stream + * @idmap: mount idmap used for permission checks + * @dir: directory containing the base object + * @base_name: UTF-16LE base name + * @base_len: length of @base_name in UTF-16 code units + * @stream_name: UTF-16LE stream name + * @stream_len: length of @stream_name in UTF-16 code units + * + * Create the stream on an existing regular file or directory. + * + * Return: Referenced stream inode on success, or ERR_PTR() on failure. + */ +struct inode *ntfs_create_named_stream(struct mnt_idmap *idmap, + struct inode *dir, __le16 *base_name, int base_len, + __le16 *stream_name, int stream_len) +{ + struct ntfs_inode *base_ni; + struct inode *base_vi, *stream_vi; + struct inode *rollback_vi = NULL; + struct ntfs_attr_search_ctx *ctx; + bool stream_created = false; + int err, rollback_err; + + base_vi = ntfs_stream_lookup_inode_by_name(dir, base_name, base_len); + if (IS_ERR(base_vi)) + return base_vi; + if (!S_ISREG(base_vi->i_mode) && !S_ISDIR(base_vi->i_mode)) { + iput(base_vi); + return ERR_PTR(-ENOTDIR); + } + err = inode_permission(idmap, base_vi, MAY_OPEN | MAY_WRITE); + if (err) + goto out_iput; + if (IS_APPEND(base_vi) || IS_IMMUTABLE(base_vi)) { + err = -EPERM; + goto out_iput; + } + if (!(NTFS_SB(base_vi->i_sb)->vol_flags & VOLUME_IS_DIRTY)) { + err = ntfs_set_volume_flags(NTFS_SB(base_vi->i_sb), + VOLUME_IS_DIRTY); + if (err) + goto out_iput; + } + + base_ni = NTFS_I(base_vi); + mutex_lock(&base_ni->mrec_lock); + if (NVolShutdown(base_ni->vol)) { + err = -EIO; + goto out_unlock; + } + if (NInoBeingDeleted(base_ni) || !base_vi->i_nlink) { + err = -ENOENT; + goto out_unlock; + } + if (IS_APPEND(base_vi) || IS_IMMUTABLE(base_vi)) { + err = -EPERM; + goto out_unlock; + } + ctx = ntfs_attr_get_search_ctx(base_ni, NULL); + if (!ctx) { + err = -ENOMEM; + goto out_unlock; + } + + err = ntfs_attr_lookup(AT_DATA, stream_name, stream_len, + IGNORE_CASE, 0, NULL, 0, ctx); + if (!err) { + if (ntfs_stream_unlinked(base_ni, stream_name, stream_len)) + err = -EBUSY; + else + err = -EEXIST; + } else if (err == -ENOENT) { + if (NVolShutdown(base_ni->vol)) { + err = -EIO; + goto out_put_ctx; + } + err = ntfs_attr_add(base_ni, AT_DATA, stream_name, stream_len, + NULL, 0); + if (!err) { + stream_created = true; + mark_mft_record_dirty(base_ni); + } + } + ntfs_attr_put_search_ctx(ctx); + if (err) + goto out_unlock; + + stream_vi = ntfs_attr_iget(base_vi, AT_DATA, stream_name, stream_len); + if (!IS_ERR(stream_vi)) + err = ntfs_stream_inode_validate(stream_vi); + else + err = PTR_ERR(stream_vi); + if (err) { + if (stream_created) { + rollback_err = ntfs_attr_remove(base_ni, AT_DATA, + stream_name, stream_len, &rollback_vi); + if (rollback_err) { + ntfs_error(base_ni->vol->sb, + "Failed to roll back named stream creation.\n"); + if (rollback_err == -ENOMEM || + rollback_err == -EINTR || + rollback_err == -ERESTARTSYS) { + err = rollback_err; + } else { + NVolSetErrors(base_ni->vol); + NVolSetShutdown(base_ni->vol); + err = -EIO; + } + } + } + mutex_unlock(&base_ni->mrec_lock); + if (stream_created) + ntfs_stream_update_base_time(base_ni); + if (!IS_ERR(stream_vi)) + iput(stream_vi); + if (rollback_vi) + iput(rollback_vi); + iput(base_vi); + return ERR_PTR(err); + } + mutex_unlock(&base_ni->mrec_lock); + if (stream_created) + ntfs_stream_update_base_time(base_ni); + iput(base_vi); + return stream_vi; + +out_put_ctx: + ntfs_attr_put_search_ctx(ctx); +out_unlock: + mutex_unlock(&base_ni->mrec_lock); +out_iput: + iput(base_vi); + return ERR_PTR(err); +} + +/* + * ntfs_unlink_named_stream() - Remove a pathname named stream + * @dir: directory containing the base object + * @dentry: dentry for the named stream + * + * Return: 0 on success, or a negative errno. + */ +int ntfs_unlink_named_stream(struct inode *dir, struct dentry *dentry) +{ + struct ntfs_volume *vol = NTFS_SB(dir->i_sb); + struct inode *base_vi; + __le16 *base_name = NULL, *stream_name = NULL; + int base_len, stream_len, path_result, err; + + path_result = ntfs_stream_path_names(vol, &dentry->d_name, &base_name, + &base_len, &stream_name, &stream_len); + if (path_result < 0) + return path_result; + if (!path_result) + return -EINVAL; + + base_vi = ntfs_stream_lookup_inode_by_name(dir, base_name, base_len); + if (IS_ERR(base_vi)) { + err = PTR_ERR(base_vi); + goto out_free; + } + if (!S_ISREG(base_vi->i_mode) && !S_ISDIR(base_vi->i_mode)) { + err = -ENOTDIR; + goto out_iput; + } + if (!ntfs_inode_is_named_stream(NTFS_I(dentry->d_inode))) { + err = -ESTALE; + goto out_iput; + } + if (NTFS_I(dentry->d_inode)->ext.base_ntfs_ino != NTFS_I(base_vi)) { + err = -ESTALE; + goto out_iput; + } + + /* + * VFS authorizes pathname removal via the parent directory, not base + * file write access. The shared helper enforces NTFS delete restrictions. + */ + err = ntfs_remove_named_stream(NTFS_I(base_vi), stream_name, + stream_len, dentry->d_inode); + +out_iput: + iput(base_vi); +out_free: + kmem_cache_free(ntfs_name_cache, stream_name); + kmem_cache_free(ntfs_name_cache, base_name); + return err; +} + +/* Obtain a dentry for the base inode behind a stream inode. */ +static struct dentry *ntfs_stream_base_dentry(struct inode *inode) +{ + struct inode *base_vi = + VFS_I(NTFS_I(inode)->ext.base_ntfs_ino); + struct dentry *dentry; + + dentry = d_find_alias(base_vi); + if (dentry) + return dentry; + if (!igrab(base_vi)) + return ERR_PTR(-ESTALE); + return d_obtain_alias(base_vi); +} + +/* + * ntfs_stream_validate_for_mutation() - Validate a stream before mutation + * @inode: stream inode to validate + * @nowait: do not wait for the base MFT-record lock + * + * Return: 0 if the backing DATA attribute is valid, -EAGAIN if a nonblocking + * lock attempt fails, or another negative errno. + */ +int ntfs_stream_validate_for_mutation(struct inode *inode, bool nowait) +{ + struct ntfs_inode *base_ni = NTFS_I(inode)->ext.base_ntfs_ino; + int err; + + if (nowait) { + if (!mutex_trylock(&base_ni->mrec_lock)) + return -EAGAIN; + } else { + mutex_lock(&base_ni->mrec_lock); + } + err = ntfs_stream_inode_validate(inode); + mutex_unlock(&base_ni->mrec_lock); + return err; +} + /* * Hold a temporary stream reference so unlink cannot remove its attribute * while an operation is using it. @@ -201,6 +794,213 @@ static int ntfs_stream_claim(struct inode *inode) return err; } +/* Stream permission checks use the base inode's permissions. */ +static int ntfs_stream_permission(struct mnt_idmap *idmap, + struct inode *inode, int mask) +{ + return inode_permission(idmap, + VFS_I(NTFS_I(inode)->ext.base_ntfs_ino), mask); +} + +/* Report base metadata with the stream's own size and allocation. */ +static int ntfs_stream_getattr(struct mnt_idmap *idmap, + const struct path *path, struct kstat *stat, + unsigned int request_mask, unsigned int query_flags) +{ + struct inode *inode = d_backing_inode(path->dentry); + struct ntfs_inode *ni = NTFS_I(inode); + struct inode *base_vi = VFS_I(ni->ext.base_ntfs_ino); + + generic_fillattr(idmap, request_mask, base_vi, stat); + stat->size = i_size_read(inode); + stat->blocks = (((u64)ni->i_dealloc_clusters << + NTFS_SB(inode->i_sb)->cluster_size_bits) >> 9) + + inode->i_blocks; + stat->blksize = NTFS_SB(inode->i_sb)->cluster_size; + stat->result_mask |= STATX_BTIME; + stat->btime = NTFS_I(base_vi)->i_crtime; + + if (NInoCompressed(ni)) + stat->attributes |= STATX_ATTR_COMPRESSED; + if (NInoEncrypted(ni)) + stat->attributes |= STATX_ATTR_ENCRYPTED; + if (base_vi->i_flags & S_IMMUTABLE) + stat->attributes |= STATX_ATTR_IMMUTABLE; + if (base_vi->i_flags & S_APPEND) + stat->attributes |= STATX_ATTR_APPEND; + stat->attributes_mask |= STATX_ATTR_COMPRESSED | STATX_ATTR_ENCRYPTED | + STATX_ATTR_IMMUTABLE | STATX_ATTR_APPEND; + stat->mode = (stat->mode & ~S_IFMT) | S_IFREG; + + if (request_mask & STATX_DIOALIGN) { + unsigned int align = + bdev_logical_block_size(inode->i_sb->s_bdev); + + stat->result_mask |= STATX_DIOALIGN; + if (!NInoCompressed(ni) && !NInoEncrypted(ni)) { + stat->dio_mem_align = align; + stat->dio_offset_align = align; + } + } + + return 0; +} + +/* + * Apply size changes to the stream and route shared inode metadata changes to + * its base inode. + */ +static int ntfs_stream_setattr_common(struct mnt_idmap *idmap, + struct inode *inode, struct iattr *attr) +{ + struct ntfs_inode *ni = NTFS_I(inode); + struct inode *base_vi = VFS_I(ni->ext.base_ntfs_ino); + struct dentry *base_dentry = NULL; + struct iattr base_attr = *attr; + int err; + + base_attr.ia_valid &= ~ATTR_FILE; + base_attr.ia_file = NULL; + if (base_attr.ia_valid & (ATTR_KILL_SUID | ATTR_KILL_SGID)) + base_attr.ia_valid &= ~ATTR_MODE; + else if (base_attr.ia_valid & ATTR_MODE) + base_attr.ia_mode = (base_attr.ia_mode & ~S_IFMT) | + (base_vi->i_mode & S_IFMT); + + if (attr->ia_valid & ATTR_SIZE) { + err = inode_permission(idmap, base_vi, MAY_WRITE); + if (err) + goto out; + if (IS_APPEND(base_vi) || IS_IMMUTABLE(base_vi)) { + err = -EPERM; + goto out; + } + if (!(ni->vol->vol_flags & VOLUME_IS_DIRTY)) { + err = ntfs_set_volume_flags(ni->vol, VOLUME_IS_DIRTY); + if (err) + goto out; + } + base_attr.ia_valid &= ~ATTR_SIZE; + } + + if ((attr->ia_valid & ATTR_SIZE) || base_attr.ia_valid) { + base_dentry = ntfs_stream_base_dentry(inode); + if (IS_ERR(base_dentry)) { + err = PTR_ERR(base_dentry); + base_dentry = NULL; + goto out; + } + inode_lock_nested(base_vi, I_MUTEX_PARENT); + err = ntfs_stream_validate_for_mutation(inode, false); + if (err) { + inode_unlock(base_vi); + goto out; + } + if (base_attr.ia_valid) + err = notify_change(idmap, base_dentry, &base_attr, + NULL); + else + err = 0; + if (!err) + ntfs_stream_inode_refresh(inode); + inode_unlock(base_vi); + if (err) + goto out; + } + + if (attr->ia_valid & ATTR_SIZE) + err = ntfs_setattr_size(inode, attr); + else + err = 0; +out: + dput(base_dentry); + return err; +} + +/* Hold the stream against unlink while applying VFS setattr operations. */ +static int ntfs_stream_setattr(struct mnt_idmap *idmap, + struct dentry *dentry, struct iattr *attr) +{ + struct inode *inode = d_inode(dentry); + struct ntfs_inode *ni = NTFS_I(inode); + bool claimed = false; + int err; + + if (NVolShutdown(ni->vol)) + return -EIO; + + if (!(attr->ia_valid & ATTR_FILE)) { + err = ntfs_stream_claim(inode); + if (err) + return err; + claimed = true; + } + + err = ntfs_stream_setattr_common(idmap, inode, attr); + if (claimed) + ntfs_stream_put(inode); + return err; +} + +/* + * Apply stream timestamp updates to the base inode after validating the + * backing attribute under its MFT-record lock. Nonblocking callers get + * -EAGAIN. + */ +static int ntfs_stream_update_time_common(struct inode *inode, + enum fs_update_time type, unsigned int flags) +{ + struct ntfs_inode *base_ni = NTFS_I(inode)->ext.base_ntfs_ino; + struct inode *base_vi = VFS_I(base_ni); + int err; + + if (NVolShutdown(base_ni->vol)) + return -EIO; + if (flags & IOCB_NOWAIT) + return -EAGAIN; + + mutex_lock(&base_ni->mrec_lock); + if (NVolShutdown(base_ni->vol)) { + err = -EIO; + goto out_mrec; + } + err = ntfs_stream_inode_validate(inode); + if (!err) { + err = generic_update_time(base_vi, type, flags); + if (!err) + ntfs_stream_inode_refresh(inode); + } +out_mrec: + mutex_unlock(&base_ni->mrec_lock); + return err; +} + +static int ntfs_stream_update_time(struct inode *inode, + enum fs_update_time type, unsigned int flags) +{ + return ntfs_stream_update_time_common(inode, type, flags); +} + +static ssize_t ntfs_stream_listxattr(struct dentry *dentry, char *buffer, + size_t size) +{ + return -EOPNOTSUPP; +} + +#ifdef CONFIG_NTFS_FS_POSIX_ACL +static struct posix_acl *ntfs_stream_get_acl(struct mnt_idmap *idmap, + struct dentry *dentry, int type) +{ + return ERR_PTR(-EOPNOTSUPP); +} + +static int ntfs_stream_set_acl(struct mnt_idmap *idmap, + struct dentry *dentry, struct posix_acl *acl, int type) +{ + return -EOPNOTSUPP; +} +#endif + /* * Remove an unlinked stream's DATA attribute after its final active reference * is released. Base-inode deletion handles the attribute when the base is @@ -335,8 +1135,9 @@ int ntfs_remove_named_stream(struct ntfs_inode *ni, __le16 *uname, clear_nlink(attr_vi); mutex_unlock(&ni->mrec_lock); - ntfs_stream_update_base_time(ni); err = ntfs_stream_finish_remove(attr_vi); + if (!err) + ntfs_stream_update_base_time(ni); iput(attr_vi); return err; @@ -349,6 +1150,80 @@ out_unlock: return err; } +/* + * Validate access against the base inode and hold the stream against unlink + * until the file is released. + */ +static int ntfs_stream_file_open(struct inode *inode, struct file *file) +{ + struct ntfs_inode *ni = NTFS_I(inode); + struct ntfs_inode *base_ni = ni->ext.base_ntfs_ino; + struct inode *base_vi = VFS_I(base_ni); + int mask = MAY_OPEN; + int err; + + if (file->f_mode & FMODE_READ) + mask |= MAY_READ; + if (file->f_mode & FMODE_WRITE) + mask |= MAY_WRITE; + err = inode_permission(file_mnt_idmap(file), base_vi, mask); + if (err) + return err; + + mutex_lock(&base_ni->mrec_lock); + if (NInoStreamUnlinked(ni)) + err = -ENOENT; + else + err = ntfs_stream_inode_validate(inode); + if (err) + goto out_unlock; + if ((file->f_mode & FMODE_WRITE) && + (IS_IMMUTABLE(base_vi) || + (IS_APPEND(base_vi) && !(file->f_flags & O_APPEND)))) { + err = -EPERM; + goto out_unlock; + } + if ((file->f_flags & O_TRUNC) && IS_APPEND(base_vi)) { + err = -EPERM; + goto out_unlock; + } + if ((file->f_flags & O_NOATIME) && + !inode_owner_or_capable(file_mnt_idmap(file), base_vi)) { + err = -EPERM; + goto out_unlock; + } + err = ntfs_file_open(inode, file); + if (err) + goto out_unlock; + if (file->f_mode & FMODE_WRITE) { + err = get_write_access(base_vi); + if (err) + goto out_unlock; + file->private_data = base_vi; + } + + atomic_inc(&ni->stream_open_count); +out_unlock: + mutex_unlock(&base_ni->mrec_lock); + return err; +} + +/* Drop open-time references and complete any deferred stream unlink. */ +static int ntfs_stream_file_release(struct inode *inode, struct file *file) +{ + int err, remove_err; + + err = ntfs_file_release(inode, file); + if (file->private_data) { + put_write_access(file->private_data); + file->private_data = NULL; + } + remove_err = ntfs_stream_put(inode); + if (!err) + err = remove_err; + return err; +} + enum ntfs_stream_ioctl_op { NTFS_STREAM_IOCTL_READ, NTFS_STREAM_IOCTL_WRITE, @@ -618,6 +1493,9 @@ static long ntfs_ioctl_stream(struct file *filp, unsigned long arg, file_accessed(filp); } else { inode_lock(stream_vi); + inode_dio_wait(stream_vi); + /* Exclude faults before taking MFT record and folio locks. */ + filemap_invalidate_lock(stream_vi->i_mapping); err = inode_newsize_ok(stream_vi, pos + data_size); if (!err) { ret = ntfs_inode_attr_pwrite(stream_vi, pos, data_size, @@ -627,6 +1505,7 @@ static long ntfs_ioctl_stream(struct file *filp, unsigned long arg, else req->bytes_returned = ret; } + filemap_invalidate_unlock(stream_vi->i_mapping); inode_unlock(stream_vi); } if (claimed) { @@ -914,3 +1793,29 @@ out_free: kvfree(kbuf); return ret; } + +const struct file_operations ntfs_stream_file_ops = { + .llseek = ntfs_file_llseek, + .read_iter = ntfs_file_read_iter, + .write_iter = ntfs_file_write_iter, + .fsync = ntfs_file_fsync, + .fallocate = ntfs_fallocate, + .mmap_prepare = ntfs_file_mmap_prepare, + .open = ntfs_stream_file_open, + .release = ntfs_stream_file_release, + .splice_read = ntfs_file_splice_read, + .splice_write = iter_file_splice_write, +}; + +const struct inode_operations ntfs_stream_inode_ops = { + .permission = ntfs_stream_permission, + .setattr = ntfs_stream_setattr, + .getattr = ntfs_stream_getattr, + .listxattr = ntfs_stream_listxattr, +#ifdef CONFIG_NTFS_FS_POSIX_ACL + .get_acl = ntfs_stream_get_acl, + .set_acl = ntfs_stream_set_acl, +#endif + .update_time = ntfs_stream_update_time, + .fiemap = ntfs_fiemap, +}; diff --git a/fs/ntfs/namei.c b/fs/ntfs/namei.c index 3cf58befd77f..9251951a76fb 100644 --- a/fs/ntfs/namei.c +++ b/fs/ntfs/namei.c @@ -8,6 +8,7 @@ #include #include +#include #include "ntfs.h" #include "time.h" @@ -15,6 +16,7 @@ #include "reparse.h" #include "object_id.h" #include "ea.h" +#include "stream.h" static const __le16 aux_name_le[3] = { cpu_to_le16('A'), cpu_to_le16('U'), cpu_to_le16('X') @@ -57,7 +59,18 @@ static inline int ntfs_check_bad_char(const __le16 *wc, unsigned int wc_len) return 0; } -static int ntfs_check_bad_windows_name(struct ntfs_volume *vol, +/* + * ntfs_check_bad_windows_name() - Validate a name against Windows rules + * @vol: NTFS volume + * @wc: UTF-16LE name + * @wc_len: length of @wc in UTF-16 code units + * + * When Windows-name checks are enabled, reject disallowed characters, + * trailing spaces or dots, and reserved DOS device names. + * + * Return: 0 if valid, or -EINVAL otherwise. + */ +int ntfs_check_bad_windows_name(struct ntfs_volume *vol, const __le16 *wc, unsigned int wc_len) { @@ -167,19 +180,29 @@ static int ntfs_check_bad_windows_name(struct ntfs_volume *vol, * * Locking: Caller must hold i_mutex on the directory. */ + static struct dentry *ntfs_lookup(struct inode *dir_ino, struct dentry *dent, unsigned int flags) { struct ntfs_volume *vol = NTFS_SB(dir_ino->i_sb); + struct ntfs_stream_path stream_path; struct inode *dent_inode; __le16 *uname; struct ntfs_name *name = NULL; u64 mref; unsigned long dent_ino; int uname_len; + int stream_path_len; ntfs_debug("Looking up %pd in directory inode 0x%llx.", dent, NTFS_I(dir_ino)->mft_no); + stream_path_len = ntfs_stream_path_parse(vol, &dent->d_name, + &stream_path); + if (stream_path_len < 0) + return ERR_PTR(stream_path_len); + if (stream_path_len) + return ntfs_lookup_stream(dir_ino, dent, &stream_path); + /* Convert the name of the dentry to Unicode. */ uname_len = ntfs_nlstoucs(vol, dent->d_name.name, dent->d_name.len, &uname, NTFS_MAX_NAME_LEN); @@ -405,6 +428,7 @@ static struct ntfs_inode *__ntfs_create(struct mnt_idmap *idmap, struct inode *d struct inode *vi; struct mft_record *ni_mrec, *dni_mrec; struct super_block *sb = dir_ni->vol->sb; + struct inode *rollback_data_vi = NULL, *rollback_sd_vi = NULL; __le64 parent_mft_ref; u64 child_mft_ref; __le16 ea_size; @@ -694,11 +718,25 @@ static struct ntfs_inode *__ntfs_create(struct mnt_idmap *idmap, struct inode *d return ni; err_out: - if (rollback_sd) - ntfs_attr_remove(ni, AT_SECURITY_DESCRIPTOR, AT_UNNAMED, 0); + if (rollback_sd) { + int rollback_err; - if (rollback_data) - ntfs_attr_remove(ni, AT_DATA, AT_UNNAMED, 0); + rollback_err = ntfs_attr_remove(ni, + AT_SECURITY_DESCRIPTOR, AT_UNNAMED, 0, + &rollback_sd_vi); + if (rollback_err) + ntfs_error(sb, + "Failed to roll back security descriptor.\n"); + } + + if (rollback_data) { + int rollback_err; + + rollback_err = ntfs_attr_remove(ni, AT_DATA, AT_UNNAMED, + 0, &rollback_data_vi); + if (rollback_err) + ntfs_error(sb, "Failed to roll back DATA attribute.\n"); + } if (rollback_reparse) ntfs_delete_reparse_index(ni); @@ -726,6 +764,10 @@ err_out: mutex_unlock(&dir_ni->mrec_lock); mutex_unlock(&ni->mrec_lock); + if (rollback_data_vi) + iput(rollback_data_vi); + if (rollback_sd_vi) + iput(rollback_sd_vi); remove_inode_hash(vi); discard_new_inode(vi); return ERR_PTR(err); @@ -736,12 +778,35 @@ static int ntfs_create(struct mnt_idmap *idmap, struct inode *dir, { struct ntfs_volume *vol = NTFS_SB(dir->i_sb); struct ntfs_inode *ni; - __le16 *uname; - int uname_len, err; + struct inode *stream_vi = NULL; + __le16 *uname, *base_name = NULL, *stream_name = NULL; + int uname_len, stream_len, path_result, err; if (NVolShutdown(vol)) return -EIO; + path_result = ntfs_stream_path_names(vol, &dentry->d_name, &base_name, + &uname_len, &stream_name, &stream_len); + if (path_result) { + if (path_result < 0) + return path_result; + stream_vi = ntfs_create_named_stream(idmap, dir, base_name, + uname_len, stream_name, stream_len); + if (IS_ERR(stream_vi)) { + err = PTR_ERR(stream_vi); + goto out_free_stream_names; + } + kmem_cache_free(ntfs_name_cache, stream_name); + kmem_cache_free(ntfs_name_cache, base_name); + d_instantiate(dentry, stream_vi); + return 0; + +out_free_stream_names: + kmem_cache_free(ntfs_name_cache, stream_name); + kmem_cache_free(ntfs_name_cache, base_name); + return err; + } + uname_len = ntfs_nlstoucs(vol, dentry->d_name.name, dentry->d_name.len, &uname, NTFS_MAX_NAME_LEN); if (uname_len < 0) { @@ -758,7 +823,8 @@ static int ntfs_create(struct mnt_idmap *idmap, struct inode *dir, ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); - ni = __ntfs_create(idmap, dir, uname, uname_len, S_IFREG | mode, 0, NULL, 0); + ni = __ntfs_create(idmap, dir, uname, uname_len, S_IFREG | mode, 0, + NULL, 0); kmem_cache_free(ntfs_name_cache, uname); if (IS_ERR(ni)) return PTR_ERR(ni); @@ -850,10 +916,10 @@ static int ntfs_delete(struct ntfs_inode *ni, struct ntfs_inode *dir_ni, struct file_name_attr *fn = NULL; bool looking_for_dos_name = false, looking_for_win32_name = false; bool case_sensitive_match = true; + bool link_count_zero = false; int err = 0; struct mft_record *ni_mrec; struct super_block *sb; - bool link_count_zero = false; ntfs_debug("Entering.\n"); @@ -1025,6 +1091,11 @@ static int ntfs_unlink(struct inode *dir, struct dentry *dentry) if (NVolShutdown(vol)) return -EIO; + if (ntfs_stream_path_has_colon(vol, &dentry->d_name)) + return ntfs_unlink_named_stream(dir, dentry); + if (NInoAttr(ni)) + return -EOPNOTSUPP; + uname_len = ntfs_nlstoucs(vol, dentry->d_name.name, dentry->d_name.len, &uname, NTFS_MAX_NAME_LEN); if (uname_len < 0) { @@ -1068,6 +1139,10 @@ static struct dentry *ntfs_mkdir(struct mnt_idmap *idmap, struct inode *dir, if (NVolShutdown(vol)) return ERR_PTR(-EIO); + err = ntfs_stream_path_check(vol, &dentry->d_name); + if (err) + return ERR_PTR(err); + uname_len = ntfs_nlstoucs(vol, dentry->d_name.name, dentry->d_name.len, &uname, NTFS_MAX_NAME_LEN); if (uname_len < 0) { @@ -1084,7 +1159,8 @@ static struct dentry *ntfs_mkdir(struct mnt_idmap *idmap, struct inode *dir, ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); - ni = __ntfs_create(idmap, dir, uname, uname_len, mode, 0, NULL, 0); + ni = __ntfs_create(idmap, dir, uname, uname_len, mode, 0, + NULL, 0); kmem_cache_free(ntfs_name_cache, uname); if (IS_ERR(ni)) { err = PTR_ERR(ni); @@ -1108,6 +1184,10 @@ static int ntfs_rmdir(struct inode *dir, struct dentry *dentry) if (NVolShutdown(vol)) return -EIO; + err = ntfs_stream_path_check(vol, &dentry->d_name); + if (err) + return err; + ni = NTFS_I(vi); uname_len = ntfs_nlstoucs(vol, dentry->d_name.name, dentry->d_name.len, &uname, NTFS_MAX_NAME_LEN); @@ -1272,6 +1352,13 @@ static int ntfs_rename(struct mnt_idmap *idmap, struct inode *old_dir, if (NVolShutdown(old_dir_ni->vol)) return -EIO; + err = ntfs_stream_path_check(vol, &old_dentry->d_name); + if (err) + return err; + err = ntfs_stream_path_check(vol, &new_dentry->d_name); + if (err) + return err; + if (flags & (RENAME_EXCHANGE | RENAME_WHITEOUT)) return -EINVAL; @@ -1419,6 +1506,10 @@ static int ntfs_symlink(struct mnt_idmap *idmap, struct inode *dir, if (NVolShutdown(vol)) return -EIO; + err = ntfs_stream_path_check(vol, &dentry->d_name); + if (err) + return err; + usrc_len = ntfs_nlstoucs(vol, dentry->d_name.name, dentry->d_name.len, &usrc, NTFS_MAX_NAME_LEN); if (usrc_len < 0) { @@ -1464,6 +1555,10 @@ static int ntfs_mknod(struct mnt_idmap *idmap, struct inode *dir, if (NVolShutdown(vol)) return -EIO; + err = ntfs_stream_path_check(vol, &dentry->d_name); + if (err) + return err; + uname_len = ntfs_nlstoucs(vol, dentry->d_name.name, dentry->d_name.len, &uname, NTFS_MAX_NAME_LEN); if (uname_len < 0) { @@ -1516,6 +1611,13 @@ static int ntfs_link(struct dentry *old_dentry, struct inode *dir, if (NVolShutdown(vol)) return -EIO; + if (NInoAttr(ni)) + return -EOPNOTSUPP; + + err = ntfs_stream_path_check(vol, &dentry->d_name); + if (err) + return err; + uname_len = ntfs_nlstoucs(vol, dentry->d_name.name, dentry->d_name.len, &uname, NTFS_MAX_NAME_LEN); if (uname_len < 0) { @@ -1669,11 +1771,19 @@ static struct dentry *ntfs_fh_to_parent(struct super_block *sb, struct fid *fid, ntfs_nfs_get_inode); } +static int ntfs_encode_fh(struct inode *inode, u32 *fh, int *max_len, + struct inode *parent) +{ + if (ntfs_inode_is_named_stream(NTFS_I(inode))) + return -EOPNOTSUPP; + return generic_encode_ino32_fh(inode, fh, max_len, parent); +} + /* * Export operations allowing NFS exporting of mounted NTFS partitions. */ const struct export_operations ntfs_export_ops = { - .encode_fh = generic_encode_ino32_fh, + .encode_fh = ntfs_encode_fh, .get_parent = ntfs_get_parent, /* Find the parent of a given directory. */ .fh_to_dentry = ntfs_fh_to_dentry, .fh_to_parent = ntfs_fh_to_parent, diff --git a/fs/ntfs/stream.h b/fs/ntfs/stream.h index da0096d2b751..5cbf6e8f6b68 100644 --- a/fs/ntfs/stream.h +++ b/fs/ntfs/stream.h @@ -7,8 +7,38 @@ struct ntfs_inode; struct ntfs_volume; +struct ntfs_stream_path { + const char *base_name; + unsigned int base_len; + const char *stream_name; + unsigned int stream_len; +}; + +int ntfs_check_bad_windows_name(struct ntfs_volume *vol, + const __le16 *name, unsigned int name_len); int ntfs_check_stream_name(const __le16 *name, unsigned int name_len); +int ntfs_stream_path_parse(struct ntfs_volume *vol, + const struct qstr *qname, struct ntfs_stream_path *path); +bool ntfs_stream_path_has_colon(struct ntfs_volume *vol, + const struct qstr *qname); +int ntfs_stream_path_check(struct ntfs_volume *vol, + const struct qstr *qname); +int ntfs_stream_path_names(struct ntfs_volume *vol, + const struct qstr *qname, __le16 **base_name, int *base_len, + __le16 **stream_name, int *stream_len); + +struct inode *ntfs_stream_lookup_inode_by_name(struct inode *dir, + __le16 *name, int name_len); +struct dentry *ntfs_lookup_stream(struct inode *dir, struct dentry *dentry, + const struct ntfs_stream_path *path); +struct inode *ntfs_create_named_stream(struct mnt_idmap *idmap, + struct inode *dir, __le16 *base_name, int base_len, + __le16 *stream_name, int stream_len); +int ntfs_unlink_named_stream(struct inode *dir, struct dentry *dentry); + +void ntfs_set_default_dentry_ops(struct super_block *sb); + void ntfs_stream_inode_refresh(struct inode *inode); int ntfs_stream_inode_validate(struct inode *inode); void ntfs_stream_update_base_time(struct ntfs_inode *base_ni); @@ -18,11 +48,19 @@ int ntfs_stream_put(struct inode *inode); int ntfs_remove_named_stream(struct ntfs_inode *ni, __le16 *name, u32 name_len, struct inode *expected_inode); +int ntfs_stream_validate_for_mutation(struct inode *inode, bool nowait); long ntfs_ioctl_stream_read(struct file *file, unsigned long arg); long ntfs_ioctl_stream_write(struct file *file, unsigned long arg); long ntfs_ioctl_stream_remove(struct file *file, unsigned long arg); int ntfs_ioctl_list_streams(struct file *file, unsigned long arg); +extern const struct file_operations ntfs_stream_file_ops; +extern const struct inode_operations ntfs_stream_inode_ops; + +/* + * Common file operations used by both ordinary files and named streams. + * Their implementations remain in file.c. + */ int ntfs_file_open(struct inode *inode, struct file *file); int ntfs_file_release(struct inode *inode, struct file *file); int ntfs_file_fsync(struct file *file, loff_t start, loff_t end, diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c index d174867b9f5f..242fb19ced2b 100644 --- a/fs/ntfs/super.c +++ b/fs/ntfs/super.c @@ -23,6 +23,7 @@ #include "ea.h" #include "bitmap.h" #include "volume.h" +#include "stream.h" /* A global default upcase table and a corresponding reference count. */ static __le16 *default_upcase; @@ -67,6 +68,17 @@ static const struct constant_table ntfs_symlink_enums[] = { {} }; +enum { + STREAMS_INTERFACE_NONE, + STREAMS_INTERFACE_WINDOWS, +}; + +static const struct constant_table ntfs_streams_interface_enums[] = { + { "none", STREAMS_INTERFACE_NONE }, + { "windows", STREAMS_INTERFACE_WINDOWS }, + {} +}; + enum { Opt_uid, Opt_gid, @@ -92,6 +104,7 @@ enum { Opt_nocase, Opt_native_symlink, Opt_symlink, + Opt_streams_interface, }; static const struct fs_parameter_spec ntfs_parameters[] = { @@ -119,6 +132,8 @@ static const struct fs_parameter_spec ntfs_parameters[] = { fsparam_flag("nocase", Opt_nocase), fsparam_enum("native_symlink", Opt_native_symlink, ntfs_native_symlink_enums), fsparam_enum("symlink", Opt_symlink, ntfs_symlink_enums), + fsparam_enum("streams_interface", Opt_streams_interface, + ntfs_streams_interface_enums), {} }; @@ -255,6 +270,22 @@ static int ntfs_parse_param(struct fs_context *fc, struct fs_parameter *param) else NVolClearSymlinkNative(vol); break; + case Opt_streams_interface: { + bool streams_windows = + result.uint_32 == STREAMS_INTERFACE_WINDOWS; + + if (fc->purpose == FS_CONTEXT_FOR_RECONFIGURE && + NVolStreamsWindows(vol) != streams_windows) { + ntfs_error(vol->sb, + "Cannot change streams_interface on remount."); + return -EINVAL; + } + if (streams_windows) + NVolSetStreamsWindows(vol); + else + NVolClearStreamsWindows(vol); + break; + } case Opt_sparse: break; default: @@ -2725,6 +2756,8 @@ static int ntfs_fill_super(struct super_block *sb, struct fs_context *fc) * operations and associated address space operations to function. */ sb->s_op = &ntfs_sops; + if (NVolStreamsWindows(vol)) + ntfs_set_default_dentry_ops(sb); tmp_ino = new_inode(sb); if (!tmp_ino) { if (!silent) diff --git a/fs/ntfs/volume.h b/fs/ntfs/volume.h index 6d9d6eb79a7c..ed5c0ccb9cd9 100644 --- a/fs/ntfs/volume.h +++ b/fs/ntfs/volume.h @@ -197,6 +197,7 @@ struct ntfs_volume { * NV_DisableSparse Disable creation of sparse regions. * NV_NativeSymlinkRel Translate absolute Windows reparse targets (native_symlink=rel). * NV_MftBootstrap Mount is still assembling $MFT's own runlist. + * NV_StreamsWindows Interpret the final path component as file:stream. */ enum { NV_Errors, @@ -218,6 +219,7 @@ enum { NV_NativeSymlinkRel, NV_SymlinkNative, NV_MftBootstrap, + NV_StreamsWindows, }; /* @@ -258,6 +260,7 @@ DEFINE_NVOL_BIT_OPS(DisableSparse) DEFINE_NVOL_BIT_OPS(NativeSymlinkRel) DEFINE_NVOL_BIT_OPS(SymlinkNative) DEFINE_NVOL_BIT_OPS(MftBootstrap) +DEFINE_NVOL_BIT_OPS(StreamsWindows) static inline void ntfs_inc_free_clusters(struct ntfs_volume *vol, s64 nr) { diff --git a/fs/ntfs/wof.c b/fs/ntfs/wof.c index 9847259e5b1a..294dbe5d711f 100644 --- a/fs/ntfs/wof.c +++ b/fs/ntfs/wof.c @@ -311,7 +311,8 @@ static int parse_wof_chunk_table(struct ntfs_inode *base_ni, goto out_unlock_mrec; } ret = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, - CASE_SENSITIVE, 0, NULL, 0, ctx); + CASE_SENSITIVE, + 0, NULL, 0, ctx); if (ret) goto out_put_ctx; @@ -399,7 +400,8 @@ static int ntfs_read_wof_chunk(struct ntfs_volume *vol, } err = ntfs_attr_lookup(wof_ni->type, wof_ni->name, wof_ni->name_len, - CASE_SENSITIVE, 0, NULL, 0, ctx); + CASE_SENSITIVE, + 0, NULL, 0, ctx); if (err) goto out_put_ctx; -- cgit v1.2.3 From 54bd2da6ce593854032bb40275a1025dd7ba3405 Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Wed, 23 Sep 2026 14:02:37 +0800 Subject: ntfs: keep the EA presence flag after creating the first entry The successful EA update path tests ea_info_qsize when updating NInoHasEA, but this size excludes the appended entry. Creating the first EA, or replacing the only entry, can therefore clear the cached presence flag despite a successful update. Reads then return ENODATA until reload. Include the appended entry in the size used for the presence test. Signed-off-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/ea.c | 1 + 1 file changed, 1 insertion(+) (limited to 'fs') diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c index 1104fd72cd2a..a62e77bc9954 100644 --- a/fs/ntfs/ea.c +++ b/fs/ntfs/ea.c @@ -403,6 +403,7 @@ alloc_new_ea: if (packed_ea_size) *packed_ea_size = p_ea_info->ea_length; + ea_info_qsize += new_ea_size; mark_mft_record_dirty(ni); out: if (!err) { -- cgit v1.2.3 From 961a92c3d9f264e355d11e21f11317af71fe9840 Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Wed, 23 Sep 2026 14:02:38 +0800 Subject: ntfs: validate EA chains and link zero-terminated tails A zero next-entry offset terminates an EA list. Appending an entry without linking the old tail makes the new entry unreachable. Invalid entries are also treated as missing names, allowing setters to proceed on corrupt metadata. Validate the entire chain before modifying it and connect a zero tail to the append position. Check name terminators and retain the full tail span when removing an entry. Return EUCLEAN for malformed EA structures and inconsistent information lengths, preserving that error through the get, set and list paths. Keep ENOENT for a missing name and translate it to ENODATA only at the xattr read boundary. Preserve lower-level read errors. Reuse the same full-chain validation in ntfs_listxattr(), including size-only queries, before copying names. Reject malformed lists with EUCLEAN instead of returning an empty or partial list. Signed-off-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/ea.c | 81 +++++++++++++++++++++++++++++++++++------------------------- 1 file changed, 48 insertions(+), 33 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c index a62e77bc9954..c7f9942d9373 100644 --- a/fs/ntfs/ea.c +++ b/fs/ntfs/ea.c @@ -53,10 +53,13 @@ static int ntfs_ea_lookup(char *ea_buf, s64 ea_buf_size, const char *name, loff_t offset, p_ea_size; unsigned int next; + if (!ea_buf_size) + return -ENOENT; + offset = 0; do { if (ea_buf_size - offset < sizeof(struct ea_attr)) - break; + return -EUCLEAN; p_ea = (const struct ea_attr *)&ea_buf[offset]; next = le32_to_cpu(p_ea->next_entry_offset); @@ -64,31 +67,31 @@ static int ntfs_ea_lookup(char *ea_buf, s64 ea_buf_size, const char *name, if (p_ea_size < sizeof(struct ea_attr) || offset + p_ea_size > ea_buf_size) - break; + return -EUCLEAN; if ((s64)p_ea->ea_name_length + 1 > p_ea_size - offsetof(struct ea_attr, ea_name)) - break; + return -EUCLEAN; actual_size = ALIGN(struct_size(p_ea, ea_name, 1 + p_ea->ea_name_length + le16_to_cpu(p_ea->ea_value_length)), 4); - if (actual_size > p_ea_size) - break; + if (actual_size > p_ea_size || + p_ea->ea_name[p_ea->ea_name_length]) + return -EUCLEAN; - if (p_ea->ea_name_length == name_len && + if (name && p_ea->ea_name_length == name_len && !memcmp(p_ea->ea_name, name, name_len)) { *ea_offset = offset; - *ea_size = next ? next : actual_size; + *ea_size = p_ea_size; if (ea_buf_size < *ea_offset + *ea_size) - goto out; + return -EUCLEAN; return 0; } offset += next; } while (next > 0 && offset < ea_buf_size); -out: return -ENOENT; } @@ -126,7 +129,7 @@ static int ntfs_get_ea(struct inode *inode, const char *name, size_t name_len, return PTR_ERR(p_ea_info); if (ea_info_size != sizeof(struct ea_information)) { kvfree(p_ea_info); - return -EIO; + return -EUCLEAN; } ea_info_qlen = le32_to_cpu(p_ea_info->ea_query_length); @@ -137,7 +140,7 @@ static int ntfs_get_ea(struct inode *inode, const char *name, size_t name_len, return PTR_ERR(ea_buf); if (ea_info_qlen > all_ea_size) { - err = -EIO; + err = -EUCLEAN; goto free_ea_buf; } @@ -162,7 +165,8 @@ static int ntfs_get_ea(struct inode *inode, const char *name, size_t name_len, return ea_value_len; } - err = -ENODATA; + if (err == -ENOENT) + err = -ENODATA; free_ea_buf: kvfree(ea_buf); return err; @@ -216,7 +220,7 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len, goto out; } if (ea_info_size != sizeof(struct ea_information)) { - err = -EIO; + err = -EUCLEAN; goto out; } @@ -255,12 +259,29 @@ create_ea_info: } if (ea_info_qsize > all_ea_size) { - err = -EIO; + err = -EUCLEAN; + goto out; + } + + /* Validate the whole chain before modifying it, including its tail. */ + err = ntfs_ea_lookup(ea_buf, ea_info_qsize, NULL, 0, &ea_off, + &ea_size); + if (err != -ENOENT) goto out; + /* A zero tail offset must be linked before appending another EA. */ + for (ea_off = 0; ea_off < ea_info_qsize; ea_off += ea_size) { + p_ea = (struct ea_attr *)(ea_buf + ea_off); + ea_size = le32_to_cpu(p_ea->next_entry_offset); + if (!ea_size) { + ea_size = ea_info_qsize - ea_off; + p_ea->next_entry_offset = cpu_to_le32(ea_size); + } } err = ntfs_ea_lookup(ea_buf, ea_info_qsize, name, name_len, &ea_off, &ea_size); + if (err && err != -ENOENT) + goto out; if (ea_info_qsize && !err) { if (flags & XATTR_CREATE) { err = -EEXIST; @@ -538,7 +559,7 @@ ssize_t ntfs_listxattr(struct dentry *dentry, char *buffer, size_t size) goto out; } if (ea_info_size != sizeof(struct ea_information)) { - err = -EIO; + err = -EUCLEAN; goto out; } @@ -551,32 +572,26 @@ ssize_t ntfs_listxattr(struct dentry *dentry, char *buffer, size_t size) goto out; } - if (ea_info_qsize > ea_buf_size || ea_info_qsize == 0) + if (ea_info_qsize > ea_buf_size) { + err = -EUCLEAN; + goto out; + } + + /* Validate the entire chain, including size-only queries. */ + err = ntfs_ea_lookup(ea_buf, ea_info_qsize, NULL, 0, &offset, + &ea_size); + if (err != -ENOENT) + goto out; + err = 0; + if (!ea_info_qsize) goto out; offset = 0; do { - if (ea_info_qsize - offset < sizeof(struct ea_attr)) { - err = -EIO; - goto out; - } - p_ea = (const struct ea_attr *)&ea_buf[offset]; next = le32_to_cpu(p_ea->next_entry_offset); ea_size = next ? next : (ea_info_qsize - offset); - if (ea_size < sizeof(struct ea_attr) || - offset + ea_size > ea_info_qsize) { - err = -EIO; - goto out; - } - - if ((int)p_ea->ea_name_length + 1 > - ea_size - offsetof(struct ea_attr, ea_name)) { - err = -EIO; - goto out; - } - if (buffer) { if (ret + p_ea->ea_name_length + 1 > size) { err = -ERANGE; -- cgit v1.2.3 From 7de73288ee23039cccf3287ef6fc86758af6e9c2 Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Wed, 23 Sep 2026 14:02:39 +0800 Subject: ntfs: propagate EA write and truncate errors ntfs_write_ea() replaces negative write errors with EIO and ignores errors when shrinking the attribute after writing a shorter list. This can report success even though the final resize failed. Preserve negative write errors, use EIO for short writes, and return truncate failures to the caller. Signed-off-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/ea.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'fs') diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c index c7f9942d9373..e5c3a5d350ea 100644 --- a/fs/ntfs/ea.c +++ b/fs/ntfs/ea.c @@ -32,7 +32,7 @@ static int ntfs_write_ea(struct ntfs_inode *ni, __le32 type, char *value, s64 ea written = ntfs_inode_attr_pwrite(ea_vi, ea_off, ea_size, value, false); if (written != ea_size) - err = -EIO; + err = written < 0 ? written : -EIO; else { struct ntfs_inode *ea_ni = NTFS_I(ea_vi); -- cgit v1.2.3 From 63426217928d3dbacbb4735a302f4b71a8160cdd Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Wed, 23 Sep 2026 14:02:40 +0800 Subject: ntfs: preserve EA attribute lookup errors ntfs_attr_exist() treats lookup failures as absence. Using it while updating EAs can select creation or removal paths after an allocation or I/O failure. Use an EA-local lookup helper that preserves errors from mapping the MFT record, allocating the search context and looking up the attribute. Check both EA attributes before modifying either and return EUCLEAN if only one exists. Keep the known presence state for the final update. Signed-off-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/ea.c | 52 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 16 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c index e5c3a5d350ea..c6339885f5a2 100644 --- a/fs/ntfs/ea.c +++ b/fs/ntfs/ea.c @@ -181,6 +181,29 @@ static inline int ea_packed_size(const struct ea_attr *p_ea) return 5 + p_ea->ea_name_length + le16_to_cpu(p_ea->ea_value_length); } +/* Unlike ntfs_attr_exist(), preserve lookup errors. */ +static int ntfs_ea_attr_lookup(struct ntfs_inode *ni, __le32 type) +{ + struct ntfs_attr_search_ctx *ctx; + struct mft_record *mrec; + int err; + + mrec = map_mft_record(ni); + if (IS_ERR(mrec)) + return PTR_ERR(mrec); + ctx = ntfs_attr_get_search_ctx(ni, mrec); + if (!ctx) { + err = -ENOMEM; + goto out; + } + err = ntfs_attr_lookup(type, AT_UNNAMED, 0, CASE_SENSITIVE, + 0, NULL, 0, ctx); + ntfs_attr_put_search_ctx(ctx); +out: + unmap_mft_record(ni); + return err; +} + /* * Set a new EA, and set EA_INFORMATION accordingly * @@ -198,7 +221,8 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len, { struct ntfs_inode *ni = NTFS_I(inode); struct ea_information *p_ea_info = NULL; - int ea_packed, err = 0; + int ea_packed, err = 0, ea_err; + bool has_ea; struct ea_attr *p_ea; u32 ea_info_qsize = 0; char *ea_buf = NULL; @@ -211,7 +235,16 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len, if (name_len > 255) return -ENAMETOOLONG; - if (ntfs_attr_exist(ni, AT_EA_INFORMATION, AT_UNNAMED, 0)) { + err = ntfs_ea_attr_lookup(ni, AT_EA_INFORMATION); + if (err && err != -ENOENT) + return err; + ea_err = ntfs_ea_attr_lookup(ni, AT_EA); + if (ea_err && ea_err != -ENOENT) + return ea_err; + if (!err != !ea_err) + return -EUCLEAN; + has_ea = !err; + if (has_ea) { p_ea_info = ntfs_attr_readall(ni, AT_EA_INFORMATION, NULL, 0, &ea_info_size); if (IS_ERR(p_ea_info)) { @@ -230,15 +263,8 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len, ea_buf = NULL; goto out; } - if (!ea_buf) { - ea_info_qsize = 0; - kvfree(p_ea_info); - goto create_ea_info; - } - ea_info_qsize = le32_to_cpu(p_ea_info->ea_query_length); } else { -create_ea_info: p_ea_info = kzalloc_obj(struct ea_information, GFP_NOFS); if (!p_ea_info) return -ENOMEM; @@ -249,12 +275,6 @@ create_ea_info: if (err) goto out; - if (ntfs_attr_exist(ni, AT_EA, AT_UNNAMED, 0)) { - err = ntfs_attr_remove(ni, AT_EA, AT_UNNAMED, 0, NULL); - if (err) - goto out; - } - goto alloc_new_ea; } @@ -405,7 +425,7 @@ alloc_new_ea: /* * no EA or EA_INFORMATION : add them */ - if (!ntfs_attr_exist(ni, AT_EA, AT_UNNAMED, 0)) { + if (!has_ea) { err = ntfs_attr_add(ni, AT_EA, AT_UNNAMED, 0, ea_buf, ea_info_qsize + new_ea_size); if (err) -- cgit v1.2.3 From 22a82237d24da41229a1a910ac4e4c03ed7cf4dd Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Wed, 23 Sep 2026 14:02:41 +0800 Subject: ntfs: distinguish empty EA values from removal The EA setter identifies removal using a zero value length together with XATTR_REPLACE. A replacement with a valid empty value is therefore mistaken for deletion. Use a NULL value to identify removal and keep zero-length values valid. Reject removal or XATTR_REPLACE for an absent entry before creating metadata, including when the file has no EA attributes. Signed-off-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/ea.c | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c index c6339885f5a2..e06ee7c2f3b9 100644 --- a/fs/ntfs/ea.c +++ b/fs/ntfs/ea.c @@ -244,6 +244,8 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len, if (!err != !ea_err) return -EUCLEAN; has_ea = !err; + if (!has_ea && (!value || (flags & XATTR_REPLACE))) + return -ENODATA; if (has_ea) { p_ea_info = ntfs_attr_readall(ni, AT_EA_INFORMATION, NULL, 0, &ea_info_size); @@ -307,7 +309,7 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len, err = -EEXIST; goto out; } - if ((flags & XATTR_REPLACE) && !val_size) { + if (!value) { old_ea_info = *p_ea_info; old_ea_buf = kvmemdup(ea_buf, all_ea_size, GFP_NOFS); if (!old_ea_buf) { @@ -317,7 +319,7 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len, } /* Check the final $EA size before removing the old entry. */ - if (val_size && + if (value && ntfs_attr_size_bounds_check(ni->vol, AT_EA, ea_info_qsize - ea_size + new_ea_size)) { err = -EFBIG; @@ -326,7 +328,7 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len, p_ea = (struct ea_attr *)(ea_buf + ea_off); - if (val_size && + if (value && le16_to_cpu(p_ea->ea_value_length) == val_size && !memcmp(p_ea->ea_name + p_ea->ea_name_length + 1, value, val_size)) @@ -341,7 +343,7 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len, 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) { + if (!value && !ea_info_qsize) { err = ntfs_attr_remove(ni, AT_EA, AT_UNNAMED, 0, NULL); if (err) goto out; @@ -357,7 +359,7 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len, goto out; } - if ((flags & XATTR_REPLACE) && !val_size) { + if (!value) { err = ntfs_write_ea(ni, AT_EA, ea_buf, 0, ea_info_qsize, true); if (err) { @@ -378,7 +380,7 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len, goto out; } } else { - if (flags & XATTR_REPLACE) { + if (!value || (flags & XATTR_REPLACE)) { err = -ENODATA; goto out; } -- cgit v1.2.3 From d940541a50c2701dee9ab9bdac9d6194566a63d0 Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Wed, 23 Sep 2026 14:02:42 +0800 Subject: ntfs: restore EA attributes when an update fails Creating or replacing an EA can leave EA data and information out of sync if a later update fails. Existing deletion rollback ignores its own errors, and creation changes metadata before preparing the new list. Prepare the complete new list while retaining both original attributes. Update EA data and information only after preparation succeeds, restore both on failure, and publish the cached presence state only on success. Keep structure errors as EUCLEAN and preserve operation errors. Mark the volume erroneous and shut it down if either restoration fails. This handles synchronous failures; it does not make the two updates atomic across power loss or guarantee restoration on a failing device. Signed-off-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/ea.c | 350 +++++++++++++++++++++++------------------------------------ 1 file changed, 138 insertions(+), 212 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c index e06ee7c2f3b9..91a4ede33e28 100644 --- a/fs/ntfs/ea.c +++ b/fs/ntfs/ea.c @@ -204,265 +204,191 @@ out: return err; } +/* A NULL value means that the attribute should not exist. */ +static int ntfs_ea_update_attr(struct ntfs_inode *ni, __le32 type, + char *value, s64 size) +{ + int err = ntfs_ea_attr_lookup(ni, type); + + if (err && err != -ENOENT) + return err; + if (!value) + return err == -ENOENT ? 0 : + ntfs_attr_remove(ni, type, AT_UNNAMED, 0, NULL); + if (err == -ENOENT) + return ntfs_attr_add(ni, type, AT_UNNAMED, 0, value, size); + return ntfs_write_ea(ni, type, value, 0, size, true); +} + /* - * Set a new EA, and set EA_INFORMATION accordingly - * - * This is roughly the same as ZwSetEaFile() on Windows, however - * the "offset to next" of the last EA should not be cleared. - * - * Consistency of the new EA is first checked. - * - * EA_INFORMATION is set first, and it is restored to its former - * state if setting EA fails. + * Prepare the new EA list before changing either NTFS attribute. Keep the + * original values until both updates succeed, so a failed update can be + * rolled back. This does not provide atomicity across a crash or power loss. */ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len, const void *value, size_t val_size, int flags, __le16 *packed_ea_size) { struct ntfs_inode *ni = NTFS_I(inode); - struct ea_information *p_ea_info = NULL; - int ea_packed, err = 0, ea_err; - bool has_ea; - 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; - - if (name_len > 255) + struct ea_information *old_info = NULL, info = {}; + char *old_ea = NULL, *ea = NULL; + struct ea_attr *entry; + s64 old_size = 0, info_size, offset = 0, entry_size = 0; + u32 size = 0, new_size, next; + size_t add_size; + int err, info_err, ea_err, packed = 0, needed = 0; + bool exists, remove = !value; + + if (name_len > U8_MAX) return -ENAMETOOLONG; + if (val_size > U16_MAX) + return -E2BIG; err = ntfs_ea_attr_lookup(ni, AT_EA_INFORMATION); if (err && err != -ENOENT) return err; - ea_err = ntfs_ea_attr_lookup(ni, AT_EA); - if (ea_err && ea_err != -ENOENT) - return ea_err; - if (!err != !ea_err) + exists = !err; + err = ntfs_ea_attr_lookup(ni, AT_EA); + if (err && err != -ENOENT) + return err; + /* Do not silently discard an orphaned attribute. */ + if (exists != !err) return -EUCLEAN; - has_ea = !err; - if (!has_ea && (!value || (flags & XATTR_REPLACE))) - return -ENODATA; - if (has_ea) { - p_ea_info = ntfs_attr_readall(ni, AT_EA_INFORMATION, NULL, 0, - &ea_info_size); - if (IS_ERR(p_ea_info)) { - err = PTR_ERR(p_ea_info); - p_ea_info = NULL; - goto out; - } - if (ea_info_size != sizeof(struct ea_information)) { + + if (exists) { + old_info = ntfs_attr_readall(ni, AT_EA_INFORMATION, NULL, 0, + &info_size); + if (IS_ERR(old_info)) + return PTR_ERR(old_info); + if (info_size != sizeof(*old_info)) { err = -EUCLEAN; goto out; } - - ea_buf = ntfs_attr_readall(ni, AT_EA, NULL, 0, &all_ea_size); - if (IS_ERR(ea_buf)) { - err = PTR_ERR(ea_buf); - ea_buf = NULL; + old_ea = ntfs_attr_readall(ni, AT_EA, NULL, 0, &old_size); + if (IS_ERR(old_ea)) { + err = PTR_ERR(old_ea); + old_ea = NULL; goto out; } - ea_info_qsize = le32_to_cpu(p_ea_info->ea_query_length); - } else { - p_ea_info = kzalloc_obj(struct ea_information, GFP_NOFS); - if (!p_ea_info) - return -ENOMEM; - - ea_info_qsize = 0; - err = ntfs_attr_add(ni, AT_EA_INFORMATION, AT_UNNAMED, 0, - (char *)p_ea_info, sizeof(struct ea_information)); - if (err) + size = le32_to_cpu(old_info->ea_query_length); + if (size > old_size) { + err = -EUCLEAN; goto out; - - goto alloc_new_ea; - } - - if (ea_info_qsize > all_ea_size) { - err = -EUCLEAN; - goto out; + } } - /* Validate the whole chain before modifying it, including its tail. */ - err = ntfs_ea_lookup(ea_buf, ea_info_qsize, NULL, 0, &ea_off, - &ea_size); + /* Validate even the entries following the one being replaced. */ + err = ntfs_ea_lookup(old_ea, size, NULL, 0, &offset, &entry_size); if (err != -ENOENT) goto out; - /* A zero tail offset must be linked before appending another EA. */ - for (ea_off = 0; ea_off < ea_info_qsize; ea_off += ea_size) { - p_ea = (struct ea_attr *)(ea_buf + ea_off); - ea_size = le32_to_cpu(p_ea->next_entry_offset); - if (!ea_size) { - ea_size = ea_info_qsize - ea_off; - p_ea->next_entry_offset = cpu_to_le32(ea_size); - } - } - - err = ntfs_ea_lookup(ea_buf, ea_info_qsize, name, name_len, &ea_off, - &ea_size); + err = ntfs_ea_lookup(old_ea, size, name, name_len, &offset, &entry_size); if (err && err != -ENOENT) goto out; - if (ea_info_qsize && !err) { + if (!err) { if (flags & XATTR_CREATE) { err = -EEXIST; goto out; } - if (!value) { - 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 (value && - 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 (value && - le16_to_cpu(p_ea->ea_value_length) == val_size && - !memcmp(p_ea->ea_name + p_ea->ea_name_length + 1, value, - val_size)) - goto out; - - le16_add_cpu(&p_ea_info->ea_length, 0 - ea_packed_size(p_ea)); - - if (p_ea->flags & NEED_EA) - le16_add_cpu(&p_ea_info->need_ea_count, -1); - - memmove((char *)p_ea, (char *)p_ea + ea_size, ea_info_qsize - (ea_off + ea_size)); - ea_info_qsize -= ea_size; - p_ea_info->ea_query_length = cpu_to_le32(ea_info_qsize); - - if (!value && !ea_info_qsize) { - err = ntfs_attr_remove(ni, AT_EA, AT_UNNAMED, 0, NULL); - if (err) - goto out; - - err = ntfs_attr_remove(ni, AT_EA_INFORMATION, AT_UNNAMED, - 0, NULL); - 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; - } - - if (!value) { - 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; + entry = (struct ea_attr *)(old_ea + offset); + if (!remove && le16_to_cpu(entry->ea_value_length) == val_size && + !memcmp(entry->ea_name + name_len + 1, value, val_size)) { + info = *old_info; + goto done; } } else { - if (!value || (flags & XATTR_REPLACE)) { + if (remove || (flags & XATTR_REPLACE)) { err = -ENODATA; goto out; } + offset = size; + entry_size = 0; + } - if (ntfs_attr_size_bounds_check(ni->vol, AT_EA, - ea_info_qsize + new_ea_size)) { - err = -EFBIG; + add_size = remove ? 0 : ALIGN(sizeof(*entry) + name_len + 1 + val_size, 4); + new_size = size - entry_size + add_size; + if (new_size && ntfs_attr_size_bounds_check(ni->vol, AT_EA, new_size)) { + err = -EFBIG; + goto out; + } + if (new_size) { + ea = kvzalloc(new_size, GFP_NOFS); + if (!ea) { + err = -ENOMEM; goto out; } + if (offset) + memcpy(ea, old_ea, offset); + if (size > offset + entry_size) + memcpy(ea + offset, old_ea + offset + entry_size, + size - offset - entry_size); } -alloc_new_ea: - new_ea_buf = kvzalloc(ea_info_qsize + new_ea_size, GFP_NOFS); - if (!new_ea_buf) { - err = -ENOMEM; - goto out; + size -= entry_size; + + /* Link a zero-terminated old tail before appending the new entry. */ + for (offset = 0; offset < size; offset += next) { + entry = (struct ea_attr *)(ea + offset); + next = le32_to_cpu(entry->next_entry_offset); + if (!next) { + next = size - offset; + entry->next_entry_offset = cpu_to_le32(next); + } + packed += ea_packed_size(entry); + if (entry->flags & NEED_EA) + needed++; } - 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. - */ - memcpy(p_ea->ea_name, name, name_len); - p_ea->ea_name_length = name_len; - p_ea->ea_name[name_len] = 0; - memcpy(p_ea->ea_name + name_len + 1, value, val_size); - p_ea->ea_value_length = cpu_to_le16(val_size); - p_ea->next_entry_offset = cpu_to_le32(new_ea_size); - - ea_packed = le16_to_cpu(p_ea_info->ea_length) + ea_packed_size(p_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) { + if (!remove) { + entry = (struct ea_attr *)(ea + size); + entry->next_entry_offset = cpu_to_le32(add_size); + entry->ea_name_length = name_len; + entry->ea_value_length = cpu_to_le16(val_size); + memcpy(entry->ea_name, name, name_len); + memcpy(entry->ea_name + name_len + 1, value, val_size); + packed += ea_packed_size(entry); + } + if (packed > U16_MAX) { err = -EFBIG; goto out; } - - /* - * no EA or EA_INFORMATION : add them - */ - if (!has_ea) { - 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, ea_buf, 0, - ea_info_qsize + new_ea_size, true); - if (err) - goto out; - } - - err = ntfs_write_ea(ni, AT_EA_INFORMATION, (char *)p_ea_info, 0, - sizeof(struct ea_information), false); - if (err) + info.ea_length = cpu_to_le16(packed); + info.need_ea_count = cpu_to_le16(needed); + info.ea_query_length = cpu_to_le32(new_size); + + /* All preparation is complete; errors from here require rollback. */ + err = ntfs_ea_update_attr(ni, AT_EA, ea, new_size); + if (!err) + err = ntfs_ea_update_attr(ni, AT_EA_INFORMATION, + new_size ? (char *)&info : NULL, + sizeof(info)); + if (err) { + /* Try both restorations even if the first one fails. */ + ea_err = ntfs_ea_update_attr(ni, AT_EA, old_ea, old_size); + info_err = ntfs_ea_update_attr(ni, AT_EA_INFORMATION, + (char *)old_info, sizeof(info)); + if (ea_err || info_err) { + NVolSetErrors(ni->vol); + NVolSetShutdown(ni->vol); + ntfs_error(inode->i_sb, + "Failed to restore EAs of inode 0x%llx (%d, %d); shutting down.", + ni->mft_no, ea_err, info_err); + err = -EIO; + } goto out; - - if (packed_ea_size) - *packed_ea_size = p_ea_info->ea_length; - ea_info_qsize += new_ea_size; + } + size = new_size; mark_mft_record_dirty(ni); +done: + if (packed_ea_size) + *packed_ea_size = info.ea_length; + if (size) + NInoSetHasEA(ni); + else + NInoClearHasEA(ni); out: - if (!err) { - if (ea_info_qsize > 0) - NInoSetHasEA(ni); - else - NInoClearHasEA(ni); - } - - kvfree(ea_buf); - kvfree(old_ea_buf); - kvfree(p_ea_info); - + kvfree(ea); + kvfree(old_ea); + kvfree(old_info); return err; } - /* * Check for the presence of an EA "$LXDEV" (used by WSL) * and return its value as a device address -- cgit v1.2.3 From c5dbcbea451b59c24ec4cf2e33091867e520e012 Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Wed, 23 Sep 2026 14:02:43 +0800 Subject: ntfs: add fileattr query support Add fileattr_get operations for files and directories, as well as the existing symlink and special inode operation tables. Report compression, encryption, immutable and append-only from cached inode state, and FS_CASEFOLD_FL from the mount-wide case sensitivity policy. This introduces querying only. It neither reads a private flag EA nor allows userspace to change the reported flags. Signed-off-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- Documentation/filesystems/ntfs.rst | 8 ++++++++ fs/ntfs/file.c | 33 +++++++++++++++++++++++++++++++++ fs/ntfs/namei.c | 1 + fs/ntfs/ntfs.h | 2 ++ 4 files changed, 44 insertions(+) (limited to 'fs') diff --git a/Documentation/filesystems/ntfs.rst b/Documentation/filesystems/ntfs.rst index 556840e0e7e9..fe42a9d22643 100644 --- a/Documentation/filesystems/ntfs.rst +++ b/Documentation/filesystems/ntfs.rst @@ -9,6 +9,7 @@ The Linux NTFS filesystem driver - Overview - Utilities support + - Linux file attributes - Supported mount options @@ -34,6 +35,13 @@ The project is available at: https://github.com/ntfsprogs-plus/ntfsprogs-plus +Linux file attributes +===================== + +The driver supports querying compression, encryption, immutable, append-only +and mount-wide case folding through lsattr(1). Case folding is reported +according to the mount options. + Supported mount options ======================= diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c index a2e843cf947c..549fea79550f 100644 --- a/fs/ntfs/file.c +++ b/fs/ntfs/file.c @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -144,6 +145,35 @@ int ntfs_file_release(struct inode *vi, struct file *filp) return 0; } +/* + * ntfs_fileattr_get - inode_operations::fileattr_get + * @dentry: dentry to report the flags of + * @fa: filled in with the flags of @dentry + */ +int ntfs_fileattr_get(struct dentry *dentry, struct file_kattr *fa) +{ + struct inode *vi = d_inode(dentry); + struct ntfs_inode *ni = NTFS_I(vi); + u32 flags = 0; + + if (NVolShutdown(ni->vol)) + return -EIO; + + if (NInoCompressed(ni) || NInoWofCompressed(ni)) + flags |= FS_COMPR_FL; + if (NInoEncrypted(ni)) + flags |= FS_ENCRYPT_FL; + if (vi->i_flags & S_IMMUTABLE) + flags |= FS_IMMUTABLE_FL; + if (vi->i_flags & S_APPEND) + flags |= FS_APPEND_FL; + if (!NVolCaseSensitive(ni->vol)) + flags |= FS_CASEFOLD_FL; + + fileattr_fill_flags(fa, flags); + return 0; +} + struct ntfs_fsync_attr { struct list_head list; struct inode *inode; @@ -1426,6 +1456,7 @@ const struct file_operations ntfs_file_ops = { }; const struct inode_operations ntfs_file_inode_ops = { + .fileattr_get = ntfs_fileattr_get, .setattr = ntfs_setattr, .getattr = ntfs_getattr, .listxattr = ntfs_listxattr, @@ -1435,12 +1466,14 @@ const struct inode_operations ntfs_file_inode_ops = { }; const struct inode_operations ntfs_symlink_inode_operations = { + .fileattr_get = ntfs_fileattr_get, .get_link = ntfs_get_link, .setattr = ntfs_setattr, .listxattr = ntfs_listxattr, }; const struct inode_operations ntfs_special_inode_operations = { + .fileattr_get = ntfs_fileattr_get, .setattr = ntfs_setattr, .getattr = ntfs_getattr, .listxattr = ntfs_listxattr, diff --git a/fs/ntfs/namei.c b/fs/ntfs/namei.c index 9251951a76fb..c8fdb479a78d 100644 --- a/fs/ntfs/namei.c +++ b/fs/ntfs/namei.c @@ -1660,6 +1660,7 @@ out: * Inode operations for directories. */ const struct inode_operations ntfs_dir_inode_ops = { + .fileattr_get = ntfs_fileattr_get, .lookup = ntfs_lookup, /* VFS: Lookup directory. */ .create = ntfs_create, .unlink = ntfs_unlink, diff --git a/fs/ntfs/ntfs.h b/fs/ntfs/ntfs.h index 9557a5c36b81..8c344794f87e 100644 --- a/fs/ntfs/ntfs.h +++ b/fs/ntfs/ntfs.h @@ -185,6 +185,8 @@ extern const struct inode_operations ntfs_dir_inode_ops; extern const struct file_operations ntfs_empty_file_ops; extern const struct inode_operations ntfs_empty_inode_ops; +int ntfs_fileattr_get(struct dentry *dentry, struct file_kattr *fa); + extern const struct export_operations ntfs_export_ops; /* -- cgit v1.2.3 From d1e72e3f385eafd41e8c2c6b557e99047d001b83 Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Wed, 23 Sep 2026 14:02:44 +0800 Subject: ntfs: persist immutable in the $LINUXFLAGS EA Add a fileattr setter for immutable and store its state in bit 0 of a little-endian u32 in the private $LINUXFLAGS EA. Reuse the standard NTFS EA attributes and restore the flag when loading an inode, so eviction and unmount do not discard the setting. Use $LINUXFLAGS to avoid the WSL-owned $LX namespace. Preserve unknown bits and delete the entry only when the entire value is zero. Reject malformed values with EUCLEAN. Reject direct xattr writes and removal to avoid bypassing fileattr permissions or stale cached state. Update the cached flags only after the EA update succeeds. Reject clearing immutable on system metadata files and files protected by sys_immutable, following inode-load policy. Do not persist a bit derived solely from this policy. Preserve append state without allowing it to be changed at this stage. Compression, encryption and mount-wide case folding remain read-only. Document the format and its Linux-only enforcement. Signed-off-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- Documentation/filesystems/ntfs.rst | 18 ++++++-- fs/ntfs/ea.c | 49 +++++++++++++++++++++ fs/ntfs/ea.h | 6 +++ fs/ntfs/file.c | 90 ++++++++++++++++++++++++++++++++++++-- fs/ntfs/inode.c | 12 +++-- fs/ntfs/inode.h | 2 + fs/ntfs/namei.c | 3 +- fs/ntfs/ntfs.h | 2 + 8 files changed, 171 insertions(+), 11 deletions(-) (limited to 'fs') diff --git a/Documentation/filesystems/ntfs.rst b/Documentation/filesystems/ntfs.rst index fe42a9d22643..a40f60eef0f2 100644 --- a/Documentation/filesystems/ntfs.rst +++ b/Documentation/filesystems/ntfs.rst @@ -38,9 +38,21 @@ The project is available at: Linux file attributes ===================== -The driver supports querying compression, encryption, immutable, append-only -and mount-wide case folding through lsattr(1). Case folding is reported -according to the mount options. +The driver supports lsattr(1) and setting immutable with chattr(1). +Immutable is stored in bit 0 of a private ``$LINUXFLAGS`` extended attribute +within the standard NTFS ``$EA``/``$EA_INFORMATION`` attributes. Its value +is a 32-bit little-endian bitmask. Unknown bits are preserved; the entry +is removed when the entire value becomes zero. The setting survives +inode eviction, unmount and reboot. Direct writes or removal through +xattr interfaces are rejected; use chattr(1) instead. + +System metadata files and files protected by ``sys_immutable`` cannot +have their immutable protection cleared. Protection derived solely from +that policy is not stored in the EA. Compression, encryption, append-only +and mount-wide case folding are reported but cannot be changed through +chattr(1). + +Windows does not enforce the Linux immutable flag stored in this EA. Supported mount options ======================= diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c index 91a4ede33e28..58c5df162fbe 100644 --- a/fs/ntfs/ea.c +++ b/fs/ntfs/ea.c @@ -481,6 +481,51 @@ int ntfs_ea_set_wsl_inode(struct inode *inode, dev_t rdev, __le16 *ea_size, return err; } +int ntfs_ea_get_linuxflags(struct inode *inode) +{ + struct ntfs_inode *ni = NTFS_I(inode); + __le32 value; + int err; + + err = ntfs_get_ea(inode, "$LINUXFLAGS", sizeof("$LINUXFLAGS") - 1, + &value, sizeof(value)); + if (err == -ENODATA) + return 0; + if (err < 0) + return err == -ERANGE ? -EUCLEAN : err; + if (err != sizeof(value)) + return -EUCLEAN; + + ni->linuxflags = le32_to_cpu(value); + if (ni->linuxflags & NTFS_LINUXFLAGS_IMMUTABLE) + inode->i_flags |= S_IMMUTABLE; + return 0; +} + +int ntfs_ea_set_linuxflags(struct inode *inode, u32 linuxflags) +{ + struct ntfs_inode *ni = NTFS_I(inode); + __le32 value; + int err; + + if (linuxflags) { + value = cpu_to_le32(linuxflags); + err = ntfs_set_ea(inode, "$LINUXFLAGS", sizeof("$LINUXFLAGS") - 1, + &value, sizeof(value), 0, NULL); + } else if (NInoHasEA(ni)) { + err = ntfs_set_ea(inode, "$LINUXFLAGS", sizeof("$LINUXFLAGS") - 1, + NULL, 0, XATTR_REPLACE, NULL); + if (err == -ENODATA) + err = 0; + } else { + err = 0; + } + + if (!err) + ni->linuxflags = linuxflags; + return err; +} + ssize_t ntfs_listxattr(struct dentry *dentry, char *buffer, size_t size) { struct inode *inode = d_inode(dentry); @@ -861,6 +906,10 @@ static int ntfs_setxattr(const struct xattr_handler *handler, if (NVolShutdown(ni->vol)) return -EIO; + /* Only fileattr_set may change the flags and their cached state. */ + if (!strcmp(name, "$LINUXFLAGS")) + return -EPERM; + if (ntfs_is_reserved_lxattr(name) && !capable(CAP_SYS_ADMIN)) return -EPERM; diff --git a/fs/ntfs/ea.h b/fs/ntfs/ea.h index acb39c2a6fbc..5ec118e29425 100644 --- a/fs/ntfs/ea.h +++ b/fs/ntfs/ea.h @@ -7,6 +7,10 @@ #define NTFS_EA_GID BIT(2) #define NTFS_EA_MODE BIT(3) +/* $LINUXFLAGS stores these bits in a single little-endian 32-bit value. */ +#define NTFS_LINUXFLAGS_IMMUTABLE BIT(0) +#define NTFS_LINUXFLAGS_MASK NTFS_LINUXFLAGS_IMMUTABLE + 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); @@ -14,6 +18,8 @@ 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); +int ntfs_ea_get_linuxflags(struct inode *inode); +int ntfs_ea_set_linuxflags(struct inode *inode, u32 linuxflags); ssize_t ntfs_listxattr(struct dentry *dentry, char *buffer, size_t size); #ifdef CONFIG_NTFS_FS_POSIX_ACL diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c index 549fea79550f..443b87249fa3 100644 --- a/fs/ntfs/file.c +++ b/fs/ntfs/file.c @@ -174,6 +174,87 @@ int ntfs_fileattr_get(struct dentry *dentry, struct file_kattr *fa) return 0; } +static bool ntfs_is_forced_immutable(struct inode *vi) +{ + struct ntfs_inode *ni = NTFS_I(vi); + + if (ni->mft_no < FILE_first_user && S_ISREG(vi->i_mode)) + return true; + + return NVolSysImmutable(ni->vol) && + (ni->flags & FILE_ATTR_SYSTEM) && + !S_ISFIFO(vi->i_mode) && !S_ISSOCK(vi->i_mode) && + !S_ISLNK(vi->i_mode); +} + +/* + * ntfs_fileattr_set - inode_operations::fileattr_set + * @idmap: idmap of the mount @dentry was found from + * @dentry: dentry to set the flags of + * @fa: flags to set + */ +int ntfs_fileattr_set(struct mnt_idmap *idmap, struct dentry *dentry, + struct file_kattr *fa) +{ + struct inode *vi = d_inode(dentry); + struct ntfs_inode *ni = NTFS_I(vi); + u32 allowed = FS_IMMUTABLE_FL; + u32 readonly = 0; + u32 linuxflags = ni->linuxflags & ~NTFS_LINUXFLAGS_MASK; + unsigned int new_fl = 0; + bool forced_immutable; + int err; + + if (NVolShutdown(ni->vol)) + return -EIO; + + if (fileattr_has_fsx(fa)) + return -EOPNOTSUPP; + + if ((fa->flags & FS_APPEND_FL) != + (IS_APPEND(vi) ? FS_APPEND_FL : 0)) + return -EOPNOTSUPP; + allowed |= fa->flags & FS_APPEND_FL; + + /* chattr passes the unchanged read-only flags back to us too. */ + if (NInoCompressed(ni) || NInoWofCompressed(ni)) + readonly |= FS_COMPR_FL; + if (NInoEncrypted(ni)) + readonly |= FS_ENCRYPT_FL; + if ((fa->flags ^ readonly) & (FS_COMPR_FL | FS_ENCRYPT_FL)) + return -EOPNOTSUPP; + allowed |= readonly; + + /* Case folding is a read-only, mount-wide property. */ + if (!NVolCaseSensitive(ni->vol)) + allowed |= FS_CASEFOLD_FL; + if (fa->flags & ~allowed) + return -EOPNOTSUPP; + + forced_immutable = ntfs_is_forced_immutable(vi); + if (!(fa->flags & FS_IMMUTABLE_FL) && forced_immutable) + return -EPERM; + + if (fa->flags & FS_IMMUTABLE_FL) { + new_fl |= S_IMMUTABLE; + /* Do not persist an immutable bit derived from the mount. */ + if (!forced_immutable || + (ni->linuxflags & NTFS_LINUXFLAGS_IMMUTABLE)) + linuxflags |= NTFS_LINUXFLAGS_IMMUTABLE; + } + + mutex_lock(&ni->mrec_lock); + err = ntfs_ea_set_linuxflags(vi, linuxflags); + mutex_unlock(&ni->mrec_lock); + if (err) + return err; + + inode_set_flags(vi, new_fl, S_IMMUTABLE); + inode_set_ctime_current(vi); + mark_inode_dirty(vi); + return 0; +} + struct ntfs_fsync_attr { struct list_head list; struct inode *inode; @@ -1456,9 +1537,10 @@ const struct file_operations ntfs_file_ops = { }; const struct inode_operations ntfs_file_inode_ops = { - .fileattr_get = ntfs_fileattr_get, .setattr = ntfs_setattr, .getattr = ntfs_getattr, + .fileattr_get = ntfs_fileattr_get, + .fileattr_set = ntfs_fileattr_set, .listxattr = ntfs_listxattr, .get_acl = ntfs_get_acl, .set_acl = ntfs_set_acl, @@ -1466,17 +1548,19 @@ const struct inode_operations ntfs_file_inode_ops = { }; const struct inode_operations ntfs_symlink_inode_operations = { - .fileattr_get = ntfs_fileattr_get, .get_link = ntfs_get_link, .setattr = ntfs_setattr, .listxattr = ntfs_listxattr, + .fileattr_get = ntfs_fileattr_get, + .fileattr_set = ntfs_fileattr_set, }; const struct inode_operations ntfs_special_inode_operations = { - .fileattr_get = ntfs_fileattr_get, .setattr = ntfs_setattr, .getattr = ntfs_getattr, .listxattr = ntfs_listxattr, + .fileattr_get = ntfs_fileattr_get, + .fileattr_set = ntfs_fileattr_set, .get_acl = ntfs_get_acl, .set_acl = ntfs_set_acl, }; diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c index 8d9475788845..206db4e99ef1 100644 --- a/fs/ntfs/inode.c +++ b/fs/ntfs/inode.c @@ -537,6 +537,7 @@ void __ntfs_init_inode(struct super_block *sb, struct ntfs_inode *ni) ni->reparse_tag = 0; ni->reparse_flags = 0; ni->target = NULL; + ni->linuxflags = 0; ni->i_dealloc_clusters = 0; } @@ -727,10 +728,8 @@ static bool ntfs_non_resident_sizes_inconsistent(struct inode *vi, * Q: What locks are held when the function is called? * A: i_state has I_NEW set, hence the inode is locked, also * i_count is set to 1, so it is not going to go away - * i_flags is set to 0 and we have no business touching it. Only an ioctl() - * is allowed to write to them. We should of course be honouring them but - * we need to do that using the IS_* macros defined in include/linux/fs.h. - * In any case ntfs_read_locked_inode() has nothing to do with i_flags. + * i_flags is initialized from the persisted Linux file attributes and + * the driver's system-file protection policy before publishing the inode. * * Return 0 on success and -errno on error. */ @@ -934,6 +933,11 @@ skip_attr_list_load: if (!err) { NInoSetHasEA(ni); ntfs_ea_get_wsl_inode(vi, &dev, flags, &has_lxmod); + err = ntfs_ea_get_linuxflags(vi); + if (err) + goto unm_err_out; + } else if (err != -ENOENT) { + goto unm_err_out; } if (ni->flags & FILE_ATTR_REPARSE_POINT) { diff --git a/fs/ntfs/inode.h b/fs/ntfs/inode.h index f4e5562d56a7..cf447fb51725 100644 --- a/fs/ntfs/inode.h +++ b/fs/ntfs/inode.h @@ -95,6 +95,7 @@ enum ntfs_inode_mutex_lock_class { * belongs. * @i_dealloc_clusters: delayed allocated clusters. * @target: symlink buffer. + * @linuxflags: Linux-specific file attributes loaded from the $LINUXFLAGS EA. */ struct ntfs_inode { rwlock_t size_lock; @@ -146,6 +147,7 @@ struct ntfs_inode { __le32 reparse_tag; __le32 reparse_flags; char *target; + u32 linuxflags; }; /* diff --git a/fs/ntfs/namei.c b/fs/ntfs/namei.c index c8fdb479a78d..0db7da017233 100644 --- a/fs/ntfs/namei.c +++ b/fs/ntfs/namei.c @@ -1660,7 +1660,6 @@ out: * Inode operations for directories. */ const struct inode_operations ntfs_dir_inode_ops = { - .fileattr_get = ntfs_fileattr_get, .lookup = ntfs_lookup, /* VFS: Lookup directory. */ .create = ntfs_create, .unlink = ntfs_unlink, @@ -1672,6 +1671,8 @@ const struct inode_operations ntfs_dir_inode_ops = { .listxattr = ntfs_listxattr, .setattr = ntfs_setattr, .getattr = ntfs_getattr, + .fileattr_get = ntfs_fileattr_get, + .fileattr_set = ntfs_fileattr_set, .symlink = ntfs_symlink, .mknod = ntfs_mknod, .link = ntfs_link, diff --git a/fs/ntfs/ntfs.h b/fs/ntfs/ntfs.h index 8c344794f87e..71e53d16b5c1 100644 --- a/fs/ntfs/ntfs.h +++ b/fs/ntfs/ntfs.h @@ -186,6 +186,8 @@ extern const struct file_operations ntfs_empty_file_ops; extern const struct inode_operations ntfs_empty_inode_ops; int ntfs_fileattr_get(struct dentry *dentry, struct file_kattr *fa); +int ntfs_fileattr_set(struct mnt_idmap *idmap, struct dentry *dentry, + struct file_kattr *fa); extern const struct export_operations ntfs_export_ops; -- cgit v1.2.3 From 17cbe71147b2d2fe46458b4251c89b32c72b2f50 Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Wed, 23 Sep 2026 14:02:45 +0800 Subject: ntfs: persist append-only in the $LINUXFLAGS EA Extend the persistent fileattr interface to append-only using bit 1 of LINUXFLAGS. Restore S_APPEND on inode load and update it after successful EA writes. Include append in the known-bit mask while retaining all other unknown bits. Reuse the existing VFS capability checks and EA safeguards. Document append-only alongside immutable. Both flags now survive inode reclaim and remount without changing the NTFS attribute format. Signed-off-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- Documentation/filesystems/ntfs.rst | 10 +++++----- fs/ntfs/ea.c | 2 ++ fs/ntfs/ea.h | 4 +++- fs/ntfs/file.c | 13 ++++++------- 4 files changed, 16 insertions(+), 13 deletions(-) (limited to 'fs') diff --git a/Documentation/filesystems/ntfs.rst b/Documentation/filesystems/ntfs.rst index a40f60eef0f2..386c1205b4a1 100644 --- a/Documentation/filesystems/ntfs.rst +++ b/Documentation/filesystems/ntfs.rst @@ -38,21 +38,21 @@ The project is available at: Linux file attributes ===================== -The driver supports lsattr(1) and setting immutable with chattr(1). -Immutable is stored in bit 0 of a private ``$LINUXFLAGS`` extended attribute +The driver supports lsattr(1) and setting immutable and append-only with chattr(1). +Immutable is stored in bit 0 and append-only in bit 1 of a private ``$LINUXFLAGS`` extended attribute within the standard NTFS ``$EA``/``$EA_INFORMATION`` attributes. Its value is a 32-bit little-endian bitmask. Unknown bits are preserved; the entry -is removed when the entire value becomes zero. The setting survives +is removed when the entire value becomes zero. These settings survive inode eviction, unmount and reboot. Direct writes or removal through xattr interfaces are rejected; use chattr(1) instead. System metadata files and files protected by ``sys_immutable`` cannot have their immutable protection cleared. Protection derived solely from -that policy is not stored in the EA. Compression, encryption, append-only +that policy is not stored in the EA. Compression, encryption and mount-wide case folding are reported but cannot be changed through chattr(1). -Windows does not enforce the Linux immutable flag stored in this EA. +Windows does not enforce the Linux immutable and append-only flags stored in this EA. Supported mount options ======================= diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c index 58c5df162fbe..a1f948c4d2ec 100644 --- a/fs/ntfs/ea.c +++ b/fs/ntfs/ea.c @@ -499,6 +499,8 @@ int ntfs_ea_get_linuxflags(struct inode *inode) ni->linuxflags = le32_to_cpu(value); if (ni->linuxflags & NTFS_LINUXFLAGS_IMMUTABLE) inode->i_flags |= S_IMMUTABLE; + if (ni->linuxflags & NTFS_LINUXFLAGS_APPEND) + inode->i_flags |= S_APPEND; return 0; } diff --git a/fs/ntfs/ea.h b/fs/ntfs/ea.h index 5ec118e29425..b988b39f5f00 100644 --- a/fs/ntfs/ea.h +++ b/fs/ntfs/ea.h @@ -9,7 +9,9 @@ /* $LINUXFLAGS stores these bits in a single little-endian 32-bit value. */ #define NTFS_LINUXFLAGS_IMMUTABLE BIT(0) -#define NTFS_LINUXFLAGS_MASK NTFS_LINUXFLAGS_IMMUTABLE +#define NTFS_LINUXFLAGS_APPEND BIT(1) +#define NTFS_LINUXFLAGS_MASK (NTFS_LINUXFLAGS_IMMUTABLE | \ + NTFS_LINUXFLAGS_APPEND) extern const struct xattr_handler *const ntfs_xattr_handlers[]; diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c index 443b87249fa3..e024a63df9d1 100644 --- a/fs/ntfs/file.c +++ b/fs/ntfs/file.c @@ -198,7 +198,7 @@ int ntfs_fileattr_set(struct mnt_idmap *idmap, struct dentry *dentry, { struct inode *vi = d_inode(dentry); struct ntfs_inode *ni = NTFS_I(vi); - u32 allowed = FS_IMMUTABLE_FL; + u32 allowed = FS_IMMUTABLE_FL | FS_APPEND_FL; u32 readonly = 0; u32 linuxflags = ni->linuxflags & ~NTFS_LINUXFLAGS_MASK; unsigned int new_fl = 0; @@ -211,11 +211,6 @@ int ntfs_fileattr_set(struct mnt_idmap *idmap, struct dentry *dentry, if (fileattr_has_fsx(fa)) return -EOPNOTSUPP; - if ((fa->flags & FS_APPEND_FL) != - (IS_APPEND(vi) ? FS_APPEND_FL : 0)) - return -EOPNOTSUPP; - allowed |= fa->flags & FS_APPEND_FL; - /* chattr passes the unchanged read-only flags back to us too. */ if (NInoCompressed(ni) || NInoWofCompressed(ni)) readonly |= FS_COMPR_FL; @@ -242,6 +237,10 @@ int ntfs_fileattr_set(struct mnt_idmap *idmap, struct dentry *dentry, (ni->linuxflags & NTFS_LINUXFLAGS_IMMUTABLE)) linuxflags |= NTFS_LINUXFLAGS_IMMUTABLE; } + if (fa->flags & FS_APPEND_FL) { + new_fl |= S_APPEND; + linuxflags |= NTFS_LINUXFLAGS_APPEND; + } mutex_lock(&ni->mrec_lock); err = ntfs_ea_set_linuxflags(vi, linuxflags); @@ -249,7 +248,7 @@ int ntfs_fileattr_set(struct mnt_idmap *idmap, struct dentry *dentry, if (err) return err; - inode_set_flags(vi, new_fl, S_IMMUTABLE); + inode_set_flags(vi, new_fl, S_IMMUTABLE | S_APPEND); inode_set_ctime_current(vi); mark_inode_dirty(vi); return 0; -- cgit v1.2.3 From 3311ea26c67cfdbb0982dc1cc864ee6590727b45 Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Wed, 23 Sep 2026 14:02:46 +0800 Subject: ntfs: persist nodump in the $LINUXFLAGS EA Store nodump in bit 2 of LINUXFLAGS and allow it through fileattr_set. Report the cached persistent bit through fileattr_get and STATX_ATTR_NODUMP, including the statx supported-attribute mask. Reuse the existing EA update and inode-load paths; nodump needs no additional inode flag enforcement. Document the complete immutable, append-only and nodump bitmask. Signed-off-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- Documentation/filesystems/ntfs.rst | 34 +++++++++++++++++++--------------- fs/ntfs/ea.h | 3 ++- fs/ntfs/file.c | 11 +++++++++-- 3 files changed, 30 insertions(+), 18 deletions(-) (limited to 'fs') diff --git a/Documentation/filesystems/ntfs.rst b/Documentation/filesystems/ntfs.rst index 386c1205b4a1..699e91f884d4 100644 --- a/Documentation/filesystems/ntfs.rst +++ b/Documentation/filesystems/ntfs.rst @@ -38,21 +38,25 @@ The project is available at: Linux file attributes ===================== -The driver supports lsattr(1) and setting immutable and append-only with chattr(1). -Immutable is stored in bit 0 and append-only in bit 1 of a private ``$LINUXFLAGS`` extended attribute -within the standard NTFS ``$EA``/``$EA_INFORMATION`` attributes. Its value -is a 32-bit little-endian bitmask. Unknown bits are preserved; the entry -is removed when the entire value becomes zero. These settings survive -inode eviction, unmount and reboot. Direct writes or removal through -xattr interfaces are rejected; use chattr(1) instead. - -System metadata files and files protected by ``sys_immutable`` cannot -have their immutable protection cleared. Protection derived solely from -that policy is not stored in the EA. Compression, encryption -and mount-wide case folding are reported but cannot be changed through -chattr(1). - -Windows does not enforce the Linux immutable and append-only flags stored in this EA. +The driver supports lsattr(1) and chattr(1). Immutable, append-only and +nodump are stored in a private ``$LINUXFLAGS`` extended attribute within the +standard NTFS ``$EA``/``$EA_INFORMATION`` attributes. Its value is a +32-bit little-endian bitmask: bit 0 is immutable, bit 1 is append-only, +and bit 2 is nodump. Unknown bits are preserved when updating the flags; +the entry is removed when the entire value becomes zero. These settings +survive inode eviction, unmount and reboot. Direct writes to this EA +through setxattr(2) or removexattr(2) are rejected; use chattr(1) instead. + +System metadata files and files protected by the ``sys_immutable`` mount +option cannot have their immutable protection cleared with chattr(1). +Protection derived solely from the mount option is not stored in the EA. +Case folding is reported according to the mount options and cannot be +configured per file. Compression and encryption are reported but cannot +be changed through chattr(1). + +These Linux flags are enforced by Linux. Storing them in an NTFS EA does +not make Windows enforce immutable, append-only or nodump semantics. + Supported mount options ======================= diff --git a/fs/ntfs/ea.h b/fs/ntfs/ea.h index b988b39f5f00..8f144fa4f9bb 100644 --- a/fs/ntfs/ea.h +++ b/fs/ntfs/ea.h @@ -10,8 +10,9 @@ /* $LINUXFLAGS stores these bits in a single little-endian 32-bit value. */ #define NTFS_LINUXFLAGS_IMMUTABLE BIT(0) #define NTFS_LINUXFLAGS_APPEND BIT(1) +#define NTFS_LINUXFLAGS_NODUMP BIT(2) #define NTFS_LINUXFLAGS_MASK (NTFS_LINUXFLAGS_IMMUTABLE | \ - NTFS_LINUXFLAGS_APPEND) + NTFS_LINUXFLAGS_APPEND | NTFS_LINUXFLAGS_NODUMP) extern const struct xattr_handler *const ntfs_xattr_handlers[]; diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c index e024a63df9d1..9c7678922177 100644 --- a/fs/ntfs/file.c +++ b/fs/ntfs/file.c @@ -167,6 +167,8 @@ int ntfs_fileattr_get(struct dentry *dentry, struct file_kattr *fa) flags |= FS_IMMUTABLE_FL; if (vi->i_flags & S_APPEND) flags |= FS_APPEND_FL; + if (ni->linuxflags & NTFS_LINUXFLAGS_NODUMP) + flags |= FS_NODUMP_FL; if (!NVolCaseSensitive(ni->vol)) flags |= FS_CASEFOLD_FL; @@ -198,7 +200,7 @@ int ntfs_fileattr_set(struct mnt_idmap *idmap, struct dentry *dentry, { struct inode *vi = d_inode(dentry); struct ntfs_inode *ni = NTFS_I(vi); - u32 allowed = FS_IMMUTABLE_FL | FS_APPEND_FL; + u32 allowed = FS_IMMUTABLE_FL | FS_APPEND_FL | FS_NODUMP_FL; u32 readonly = 0; u32 linuxflags = ni->linuxflags & ~NTFS_LINUXFLAGS_MASK; unsigned int new_fl = 0; @@ -241,6 +243,8 @@ int ntfs_fileattr_set(struct mnt_idmap *idmap, struct dentry *dentry, new_fl |= S_APPEND; linuxflags |= NTFS_LINUXFLAGS_APPEND; } + if (fa->flags & FS_NODUMP_FL) + linuxflags |= NTFS_LINUXFLAGS_NODUMP; mutex_lock(&ni->mrec_lock); err = ntfs_ea_set_linuxflags(vi, linuxflags); @@ -619,9 +623,12 @@ int ntfs_getattr(struct mnt_idmap *idmap, const struct path *path, if (inode->i_flags & S_APPEND) stat->attributes |= STATX_ATTR_APPEND; + if (ni->linuxflags & NTFS_LINUXFLAGS_NODUMP) + stat->attributes |= STATX_ATTR_NODUMP; stat->attributes_mask |= STATX_ATTR_COMPRESSED | STATX_ATTR_ENCRYPTED | - STATX_ATTR_IMMUTABLE | STATX_ATTR_APPEND; + STATX_ATTR_IMMUTABLE | STATX_ATTR_APPEND | + STATX_ATTR_NODUMP; /* * If it's a compressed or encrypted file, NTFS currently -- cgit v1.2.3 From c505a2fabcdc1b0577dfdc13de3fbd61dda7873f Mon Sep 17 00:00:00 2001 From: Zhan Xusheng Date: Mon, 12 Oct 2026 14:20:39 +0800 Subject: ntfs: do not read ahead encrypted files ntfs_read_folio() refuses EFS-encrypted files with -EOPNOTSUPP because decryption is not implemented. ntfs_readahead() has no NInoEncrypted() test and passes them to iomap_readahead(), which maps the runlist and fills the folios with the raw on-disk ciphertext. The folios are marked uptodate, so the read that follows never reaches ->read_folio and returns ciphertext instead of -EOPNOTSUPP. ntfs_read_locked_inode() sets NInoEncrypted() and NInoNonResident() for a non-resident encrypted $DATA attribute and rejects the encrypted-and-compressed combination, so none of the three existing conditions fires. read(), mmap(), splice() and fadvise() all populate the page cache through readahead. Skipping readahead leaves the folio unpopulated, so the read falls back to ntfs_read_folio() and gets the intended error. Fixes: b041ca562526 ("ntfs: update iomap and address space operations") Signed-off-by: Zhan Xusheng Reviewed-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/aops.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/aops.c b/fs/ntfs/aops.c index 517d9a1563e7..ad781674fef1 100644 --- a/fs/ntfs/aops.c +++ b/fs/ntfs/aops.c @@ -240,10 +240,11 @@ static void ntfs_readahead(struct readahead_control *rac) }; /* - * Resident files are not cached in the page cache, - * and readahead is not implemented for compressed files. + * Resident files are not cached in the page cache, readahead is not + * implemented for compressed files, and ntfs_read_folio() refuses + * encrypted files because decryption is not implemented. */ - if (!NInoNonResident(ni) || NInoCompressed(ni) || + if (!NInoNonResident(ni) || NInoEncrypted(ni) || NInoCompressed(ni) || NInoWofCompressed(ni)) return; iomap_readahead(&ntfs_read_iomap_ops, &ctx, NULL); -- cgit v1.2.3 From 53cb23e5ea54f4ec6e3caf29ab98ea99b00bb02f Mon Sep 17 00:00:00 2001 From: Zhan Xusheng Date: Mon, 12 Oct 2026 14:20:40 +0800 Subject: ntfs: reject direct I/O reads of encrypted files ntfs_file_read_iter() refuses direct I/O for compressed and WOF compressed attributes but lets encrypted ones reach iomap_dio_rw(). ntfs_read_iomap_begin_non_resident() has no NInoEncrypted() test, so it reports IOMAP_MAPPED for the attribute's real clusters and the raw on-disk ciphertext is copied to the caller. Buffered reads are refused by ntfs_read_folio(), but direct I/O never calls it. Fixes: 9c87959601e8 ("ntfs: update file operations") Signed-off-by: Zhan Xusheng Reviewed-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/file.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c index 9c7678922177..58afd310d57b 100644 --- a/fs/ntfs/file.c +++ b/fs/ntfs/file.c @@ -706,8 +706,8 @@ ssize_t ntfs_file_read_iter(struct kiocb *iocb, struct iov_iter *to) if (NVolShutdown(NTFS_SB(sb))) return -EIO; - if ((NInoCompressed(NTFS_I(vi)) || NInoWofCompressed(NTFS_I(vi))) && - iocb->ki_flags & IOCB_DIRECT) + if ((NInoCompressed(NTFS_I(vi)) || NInoWofCompressed(NTFS_I(vi)) || + NInoEncrypted(NTFS_I(vi))) && iocb->ki_flags & IOCB_DIRECT) return -EOPNOTSUPP; inode_lock_shared(vi); -- cgit v1.2.3 From 5e25af0a5d11e9fce5dcf3ec0779da25c46c662b Mon Sep 17 00:00:00 2001 From: Zhan Xusheng Date: Fri, 9 Oct 2026 15:55:23 +0800 Subject: ntfs: widen the sub-block offsets in ntfs_decompress() ntfs_decompress() tracks where the current sub-block starts and ends inside the destination page with two u16s: u16 do_sb_start; u16 do_sb_end; ... do_sb_start = *dest_ofs; do_sb_end = do_sb_start + NTFS_SB_SIZE; *dest_ofs is an int page offset masked with ~PAGE_MASK, and NTFS_SB_SIZE is 0x1000, so do_sb_end needs PAGE_SIZE + 0xfff of range. That exceeds u16 once PAGE_SIZE reaches 0x10000: the last sub-block of a page starts at 0xf000 and do_sb_end wraps from 0x10000 to 0. The zero-fill that pads a short sub-block then computes a negative length: if (dp_addr < dp_sb_end) { int nr_bytes = do_sb_end - *dest_ofs; memset(dp_addr, 0, nr_bytes); A sub-block made only of symbol tokens reaches this without consulting do_sb_end on the way, because that path just does *dp_addr++ = *cb++; ++*dest_ofs; and is bounded by dp_addr against dp_sb_end. With do_sb_start at 0xf000 and eight literal bytes emitted, nr_bytes is -61448 and the memset() runs for 18446744073709490168 bytes. Getting there is cheap: fifteen sub-blocks of eleven bytes each advance *dest_ofs to 0xf000, since the same zero-fill pads each of them out to 0x1000 while do_sb_end is still exact, and the sixteenth one triggers the wrap. A compression block is 0x10000 bytes with 4 KiB clusters and the fixed compression unit of 4, so all sixteen fit with room to spare. Phrase tokens are bounded against do_sb_end and so return -EOVERFLOW instead, and the if (*dest_index == dest_max_index && do_sb_end > dest_max_ofs) check at the top of the sub-block stops firing for the same reason. Use int for both, matching *dest_ofs and dest_max_ofs. The narrow type came in with the original import and survived the removal and revert of the driver. Fixes: 1e9ea7e04472 ("Revert "fs: Remove NTFS classic"") Signed-off-by: Zhan Xusheng Reviewed-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/compress.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/compress.c b/fs/ntfs/compress.c index 075b57fc1de6..82c7c7d29073 100644 --- a/fs/ntfs/compress.c +++ b/fs/ntfs/compress.c @@ -182,8 +182,8 @@ static int ntfs_decompress(struct page *dest_pages[], int completed_pages[], 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). */ - u16 do_sb_start; /* @dest_ofs when starting this sub-block. */ - u16 do_sb_end; /* @dest_ofs of end of this sb (do_sb_start + NTFS_SB_SIZE). */ + int do_sb_start; /* @dest_ofs when starting this sub-block. */ + int do_sb_end; /* @dest_ofs of end of this sb (do_sb_start + NTFS_SB_SIZE). */ /* Variables for tag and token parsing. */ u8 tag; /* Current tag. */ -- cgit v1.2.3 From 35829c450497dc7a2fb2eac39b11ef4b1ac4de4f Mon Sep 17 00:00:00 2001 From: DaeMyung Kang Date: Mon, 12 Oct 2026 20:27:55 +0900 Subject: ntfs: compare streams_interface with mounted volume on remount During reconfiguration, fc->s_fs_info points to a newly allocated ntfs_volume. It does not contain the mounted volume's stream setting. Consequently, a windows mount rejects a remount with the same streams_interface=windows setting, but accepts streams_interface=none without changing the active pathname interface. This also breaks an ordinary read-only remount when the existing windows setting is passed again, for example from fstab. Compare the requested setting with the mounted volume instead. Report an unsupported change with ntfs_warning() so it does not invoke the mounted filesystem's error-handling policy. Fixes: 2de2cdd9cc08 ("ntfs: add pathname access for named streams") Signed-off-by: DaeMyung Kang Reviewed-by: Baolin Liu Signed-off-by: Namjae Jeon --- fs/ntfs/super.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c index 242fb19ced2b..ed9f29b16c7c 100644 --- a/fs/ntfs/super.c +++ b/fs/ntfs/super.c @@ -275,9 +275,9 @@ static int ntfs_parse_param(struct fs_context *fc, struct fs_parameter *param) result.uint_32 == STREAMS_INTERFACE_WINDOWS; if (fc->purpose == FS_CONTEXT_FOR_RECONFIGURE && - NVolStreamsWindows(vol) != streams_windows) { - ntfs_error(vol->sb, - "Cannot change streams_interface on remount."); + NVolStreamsWindows(NTFS_SB(fc->root->d_sb)) != streams_windows) { + ntfs_warning(fc->root->d_sb, + "Cannot change streams_interface on remount."); return -EINVAL; } if (streams_windows) -- cgit v1.2.3 From b40cddb8dac4f9530f912f0dfe87ac4942623db0 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Tue, 13 Oct 2026 09:15:00 +0900 Subject: ntfs: fix NULL dereference on index allocation open failure ntfs_index_lookup() returns -ENOENT when ntfs_ia_open() fails to open the index allocation. This error path leaves icx->ib unset, unlike a missing key in an otherwise valid index, which returns an insertion context. ntfs_ie_add() treats -ENOENT as a valid insertion position and dereferences icx->ib when the lookup is not in the index root. A corrupt $I30 bitmap attribute can therefore cause a NULL pointer dereference when adding a directory entry. Return -EIO for index allocation open failures so insertion aborts through the existing error path. Keep -ENOENT for genuinely missing keys and document that other errors leave no valid index entry. Fixes: 0a8ac0c1fa0b ("ntfs: update directory operations") Reported-by: 1sh1ro Signed-off-by: Namjae Jeon --- fs/ntfs/index.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'fs') diff --git a/fs/ntfs/index.c b/fs/ntfs/index.c index 4410df0a8f31..ac22d8407a77 100644 --- a/fs/ntfs/index.c +++ b/fs/ntfs/index.c @@ -812,6 +812,9 @@ static int ntfs_icx_parent_dec(struct ntfs_index_context *icx) * after the search @key, i.e. this is the position in the index at which * an index entry with a key of @key would need to be inserted. * + * If the index cannot be searched, a negative error code other than -ENOENT + * is returned and @icx does not describe a valid index entry. + * * When finished with the entry and its data, call ntfs_index_ctx_put() to free * the context and other associated resources. * @@ -877,7 +880,7 @@ int ntfs_index_lookup(const void *key, const u32 key_len, struct ntfs_index_cont /* Child node present, descend into it. */ icx->ia_ni = ntfs_ia_open(icx, ni); if (!icx->ia_ni) { - err = -ENOENT; + err = -EIO; goto err_out; } -- cgit v1.2.3 From 8a69be3669e2ef632e0333eac0ae80fd91c3c1f3 Mon Sep 17 00:00:00 2001 From: DaeMyung Kang Date: Tue, 13 Oct 2026 23:58:45 +0900 Subject: ntfs: report block device flush failures from fsync ntfs_file_fsync() calls blkdev_issue_flush() after writeback but discards its return value. If the device cannot flush its write cache, fsync() reports success even though the file is not guaranteed to be durable. Reproduce this with QEMU blkdebug failing flush_to_disk with -EIO. On ntfs-next at 04fb910da2c8, a kretprobe recorded -EIO from blkdev_issue_flush() while fsync() returned 0. With this change, fsync() returns -EIO and logs the failure. The syncfs() path already reports this error. Fixes: 9c87959601e8 ("ntfs: update file operations") Cc: stable@vger.kernel.org Signed-off-by: DaeMyung Kang Reviewed-by: Baolin Liu Signed-off-by: Namjae Jeon --- fs/ntfs/file.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c index 58afd310d57b..afc2efb4b699 100644 --- a/fs/ntfs/file.c +++ b/fs/ntfs/file.c @@ -417,14 +417,14 @@ sync_volume: err = sync_blockdev(vi->i_sb->s_bdev); if (unlikely(err && !ret)) ret = err; + if (!ret) + ret = blkdev_issue_flush(vi->i_sb->s_bdev); if (likely(!ret)) ntfs_debug("Done."); else ntfs_warning(vi->i_sb, "Failed to f%ssync inode 0x%llx. Error %u.", datasync ? "data" : "", ni->mft_no, -ret); - if (!ret) - blkdev_issue_flush(vi->i_sb->s_bdev); return ret; } -- cgit v1.2.3 From 950f860839f31b7617190cf0bdd277abf3e43125 Mon Sep 17 00:00:00 2001 From: DaeMyung Kang Date: Tue, 13 Oct 2026 23:58:46 +0900 Subject: ntfs: flush the block device when syncing a directory ntfs_dir_fsync() writes directory metadata and calls sync_blockdev(), but does not flush the block device write cache. A successful fsync() can therefore leave the directory's metadata in a volatile device cache. Issue a cache flush after successful writeback and return any flush error. This matches the regular-file fsync and syncfs paths. With QEMU blkdebug injecting -EIO for flush_to_disk, fsync() on an NTFS directory returned 0 before this change while syncfs() returned -EIO. After this change, both return -EIO. A kretprobe confirmed the flush error in the directory fsync path. Fixes: 1e9ea7e04472 ("Revert "fs: Remove NTFS classic"") Cc: stable@vger.kernel.org Signed-off-by: DaeMyung Kang Reviewed-by: Baolin Liu Signed-off-by: Namjae Jeon --- fs/ntfs/dir.c | 2 ++ 1 file changed, 2 insertions(+) (limited to 'fs') diff --git a/fs/ntfs/dir.c b/fs/ntfs/dir.c index 5a0e21e7c283..fce10baa2b93 100644 --- a/fs/ntfs/dir.c +++ b/fs/ntfs/dir.c @@ -1192,6 +1192,8 @@ static int ntfs_dir_fsync(struct file *filp, loff_t start, loff_t end, err = sync_blockdev(vi->i_sb->s_bdev); if (unlikely(err && !ret)) ret = err; + if (!ret) + ret = blkdev_issue_flush(vi->i_sb->s_bdev); if (likely(!ret)) ntfs_debug("Done."); else -- cgit v1.2.3 From 5575030c79c3be47f0e3a2e23721761ccf6f72e8 Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Wed, 14 Oct 2026 07:43:10 +0800 Subject: ntfs: support FS_IOC_GETFSUUID NTFS reads the 64-bit volume serial number from the boot sector but leaves the superblock UUID unset. As a result, the generic FS_IOC_GETFSUUID handler returns ENOTTY for files and directories on an NTFS volume. Register the on-disk little-endian serial number as an eight-byte filesystem identifier with super_set_uuid(). The generic VFS handler can then expose the existing volume identifier. Signed-off-by: Baolin Liu Signed-off-by: Namjae Jeon --- fs/ntfs/super.c | 2 ++ 1 file changed, 2 insertions(+) (limited to 'fs') diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c index ed9f29b16c7c..71f0aea46bf8 100644 --- a/fs/ntfs/super.c +++ b/fs/ntfs/super.c @@ -997,6 +997,8 @@ static bool parse_ntfs_boot_sector(struct ntfs_volume *vol, vol->mft_record_size_bits; ntfs_debug("vol->mftmirr_size = %i", vol->mftmirr_size); vol->serial_no = le64_to_cpu(b->volume_serial_number); + super_set_uuid(vol->sb, (const u8 *)&b->volume_serial_number, + sizeof(b->volume_serial_number)); ntfs_debug("vol->serial_no = 0x%llx", vol->serial_no); vol->sparse_compression_unit = 4; -- cgit v1.2.3 From 279b1a14cdbecdb134069748230a73ae25d4fd72 Mon Sep 17 00:00:00 2001 From: Xuewen Wang Date: Wed, 14 Oct 2026 09:18:24 +0800 Subject: ntfs: report birth time for symbolic links NTFS already loads the creation time into i_crtime and reports it through ntfs_getattr(). However, the symbolic-link inode operations omit getattr, so statx() with AT_SYMLINK_NOFOLLOW falls back to generic_fillattr() and does not return STATX_BTIME for the link itself. Use ntfs_getattr() for symbolic links to expose their stored creation time. Signed-off-by: Xuewen Wang Reviewed-by: Baolin Liu Signed-off-by: Namjae Jeon --- fs/ntfs/file.c | 1 + 1 file changed, 1 insertion(+) (limited to 'fs') diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c index afc2efb4b699..7d86070cf468 100644 --- a/fs/ntfs/file.c +++ b/fs/ntfs/file.c @@ -1556,6 +1556,7 @@ const struct inode_operations ntfs_file_inode_ops = { const struct inode_operations ntfs_symlink_inode_operations = { .get_link = ntfs_get_link, .setattr = ntfs_setattr, + .getattr = ntfs_getattr, .listxattr = ntfs_listxattr, .fileattr_get = ntfs_fileattr_get, .fileattr_set = ntfs_fileattr_set, -- cgit v1.2.3 From 1205ab57465810341fb1436bab310cebdfd0ecdc Mon Sep 17 00:00:00 2001 From: Hongling Zeng Date: Tue, 13 Oct 2026 09:56:52 +0800 Subject: ntfs: roll back the ACL xattr commit when the WSL mode update fails ntfs_set_acl_ex() commits the ACL xattr before it rewrites the WSL mode EA, and removes it again when the rewrite fails. That only undoes a set that created the xattr: when the update replaced or removed an existing one, the old ACL is discarded along with the failed update. No error is recorded when the undo fails, so the new, typically more permissive, ACL is left on disk while i_mode, the mode EA and the cached ACL keep the old values. After a remount, a chmod 0640 -> 0777 that reported failure leaves the file accessible to the relaxed ACL while stat() still reports 0640. Save the previous xattr before committing the update, and restore it when the mode EA rewrite fails. A removed xattr is created again, since XATTR_REPLACE fails with -ENODATA once the removal dropped the entry. Remove the new xattr only when there was no previous one, and skip the undo when the commit changed nothing. A failed rollback is recorded: NVolSetErrors(), which the next persistence point persists as VOLUME_IS_DIRTY, and a log message the errors= policy acts on. The flag cannot be written to disk here directly: the target inode's mrec_lock is held and the $Volume mrec_lock cannot be nested under it. Fixes: fc053f05ca28 ("ntfs: add reparse and ea operations") Cc: stable@vger.kernel.org Suggested-by: Hyunchul Lee Signed-off-by: Hongling Zeng Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/ea.c | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 4 deletions(-) (limited to 'fs') diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c index a1f948c4d2ec..9f62056c1d6f 100644 --- a/fs/ntfs/ea.c +++ b/fs/ntfs/ea.c @@ -1053,8 +1053,9 @@ static noinline int ntfs_set_acl_ex(struct mnt_idmap *idmap, { const char *name; size_t size, name_len; - void *value; - int err; + void *value, *old_value = NULL; + int err, old_len = -ENODATA; + bool acl_committed = false; int flags; umode_t mode; @@ -1098,7 +1099,34 @@ static noinline int ntfs_set_acl_ex(struct mnt_idmap *idmap, } mutex_lock(&NTFS_I(inode)->mrec_lock); + if (!init_acl) { + /* Save the old xattr for rollback. */ + old_len = ntfs_get_ea(inode, name, name_len, NULL, 0); + if (old_len >= 0) { + /* kmalloc(0) returns the non-NULL ZERO_SIZE_PTR. */ + old_value = kmalloc(old_len, GFP_NOFS); + if (!old_value) { + err = -ENOMEM; + goto out_unlock; + } + err = ntfs_get_ea(inode, name, name_len, + old_value, old_len); + if (err != old_len) { + kfree(old_value); + old_value = NULL; + if (err >= 0) + err = -EIO; + goto out_unlock; + } + } else if (old_len != -ENODATA) { + err = old_len; + goto out_unlock; + } + } err = ntfs_set_ea(inode, name, name_len, value, size, flags, NULL); + if (!err) + acl_committed = true; +out_unlock: mutex_unlock(&NTFS_I(inode)->mrec_lock); if (err == -ENODATA && !size) err = 0; /* Removing non existed xattr. */ @@ -1110,8 +1138,38 @@ static noinline int ntfs_set_acl_ex(struct mnt_idmap *idmap, mutex_lock(&NTFS_I(inode)->mrec_lock); err = ntfs_ea_set_wsl_inode(inode, 0, &ea_size, NTFS_EA_MODE); if (err) { - ntfs_set_ea(inode, name, name_len, NULL, 0, - XATTR_REPLACE, NULL); + if (acl_committed) { + int undo_err; + + if (old_len >= 0) { + /* + * A removed or zero-length xattr must + * be recreated without XATTR_REPLACE. + */ + int undo_flags = acl && old_len ? + XATTR_REPLACE : 0; + + undo_err = ntfs_set_ea(inode, name, + name_len, old_value, + old_len, undo_flags, + NULL); + } else { + undo_err = ntfs_set_ea(inode, name, + name_len, NULL, 0, + XATTR_REPLACE, NULL); + } + if (undo_err) { + /* + * Record a failed rollback so the next + * persistence point marks the volume + * dirty. Run chkdsk. + */ + NVolSetErrors(NTFS_I(inode)->vol); + ntfs_error(inode->i_sb, + "Failed to undo the ACL xattr: %d", + undo_err); + } + } mutex_unlock(&NTFS_I(inode)->mrec_lock); inode->i_mode = old_mode; goto out; @@ -1124,6 +1182,7 @@ static noinline int ntfs_set_acl_ex(struct mnt_idmap *idmap, } out: + kfree(old_value); kfree(value); return err; -- cgit v1.2.3 From f7b2126f67f1f6de66fbdf4d2e0a78bc1a52502f Mon Sep 17 00:00:00 2001 From: Xuewen Wang Date: Fri, 16 Oct 2026 11:09:57 +0800 Subject: ntfs: support persistent noatime attribute NTFS currently rejects FS_NOATIME_FL in its fileattr setter. This prevents users from disabling automatic access-time updates on individual files or directories with chattr +A. Store noatime in bit 3 of the private $LINUXFLAGS EA and restore S_NOATIME when loading an inode. Report and accept FS_NOATIME_FL in the fileattr callbacks, updating the in-memory flag only after the EA update succeeds. The VFS then suppresses automatic atime updates for the inode, while chattr -A restores the mount's atime policy. Document the new on-disk bit and that newly created children do not inherit the attribute. Suggested-by: Baolin Liu Reviewed-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Xuewen Wang Signed-off-by: Namjae Jeon --- Documentation/filesystems/ntfs.rst | 15 ++++++++++----- fs/ntfs/ea.c | 2 ++ fs/ntfs/ea.h | 4 +++- fs/ntfs/file.c | 11 +++++++++-- 4 files changed, 24 insertions(+), 8 deletions(-) (limited to 'fs') diff --git a/Documentation/filesystems/ntfs.rst b/Documentation/filesystems/ntfs.rst index 699e91f884d4..d5819da6ab53 100644 --- a/Documentation/filesystems/ntfs.rst +++ b/Documentation/filesystems/ntfs.rst @@ -38,15 +38,20 @@ The project is available at: Linux file attributes ===================== -The driver supports lsattr(1) and chattr(1). Immutable, append-only and -nodump are stored in a private ``$LINUXFLAGS`` extended attribute within the +The driver supports lsattr(1) and chattr(1). Immutable, append-only, nodump +and noatime are stored in a private ``$LINUXFLAGS`` extended attribute within the standard NTFS ``$EA``/``$EA_INFORMATION`` attributes. Its value is a 32-bit little-endian bitmask: bit 0 is immutable, bit 1 is append-only, -and bit 2 is nodump. Unknown bits are preserved when updating the flags; -the entry is removed when the entire value becomes zero. These settings +bit 2 is nodump, and bit 3 is noatime. Unknown bits are preserved when updating +the flags; the entry is removed when the entire value becomes zero. These settings survive inode eviction, unmount and reboot. Direct writes to this EA through setxattr(2) or removexattr(2) are rejected; use chattr(1) instead. +The noatime attribute (``chattr +A``) suppresses automatic access-time +updates for the inode, including when mounted with ``strictatime``. It +applies to directories as well as files, but is not inherited by newly +created children. Use ``chattr -A`` to restore the mount's access-time policy. + System metadata files and files protected by the ``sys_immutable`` mount option cannot have their immutable protection cleared with chattr(1). Protection derived solely from the mount option is not stored in the EA. @@ -55,7 +60,7 @@ configured per file. Compression and encryption are reported but cannot be changed through chattr(1). These Linux flags are enforced by Linux. Storing them in an NTFS EA does -not make Windows enforce immutable, append-only or nodump semantics. +not make Windows enforce immutable, append-only, nodump or noatime semantics. Supported mount options diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c index 9f62056c1d6f..199a602348d4 100644 --- a/fs/ntfs/ea.c +++ b/fs/ntfs/ea.c @@ -501,6 +501,8 @@ int ntfs_ea_get_linuxflags(struct inode *inode) inode->i_flags |= S_IMMUTABLE; if (ni->linuxflags & NTFS_LINUXFLAGS_APPEND) inode->i_flags |= S_APPEND; + if (ni->linuxflags & NTFS_LINUXFLAGS_NOATIME) + inode->i_flags |= S_NOATIME; return 0; } diff --git a/fs/ntfs/ea.h b/fs/ntfs/ea.h index 8f144fa4f9bb..3ad20431877b 100644 --- a/fs/ntfs/ea.h +++ b/fs/ntfs/ea.h @@ -11,8 +11,10 @@ #define NTFS_LINUXFLAGS_IMMUTABLE BIT(0) #define NTFS_LINUXFLAGS_APPEND BIT(1) #define NTFS_LINUXFLAGS_NODUMP BIT(2) +#define NTFS_LINUXFLAGS_NOATIME BIT(3) #define NTFS_LINUXFLAGS_MASK (NTFS_LINUXFLAGS_IMMUTABLE | \ - NTFS_LINUXFLAGS_APPEND | NTFS_LINUXFLAGS_NODUMP) + NTFS_LINUXFLAGS_APPEND | NTFS_LINUXFLAGS_NODUMP | \ + NTFS_LINUXFLAGS_NOATIME) extern const struct xattr_handler *const ntfs_xattr_handlers[]; diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c index 7d86070cf468..d810ca5904bd 100644 --- a/fs/ntfs/file.c +++ b/fs/ntfs/file.c @@ -167,6 +167,8 @@ int ntfs_fileattr_get(struct dentry *dentry, struct file_kattr *fa) flags |= FS_IMMUTABLE_FL; if (vi->i_flags & S_APPEND) flags |= FS_APPEND_FL; + if (vi->i_flags & S_NOATIME) + flags |= FS_NOATIME_FL; if (ni->linuxflags & NTFS_LINUXFLAGS_NODUMP) flags |= FS_NODUMP_FL; if (!NVolCaseSensitive(ni->vol)) @@ -200,7 +202,8 @@ int ntfs_fileattr_set(struct mnt_idmap *idmap, struct dentry *dentry, { struct inode *vi = d_inode(dentry); struct ntfs_inode *ni = NTFS_I(vi); - u32 allowed = FS_IMMUTABLE_FL | FS_APPEND_FL | FS_NODUMP_FL; + u32 allowed = FS_IMMUTABLE_FL | FS_APPEND_FL | FS_NODUMP_FL | + FS_NOATIME_FL; u32 readonly = 0; u32 linuxflags = ni->linuxflags & ~NTFS_LINUXFLAGS_MASK; unsigned int new_fl = 0; @@ -245,6 +248,10 @@ int ntfs_fileattr_set(struct mnt_idmap *idmap, struct dentry *dentry, } if (fa->flags & FS_NODUMP_FL) linuxflags |= NTFS_LINUXFLAGS_NODUMP; + if (fa->flags & FS_NOATIME_FL) { + new_fl |= S_NOATIME; + linuxflags |= NTFS_LINUXFLAGS_NOATIME; + } mutex_lock(&ni->mrec_lock); err = ntfs_ea_set_linuxflags(vi, linuxflags); @@ -252,7 +259,7 @@ int ntfs_fileattr_set(struct mnt_idmap *idmap, struct dentry *dentry, if (err) return err; - inode_set_flags(vi, new_fl, S_IMMUTABLE | S_APPEND); + inode_set_flags(vi, new_fl, S_IMMUTABLE | S_APPEND | S_NOATIME); inode_set_ctime_current(vi); mark_inode_dirty(vi); return 0; -- cgit v1.2.3