[PATCH] exfat: clean stale bdev buffers of newly allocated clusters

From: Nicolas Falesy

Date: Sat Oct 10 2026 - 15:15:47 EST


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@xxxxxxxxxxxxxxxxxxx/
Suggested-by: Namjae Jeon <linkinjeon@xxxxxxxxxx>
Assisted-by: Claude-Opus-5.5
Signed-off-by: Nicolas Falesy <nicolas@xxxxxxxxxxxxxxxxx>
---
This replaces the candidate fix from my report. As Namjae suggested, the
cleanup now happens in exfat_alloc_cluster(), so clusters preallocated
by fallocate() or an extending truncate() are covered too; the old
candidate still corrupted those cases in 10/10 runs each.

Based on the exfat dev branch (513192ce3669). Not run: xfstests.

fs/exfat/fatent.c | 27 +++++++++++++++++++++++++++
1 file changed, 27 insertions(+)

diff --git a/fs/exfat/fatent.c b/fs/exfat/fatent.c
index 3c8bdc131..9f4607fce 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;
--
2.55.0