From 4483ab85d1e0d934dfc827a828c8b265e82cdb4f Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Wed, 26 Aug 2026 23:19:58 +0900 Subject: [PATCH 01/20] exfat: map allocated extents for swap activation exFAT reports allocated ranges beyond valid_size as IOMAP_HOLE when IOMAP_REPORT is set. iomap_swapfile_activate() also uses IOMAP_REPORT while collecting physical extents for a swap file, so it treats the preallocated tail as unallocated and rejects the file with -EINVAL. Use swap-specific iomap operations that suppress IOMAP_REPORT before mapping the file. This reports physically allocated ranges as IOMAP_UNWRITTEN during swap activation without changing the byte-accurate SEEK_HOLE and SEEK_DATA behavior of the regular iomap operations. Tested-by: Petr Vorel Signed-off-by: Namjae Jeon --- fs/exfat/iomap.c | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/fs/exfat/iomap.c b/fs/exfat/iomap.c index 8911aa84a730..533cdb4f0929 100644 --- a/fs/exfat/iomap.c +++ b/fs/exfat/iomap.c @@ -157,6 +157,27 @@ const struct iomap_ops exfat_iomap_ops = { .iomap_next = exfat_iomap_next, }; +#ifdef CONFIG_SWAP +static int exfat_swap_iomap_begin(struct inode *inode, loff_t offset, + loff_t length, unsigned int flags, struct iomap *iomap, + struct iomap *srcmap) +{ + /* + * Swap activation needs the physical mappings of preallocated + * ranges. Do not report the VDL tail as a hole. + */ + return __exfat_iomap_begin(inode, offset, length, + flags & ~IOMAP_REPORT, iomap, false); +} + +static DEFINE_IOMAP_ITER_NEXT(exfat_swap_iomap_next, + exfat_swap_iomap_begin); + +static const struct iomap_ops exfat_swap_iomap_ops = { + .iomap_next = exfat_swap_iomap_next, +}; +#endif + /* * exfat_write_iomap_end - Update the state after write * @@ -275,5 +296,10 @@ const struct iomap_read_ops exfat_iomap_bio_read_ops = { int exfat_iomap_swap_activate(struct swap_info_struct *sis, struct file *file, sector_t *span) { - return iomap_swapfile_activate(sis, file, span, &exfat_iomap_ops); +#ifdef CONFIG_SWAP + return iomap_swapfile_activate(sis, file, span, + &exfat_swap_iomap_ops); +#else + return -EIO; +#endif } From cb9dbf8a4e34b4fdf311598835ab4d76ed7e07c1 Mon Sep 17 00:00:00 2001 From: Chi Zhiling Date: Sat, 5 Sep 2026 13:49:50 +0800 Subject: [PATCH 02/20] exfat: fix used_clusters accounting during cluster allocation In the current exfat_alloc_cluster(), p_chain->size is incremented as each cluster is allocated so that the partially allocated chain can be freed by __exfat_free_cluster() on the error path. However, sbi->used_clusters is still updated only after all clusters have been allocated, while __exfat_free_cluster() unconditionally subtracts the number of freed clusters from sbi->used_clusters. Therefore, when allocation fails partway through, the partially allocated clusters are subtracted from sbi->used_clusters even though they were never added to it. This corrupts the free-space accounting and can cause sbi->used_clusters to underflow. Increment sbi->used_clusters together with p_chain->size as each cluster is allocated. This keeps the accounting consistent with __exfat_free_cluster() on both the success and error paths. This issue is caught by generic/476 with 256k cluster size: Ran: generic/476 Failures: generic/476 Failed 1 of 1 tests *** fsck.exfat output *** ERROR: : more clusters are allocated. truncate to N bytes ... /dev/vdc: corrupted. directories 331, files 526 /dev/vdc: files corrupted 9, files fixed 0 *** end fsck.exfat output *** Fixes: d5c514b6a0c0 ("exfat: fix the newly allocated clusters are not freed in error handling") Signed-off-by: Chi Zhiling Signed-off-by: Namjae Jeon --- fs/exfat/fatent.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/exfat/fatent.c b/fs/exfat/fatent.c index a8b11e2ce43f..a6728c361289 100644 --- a/fs/exfat/fatent.c +++ b/fs/exfat/fatent.c @@ -509,13 +509,13 @@ int exfat_alloc_cluster(struct inode *inode, unsigned int num_alloc, } } p_chain->size++; + sbi->used_clusters++; last_clu = new_clu; if (p_chain->size == num_alloc) { done: sbi->clu_srch_ptr = hint_clu; - sbi->used_clusters += p_chain->size; mutex_unlock(&sbi->bitmap_lock); return 0; } From 48acff2eedef7fecdfbd39be1b6f4463a9e43e3e Mon Sep 17 00:00:00 2001 From: Chi Zhiling Date: Sat, 5 Sep 2026 13:49:51 +0800 Subject: [PATCH 03/20] exfat: take bitmap_lock at the start of exfat_alloc_cluster() exfat_alloc_cluster() checks sbi->used_clusters against the total number of data clusters before acquiring sbi->bitmap_lock. A concurrent allocation can update sbi->used_clusters after the check but before the lock is acquired, making the check stale. This can allow the allocation to proceed even though there are not enough free clusters, causing it to fail partway through. Acquire sbi->bitmap_lock before checking sbi->used_clusters so that the free-space check and subsequent cluster allocation are serialized with concurrent allocations. Fixes: f55c096f62f1 ("exfat: do not zero the extended part") Signed-off-by: Chi Zhiling Signed-off-by: Namjae Jeon --- fs/exfat/fatent.c | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/fs/exfat/fatent.c b/fs/exfat/fatent.c index a6728c361289..3c8bdc131f6f 100644 --- a/fs/exfat/fatent.c +++ b/fs/exfat/fatent.c @@ -427,19 +427,22 @@ int exfat_alloc_cluster(struct inode *inode, unsigned int num_alloc, struct super_block *sb = inode->i_sb; struct exfat_sb_info *sbi = EXFAT_SB(sb); + mutex_lock(&sbi->bitmap_lock); + total_cnt = EXFAT_DATA_CLUSTER_COUNT(sbi); if (unlikely(total_cnt < sbi->used_clusters)) { exfat_fs_error_ratelimit(sb, "%s: invalid used clusters(t:%u,u:%u)\n", __func__, total_cnt, sbi->used_clusters); - return -EIO; + ret = -EIO; + goto unlock; } - if (num_alloc > total_cnt - sbi->used_clusters) - return -ENOSPC; - - mutex_lock(&sbi->bitmap_lock); + if (num_alloc > total_cnt - sbi->used_clusters) { + ret = -ENOSPC; + goto unlock; + } hint_clu = p_chain->dir; /* find new cluster */ @@ -516,8 +519,8 @@ int exfat_alloc_cluster(struct inode *inode, unsigned int num_alloc, if (p_chain->size == num_alloc) { done: sbi->clu_srch_ptr = hint_clu; - mutex_unlock(&sbi->bitmap_lock); - return 0; + ret = 0; + goto unlock; } hint_clu = new_clu + 1; From 9b68707fc7c27780f7920855f922d857ba9d1095 Mon Sep 17 00:00:00 2001 From: Yang Wen Date: Fri, 28 Aug 2026 20:51:54 +0800 Subject: [PATCH 04/20] exfat: validate vendor allocation directory entries The exfat entry validator checks the type and ordering of benign secondary entries, but does not validate Vendor Allocation entry fields. In addition, exfat_find() reads only the first two entries, allowing malformed trailing Vendor Allocation entries to bypass validation. Validate the allocation flags, VendorGuid, FirstCluster, DataLength, and NoFatChain extent. Read the complete entry set during lookup so malformed Vendor Allocation entries are rejected. Signed-off-by: Yang Wen Signed-off-by: Namjae Jeon --- fs/exfat/dir.c | 49 +++++++++++++++++++++++++++++++++++++++++++++--- fs/exfat/namei.c | 3 ++- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/fs/exfat/dir.c b/fs/exfat/dir.c index fe73b1380c5d..fa9abedf4d84 100644 --- a/fs/exfat/dir.c +++ b/fs/exfat/dir.c @@ -678,11 +678,54 @@ enum exfat_validate_dentry_mode { ES_MODE_GET_BENIGN_SEC_ENTRY, }; -static bool exfat_validate_entry(unsigned int type, - enum exfat_validate_dentry_mode *mode) +static bool exfat_validate_vendor_alloc(struct super_block *sb, + struct exfat_dentry *ep) { + struct exfat_sb_info *sbi = EXFAT_SB(sb); + u8 flags = ep->dentry.vendor_alloc.flags; + u32 start_clu = le32_to_cpu(ep->dentry.vendor_alloc.start_clu); + u64 size = le64_to_cpu(ep->dentry.vendor_alloc.size); + u64 max_size = exfat_cluster_to_bytes(sbi, + (u64)EXFAT_DATA_CLUSTER_COUNT(sbi)); + u64 num_clusters; + + /* AllocationPossible is required for Vendor Allocation entries. */ + if (!(flags & ALLOC_POSSIBLE)) + return false; + + /* The null GUID does not identify a valid vendor allocation. */ + if (!memchr_inv(ep->dentry.vendor_alloc.vendor_guid, 0, + sizeof(ep->dentry.vendor_alloc.vendor_guid))) + return false; + + if (!start_clu) + return !size && !(flags & (ALLOC_NO_FAT_CHAIN ^ ALLOC_FAT_CHAIN)); + + if (!is_valid_cluster(sbi, start_clu) || size > max_size) + return false; + + if ((flags & ALLOC_NO_FAT_CHAIN) == ALLOC_NO_FAT_CHAIN) { + if (!size) + return false; + + num_clusters = DIV_ROUND_UP_ULL(size, sbi->cluster_size); + if (num_clusters > sbi->num_clusters - start_clu) + return false; + } + + return true; +} + +static bool exfat_validate_entry(struct super_block *sb, + struct exfat_dentry *ep, enum exfat_validate_dentry_mode *mode) +{ + unsigned int type = exfat_get_entry_type(ep); + if (type == TYPE_UNUSED || type == TYPE_DELETED) return false; + if (type == TYPE_VENDOR_ALLOC && + !exfat_validate_vendor_alloc(sb, ep)) + return false; switch (*mode) { case ES_MODE_GET_FILE_ENTRY: @@ -836,7 +879,7 @@ int exfat_get_dentry_set(struct exfat_entry_set_cache *es, /* validate cached dentries */ for (i = ES_IDX_STREAM; i < es->num_entries; i++) { ep = exfat_get_dentry_cached(es, i); - if (!exfat_validate_entry(exfat_get_entry_type(ep), &mode)) + if (!exfat_validate_entry(sb, ep, &mode)) goto put_es; } return 0; diff --git a/fs/exfat/namei.c b/fs/exfat/namei.c index 5e7836af63b8..1cd778a2380b 100644 --- a/fs/exfat/namei.c +++ b/fs/exfat/namei.c @@ -645,7 +645,8 @@ static int exfat_find(struct inode *dir, const struct qstr *qname, info->entry = dentry; info->num_subdirs = 0; - if (exfat_get_dentry_set(&es, sb, &cdir, dentry, ES_2_ENTRIES)) + /* Validate the complete set, including recognized benign entries. */ + if (exfat_get_dentry_set(&es, sb, &cdir, dentry, ES_ALL_ENTRIES)) return -EIO; ep = exfat_get_dentry_cached(&es, ES_IDX_FILE); ep2 = exfat_get_dentry_cached(&es, ES_IDX_STREAM); From 61011fb95c5d1f4aa2de4c0d75c5f5ad08917c4b Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Mon, 31 Aug 2026 16:55:05 +0900 Subject: [PATCH 05/20] exfat: doc: add documentation Add documentation for the Linux exFAT filesystem driver, including supported mount options and exfatprogs. Signed-off-by: Namjae Jeon --- Documentation/filesystems/exfat.rst | 117 ++++++++++++++++++++++++++++ Documentation/filesystems/index.rst | 1 + 2 files changed, 118 insertions(+) create mode 100644 Documentation/filesystems/exfat.rst diff --git a/Documentation/filesystems/exfat.rst b/Documentation/filesystems/exfat.rst new file mode 100644 index 000000000000..ce5c9344a7b2 --- /dev/null +++ b/Documentation/filesystems/exfat.rst @@ -0,0 +1,117 @@ +.. SPDX-License-Identifier: GPL-2.0 + +================================== +The Linux exFAT filesystem driver +================================== + + +.. Table of contents + + - Overview + - Utilities support + - Supported mount options + + +Overview +======== + +exFAT is a filesystem designed for removable storage and other devices that +need to store large files. The Linux exFAT filesystem driver provides read +and write support for exFAT volumes. + +To mount an exFAT volume, use the ``exfat`` filesystem type:: + + mount -t exfat /dev/sdX1 /mnt + + +Utilities support +================= + +The exfatprogs project provides userspace utilities for creating, checking, +repairing, inspecting, and tuning exFAT filesystems. Use exfatprogs when +creating or checking an exFAT filesystem. For example, use ``mkfs.exfat`` +to create a filesystem and ``fsck.exfat`` to check or repair one. + +The project is available at: + + https://github.com/exfatprogs/exfatprogs + + +Supported mount options +======================= + +The exFAT driver supports the following mount options: + +======================= ==================================================== +uid= +gid= Set the owner and group of all files and + directories. The default is the uid and gid of + the process mounting the filesystem. + +umask= Set the permission mask for files and directories. + The default is the umask of the process mounting + the filesystem. + +dmask= Set the permission mask for directories. + +fmask= Set the permission mask for files. + +allow_utime= Control the permission check for changing file + timestamps. Only permission bits 0022 are used. + Permission bit 0020 allows members of the file's + group to change timestamps, and permission bit 0002 + allows other users to change timestamps. The + default is derived from dmask (``~dmask & 0022``). + +iocharset=name Character set used to convert between user-visible + filenames and the UTF-16 character encoding used by + exFAT. The default is + CONFIG_EXFAT_DEFAULT_IOCHARSET, which is ``utf8`` + unless changed at kernel configuration time. Use + ``iocharset=utf8`` for UTF-8 filename handling. + +errors= Specify exFAT behavior on filesystem errors. The + value must be ``panic``, ``continue``, or + ``remount-ro``. These respectively panic, continue + without changing the filesystem, or remount the + filesystem read-only. The default is + ``remount-ro``. + +discard Issue discard/TRIM requests to the block device + when clusters are freed. This is disabled by + default. ``nodiscard`` disables it explicitly. + +keep_last_dots Keep trailing periods in path components during + lookup. Without this option, trailing periods are + stripped. Existing entries with trailing periods + can be accessed when this option is enabled, but + creating new entries with trailing periods is + rejected. + +sys_tz Use the system timezone as the UTC offset when an + exFAT timestamp does not contain a valid timezone + offset. This takes precedence over time_offset. + +time_offset=minutes Set the UTC offset, in minutes, used when an exFAT + timestamp does not contain a valid timezone offset. + Values from -1440 to 1440 are accepted. The default + is 0. This option is ignored when sys_tz is set. + +zero_size_dir Create directories with zero size and without + allocating a cluster. This is disabled by default; + the default behavior allocates a cluster for a new + directory. ``nozero_size_dir`` disables it + explicitly. +======================= ==================================================== + + +Deprecated mount options +------------------------ + +The following options are accepted for compatibility but should not be used: + +``utf8`` + Deprecated. Use ``iocharset=utf8`` instead. + +``debug``, ``namecase=``, ``codepage=`` + Deprecated and ignored by the exFAT driver. diff --git a/Documentation/filesystems/index.rst b/Documentation/filesystems/index.rst index 7cce7cc28d12..1100130ccf0a 100644 --- a/Documentation/filesystems/index.rst +++ b/Documentation/filesystems/index.rst @@ -86,6 +86,7 @@ Documentation for filesystem implementations. ecryptfs efivarfs erofs + exfat ext2 ext3 ext4/index From 7d390ca06fe71b6959c8c983a9e43230220029e4 Mon Sep 17 00:00:00 2001 From: Chi Zhiling Date: Fri, 4 Sep 2026 11:26:41 +0800 Subject: [PATCH 06/20] exfat: clear the volume dirty flag only when remounting read-only exfat_reconfigure() does not hold s_lock while calling sync_filesystem(). Therefore, the filesystem can become dirty again between the sync and clearing the volume dirty flag, leaving a dirty filesystem with a clean volume flag. Holding s_lock across sync_filesystem() is not an option because it can lead to a deadlock: writeback takes s_lock in exfat_write_inode(). Instead, clear the volume dirty flag only when remounting the filesystem read-only, where no writer can modify the filesystem after the sync. For a normal read-only remount, reconfigure_super() calls sb_prepare_remount_readonly() before ->reconfigure(), which returns -EBUSY if any writer is still active. Afterwards sb_start_ro_state_change() sets sb->s_readonly_remount, so mnt_get_write_access() fails with -EROFS and no new writer can start until the reconfiguration finishes. Therefore no writer can race with the sync and the clearing. Forced remounts are different: with SB_FORCE, reconfigure_super() skips sb_prepare_remount_readonly(), so the writers active at that moment are not blocked and can still dirty the filesystem after the sync. Do not clear the volume dirty flag in this case. Signed-off-by: Chi Zhiling Signed-off-by: Namjae Jeon --- fs/exfat/super.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/fs/exfat/super.c b/fs/exfat/super.c index a9ea36ba2693..84f599b37a45 100644 --- a/fs/exfat/super.c +++ b/fs/exfat/super.c @@ -775,9 +775,13 @@ static int exfat_reconfigure(struct fs_context *fc) fc->sb_flags |= SB_NODIRATIME; sync_filesystem(sb); - mutex_lock(&sbi->s_lock); - exfat_clear_volume_dirty(sb); - mutex_unlock(&sbi->s_lock); + + if ((fc->sb_flags & (SB_FORCE | SB_RDONLY)) == SB_RDONLY && + !sb_rdonly(sb)) { + mutex_lock(&sbi->s_lock); + exfat_clear_volume_dirty(sb); + mutex_unlock(&sbi->s_lock); + } if (new_opts->allow_utime == (unsigned short)-1) new_opts->allow_utime = ~new_opts->fs_dmask & 0022; From d090f9f99a73786c85176c41a91020068bca008f Mon Sep 17 00:00:00 2001 From: David Timber Date: Sat, 5 Sep 2026 17:10:42 +0000 Subject: [PATCH 07/20] exfat: update percent_in_use field when updating volume flags Currently, the exfat fs implementation does not modify the percent_in_use in the boot sector header at all. Although the field is only an advisory field for situations where bit counting is not feasible, not updating the field at all can cause confusion if the the value is actually exposed to the user. Calculate the value using integer arithmetic only when the dirty flag is cleared. If the dirty flag is set, set to 0xFF. It is set to 0xFF when mounting under the assumption that the number of used clusters will change - this is basically what is recommended in the SD Association's specs. When clearing the dirty flag, if overridden by sbi->vol_flags_persistent, set to 0xFF. This is intentional because if there are orphan clusters, the calculated value would be incorrect and correcting the fs error is fsck.exfat's job. Signed-off-by: David Timber Signed-off-by: Namjae Jeon --- fs/exfat/super.c | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/fs/exfat/super.c b/fs/exfat/super.c index 84f599b37a45..c13852eb1cc5 100644 --- a/fs/exfat/super.c +++ b/fs/exfat/super.c @@ -18,6 +18,7 @@ #include #include #include +#include #include "exfat_raw.h" #include "exfat_fs.h" @@ -69,16 +70,28 @@ static int exfat_statfs(struct dentry *dentry, struct kstatfs *buf) return 0; } +static inline __u8 exfat_calc_perc_in_use(const struct exfat_sb_info *sbi) +{ + return (__u8)mul_u64_u32_div(sbi->used_clusters, 100, + EXFAT_DATA_CLUSTER_COUNT(sbi)); +} + static int exfat_set_vol_flags(struct super_block *sb, unsigned short new_flags) { struct exfat_sb_info *sbi = EXFAT_SB(sb); struct boot_sector *p_boot = (struct boot_sector *)sbi->boot_bh->b_data; + __u8 new_piu; /* retain persistent-flags */ new_flags |= sbi->vol_flags_persistent; + if (new_flags & VOLUME_DIRTY) + new_piu = 0xFF; + else + new_piu = exfat_calc_perc_in_use(sbi); + /* flags are not changed */ - if (sbi->vol_flags == new_flags) + if (sbi->vol_flags == new_flags && new_piu == p_boot->percent_in_use) return 0; sbi->vol_flags = new_flags; @@ -90,6 +103,7 @@ static int exfat_set_vol_flags(struct super_block *sb, unsigned short new_flags) return 0; p_boot->vol_flags = cpu_to_le16(new_flags); + p_boot->percent_in_use = new_piu; set_buffer_uptodate(sbi->boot_bh); mark_buffer_dirty(sbi->boot_bh); From 775100031abe00e416585102deafc21e437866e7 Mon Sep 17 00:00:00 2001 From: David Timber Date: Sat, 5 Sep 2026 17:10:43 +0000 Subject: [PATCH 08/20] exfat: remove runtime integer division Replace the arithmetic ops that compiles to the use of actual integer division with bitwise ops. As the divisors involved are all power of two, integer division is not necessary. No functional change. Signed-off-by: David Timber Signed-off-by: Namjae Jeon --- fs/exfat/dir.c | 3 ++- fs/exfat/exfat_fs.h | 1 + fs/exfat/namei.c | 4 ++-- fs/exfat/super.c | 4 ++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/fs/exfat/dir.c b/fs/exfat/dir.c index fa9abedf4d84..46514b13bebd 100644 --- a/fs/exfat/dir.c +++ b/fs/exfat/dir.c @@ -1309,7 +1309,8 @@ static int exfat_get_volume_label_dentry(struct super_block *sb, es->bh = es->__bh; es->bh[0] = bh; es->num_bh = 1; - es->start_off = exfat_dentries_to_bytes(i) % sb->s_blocksize; + es->start_off = exfat_dentries_to_bytes(i) & + ((u32)sb->s_blocksize - 1); return 0; } diff --git a/fs/exfat/exfat_fs.h b/fs/exfat/exfat_fs.h index 5acfe7ef57d6..5f258e96fce9 100644 --- a/fs/exfat/exfat_fs.h +++ b/fs/exfat/exfat_fs.h @@ -232,6 +232,7 @@ struct exfat_sb_info { unsigned int num_FAT_sectors; /* num of FAT sectors */ unsigned int root_dir; /* root dir cluster */ unsigned int dentries_per_clu; /* num of dentries per cluster */ + unsigned int dentries_per_clu_bits; unsigned int vol_flags; /* volume flags */ unsigned int vol_flags_persistent; /* volume flags to retain */ struct buffer_head *boot_bh; /* buffer_head of BOOT sector */ diff --git a/fs/exfat/namei.c b/fs/exfat/namei.c index 1cd778a2380b..d116c89d724e 100644 --- a/fs/exfat/namei.c +++ b/fs/exfat/namei.c @@ -386,7 +386,7 @@ int exfat_find_empty_entry(struct inode *inode, } p_dir->dir = exfat_sector_to_cluster(sbi, es->bh[0]->b_blocknr); - p_dir->size -= dentry / sbi->dentries_per_clu; + p_dir->size -= dentry >> sbi->dentries_per_clu_bits; return dentry & (sbi->dentries_per_clu - 1); } @@ -638,7 +638,7 @@ static int exfat_find(struct inode *dir, const struct qstr *qname, /* adjust cdir to the optimized value */ cdir.dir = hint_opt.clu; if (cdir.flags & ALLOC_NO_FAT_CHAIN) - cdir.size -= dentry / sbi->dentries_per_clu; + cdir.size -= dentry >> sbi->dentries_per_clu_bits; dentry = hint_opt.eidx; info->dir = cdir; diff --git a/fs/exfat/super.c b/fs/exfat/super.c index c13852eb1cc5..4943cef97741 100644 --- a/fs/exfat/super.c +++ b/fs/exfat/super.c @@ -519,8 +519,8 @@ static int exfat_read_boot_sector(struct super_block *sb) EXFAT_RESERVED_CLUSTERS; sbi->root_dir = le32_to_cpu(p_boot->root_cluster); - sbi->dentries_per_clu = 1 << - (sbi->cluster_size_bits - DENTRY_SIZE_BITS); + sbi->dentries_per_clu_bits = sbi->cluster_size_bits - DENTRY_SIZE_BITS; + sbi->dentries_per_clu = 1 << sbi->dentries_per_clu_bits; sbi->vol_flags = le16_to_cpu(p_boot->vol_flags); sbi->vol_flags_persistent = sbi->vol_flags & (VOLUME_DIRTY | MEDIA_FAILURE); From 1306c1e6b0fe0a2b82eefda90987284854622c82 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 13 Sep 2026 20:45:43 +0900 Subject: [PATCH 09/20] MAINTAINERS: add exFAT documentation Add the exFAT filesystem documentation to the exfat entry in MAINTAINERS. Cc: David Timber Signed-off-by: Namjae Jeon --- MAINTAINERS | 1 + 1 file changed, 1 insertion(+) diff --git a/MAINTAINERS b/MAINTAINERS index 4b089108bf11..800d772a6e4b 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -9844,6 +9844,7 @@ R: Yuezhang Mo L: exfat@lists.linux.dev S: Maintained T: git git://git.kernel.org/pub/scm/linux/kernel/git/linkinjeon/exfat.git +F: Documentation/filesystems/exfat.rst F: fs/exfat/ EXPRESSWIRE PROTOCOL LIBRARY From b70ab407503706289163dd59f1ac3e942bacc37d Mon Sep 17 00:00:00 2001 From: Chi Zhiling Date: Sun, 4 Oct 2026 18:19:30 +0800 Subject: [PATCH 10/20] exfat: advance valid_size to EOF for append writes For an append write, the write position is recalculated by generic_write_checks(). Therefore, the write position must be obtained after generic_write_checks() returns. Using the original write position to advance valid_size may cause the valid_size update to be skipped, potentially exposing stale data. Also move the truncate_pagecache() check below the recalculated position, so that pos is never read before it is assigned. Fixes: 11a347fb6cef ("exfat: change to get file size from DataLength") Cc: stable@vger.kernel.org Signed-off-by: Chi Zhiling Signed-off-by: Namjae Jeon --- fs/exfat/file.c | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/fs/exfat/file.c b/fs/exfat/file.c index 3867e78c2312..87afb6bffd35 100644 --- a/fs/exfat/file.c +++ b/fs/exfat/file.c @@ -854,8 +854,7 @@ static ssize_t exfat_file_write_iter(struct kiocb *iocb, struct iov_iter *iter) struct file *file = iocb->ki_filp; struct inode *inode = file_inode(file); struct exfat_inode_info *ei = EXFAT_I(inode); - loff_t pos = iocb->ki_pos; - loff_t valid_size; + loff_t pos, valid_size; int err; if (unlikely(exfat_forced_shutdown(inode->i_sb))) @@ -863,11 +862,6 @@ static ssize_t exfat_file_write_iter(struct kiocb *iocb, struct iov_iter *iter) inode_lock(inode); - if (pos > i_size_read(inode)) - truncate_pagecache(inode, i_size_read(inode)); - - valid_size = ei->valid_size; - ret = generic_write_checks(iocb, iter); if (ret <= 0) goto unlock; @@ -878,6 +872,11 @@ static ssize_t exfat_file_write_iter(struct kiocb *iocb, struct iov_iter *iter) goto unlock; } + pos = iocb->ki_pos; + if (pos > i_size_read(inode)) + truncate_pagecache(inode, i_size_read(inode)); + + valid_size = ei->valid_size; if (pos > valid_size) { ret = exfat_extend_valid_size(inode, pos); if (ret < 0 && ret != -ENOSPC) { @@ -887,6 +886,8 @@ static ssize_t exfat_file_write_iter(struct kiocb *iocb, struct iov_iter *iter) } if (ret < 0) goto unlock; + + pos = valid_size; } if (iocb->ki_flags & IOCB_DIRECT) @@ -899,9 +900,6 @@ static ssize_t exfat_file_write_iter(struct kiocb *iocb, struct iov_iter *iter) inode_unlock(inode); - if (pos > valid_size) - pos = valid_size; - if (iocb->ki_pos > pos) { ssize_t err = generic_write_sync(iocb, iocb->ki_pos - pos); From 646df471c37483eb31548d76a1b062775db71d60 Mon Sep 17 00:00:00 2001 From: Chi Zhiling Date: Sun, 4 Oct 2026 18:19:31 +0800 Subject: [PATCH 11/20] exfat: hold the invalidate lock while truncating truncate_setsize() ends in truncate_inode_pages(), which is documented as being "Called under (and serialised by) inode->i_rwsem and mapping->invalidate_lock". Hold invalidate_lock while executing truncate_setsize() to ensure that truncate and mmap writes are mutually exclusive. This can simplify the handling in exfat_page_mkwrite(). Fixes: 82a81a7352bc ("exfat: add iomap buffered I/O support") Cc: stable@vger.kernel.org Signed-off-by: Chi Zhiling Signed-off-by: Namjae Jeon --- fs/exfat/file.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fs/exfat/file.c b/fs/exfat/file.c index 87afb6bffd35..871011750990 100644 --- a/fs/exfat/file.c +++ b/fs/exfat/file.c @@ -413,6 +413,7 @@ int exfat_setattr(const struct mnt_idmap *idmap, struct dentry *dentry, * about to be freed. */ inode_dio_wait(inode); + filemap_invalidate_lock(inode->i_mapping); truncate_setsize(inode, attr->ia_size); /* @@ -420,6 +421,7 @@ int exfat_setattr(const struct mnt_idmap *idmap, struct dentry *dentry, * is already written by it, so mark_inode_dirty() is unneeded. */ exfat_truncate(inode); + filemap_invalidate_unlock(inode->i_mapping); } else mark_inode_dirty(inode); @@ -784,8 +786,10 @@ static int exfat_extend_valid_size(struct inode *inode, loff_t new_valid_size) ret = exfat_zero_new_range(inode, gap_start, new_valid_size); filemap_invalidate_unlock(inode->i_mapping); if (ret) { + filemap_invalidate_lock(inode->i_mapping); truncate_setsize(inode, old_valid_size); exfat_truncate(inode); + filemap_invalidate_unlock(inode->i_mapping); return ret; } From 03ac97285b699954fa7bc24f8b6cdc1830c94d28 Mon Sep 17 00:00:00 2001 From: Chi Zhiling Date: Sun, 4 Oct 2026 18:19:32 +0800 Subject: [PATCH 12/20] exfat: dirty all new pages when extending valid_size In the current code, exfat_zero_new_range() does not mark non-uptodate pages as dirty, which may cause some blocks within a page to not be written back. This commit no longer checks each block in a non-uptodate page individually. Instead, it reads the entire page back into the cache and then writes it back as a whole. This allows concurrent writes and zeroing operations to be synchronized using the folio lock. Fixes: 1135704ed22f ("exfat: fix valid_size extension over a shared writable mapping") Cc: stable@vger.kernel.org Signed-off-by: Chi Zhiling Signed-off-by: Namjae Jeon --- fs/exfat/file.c | 106 ++++++++++++------------------------------------ 1 file changed, 25 insertions(+), 81 deletions(-) diff --git a/fs/exfat/file.c b/fs/exfat/file.c index 871011750990..1aad24fd313f 100644 --- a/fs/exfat/file.c +++ b/fs/exfat/file.c @@ -656,98 +656,42 @@ int exfat_file_fsync(struct file *filp, loff_t start, loff_t end, int datasync) } /* - * exfat_zero_new_range - zero [start, end) without overwriting uptodate blocks + * exfat_prepare_valid_range - populate and dirty folios covering [start, end) * - * Uptodate blocks may contain data written through a shared mapping beyond - * valid_size. + * Populate missing blocks via the read path, which zero-fills data + * beyond the current valid_size and skips uptodate blocks. + * Mark entire folios dirty so the newly valid range is written back. + * + * Call before advancing valid_size. */ -static int exfat_zero_new_range(struct inode *inode, loff_t start, loff_t end) +static int exfat_prepare_valid_range(struct inode *inode, loff_t start, + loff_t end) { struct address_space *mapping = inode->i_mapping; - unsigned int blocksize = i_blocksize(inode); - loff_t pos = start; - int err; + loff_t next, pos = start; + struct folio *folio; + pgoff_t index; + + if (pos >= end) + return 0; while (pos < end) { - loff_t next = min_t(loff_t, - round_down(pos, PAGE_SIZE) + PAGE_SIZE, end); - struct folio *folio; - loff_t bpos; + index = pos >> PAGE_SHIFT; + next = min(((loff_t)index + 1) << PAGE_SHIFT, end); - folio = filemap_get_folio(mapping, pos >> PAGE_SHIFT); - if (IS_ERR(folio)) { - err = iomap_zero_range(inode, pos, next - pos, NULL, - &exfat_iomap_ops, NULL, NULL); - if (err < 0) - return err; - pos = next; - continue; - } + balance_dirty_pages_ratelimited(mapping); - if (folio_test_uptodate(folio)) { - folio_lock(folio); - if (folio->mapping == mapping) - folio_mark_dirty(folio); - folio_unlock(folio); - folio_put(folio); - pos = next; - continue; - } + folio = read_mapping_folio(mapping, index, NULL); + if (IS_ERR(folio)) + return PTR_ERR(folio); - /* - * Zero not-uptodate block runs. iomap_zero_range() requires an - * unlocked folio, so recheck ->mapping after each call. - */ folio_lock(folio); - bpos = pos; - while (bpos < next) { - loff_t rstart, rend; - - if (folio->mapping != mapping) { - folio_unlock(folio); - err = iomap_zero_range(inode, bpos, next - bpos, - NULL, &exfat_iomap_ops, NULL, NULL); - if (err < 0) { - folio_put(folio); - return err; - } - folio_lock(folio); - break; - } - - if (iomap_is_partially_uptodate(folio, - offset_in_folio(folio, bpos), blocksize)) { - bpos += blocksize; - continue; - } - - rstart = bpos; - rend = min_t(loff_t, bpos + blocksize, next); - while (rend < next && - !iomap_is_partially_uptodate(folio, - offset_in_folio(folio, rend), blocksize)) - rend = min_t(loff_t, rend + blocksize, next); - - folio_unlock(folio); - err = iomap_zero_range(inode, rstart, rend - rstart, - NULL, &exfat_iomap_ops, NULL, NULL); - if (err < 0) { - folio_put(folio); - return err; - } - folio_lock(folio); - bpos = rend; - } - - /* - * Dirty only a fully uptodate folio. Dirtying a partial folio could - * write uninitialised cache contents over valid on-disk blocks. - */ - if (folio->mapping == mapping && folio_test_uptodate(folio)) + if (folio->mapping == mapping) { folio_mark_dirty(folio); + pos = next; + } folio_unlock(folio); folio_put(folio); - pos = next; } return 0; @@ -783,7 +727,7 @@ static int exfat_extend_valid_size(struct inode *inode, loff_t new_valid_size) if (gap_start < new_valid_size) unmap_mapping_range(inode->i_mapping, gap_start, new_valid_size - gap_start, 0); - ret = exfat_zero_new_range(inode, gap_start, new_valid_size); + ret = exfat_prepare_valid_range(inode, gap_start, new_valid_size); filemap_invalidate_unlock(inode->i_mapping); if (ret) { filemap_invalidate_lock(inode->i_mapping); @@ -965,7 +909,7 @@ static vm_fault_t exfat_page_mkwrite(struct vm_fault *vmf) * fault populated its folio and iomap_page_mkwrite() * will dirty it. */ - err = exfat_zero_new_range(inode, ei->zeroed_size, + err = exfat_prepare_valid_range(inode, ei->zeroed_size, fault_page_start); if (err < 0) { inode_unlock(inode); From 6bb112f5d90a78461648e3bd89093ea299196be5 Mon Sep 17 00:00:00 2001 From: Chi Zhiling Date: Sun, 4 Oct 2026 18:19:33 +0800 Subject: [PATCH 13/20] exfat: make valid_size and zeroed_size atomic ei->valid_size and ei->zeroed_size are 64-bit fields read from bio completion context (exfat_iomap_read_end_io()) and from ->iomap_begin without i_rwsem, while writers update them under i_rwsem. On 32-bit these loads can tear, and the "if (x < new) x = new" read-modify-write can lose an update or regress the value when two writers race. Convert both to atomic64_t and funnel updates through a cmpxchg-based monotonic advance helper so the value only moves forward; plain reads use exfat_get_*(), and the few shrink/init paths (truncate, inode fill, create) use exfat_set_*(). This is a prerequisite for taking exfat_page_mkwrite() out from under i_rwsem. Fixes: 82a81a7352bc ("exfat: add iomap buffered I/O support") Cc: stable@vger.kernel.org Signed-off-by: Chi Zhiling Signed-off-by: Namjae Jeon --- fs/exfat/exfat_fs.h | 60 +++++++++++++++++++++++++++++++++++++++++++-- fs/exfat/file.c | 32 +++++++++++++----------- fs/exfat/inode.c | 16 ++++++------ fs/exfat/iomap.c | 20 +++++++-------- fs/exfat/namei.c | 4 +-- 5 files changed, 96 insertions(+), 36 deletions(-) diff --git a/fs/exfat/exfat_fs.h b/fs/exfat/exfat_fs.h index 5f258e96fce9..6ebda3cb4d76 100644 --- a/fs/exfat/exfat_fs.h +++ b/fs/exfat/exfat_fs.h @@ -7,6 +7,7 @@ #define _EXFAT_FS_H #include +#include #include #include #include @@ -294,9 +295,16 @@ struct exfat_inode_info { /* on-disk position of directory entry or 0 */ loff_t i_pos; - loff_t valid_size; + /* + * valid_size and zeroed_size are updated from multiple contexts that + * are not serialised against each other (page_mkwrite runs without + * i_rwsem, while buffered/DIO writes advance them under i_rwsem). Keep + * them atomic and only ever advance them with a cmpxchg loop so a + * concurrent update can never regress the value or tear on 32-bit. + */ + atomic64_t valid_size; /* block-aligned size zeroed in the page cache (>= valid_size) */ - loff_t zeroed_size; + atomic64_t zeroed_size; /* hash by i_location */ struct hlist_node i_hash_fat; struct inode vfs_inode; @@ -314,6 +322,54 @@ static inline struct exfat_inode_info *EXFAT_I(struct inode *inode) return container_of(inode, struct exfat_inode_info, vfs_inode); } +static inline loff_t exfat_get_valid_size(struct exfat_inode_info *ei) +{ + return atomic64_read(&ei->valid_size); +} + +static inline loff_t exfat_get_zeroed_size(struct exfat_inode_info *ei) +{ + return atomic64_read(&ei->zeroed_size); +} + +static inline void exfat_set_valid_size(struct exfat_inode_info *ei, loff_t v) +{ + atomic64_set(&ei->valid_size, v); +} + +static inline void exfat_set_zeroed_size(struct exfat_inode_info *ei, loff_t v) +{ + atomic64_set(&ei->zeroed_size, v); +} + +/* + * Monotonically advance @v to at least @new. Returns true if the value was + * actually raised, so callers can decide whether to mark the inode dirty. + */ +static inline bool exfat_size_advance(atomic64_t *v, loff_t new) +{ + loff_t old = atomic64_read(v); + + do { + if (old >= new) + return false; + } while (!atomic64_try_cmpxchg(v, &old, new)); + + return true; +} + +static inline bool exfat_advance_valid_size(struct exfat_inode_info *ei, + loff_t new) +{ + return exfat_size_advance(&ei->valid_size, new); +} + +static inline bool exfat_advance_zeroed_size(struct exfat_inode_info *ei, + loff_t new) +{ + return exfat_size_advance(&ei->zeroed_size, new); +} + static inline int exfat_forced_shutdown(struct super_block *sb) { return test_bit(EXFAT_FLAGS_SHUTDOWN, &EXFAT_SB(sb)->s_exfat_flags); diff --git a/fs/exfat/file.c b/fs/exfat/file.c index 1aad24fd313f..054af3bf7dc2 100644 --- a/fs/exfat/file.c +++ b/fs/exfat/file.c @@ -247,8 +247,10 @@ int __exfat_truncate(struct inode *inode) ei->start_clu = EXFAT_EOF_CLUSTER; } - if (i_size_read(inode) < ei->valid_size) - ei->valid_size = ei->zeroed_size = i_size_read(inode); + if (i_size_read(inode) < exfat_get_valid_size(ei)) { + exfat_set_valid_size(ei, i_size_read(inode)); + exfat_set_zeroed_size(ei, i_size_read(inode)); + } if (ei->type == TYPE_FILE) ei->attr |= EXFAT_ATTR_ARCHIVE; @@ -700,12 +702,12 @@ static int exfat_prepare_valid_range(struct inode *inode, loff_t start, static int exfat_extend_valid_size(struct inode *inode, loff_t new_valid_size) { struct exfat_inode_info *ei = EXFAT_I(inode); - loff_t old_valid_size = ei->valid_size; + loff_t old_valid_size = exfat_get_valid_size(ei); int ret = 0; if (old_valid_size < new_valid_size) { /* Do not re-zero blocks already covered by zeroed_size. */ - loff_t gap_start = max(old_valid_size, ei->zeroed_size); + loff_t gap_start = max(old_valid_size, exfat_get_zeroed_size(ei)); if (i_size_read(inode) < new_valid_size) { /* @@ -737,9 +739,9 @@ static int exfat_extend_valid_size(struct inode *inode, loff_t new_valid_size) return ret; } - ei->valid_size = new_valid_size; - if (ei->zeroed_size < round_up(new_valid_size, i_blocksize(inode))) - ei->zeroed_size = round_up(new_valid_size, i_blocksize(inode)); + exfat_set_valid_size(ei, new_valid_size); + exfat_advance_zeroed_size(ei, + round_up(new_valid_size, i_blocksize(inode))); mark_inode_dirty(inode); } @@ -824,7 +826,7 @@ static ssize_t exfat_file_write_iter(struct kiocb *iocb, struct iov_iter *iter) if (pos > i_size_read(inode)) truncate_pagecache(inode, i_size_read(inode)); - valid_size = ei->valid_size; + valid_size = exfat_get_valid_size(ei); if (pos > valid_size) { ret = exfat_extend_valid_size(inode, pos); if (ret < 0 && ret != -ENOSPC) { @@ -900,8 +902,10 @@ static vm_fault_t exfat_page_mkwrite(struct vm_fault *vmf) fault_page_start = ((loff_t)vmf->pgoff) << PAGE_SHIFT; new_valid_size = min(mmap_valid_size, i_size_read(inode)); - if (ei->valid_size < new_valid_size) { - if (ei->zeroed_size < fault_page_start) { + if (exfat_get_valid_size(ei) < new_valid_size) { + loff_t zeroed_size = exfat_get_zeroed_size(ei); + + if (zeroed_size < fault_page_start) { int err; /* @@ -909,7 +913,7 @@ static vm_fault_t exfat_page_mkwrite(struct vm_fault *vmf) * fault populated its folio and iomap_page_mkwrite() * will dirty it. */ - err = exfat_prepare_valid_range(inode, ei->zeroed_size, + err = exfat_prepare_valid_range(inode, zeroed_size, fault_page_start); if (err < 0) { inode_unlock(inode); @@ -922,9 +926,9 @@ static vm_fault_t exfat_page_mkwrite(struct vm_fault *vmf) * at i_size recording blocks wholly beyond it could skip a * later required zeroing. */ - if (ei->zeroed_size < round_up(new_valid_size, i_blocksize(inode))) - ei->zeroed_size = round_up(new_valid_size, i_blocksize(inode)); - ei->valid_size = new_valid_size; + exfat_advance_zeroed_size(ei, + round_up(new_valid_size, i_blocksize(inode))); + exfat_set_valid_size(ei, new_valid_size); mark_inode_dirty(inode); } diff --git a/fs/exfat/inode.c b/fs/exfat/inode.c index ccd13630187e..5129df1e79b4 100644 --- a/fs/exfat/inode.c +++ b/fs/exfat/inode.c @@ -29,6 +29,7 @@ int __exfat_write_inode(struct inode *inode, int sync) struct exfat_sb_info *sbi = EXFAT_SB(sb); struct exfat_inode_info *ei = EXFAT_I(inode); bool is_dir = (ei->type == TYPE_DIR); + loff_t valid_size = exfat_get_valid_size(ei); struct timespec64 ts; if (inode->i_ino == EXFAT_ROOT_INO) @@ -81,7 +82,7 @@ int __exfat_write_inode(struct inode *inode, int sync) * preventing fsck from reporting "more clusters are allocated". */ on_disk_size = max_t(unsigned long long, i_size_read(inode), - ei->valid_size); + valid_size); if (ei->start_clu == EXFAT_EOF_CLUSTER) on_disk_size = 0; @@ -89,7 +90,7 @@ int __exfat_write_inode(struct inode *inode, int sync) * valid_size on disk must reflect only confirmed data (up to i_size) * and must not exceed on_disk_size. */ - on_disk_valid_size = min_t(unsigned long long, ei->valid_size, + on_disk_valid_size = min_t(unsigned long long, valid_size, i_size_read(inode)); if (ei->start_clu == EXFAT_EOF_CLUSTER) on_disk_valid_size = 0; @@ -258,15 +259,16 @@ static void exfat_readahead(struct readahead_control *rac) struct inode *inode = mapping->host; struct exfat_inode_info *ei = EXFAT_I(inode); loff_t pos = readahead_pos(rac); + loff_t valid_size = exfat_get_valid_size(ei); struct iomap_read_folio_ctx ctx = { .ops = &exfat_iomap_bio_read_ops, .rac = rac, }; /* Range cross valid_size, read it page by page. */ - if (ei->valid_size < i_size_read(inode) && - pos <= ei->valid_size && - ei->valid_size < pos + readahead_length(rac)) + if (valid_size < i_size_read(inode) && + pos <= valid_size && + valid_size < pos + readahead_length(rac)) return; iomap_readahead(&exfat_iomap_ops, &ctx, NULL); @@ -371,8 +373,8 @@ static int exfat_fill_inode(struct inode *inode, struct exfat_dir_entry *info) ei->start_clu = info->start_clu; ei->flags = info->flags; ei->type = info->type; - ei->valid_size = info->valid_size; - ei->zeroed_size = info->valid_size; + exfat_set_valid_size(ei, info->valid_size); + exfat_set_zeroed_size(ei, info->valid_size); ei->version = 0; ei->hint_stat.eidx = 0; diff --git a/fs/exfat/iomap.c b/fs/exfat/iomap.c index 533cdb4f0929..291367e10c1e 100644 --- a/fs/exfat/iomap.c +++ b/fs/exfat/iomap.c @@ -46,6 +46,7 @@ static int __exfat_iomap_begin(struct inode *inode, loff_t offset, loff_t length struct exfat_inode_info *ei = EXFAT_I(inode); unsigned int cluster, num_clusters; loff_t cluster_offset, cluster_length; + loff_t valid_size = exfat_get_valid_size(ei); int err; bool balloc = false; @@ -88,7 +89,7 @@ static int __exfat_iomap_begin(struct inode *inode, loff_t offset, loff_t length if (may_alloc || flags & IOMAP_ZERO) { if (balloc) iomap->flags |= IOMAP_F_NEW; - else if (iomap->offset + iomap->length >= ei->valid_size) { + else if (iomap->offset + iomap->length >= valid_size) { /* * This is a write that starts at or extends beyond * the current valid_size. The region between the old @@ -114,10 +115,10 @@ static int __exfat_iomap_begin(struct inode *inode, loff_t offset, loff_t length * return IOMAP_UNWRITTEN so the write path can * distinguish it from a real hole. */ - if (offset >= ei->valid_size) { + if (offset >= valid_size) { iomap->type = flags & IOMAP_REPORT ? IOMAP_HOLE : IOMAP_UNWRITTEN; - } else if (offset + iomap->length > ei->valid_size) { + } else if (offset + iomap->length > valid_size) { if (flags & IOMAP_REPORT) { /* * For SEEK_HOLE/SEEK_DATA, clip the length @@ -125,9 +126,9 @@ static int __exfat_iomap_begin(struct inode *inode, loff_t offset, loff_t length * This ensures the caller gets the precise * hole position in byte units. */ - iomap->length = ei->valid_size - iomap->offset; + iomap->length = valid_size - iomap->offset; } else - iomap->length = round_up(ei->valid_size, + iomap->length = round_up(valid_size, i_blocksize(inode)) - iomap->offset; } @@ -196,10 +197,8 @@ static int exfat_write_iomap_end(struct inode *inode, loff_t pos, loff_t length, end = pos + written; - if (ei->valid_size < end) { - ei->valid_size = end; + if (exfat_advance_valid_size(ei, end)) dirtied = true; - } /* * IOMAP_F_ZERO_TAIL zeroes the remainder of the last block. Track that @@ -207,8 +206,7 @@ static int exfat_write_iomap_end(struct inode *inode, loff_t pos, loff_t length, */ if (iomap->flags & IOMAP_F_ZERO_TAIL) end = round_up(end, i_blocksize(inode)); - if (ei->zeroed_size < end) - ei->zeroed_size = end; + exfat_advance_zeroed_size(ei, end); if (dirtied || iomap->flags & IOMAP_F_SIZE_CHANGED) mark_inode_dirty(inode); @@ -271,7 +269,7 @@ static void exfat_iomap_read_end_io(struct bio *bio) s64 valid_size; loff_t pos = folio_pos(folio); - valid_size = ei->valid_size; + valid_size = exfat_get_valid_size(ei); if (pos + iter.offset < valid_size && pos + iter.offset + iter.length > valid_size) folio_zero_segment(folio, offset_in_folio(folio, valid_size), diff --git a/fs/exfat/namei.c b/fs/exfat/namei.c index d116c89d724e..68ddb9c5504f 100644 --- a/fs/exfat/namei.c +++ b/fs/exfat/namei.c @@ -380,7 +380,7 @@ int exfat_find_empty_entry(struct inode *inode, /* directory inode should be updated in here */ i_size_write(inode, size); - ei->valid_size += sbi->cluster_size; + atomic64_add(sbi->cluster_size, &ei->valid_size); ei->flags = p_dir->flags; inode->i_blocks += sbi->cluster_size >> 9; } @@ -1249,7 +1249,7 @@ static int __exfat_rename(struct inode *old_parent_inode, } i_size_write(new_inode, 0); - new_ei->valid_size = 0; + exfat_set_valid_size(new_ei, 0); new_ei->start_clu = EXFAT_EOF_CLUSTER; new_ei->flags = ALLOC_NO_FAT_CHAIN; } From acea17f0f4c7742118375e96d3f876be34e449a5 Mon Sep 17 00:00:00 2001 From: Jiale Yao Date: Sat, 3 Oct 2026 17:30:35 +0800 Subject: [PATCH 14/20] exfat: drain in-flight DIO before buffered writes An asynchronous direct write can remain in flight after the inode lock is released. If a buffered operation dirties page cache while the direct write is still pending, the direct write's post-I/O invalidation can find the dirty pages, report a page cache invalidation failure, and record -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. ExFAT does not drain in-flight DIO before a regular buffered write, the buffered fallback after iomap_dio_rw() returns -ENOTBLK or a short write, or the page-cache operations used to extend valid_size. Wait for in-flight DIO before these paths can dirty page cache. 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: 867b9c96dc83 ("exfat: add iomap direct I/O support") Cc: stable@vger.kernel.org Link: https://lore.kernel.org/r/20260629113827.4074335-3-libaokun@linux.alibaba.com Signed-off-by: Jiale Yao Signed-off-by: Namjae Jeon --- fs/exfat/file.c | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/fs/exfat/file.c b/fs/exfat/file.c index 054af3bf7dc2..a40fc1cd14b7 100644 --- a/fs/exfat/file.c +++ b/fs/exfat/file.c @@ -706,6 +706,8 @@ static int exfat_extend_valid_size(struct inode *inode, loff_t new_valid_size) int ret = 0; if (old_valid_size < new_valid_size) { + inode_dio_wait(inode); + /* Do not re-zero blocks already covered by zeroed_size. */ loff_t gap_start = max(old_valid_size, exfat_get_zeroed_size(ei)); @@ -757,6 +759,8 @@ static ssize_t exfat_fallback_buffered_write(struct kiocb *iocb, iocb->ki_flags &= ~IOCB_DIRECT; + inode_dio_wait(file_inode(iocb->ki_filp)); + written = iomap_file_buffered_write(iocb, from, &exfat_write_iomap_ops, NULL, NULL); if (written < 0) @@ -840,11 +844,17 @@ static ssize_t exfat_file_write_iter(struct kiocb *iocb, struct iov_iter *iter) pos = valid_size; } - if (iocb->ki_flags & IOCB_DIRECT) + if (iocb->ki_flags & IOCB_DIRECT) { ret = exfat_dio_write_iter(iocb, iter); - else + } else { + /* + * 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(inode); ret = iomap_file_buffered_write(iocb, iter, &exfat_write_iomap_ops, NULL, NULL); + } if (ret < 0) goto unlock; From 748ad4ba13cd14163ed7262c6ac304c380804167 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Tue, 6 Oct 2026 13:06:39 +0900 Subject: [PATCH 15/20] exfat: zero invalid tail on partial overwrites Buffered overwrite that ends before valid_size in the same filesystem block reads the entire block without IOMAP_F_ZERO_TAIL. If an existing cluster contains stale bytes beyond valid_size, the synchronous read loads them into the page cache and marks the block uptodate. Subsequent buffered reads can return these bytes while i_size exceeds valid_size. For example, with 512-byte blocks, valid_size of 1000 and i_size of 2048, a cold-cache overwrite of [900, 950) exposes stale data in [1000, 1024). Setting ZERO_TAIL for this write would also erase valid data in [950, 1000). Use iomap_truncate_page() to zero only the invalid tail at valid_size before preparing such buffered overwrites. Call it after dropping s_lock and before iomap locks the write folio. The inode lock held by buffered writes serializes valid_size against truncation and page_mkwrite. Skip direct I/O, fault mappings and new allocations. Writes that reach valid_size continue to use IOMAP_F_ZERO_TAIL. Fixes: 82a81a7352bc ("exfat: add iomap buffered I/O support") Cc: stable@vger.kernel.org Signed-off-by: Namjae Jeon --- fs/exfat/iomap.c | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/fs/exfat/iomap.c b/fs/exfat/iomap.c index 291367e10c1e..bec0b35b9420 100644 --- a/fs/exfat/iomap.c +++ b/fs/exfat/iomap.c @@ -149,7 +149,27 @@ static int exfat_iomap_begin(struct inode *inode, loff_t offset, loff_t length, static int exfat_write_iomap_begin(struct inode *inode, loff_t offset, loff_t length, unsigned int flags, struct iomap *iomap, struct iomap *srcmap) { - return __exfat_iomap_begin(inode, offset, length, flags, iomap, true); + struct exfat_inode_info *ei = EXFAT_I(inode); + loff_t valid_size = exfat_get_valid_size(ei); + loff_t end; + int err; + + err = __exfat_iomap_begin(inode, offset, length, flags, iomap, true); + if (err || flags & (IOMAP_DIRECT | IOMAP_FAULT) || + iomap->flags & IOMAP_F_NEW) + return err; + + end = iomap->offset + iomap->length; + if (end >= valid_size || + round_up(end, i_blocksize(inode)) <= valid_size) + return 0; + + /* + * Zero the invalid bytes from valid_size to the end of the block + * to prevent stale data from being exposed through the page cache. + */ + return iomap_truncate_page(inode, valid_size, NULL, &exfat_iomap_ops, + NULL, NULL); } static DEFINE_IOMAP_ITER_NEXT(exfat_iomap_next, exfat_iomap_begin); From 73cababf93cb371e2e9d06da5261ca6ddd2404b6 Mon Sep 17 00:00:00 2001 From: Hengyu Liang Date: Sat, 10 Oct 2026 00:44:20 -0400 Subject: [PATCH 16/20] exfat: fix a large buffered write failing when the volume is nearly full Commit 82a81a7352bc ("exfat: add iomap buffered I/O support") made a buffered write map the whole range of the write in one step, so the clusters for all of it are allocated at once. However, exfat_alloc_cluster() returns -ENOSPC when fewer clusters are free than it is asked for. As of now, a write() that is larger than the free space writes nothing and fails with ENOSPC, while it used to write as much as fits and return a short count. The issue can be reproduced with a 16M image made by mkfs.exfat that is filled except for 256K: mount -o loop /img /mnt dd if=/dev/zero of=/mnt/hole bs=256k count=1 dd if=/dev/zero of=/mnt/fill bs=4k rm /mnt/hole dd if=/dev/zero of=/mnt/f bs=1M count=1 wc -c < /mnt/f Before commit 82a81a7352bc ("exfat: add iomap buffered I/O support"), the result is: 262144 After that commit, the result is: 0 This patch will map a single cluster when the clusters for the whole range cannot be allocated, so that the write goes on until the volume is full. Direct I/O is not changed, it still fails without writing as it did before that commit. Fixes: 82a81a7352bc ("exfat: add iomap buffered I/O support") Cc: stable@vger.kernel.org Signed-off-by: Hengyu Liang Signed-off-by: Namjae Jeon --- fs/exfat/iomap.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/fs/exfat/iomap.c b/fs/exfat/iomap.c index bec0b35b9420..e50ecbfbfbe0 100644 --- a/fs/exfat/iomap.c +++ b/fs/exfat/iomap.c @@ -75,6 +75,17 @@ static int __exfat_iomap_begin(struct inode *inode, loff_t offset, loff_t length err = exfat_map_cluster(inode, exfat_bytes_to_cluster(sbi, offset), &cluster, &num_clusters, may_alloc, &balloc); + if (err == -ENOSPC && !(flags & IOMAP_DIRECT)) { + /* + * There are not enough free clusters for the whole range. A + * buffered write can be short, so map what still fits. + */ + num_clusters = 1; + err = exfat_map_cluster(inode, + exfat_bytes_to_cluster(sbi, offset), + &cluster, &num_clusters, may_alloc, + &balloc); + } if (err) goto out; From 1043264c3cfa43b0668f7472a749868c1e21749b Mon Sep 17 00:00:00 2001 From: Yuezhang Mo Date: Sat, 10 Oct 2026 18:12:01 +0800 Subject: [PATCH 17/20] exfat: mark straddling folio RO and zero the post-EOF range When extending a file across a non-page-aligned EOF, the folio containing the old EOF must be marked RO so that subsequent mmap writes trigger ->page_mkwrite() and update valid_size. The straddling folio remains mapped to the same filesystem block after the file is extended. As a result, the existing writable PTE may remain in place, allowing mmap writes to bypass ->page_mkwrite() and leave valid_size unchanged. Furthermore, writes to the post-EOF range may occur between truncate_pagecache() and locking the folio. So the post-EOF range should be zeroed after marking the straddling folio RO, and using truncate_pagecache() to zero out the post-EOF range is redundant. Introduce exfat_pagecache_isize_extended() to mark the straddling folio RO and zero the post-EOF range after clearing writable mapping. This ensures that data written beyond the old EOF before the extension is not exposed as valid file data, and that subsequent mmap writes trigger ->page_mkwrite() to update valid_size. Fixes: 82a81a7352bc ("exfat: add iomap buffered I/O support") Cc: stable@vger.kernel.org Signed-off-by: Yuezhang Mo Signed-off-by: Namjae Jeon --- fs/exfat/file.c | 72 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 63 insertions(+), 9 deletions(-) diff --git a/fs/exfat/file.c b/fs/exfat/file.c index a40fc1cd14b7..22cb32de5515 100644 --- a/fs/exfat/file.c +++ b/fs/exfat/file.c @@ -17,11 +17,70 @@ #include #include #include +#include #include "exfat_raw.h" #include "exfat_fs.h" #include "iomap.h" +/* + * Unlike pagecache_isize_extended(), this function updates i_size while + * holding the straddling folio lock, and does not skip the straddling + * folio when the filesystem block size is >= PAGE_SIZE or when 'from' + * and 'to' fall within the same filesystem block. + * + * This ensures that the folio is marked RO and the post-EOF range is + * zeroed before readers or writeback can observe the updated i_size. + */ +static void exfat_pagecache_isize_extended(struct inode *inode, loff_t from, + loff_t to) +{ + struct folio *folio; + + if (!(from & (PAGE_SIZE - 1))) { + i_size_write(inode, to); + return; + } + + folio = filemap_lock_folio(inode->i_mapping, from >> PAGE_SHIFT); + + /* Folio not cached? Nothing to do */ + if (IS_ERR(folio)) { + i_size_write(inode, to); + return; + } + + /* + * See folio_clear_dirty_for_io() for details why folio_mark_dirty() + * is needed. + */ + if (folio_mkclean(folio)) + folio_mark_dirty(folio); + + /* + * Zero the post-EOF range before publishing the new size. Writeback + * normally does this, but the extension would expose the tail to + * readers before writeback gets a chance to zero it. + */ + if (folio_test_dirty(folio)) { + loff_t offset, end; + + offset = from - folio_pos(folio); + end = min_t(loff_t, to - folio_pos(folio), + folio_size(folio)); + folio_zero_segment(folio, offset, end); + } + + /* + * Publish the new size after zeroing the tail. Uptodate folios can + * be read without taking the folio lock. + */ + i_size_write(inode, to); + + folio_unlock(folio); + folio_put(folio); +} + static int exfat_cont_expand(struct inode *inode, loff_t size) { int ret; @@ -32,8 +91,6 @@ static int exfat_cont_expand(struct inode *inode, loff_t size) struct exfat_chain clu; loff_t oldsize = i_size_read(inode); - truncate_pagecache(inode, oldsize); - ret = inode_newsize_ok(inode, size); if (ret) return ret; @@ -81,15 +138,12 @@ static int exfat_cont_expand(struct inode *inode, loff_t size) out: inode_set_mtime_to_ts(inode, inode_set_ctime_current(inode)); - /* Expanded range not zeroed, do not update valid_size */ - i_size_write(inode, size); /* - * When extending file size, call truncate_pagecache() first, - * then update i_size, and call pagecache_isize_extended() - * to ensures the straddling folio is properly marked RO so - * page_mkwrite() is called and post-EOF area is zeroed. + * When extending file size, call exfat_pagecache_isize_extended() + * to update i_size and ensure the straddling folio is properly + * marked RO so page_mkwrite() is called and post-EOF area is zeroed. */ - pagecache_isize_extended(inode, oldsize, inode->i_size); + exfat_pagecache_isize_extended(inode, oldsize, size); inode->i_blocks = round_up(size, sbi->cluster_size) >> 9; mark_inode_dirty(inode); From 0550ac7f5f577e34e5fcf2bd7a51965f5c1b0194 Mon Sep 17 00:00:00 2001 From: Nicolas Falesy Date: Sat, 10 Oct 2026 19:15:13 +0000 Subject: [PATCH 18/20] exfat: clean stale bdev buffers of newly allocated clusters Directory entries are read and written through buffer_heads of the block device. Removing the entries of a directory dirties such a buffer. If the directory is then removed and its cluster is reused for file data before writeback, the stale buffer stays dirty in the block device mapping, and the next flush of the block device (fsync, syncfs or umount) writes it over the new file data. The file silently reads back with one sector replaced by the deleted directory entries. This was harmless while file data went through exfat_get_block(), as __block_write_begin_int() and do_direct_IO() call clean_bdev_aliases() for newly allocated blocks. Since data I/O moved to iomap, nothing drops these buffers any more. Clean the aliases in exfat_alloc_cluster(), once per contiguous run of allocated clusters. Doing it at allocation time covers every path that allocates clusters: buffered and direct writes, and also fallocate() and an extending truncate(), which allocate clusters before the data is written, so the later iomap mapping does not see them as new. Tested on the exfat dev branch in a KVM guest with a reproducer that removes the entries of a directory, removes the directory and writes a file over its freed cluster: buffered with and without fsync, O_DIRECT, after fallocate(), after an extending ftruncate(), and with the freed cluster in the middle of a fragmented allocation. Without this patch every run corrupted the file (55/55); with it, none did (0/55). A churn test that creates, removes, preallocates and truncates directories and files, then runs fsck.exfat, is clean with and without the patch. Fixes: 82a81a7352bc ("exfat: add iomap buffered I/O support") Fixes: 867b9c96dc83 ("exfat: add iomap direct I/O support") Link: https://lore.kernel.org/all/010001a1222f66ba-0c745f29-d4fe-4862-93ff-1db2055b05fc-000000@email.amazonses.com/ Suggested-by: Namjae Jeon Assisted-by: Claude-Opus-5.5 Signed-off-by: Nicolas Falesy Signed-off-by: Namjae Jeon --- fs/exfat/fatent.c | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/fs/exfat/fatent.c b/fs/exfat/fatent.c index 3c8bdc131f6f..9f4607fced7f 100644 --- a/fs/exfat/fatent.c +++ b/fs/exfat/fatent.c @@ -418,12 +418,28 @@ int exfat_zeroed_cluster(struct inode *dir, unsigned int clu) return 0; } +/* + * Newly allocated clusters may still have dirty buffer_heads in the block + * device mapping, e.g. the dentries of a directory that was just removed. + * File data does not go through those buffers, so drop them; otherwise a + * later flush of the block device writes them over the new data. + */ +static void exfat_clean_bdev_aliases(struct super_block *sb, + unsigned int clu, unsigned int count) +{ + struct exfat_sb_info *sbi = EXFAT_SB(sb); + + clean_bdev_aliases(sb->s_bdev, exfat_cluster_to_sector(sbi, clu), + (sector_t)count << sbi->sect_per_clus_bits); +} + int exfat_alloc_cluster(struct inode *inode, unsigned int num_alloc, struct exfat_chain *p_chain, bool sync_bmap, bool contig) { int ret = -ENOSPC; unsigned int total_cnt; unsigned int hint_clu, new_clu, last_clu = EXFAT_EOF_CLUSTER; + unsigned int run_clu = EXFAT_EOF_CLUSTER, run_len = 0; struct super_block *sb = inode->i_sb; struct exfat_sb_info *sbi = EXFAT_SB(sb); @@ -514,10 +530,21 @@ int exfat_alloc_cluster(struct inode *inode, unsigned int num_alloc, p_chain->size++; sbi->used_clusters++; + /* clean stale buffers once per contiguous run of clusters */ + if (run_len && new_clu == run_clu + run_len) { + run_len++; + } else { + if (run_len) + exfat_clean_bdev_aliases(sb, run_clu, run_len); + run_clu = new_clu; + run_len = 1; + } + last_clu = new_clu; if (p_chain->size == num_alloc) { done: + exfat_clean_bdev_aliases(sb, run_clu, run_len); sbi->clu_srch_ptr = hint_clu; ret = 0; goto unlock; From b4c282c0d82e8715ea9ed0b35813a3f365e3f952 Mon Sep 17 00:00:00 2001 From: Hengyu Liang Date: Mon, 12 Oct 2026 05:40:46 -0400 Subject: [PATCH 19/20] exfat: free clusters beyond EOF when a write fails or is short Commit 82a81a7352bc ("exfat: add iomap buffered I/O support") and commit 867b9c96dc83 ("exfat: add iomap direct I/O support") made a write allocate the clusters for its whole range in one step. They also dropped exfat_write_failed(), which freed the clusters beyond the end of the file after a failed or short write. However, a write can stop early, for example when the process is killed or when a part of the buffer cannot be read. As of now, the clusters for the part that was not written stay allocated beyond the end of the file. exfat_cont_expand() does not expect them, so a later write or truncate that extends the file fails with ENOSPC. The directory entry records only the size of the file, so the space can be lost for good once the volume is unmounted. The issue can be reproduced on a 64M volume made by mkfs.exfat and mounted on /mnt, with a write of 1M from a buffer that has only its first 4K mapped: #include #include #include #include #include int main(void) { char *p = mmap(NULL, 8192, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); int fd = open("/mnt/f", O_CREAT | O_WRONLY, 0644); struct stat st; munmap(p + 4096, 4096); printf("write: %zd\n", write(fd, p, 1 << 20)); fstat(fd, &st); printf("blocks: %ld\n", (long)st.st_blocks); printf("ftruncate: %d\n", ftruncate(fd, 1 << 16)); return 0; } Before commit 82a81a7352bc ("exfat: add iomap buffered I/O support"), the result is: write: 4096 blocks: 8 ftruncate: 0 After that commit, the result is: write: 4096 blocks: 2048 ftruncate: -1 This patch will bring back exfat_write_failed() and call it when a write ends before the end of its range, so that the clusters beyond the end of the file are freed again. It waits for direct I/O in flight first, as exfat_setattr() does before it frees clusters. Fixes: 82a81a7352bc ("exfat: add iomap buffered I/O support") Fixes: 867b9c96dc83 ("exfat: add iomap direct I/O support") Cc: stable@vger.kernel.org Assisted-by: LLM Signed-off-by: Hengyu Liang Signed-off-by: Namjae Jeon --- fs/exfat/file.c | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/fs/exfat/file.c b/fs/exfat/file.c index 22cb32de5515..1386149916bb 100644 --- a/fs/exfat/file.c +++ b/fs/exfat/file.c @@ -804,6 +804,19 @@ static int exfat_extend_valid_size(struct inode *inode, loff_t new_valid_size) return ret; } +/* + * The clusters for a write are allocated before the data is copied. Free the + * ones that a failed or short write has left beyond the end of the file. + */ +static void exfat_write_failed(struct inode *inode) +{ + inode_dio_wait(inode); + filemap_invalidate_lock(inode->i_mapping); + truncate_pagecache(inode, i_size_read(inode)); + exfat_truncate(inode); + filemap_invalidate_unlock(inode->i_mapping); +} + static ssize_t exfat_fallback_buffered_write(struct kiocb *iocb, struct iov_iter *from) { @@ -862,7 +875,7 @@ static ssize_t exfat_file_write_iter(struct kiocb *iocb, struct iov_iter *iter) struct file *file = iocb->ki_filp; struct inode *inode = file_inode(file); struct exfat_inode_info *ei = EXFAT_I(inode); - loff_t pos, valid_size; + loff_t pos, valid_size, end; int err; if (unlikely(exfat_forced_shutdown(inode->i_sb))) @@ -881,6 +894,7 @@ static ssize_t exfat_file_write_iter(struct kiocb *iocb, struct iov_iter *iter) } pos = iocb->ki_pos; + end = pos + iov_iter_count(iter); if (pos > i_size_read(inode)) truncate_pagecache(inode, i_size_read(inode)); @@ -909,6 +923,8 @@ static ssize_t exfat_file_write_iter(struct kiocb *iocb, struct iov_iter *iter) ret = iomap_file_buffered_write(iocb, iter, &exfat_write_iomap_ops, NULL, NULL); } + if (ret != -EIOCBQUEUED && end > i_size_read(inode)) + exfat_write_failed(inode); if (ret < 0) goto unlock; From 8534eb11ac056dc2ca08007cf76d3ae7cbe42684 Mon Sep 17 00:00:00 2001 From: Hengyu Liang Date: Thu, 15 Oct 2026 10:49:34 -0400 Subject: [PATCH 20/20] exfat: fix reading a directory whose FAT chain is longer than its size Commit 08cf4a8181b4 ("exfat: use exfat_chain_advance helper") made the directory code walk the clusters of a directory with exfat_chain_advance(). However, exfat_chain_advance() returns -EIO once it has walked as many clusters as the size of the directory says. The FAT chain of a directory can be longer than that, because some cameras stop updating the size, see commit 1e5654de0f51 ("exfat: handle wrong stream entry size in exfat_readdir()"). As of now, opening such a directory fails with EIO as soon as the chain is two clusters longer than the size. Reading a directory that has a FAT chain after a seek past its end fails with EIO too. The issue can be reproduced with an image in which the size of a directory with 200 files and 7 clusters is lowered to 2 clusters: mount -o loop exfat.img /mnt ls /mnt/d | wc -l Before commit 08cf4a8181b4 ("exfat: use exfat_chain_advance helper"), the result is: 200 After that commit, ls fails with "Input/output error" and the result is: 0 This patch will make exfat_chain_advance() follow the FAT to its end when the directory has a FAT chain, as the code did before, and keep the size check for a directory without one. Fixes: 08cf4a8181b4 ("exfat: use exfat_chain_advance helper") Cc: stable@vger.kernel.org Signed-off-by: Hengyu Liang Reviewed-by: Chi Zhiling Signed-off-by: Namjae Jeon --- fs/exfat/exfat_fs.h | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/fs/exfat/exfat_fs.h b/fs/exfat/exfat_fs.h index 6ebda3cb4d76..eee573d1697c 100644 --- a/fs/exfat/exfat_fs.h +++ b/fs/exfat/exfat_fs.h @@ -680,19 +680,29 @@ static inline int exfat_chain_advance(struct super_block *sb, struct exfat_chain *chain, unsigned int step) { unsigned int clu = chain->dir; + unsigned int i; - if (unlikely(chain->size < step)) - return -EIO; + if (chain->flags == ALLOC_NO_FAT_CHAIN) { + if (unlikely(chain->size < step)) + return -EIO; - if (exfat_cluster_walk(sb, &clu, step, chain->flags)) - return -EIO; + chain->size -= step; + chain->dir = chain->size ? clu + step : EXFAT_EOF_CLUSTER; + return 0; + } - chain->size -= step; + /* + * Only the FAT tells where a chain of this kind ends. The size can be + * smaller than the chain, see commit 1e5654de0f51 ("exfat: handle + * wrong stream entry size in exfat_readdir()"). + */ + for (i = 0; i < step && clu != EXFAT_EOF_CLUSTER; i++) { + if (exfat_get_next_cluster(sb, &clu)) + return -EIO; + } - if (chain->size == 0 && chain->flags == ALLOC_NO_FAT_CHAIN) - chain->dir = EXFAT_EOF_CLUSTER; - else - chain->dir = clu; + chain->size -= min(chain->size, step); + chain->dir = clu; return 0; }