[PATCH] exfat: validate cached iomaps during buffered writes and writeback

From: Chi Zhiling

Date: Tue Jun 30 2026 - 20:55:25 EST


From: Chi Zhiling <chizhiling@xxxxxxxxxx>

Writeback does not hold i_rwsem and can race with truncate or other
operations that change the file's block mapping. This may leave cached
iomaps stale, causing writeback to write data to blocks that no longer
belong to the file.

Track iomap validity with exfat's cache_valid_id and implement
iomap_valid() so cached iomaps are revalidated after the folio is locked.
If the mapping has changed, a new iomap is obtained before writeback
continues.

This matches the iomap validity model used by XFS and prevents data
corruption from stale cached iomaps.

Signed-off-by: Chi Zhiling <chizhiling@xxxxxxxxxx>
---
fs/exfat/cache.c | 7 ++++---
fs/exfat/file.c | 4 ++--
fs/exfat/iomap.c | 15 +++++++++++++--
fs/exfat/iomap.h | 1 +
4 files changed, 20 insertions(+), 7 deletions(-)

diff --git a/fs/exfat/cache.c b/fs/exfat/cache.c
index 7c8b4182f5de..71631bab182c 100644
--- a/fs/exfat/cache.c
+++ b/fs/exfat/cache.c
@@ -228,9 +228,10 @@ static void __exfat_cache_inval_inode(struct inode *inode)
exfat_cache_free(cache);
}
/* Update. The copy of caches before this id is discarded. */
- ei->cache_valid_id++;
- if (ei->cache_valid_id == EXFAT_CACHE_VALID)
- ei->cache_valid_id++;
+ if (ei->cache_valid_id + 1 == EXFAT_CACHE_VALID)
+ ei->cache_valid_id = EXFAT_CACHE_VALID + 1;
+ else
+ ei->cache_valid_id = ei->cache_valid_id + 1;
}

void exfat_cache_inval_inode(struct inode *inode)
diff --git a/fs/exfat/file.c b/fs/exfat/file.c
index 5fc13378d35f..4258398ca641 100644
--- a/fs/exfat/file.c
+++ b/fs/exfat/file.c
@@ -772,8 +772,8 @@ static ssize_t exfat_file_write_iter(struct kiocb *iocb, struct iov_iter *iter)
if (iocb->ki_flags & IOCB_DIRECT)
ret = exfat_dio_write_iter(iocb, iter);
else
- ret = iomap_file_buffered_write(iocb, iter,
- &exfat_write_iomap_ops, NULL, NULL);
+ ret = iomap_file_buffered_write(iocb, iter, &exfat_write_iomap_ops,
+ &exfat_iomap_write_ops, NULL);
if (ret < 0)
goto unlock;

diff --git a/fs/exfat/iomap.c b/fs/exfat/iomap.c
index 1aac38e63fe6..efced320a5ef 100644
--- a/fs/exfat/iomap.c
+++ b/fs/exfat/iomap.c
@@ -56,6 +56,7 @@ static int __exfat_iomap_begin(struct inode *inode, loff_t offset, loff_t length
iomap->addr = IOMAP_NULL_ADDR;
iomap->offset = offset;
iomap->length = length;
+ iomap->validity_cookie = ei->cache_valid_id;
return 0;
}

@@ -133,7 +134,7 @@ static int __exfat_iomap_begin(struct inode *inode, loff_t offset, loff_t length
}
}

- iomap->flags |= IOMAP_F_MERGED;
+ iomap->validity_cookie = ei->cache_valid_id;
out:
mutex_unlock(&sbi->s_lock);
return err;
@@ -155,6 +156,15 @@ const struct iomap_ops exfat_iomap_ops = {
.iomap_begin = exfat_iomap_begin,
};

+static bool exfat_iomap_valid(struct inode *inode, const struct iomap *iomap)
+{
+ return EXFAT_I(inode)->cache_valid_id == iomap->validity_cookie;
+}
+
+const struct iomap_write_ops exfat_iomap_write_ops = {
+ .iomap_valid = exfat_iomap_valid,
+};
+
/*
* exfat_write_iomap_end - Update the state after write
*
@@ -201,7 +211,8 @@ static ssize_t exfat_writeback_range(struct iomap_writepage_ctx *wpc,
struct folio *folio, u64 offset, unsigned int len, u64 end_pos)
{
if (offset < wpc->iomap.offset ||
- offset >= wpc->iomap.offset + wpc->iomap.length) {
+ offset >= wpc->iomap.offset + wpc->iomap.length ||
+ !exfat_iomap_valid(wpc->inode, &wpc->iomap)) {
int error;

error = __exfat_iomap_begin(wpc->inode, offset, len,
diff --git a/fs/exfat/iomap.h b/fs/exfat/iomap.h
index fd8a913f7794..39c93f8cd790 100644
--- a/fs/exfat/iomap.h
+++ b/fs/exfat/iomap.h
@@ -9,6 +9,7 @@
extern const struct iomap_dio_ops exfat_write_dio_ops;
extern const struct iomap_ops exfat_iomap_ops;
extern const struct iomap_ops exfat_write_iomap_ops;
+extern const struct iomap_write_ops exfat_iomap_write_ops;
extern const struct iomap_writeback_ops exfat_writeback_ops;
extern const struct iomap_read_ops exfat_iomap_bio_read_ops;

--
2.43.0