* [PATCH v3 1/8] iomap: export iomap_bio_read_folio_range_sync()
2026-10-16 3:33 [PATCH v3 0/8] exfat: fix valid_size update races Chi Zhiling
@ 2026-10-16 3:33 ` Chi Zhiling
2026-10-16 3:33 ` [PATCH v3 2/8] exfat: zero the invalid tail of the straddle block under the folio lock Chi Zhiling
` (6 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Chi Zhiling @ 2026-10-16 3:33 UTC (permalink / raw)
To: linux-kernel, linux-xfs, linux-fsdevel, exfat
Cc: Namjae Jeon, Sungjong Seo, Yuezhang Mo, Christian Brauner,
Darrick J. Wong, Christoph Hellwig, Chi Zhiling
From: Chi Zhiling <chizhiling@kylinos.cn>
Filesystems that provide a buffered write ->read_folio_range handler
need to perform the same synchronous bio read as the default handler.
Move the declaration of iomap_bio_read_folio_range_sync() from the
internal header to <linux/iomap.h>, together with the !CONFIG_BLOCK
fallback, and export it so that modular filesystems can call it.
exfat will call it from its own ->read_folio_range handler to zero
the stale bytes between valid_size and the end of the block under
the folio lock during buffered writes.
Signed-off-by: Chi Zhiling <chizhiling@kylinos.cn>
---
fs/iomap/bio.c | 1 +
fs/iomap/internal.h | 12 ------------
include/linux/iomap.h | 9 +++++++++
3 files changed, 10 insertions(+), 12 deletions(-)
diff --git a/fs/iomap/bio.c b/fs/iomap/bio.c
index 48100c614431..abb649f024a3 100644
--- a/fs/iomap/bio.c
+++ b/fs/iomap/bio.c
@@ -187,3 +187,4 @@ int iomap_bio_read_folio_range_sync(const struct iomap_iter *iter,
bio_uninit(&bio);
return error;
}
+EXPORT_SYMBOL_GPL(iomap_bio_read_folio_range_sync);
diff --git a/fs/iomap/internal.h b/fs/iomap/internal.h
index 74e898b196dc..d13aff5e1e91 100644
--- a/fs/iomap/internal.h
+++ b/fs/iomap/internal.h
@@ -20,16 +20,4 @@ static inline size_t iomap_max_bio_size(const struct iomap *iomap)
u32 iomap_finish_ioend_buffered_read(struct iomap_ioend *ioend);
u32 iomap_finish_ioend_direct(struct iomap_ioend *ioend);
-#ifdef CONFIG_BLOCK
-int iomap_bio_read_folio_range_sync(const struct iomap_iter *iter,
- struct folio *folio, loff_t pos, size_t len);
-#else
-static inline int iomap_bio_read_folio_range_sync(const struct iomap_iter *iter,
- struct folio *folio, loff_t pos, size_t len)
-{
- WARN_ON_ONCE(1);
- return -EIO;
-}
-#endif /* CONFIG_BLOCK */
-
#endif /* _IOMAP_INTERNAL_H */
diff --git a/include/linux/iomap.h b/include/linux/iomap.h
index bc7ae6327dbf..35b686cbb586 100644
--- a/include/linux/iomap.h
+++ b/include/linux/iomap.h
@@ -763,6 +763,8 @@ extern struct bio_set iomap_ioend_bioset;
#ifdef CONFIG_BLOCK
int iomap_bio_read_folio_range(const struct iomap_iter *iter,
struct iomap_read_folio_ctx *ctx, size_t plen);
+int iomap_bio_read_folio_range_sync(const struct iomap_iter *iter,
+ struct folio *folio, loff_t pos, size_t len);
void iomap_bio_submit_read_endio(const struct iomap_iter *iter,
struct iomap_read_folio_ctx *ctx, bio_end_io_t end_io);
@@ -789,6 +791,13 @@ static inline void iomap_bio_readahead(struct readahead_control *rac,
iomap_readahead(ops, &ctx, NULL);
}
+#else /* !CONFIG_BLOCK */
+static inline int iomap_bio_read_folio_range_sync(const struct iomap_iter *iter,
+ struct folio *folio, loff_t pos, size_t len)
+{
+ WARN_ON_ONCE(1);
+ return -EIO;
+}
#endif /* CONFIG_BLOCK */
#endif /* LINUX_IOMAP_H */
--
2.53.0
^ permalink raw reply [flat|nested] 9+ messages in thread* [PATCH v3 2/8] exfat: zero the invalid tail of the straddle block under the folio lock
2026-10-16 3:33 [PATCH v3 0/8] exfat: fix valid_size update races Chi Zhiling
2026-10-16 3:33 ` [PATCH v3 1/8] iomap: export iomap_bio_read_folio_range_sync() Chi Zhiling
@ 2026-10-16 3:33 ` Chi Zhiling
2026-10-16 3:33 ` [PATCH v3 3/8] exfat: advance valid_size under the folio lock for buffered writes Chi Zhiling
` (5 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Chi Zhiling @ 2026-10-16 3:33 UTC (permalink / raw)
To: linux-kernel, linux-xfs, linux-fsdevel, exfat
Cc: Namjae Jeon, Sungjong Seo, Yuezhang Mo, Christian Brauner,
Darrick J. Wong, Christoph Hellwig, Chi Zhiling
From: Chi Zhiling <chizhiling@kylinos.cn>
A buffered write that does not fully cover the block containing
valid_size makes iomap read that block from disk, loading the stale
bytes beyond valid_size into the page cache. exfat_write_iomap_begin()
zeroes them with iomap_truncate_page() before iomap locks the write
folio.
That relied on i_rwsem serializing valid_size against page_mkwrite().
Once exfat_page_mkwrite() stops taking i_rwsem, a fault can advance
valid_size and store data into the tail after exfat_write_iomap_begin()
read valid_size but before iomap_truncate_page() zeroes it, erasing the
stored data.
Moving the zeroing logic from exfat_write_iomap_begin() to ->put_folio
won't solve it, because the folio has already been marked uptodate
before ->put_folio is called.
Do the zeroing from an ->read_folio_range handler instead. iomap calls
it with the folio locked, so reading valid_size and zeroing
[valid_size, end) is serialized against page_mkwrite() advancing
valid_size for the same folio. This mirrors the read path, which
already zeroes the invalid tail in exfat_iomap_read_end_io().
Signed-off-by: Chi Zhiling <chizhiling@kylinos.cn>
---
fs/exfat/file.c | 5 +++--
fs/exfat/iomap.c | 54 +++++++++++++++++++++++++++++-------------------
fs/exfat/iomap.h | 1 +
3 files changed, 37 insertions(+), 23 deletions(-)
diff --git a/fs/exfat/file.c b/fs/exfat/file.c
index dc57d2c2f0f0..aa0e1e47e3d1 100644
--- a/fs/exfat/file.c
+++ b/fs/exfat/file.c
@@ -829,7 +829,7 @@ static ssize_t exfat_fallback_buffered_write(struct kiocb *iocb,
inode_dio_wait(file_inode(iocb->ki_filp));
written = iomap_file_buffered_write(iocb, from, &exfat_write_iomap_ops,
- NULL, NULL);
+ &exfat_iomap_write_ops, NULL);
if (written < 0)
return written;
@@ -921,7 +921,8 @@ static ssize_t exfat_file_write_iter(struct kiocb *iocb, struct iov_iter *iter)
*/
inode_dio_wait(inode);
ret = iomap_file_buffered_write(iocb, iter,
- &exfat_write_iomap_ops, NULL, NULL);
+ &exfat_write_iomap_ops, &exfat_iomap_write_ops,
+ NULL);
}
if (ret != -EIOCBQUEUED && end > i_size_read(inode))
exfat_write_failed(inode);
diff --git a/fs/exfat/iomap.c b/fs/exfat/iomap.c
index e50ecbfbfbe0..32ca90b61b1a 100644
--- a/fs/exfat/iomap.c
+++ b/fs/exfat/iomap.c
@@ -160,27 +160,7 @@ 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)
{
- 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);
+ return __exfat_iomap_begin(inode, offset, length, flags, iomap, true);
}
static DEFINE_IOMAP_ITER_NEXT(exfat_iomap_next, exfat_iomap_begin);
@@ -252,6 +232,38 @@ const struct iomap_ops exfat_write_iomap_ops = {
.iomap_next = exfat_write_iomap_next,
};
+/*
+ * exfat_iomap_read_folio_range - Read a block range during a buffered write
+ *
+ * A buffered write that does not fully cover the block containing
+ * valid_size makes iomap read that "straddle block" from disk. The bytes
+ * beyond valid_size in this block are stale and must be zeroed to prevent
+ * them from being exposed through the page cache, like the read path does
+ * in exfat_iomap_read_end_io().
+ */
+static int exfat_iomap_read_folio_range(const struct iomap_iter *iter,
+ struct folio *folio, loff_t pos, size_t len)
+{
+ loff_t valid_size;
+ int ret;
+
+ ret = iomap_bio_read_folio_range_sync(iter, folio, pos, len);
+ if (ret)
+ return ret;
+
+ valid_size = exfat_get_valid_size(EXFAT_I(iter->inode));
+ if (pos <= valid_size && valid_size < pos + len)
+ folio_zero_segment(folio,
+ offset_in_folio(folio, valid_size),
+ offset_in_folio(folio, pos + len));
+
+ return 0;
+}
+
+const struct iomap_write_ops exfat_iomap_write_ops = {
+ .read_folio_range = exfat_iomap_read_folio_range,
+};
+
/*
* exfat_writeback_range - Map folio during writeback
*
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.53.0
^ permalink raw reply [flat|nested] 9+ messages in thread* [PATCH v3 3/8] exfat: advance valid_size under the folio lock for buffered writes
2026-10-16 3:33 [PATCH v3 0/8] exfat: fix valid_size update races Chi Zhiling
2026-10-16 3:33 ` [PATCH v3 1/8] iomap: export iomap_bio_read_folio_range_sync() Chi Zhiling
2026-10-16 3:33 ` [PATCH v3 2/8] exfat: zero the invalid tail of the straddle block under the folio lock Chi Zhiling
@ 2026-10-16 3:33 ` Chi Zhiling
2026-10-16 3:33 ` [PATCH v3 4/8] exfat: advance valid_size while zeroing new ranges Chi Zhiling
` (4 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Chi Zhiling @ 2026-10-16 3:33 UTC (permalink / raw)
To: linux-kernel, linux-xfs, linux-fsdevel, exfat
Cc: Namjae Jeon, Sungjong Seo, Yuezhang Mo, Christian Brauner,
Darrick J. Wong, Christoph Hellwig, Chi Zhiling
From: Chi Zhiling <chizhiling@kylinos.cn>
Buffered writes currently advance valid_size in
exfat_write_iomap_end(), which runs after the folio has been unlocked.
As a result, a folio beyond valid_size can become uptodate and dirty,
and be observed by writeback or reclaim while valid_size still points
to an earlier position.
If reclaim drops such a folio before valid_size covers its range, a
subsequent read treats the range as unwritten and zero-fills it without
reading from disk. When this zero-filled page is later written back,
the zeros overwrite the data that was just written, silently losing
the write even if valid_size has since been advanced.
Advance valid_size from the buffered write path via an iomap
put_folio callback while the folio is still locked. This ensures
valid_size is updated before the folio becomes visible to writeback,
preventing the written data from being lost. Mark the inode dirty
whenever valid_size advances.
Fixes: 82a81a7352bc ("exfat: add iomap buffered I/O support")
Signed-off-by: Chi Zhiling <chizhiling@kylinos.cn>
---
fs/exfat/file.c | 3 +++
fs/exfat/iomap.c | 13 +++++++++++++
2 files changed, 16 insertions(+)
diff --git a/fs/exfat/file.c b/fs/exfat/file.c
index aa0e1e47e3d1..49c63364f747 100644
--- a/fs/exfat/file.c
+++ b/fs/exfat/file.c
@@ -929,6 +929,9 @@ static ssize_t exfat_file_write_iter(struct kiocb *iocb, struct iov_iter *iter)
if (ret < 0)
goto unlock;
+ if (exfat_get_valid_size(ei) > valid_size)
+ mark_inode_dirty(inode);
+
inode_unlock(inode);
if (iocb->ki_pos > pos) {
diff --git a/fs/exfat/iomap.c b/fs/exfat/iomap.c
index 32ca90b61b1a..10416335e8ee 100644
--- a/fs/exfat/iomap.c
+++ b/fs/exfat/iomap.c
@@ -232,6 +232,18 @@ const struct iomap_ops exfat_write_iomap_ops = {
.iomap_next = exfat_write_iomap_next,
};
+static void exfat_iomap_put_folio(struct inode *inode, loff_t pos,
+ unsigned int copied, struct folio *folio)
+{
+ struct exfat_inode_info *ei = EXFAT_I(inode);
+
+ if (copied)
+ exfat_advance_valid_size(ei, pos + copied);
+
+ folio_unlock(folio);
+ folio_put(folio);
+}
+
/*
* exfat_iomap_read_folio_range - Read a block range during a buffered write
*
@@ -261,6 +273,7 @@ static int exfat_iomap_read_folio_range(const struct iomap_iter *iter,
}
const struct iomap_write_ops exfat_iomap_write_ops = {
+ .put_folio = exfat_iomap_put_folio,
.read_folio_range = exfat_iomap_read_folio_range,
};
--
2.53.0
^ permalink raw reply [flat|nested] 9+ messages in thread* [PATCH v3 4/8] exfat: advance valid_size while zeroing new ranges
2026-10-16 3:33 [PATCH v3 0/8] exfat: fix valid_size update races Chi Zhiling
` (2 preceding siblings ...)
2026-10-16 3:33 ` [PATCH v3 3/8] exfat: advance valid_size under the folio lock for buffered writes Chi Zhiling
@ 2026-10-16 3:33 ` Chi Zhiling
2026-10-16 3:33 ` [PATCH v3 5/8] exfat: validate cached iomaps during buffered writes Chi Zhiling
` (3 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Chi Zhiling @ 2026-10-16 3:33 UTC (permalink / raw)
To: linux-kernel, linux-xfs, linux-fsdevel, exfat
Cc: Namjae Jeon, Sungjong Seo, Yuezhang Mo, Christian Brauner,
Darrick J. Wong, Christoph Hellwig, Chi Zhiling
From: Chi Zhiling <chizhiling@kylinos.cn>
valid_size is currently updated only after the range has been zeroed, so
it lags the folios that were just marked uptodate and dirty.
Advance it from exfat_prepare_valid_range() page by page, under the folio
lock and right after each folio is marked dirty; the same helper advances
zeroed_size and dirties the inode once the range is prepared.
When the gap is already covered by zeroed_size no folio work is needed
and the helper publishes valid_size up to end directly, so callers can
rely on valid_size reaching end on success and exfat_extend_valid_size()
no longer publishes it itself. Callers guarantee that end lies within
i_size, so valid_size never overtakes i_size here.
exfat_page_mkwrite() is adapted to the new helper: it now calls it
unconditionally, and publishes valid_size and zeroed_size only after
iomap_page_mkwrite() has returned the locked folio instead of up front,
so a failed fault no longer advances valid_size and the publication
happens under the folio lock.
Fixes: 1135704ed22f ("exfat: fix valid_size extension over a shared writable mapping")
Signed-off-by: Chi Zhiling <chizhiling@kylinos.cn>
---
fs/exfat/file.c | 89 +++++++++++++++++++++++++------------------------
1 file changed, 45 insertions(+), 44 deletions(-)
diff --git a/fs/exfat/file.c b/fs/exfat/file.c
index 49c63364f747..6d4f50795332 100644
--- a/fs/exfat/file.c
+++ b/fs/exfat/file.c
@@ -712,24 +712,30 @@ int exfat_file_fsync(struct file *filp, loff_t start, loff_t end, int datasync)
}
/*
- * exfat_prepare_valid_range - populate and dirty folios covering [start, end)
+ * exfat_prepare_valid_range - populate and dirty folios covering [valid_size, end)
*
* 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.
+ * Advance valid_size up to end, publishing it directly when the gap is
+ * already covered by zeroed_size.
*/
-static int exfat_prepare_valid_range(struct inode *inode, loff_t start,
- loff_t end)
+static int exfat_prepare_valid_range(struct inode *inode, loff_t end)
{
struct address_space *mapping = inode->i_mapping;
- loff_t next, pos = start;
+ struct exfat_inode_info *ei = EXFAT_I(inode);
+ loff_t zeroed_size = exfat_get_zeroed_size(ei);
+ loff_t next, pos;
struct folio *folio;
pgoff_t index;
- if (pos >= end)
+ pos = max(zeroed_size, exfat_get_valid_size(ei));
+ if (pos >= end) {
+ if (exfat_advance_valid_size(ei, end))
+ mark_inode_dirty(inode);
return 0;
+ }
while (pos < end) {
index = pos >> PAGE_SHIFT;
@@ -744,12 +750,21 @@ static int exfat_prepare_valid_range(struct inode *inode, loff_t start,
folio_lock(folio);
if (folio->mapping == mapping) {
folio_mark_dirty(folio);
+ exfat_advance_valid_size(ei, next);
pos = next;
}
folio_unlock(folio);
folio_put(folio);
}
+ /*
+ * Track zeroed_size by block, not page, because writeback stops
+ * at i_size recording blocks wholly beyond it could skip a
+ * later required zeroing.
+ */
+ exfat_advance_zeroed_size(ei, round_up(next, i_blocksize(inode)));
+ mark_inode_dirty(inode);
+
return 0;
}
@@ -785,20 +800,14 @@ 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_prepare_valid_range(inode, gap_start, new_valid_size);
- filemap_invalidate_unlock(inode->i_mapping);
+ ret = exfat_prepare_valid_range(inode, new_valid_size);
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;
}
-
- 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);
+ filemap_invalidate_unlock(inode->i_mapping);
}
return ret;
@@ -978,42 +987,22 @@ static vm_fault_t exfat_page_mkwrite(struct vm_fault *vmf)
struct exfat_inode_info *ei = EXFAT_I(inode);
vm_fault_t ret;
loff_t new_valid_size, mmap_valid_size, fault_page_start;
+ int err;
if (!inode_trylock(inode))
return VM_FAULT_RETRY;
- mmap_valid_size = ((loff_t)vmf->pgoff + 1) << PAGE_SHIFT;
fault_page_start = ((loff_t)vmf->pgoff) << PAGE_SHIFT;
- new_valid_size = min(mmap_valid_size, i_size_read(inode));
-
- 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;
-
- /*
- * Zero only the gap below the faulting page. The read
- * fault populated its folio and iomap_page_mkwrite()
- * will dirty it.
- */
- err = exfat_prepare_valid_range(inode, zeroed_size,
- fault_page_start);
- if (err < 0) {
- inode_unlock(inode);
- return vmf_fs_error(err);
- }
- }
- /*
- * Track zeroed_size by block, not page, because writeback stops
- * at i_size recording blocks wholly beyond it could skip a
- * later required zeroing.
- */
- 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);
+ /*
+ * Zero only the gap below the faulting page. The read
+ * fault populated its folio and iomap_page_mkwrite()
+ * will dirty it.
+ */
+ err = exfat_prepare_valid_range(inode, fault_page_start);
+ if (err < 0) {
+ inode_unlock(inode);
+ return vmf_fs_error(err);
}
sb_start_pagefault(inode->i_sb);
@@ -1021,6 +1010,18 @@ static vm_fault_t exfat_page_mkwrite(struct vm_fault *vmf)
filemap_invalidate_lock_shared(inode->i_mapping);
ret = iomap_page_mkwrite(vmf, &exfat_iomap_ops, NULL);
+
+ if (ret == VM_FAULT_LOCKED) {
+ mmap_valid_size = ((loff_t)vmf->pgoff + 1) << PAGE_SHIFT;
+ new_valid_size = min(mmap_valid_size, i_size_read(inode));
+
+ if (exfat_advance_valid_size(ei, new_valid_size))
+ mark_inode_dirty(inode);
+
+ exfat_advance_zeroed_size(ei,
+ round_up(new_valid_size, i_blocksize(inode)));
+ }
+
filemap_invalidate_unlock_shared(inode->i_mapping);
sb_end_pagefault(inode->i_sb);
inode_unlock(inode);
--
2.53.0
^ permalink raw reply [flat|nested] 9+ messages in thread* [PATCH v3 5/8] exfat: validate cached iomaps during buffered writes
2026-10-16 3:33 [PATCH v3 0/8] exfat: fix valid_size update races Chi Zhiling
` (3 preceding siblings ...)
2026-10-16 3:33 ` [PATCH v3 4/8] exfat: advance valid_size while zeroing new ranges Chi Zhiling
@ 2026-10-16 3:33 ` Chi Zhiling
2026-10-16 3:33 ` [PATCH v3 6/8] exfat: drop i_rwsem from exfat_page_mkwrite Chi Zhiling
` (2 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Chi Zhiling @ 2026-10-16 3:33 UTC (permalink / raw)
To: linux-kernel, linux-xfs, linux-fsdevel, exfat
Cc: Namjae Jeon, Sungjong Seo, Yuezhang Mo, Christian Brauner,
Darrick J. Wong, Christoph Hellwig, Chi Zhiling
From: Chi Zhiling <chizhiling@kylinos.cn>
The next patch, "exfat: drop i_rwsem from exfat_page_mkwrite()", allows a
fault to advance valid_size under the folio lock without serializing
against an in-flight buffered write. As a result, valid_size may advance
into a folio covered by a cached iomap. Reusing that iomap would cause
__iomap_write_begin() to zero the wrong part of the block.
Add an ->iomap_valid callback that compares ->validity_cookie against the
current valid_size and forces a remap if valid_size has advanced into the
locked folio, whose pointer is stored in ->private by ->get_folio.
Signed-off-by: Chi Zhiling <chizhiling@kylinos.cn>
---
fs/exfat/iomap.c | 38 +++++++++++++++++++++++++++++++++++++-
1 file changed, 37 insertions(+), 1 deletion(-)
diff --git a/fs/exfat/iomap.c b/fs/exfat/iomap.c
index 10416335e8ee..b25d149f8004 100644
--- a/fs/exfat/iomap.c
+++ b/fs/exfat/iomap.c
@@ -57,6 +57,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 = valid_size;
return 0;
}
@@ -145,7 +146,7 @@ static int __exfat_iomap_begin(struct inode *inode, loff_t offset, loff_t length
}
}
- iomap->flags |= IOMAP_F_MERGED;
+ iomap->validity_cookie = valid_size;
out:
mutex_unlock(&sbi->s_lock);
return err;
@@ -232,6 +233,17 @@ const struct iomap_ops exfat_write_iomap_ops = {
.iomap_next = exfat_write_iomap_next,
};
+static struct folio *exfat_iomap_get_folio(struct iomap_iter *iter, loff_t pos,
+ unsigned int len)
+{
+ struct folio *folio = iomap_get_folio(iter, pos, len);
+
+ if (!IS_ERR(folio))
+ iter->iomap.private = folio;
+
+ return folio;
+}
+
static void exfat_iomap_put_folio(struct inode *inode, loff_t pos,
unsigned int copied, struct folio *folio)
{
@@ -244,6 +256,28 @@ static void exfat_iomap_put_folio(struct inode *inode, loff_t pos,
folio_put(folio);
}
+static bool exfat_iomap_valid(struct inode *inode, const struct iomap *iomap)
+{
+ struct folio *folio = iomap->private;
+ loff_t old_valid_size = iomap->validity_cookie;
+ loff_t valid_size = exfat_get_valid_size(EXFAT_I(inode));
+
+ if (valid_size == old_valid_size)
+ return true;
+
+ if (!folio)
+ return false;
+
+ /* Still valid for this folio. */
+ if (old_valid_size >= folio_next_pos(folio))
+ return true;
+
+ if (valid_size <= folio_pos(folio))
+ return true;
+
+ return false;
+}
+
/*
* exfat_iomap_read_folio_range - Read a block range during a buffered write
*
@@ -273,7 +307,9 @@ static int exfat_iomap_read_folio_range(const struct iomap_iter *iter,
}
const struct iomap_write_ops exfat_iomap_write_ops = {
+ .get_folio = exfat_iomap_get_folio,
.put_folio = exfat_iomap_put_folio,
+ .iomap_valid = exfat_iomap_valid,
.read_folio_range = exfat_iomap_read_folio_range,
};
--
2.53.0
^ permalink raw reply [flat|nested] 9+ messages in thread* [PATCH v3 6/8] exfat: drop i_rwsem from exfat_page_mkwrite
2026-10-16 3:33 [PATCH v3 0/8] exfat: fix valid_size update races Chi Zhiling
` (4 preceding siblings ...)
2026-10-16 3:33 ` [PATCH v3 5/8] exfat: validate cached iomaps during buffered writes Chi Zhiling
@ 2026-10-16 3:33 ` Chi Zhiling
2026-10-16 3:33 ` [PATCH v3 7/8] exfat: complete direct writes synchronously when they update metadata Chi Zhiling
2026-10-16 3:33 ` [PATCH v3 8/8] exfat: publish i_size and valid_size on direct I/O completion Chi Zhiling
7 siblings, 0 replies; 9+ messages in thread
From: Chi Zhiling @ 2026-10-16 3:33 UTC (permalink / raw)
To: linux-kernel, linux-xfs, linux-fsdevel, exfat
Cc: Namjae Jeon, Sungjong Seo, Yuezhang Mo, Christian Brauner,
Darrick J. Wong, Christoph Hellwig, Chi Zhiling
From: Chi Zhiling <chizhiling@kylinos.cn>
exfat_page_mkwrite() takes i_rwsem with inode_trylock() and returns
VM_FAULT_RETRY when that fails. The MM subsystem does not propagate
VM_FAULT_RETRY from ->page_mkwrite(), so the fault still completes and
the page becomes writable without exfat_page_mkwrite() ever advancing
valid_size.
Blocking on i_rwsem is not an option either: ->page_mkwrite() runs under
mmap_lock, so waiting for i_rwsem would create an mmap_lock -> i_rwsem
ordering that can deadlock.
Now that valid_size is advanced under the folio lock, i_rwsem is no
longer needed here. Take mapping->invalidate_lock shared instead, which
serializes the fault against truncate.
Without i_rwsem, i_size can shrink between the read fault and
->page_mkwrite(), so re-check that the faulting page still starts within
i_size before preparing the range below it; a truncate that slipped in
between would otherwise let the helper populate and dirty folios beyond
EOF and advance valid_size past i_size.
Fixes: 6630ea49103c ("exfat: move extend valid_size into ->page_mkwrite()")
Signed-off-by: Chi Zhiling <chizhiling@kylinos.cn>
---
fs/exfat/file.c | 32 +++++++++++++++-----------------
1 file changed, 15 insertions(+), 17 deletions(-)
diff --git a/fs/exfat/file.c b/fs/exfat/file.c
index 6d4f50795332..23b8a36acaf7 100644
--- a/fs/exfat/file.c
+++ b/fs/exfat/file.c
@@ -989,26 +989,25 @@ static vm_fault_t exfat_page_mkwrite(struct vm_fault *vmf)
loff_t new_valid_size, mmap_valid_size, fault_page_start;
int err;
- if (!inode_trylock(inode))
- return VM_FAULT_RETRY;
+ sb_start_pagefault(inode->i_sb);
+ file_update_time(vmf->vma->vm_file);
+ filemap_invalidate_lock_shared(inode->i_mapping);
fault_page_start = ((loff_t)vmf->pgoff) << PAGE_SHIFT;
- /*
- * Zero only the gap below the faulting page. The read
- * fault populated its folio and iomap_page_mkwrite()
- * will dirty it.
- */
- err = exfat_prepare_valid_range(inode, fault_page_start);
- if (err < 0) {
- inode_unlock(inode);
- return vmf_fs_error(err);
+ if (i_size_read(inode) > fault_page_start) {
+ /*
+ * Zero only the gap below the faulting page. The read
+ * fault populated its folio and iomap_page_mkwrite()
+ * will dirty it.
+ */
+ err = exfat_prepare_valid_range(inode, fault_page_start);
+ if (err < 0) {
+ ret = vmf_fs_error(err);
+ goto out;
+ }
}
- sb_start_pagefault(inode->i_sb);
- file_update_time(vmf->vma->vm_file);
-
- filemap_invalidate_lock_shared(inode->i_mapping);
ret = iomap_page_mkwrite(vmf, &exfat_iomap_ops, NULL);
if (ret == VM_FAULT_LOCKED) {
@@ -1021,10 +1020,9 @@ static vm_fault_t exfat_page_mkwrite(struct vm_fault *vmf)
exfat_advance_zeroed_size(ei,
round_up(new_valid_size, i_blocksize(inode)));
}
-
+out:
filemap_invalidate_unlock_shared(inode->i_mapping);
sb_end_pagefault(inode->i_sb);
- inode_unlock(inode);
return ret;
}
--
2.53.0
^ permalink raw reply [flat|nested] 9+ messages in thread* [PATCH v3 7/8] exfat: complete direct writes synchronously when they update metadata
2026-10-16 3:33 [PATCH v3 0/8] exfat: fix valid_size update races Chi Zhiling
` (5 preceding siblings ...)
2026-10-16 3:33 ` [PATCH v3 6/8] exfat: drop i_rwsem from exfat_page_mkwrite Chi Zhiling
@ 2026-10-16 3:33 ` Chi Zhiling
2026-10-16 3:33 ` [PATCH v3 8/8] exfat: publish i_size and valid_size on direct I/O completion Chi Zhiling
7 siblings, 0 replies; 9+ messages in thread
From: Chi Zhiling @ 2026-10-16 3:33 UTC (permalink / raw)
To: linux-kernel, linux-xfs, linux-fsdevel, exfat
Cc: Namjae Jeon, Sungjong Seo, Yuezhang Mo, Christian Brauner,
Darrick J. Wong, Christoph Hellwig, Chi Zhiling
From: Chi Zhiling <chizhiling@kylinos.cn>
The inode metadata for a direct write is updated in ->end_io(). That
handler runs without holding i_rwsem whenever the I/O completes
asynchronously. As a result, the i_size update can race with other
operations.
Whenever a write extends beyond the current valid_size, that is,
whenever ->end_io() will have metadata to update, pass
IOMAP_DIO_FORCE_WAIT to iomap_dio_rw(). This guarantees that the
metadata update happens under the inode lock held across the write.
Note that this forces extending O_DIRECT writes, the common append case
for AIO and io_uring, to complete synchronously. Pure overwrites still
complete asynchronously because ->end_io() has nothing to publish for
them.
Fixes: 867b9c96dc83 ("exfat: add iomap direct I/O support")
Signed-off-by: Chi Zhiling <chizhiling@kylinos.cn>
---
fs/exfat/file.c | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/fs/exfat/file.c b/fs/exfat/file.c
index 23b8a36acaf7..27050f0358ee 100644
--- a/fs/exfat/file.c
+++ b/fs/exfat/file.c
@@ -857,10 +857,16 @@ static ssize_t exfat_fallback_buffered_write(struct kiocb *iocb,
static ssize_t exfat_dio_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
+ struct inode *inode = file_inode(iocb->ki_filp);
+ loff_t end = iocb->ki_pos + iov_iter_count(from);
+ unsigned int dio_flags = 0;
ssize_t ret;
+ if (end > exfat_get_valid_size(EXFAT_I(inode)))
+ dio_flags |= IOMAP_DIO_FORCE_WAIT;
+
ret = iomap_dio_rw(iocb, from, &exfat_write_iomap_ops,
- &exfat_write_dio_ops, 0, NULL, 0);
+ &exfat_write_dio_ops, dio_flags, NULL, 0);
if (ret == -ENOTBLK)
ret = 0;
else if (ret < 0)
--
2.53.0
^ permalink raw reply [flat|nested] 9+ messages in thread* [PATCH v3 8/8] exfat: publish i_size and valid_size on direct I/O completion
2026-10-16 3:33 [PATCH v3 0/8] exfat: fix valid_size update races Chi Zhiling
` (6 preceding siblings ...)
2026-10-16 3:33 ` [PATCH v3 7/8] exfat: complete direct writes synchronously when they update metadata Chi Zhiling
@ 2026-10-16 3:33 ` Chi Zhiling
7 siblings, 0 replies; 9+ messages in thread
From: Chi Zhiling @ 2026-10-16 3:33 UTC (permalink / raw)
To: linux-kernel, linux-xfs, linux-fsdevel, exfat
Cc: Namjae Jeon, Sungjong Seo, Yuezhang Mo, Christian Brauner,
Darrick J. Wong, Christoph Hellwig, Chi Zhiling
From: Chi Zhiling <chizhiling@kylinos.cn>
exfat_write_iomap_end() advances valid_size from the submission path.
A direct write that later fails or completes short has therefore already
marked its range valid, and __exfat_write_inode() records
max(i_size, valid_size) as DataLength, growing the file on disk even
though no data was written.
Advancing valid_size early also lets a racing mmap fault map the range
and pull the not-yet-overwritten contents of the recycled cluster into
the page cache.
Publish both fields from ->end_io() instead, once the transfer has
completed successfully, and stop advancing valid_size from ->iomap_end();
buffered writes still publish valid_size under the folio lock via
->put_folio().
Note the trade-off: clusters for an extending direct write are allocated
in ->iomap_begin() while i_size is published only at completion. An
inode writeback racing with an in-flight direct write, or a crash before
it completes, can therefore temporarily leave DataLength smaller than
the allocated cluster chain, which fsck reports as "more clusters are
allocated".
Fixes: 867b9c96dc83 ("exfat: add iomap direct I/O support")
Signed-off-by: Chi Zhiling <chizhiling@kylinos.cn>
---
fs/exfat/inode.c | 9 +++------
fs/exfat/iomap.c | 34 +++++++++++++++++++---------------
2 files changed, 22 insertions(+), 21 deletions(-)
diff --git a/fs/exfat/inode.c b/fs/exfat/inode.c
index 5129df1e79b4..f627b53eb3ca 100644
--- a/fs/exfat/inode.c
+++ b/fs/exfat/inode.c
@@ -74,12 +74,9 @@ int __exfat_write_inode(struct inode *inode, int sync)
NULL);
/*
- * During a DIO write, valid_size is updated eagerly in iomap_end (so
- * that concurrent buffered reads see IOMAP_MAPPED) while i_size is
- * updated asynchronously in end_io. The FAT chain was already
- * extended to cover ceil(valid_size/cluster_size) clusters. Use the
- * maximum so the on-disk size field always covers the FAT chain,
- * preventing fsck from reporting "more clusters are allocated".
+ * TODO: DIO allocates clusters in ->iomap_begin() but updates i_size
+ * only at completion. A crash after the inode was written back in
+ * that window makes fsck report "more clusters are allocated".
*/
on_disk_size = max_t(unsigned long long, i_size_read(inode),
valid_size);
diff --git a/fs/exfat/iomap.c b/fs/exfat/iomap.c
index b25d149f8004..46bf057efcdc 100644
--- a/fs/exfat/iomap.c
+++ b/fs/exfat/iomap.c
@@ -15,22 +15,31 @@
/*
* exfat_file_write_dio_end_io - Direct I/O write completion handler
*
- * Updates i_size if the write extended the file. Called from the dio layer
- * after I/O completion.
+ * Updates i_size and valid_size after the transfer has completed
+ * successfully. Whenever metadata is published, this runs in process
+ * context and under i_rwsem.
*/
static int exfat_file_write_dio_end_io(struct kiocb *iocb, ssize_t size,
int error, unsigned int flags)
{
struct inode *inode = file_inode(iocb->ki_filp);
+ loff_t end = iocb->ki_pos + size;
+ bool dirtied = false;
- if (error)
+ if (error || !size)
return error;
- if (size && i_size_read(inode) < iocb->ki_pos + size) {
- i_size_write(inode, iocb->ki_pos + size);
- mark_inode_dirty(inode);
+ if (i_size_read(inode) < end) {
+ i_size_write(inode, end);
+ dirtied = true;
}
+ if (exfat_advance_valid_size(EXFAT_I(inode), end))
+ dirtied = true;
+
+ if (dirtied)
+ mark_inode_dirty(inode);
+
return 0;
}
@@ -193,15 +202,11 @@ static const struct iomap_ops exfat_swap_iomap_ops = {
/*
* exfat_write_iomap_end - Update the state after write
- *
- * Extends ->valid_size to cover the newly written range.
- * Marks the inode dirty if metadata was changed.
*/
static int exfat_write_iomap_end(struct inode *inode, loff_t pos, loff_t length,
ssize_t written, unsigned int flags, struct iomap *iomap)
{
struct exfat_inode_info *ei = EXFAT_I(inode);
- bool dirtied = false;
loff_t end;
if (!written)
@@ -209,18 +214,17 @@ static int exfat_write_iomap_end(struct inode *inode, loff_t pos, loff_t length,
end = pos + written;
- if (exfat_advance_valid_size(ei, end))
- dirtied = true;
-
/*
* IOMAP_F_ZERO_TAIL zeroes the remainder of the last block. Track that
* block as zeroed so later valid_size extensions do not zero it again.
*/
if (iomap->flags & IOMAP_F_ZERO_TAIL)
end = round_up(end, i_blocksize(inode));
- exfat_advance_zeroed_size(ei, end);
- if (dirtied || iomap->flags & IOMAP_F_SIZE_CHANGED)
+ if (!(flags & IOMAP_DIRECT))
+ exfat_advance_zeroed_size(ei, end);
+
+ if (iomap->flags & IOMAP_F_SIZE_CHANGED)
mark_inode_dirty(inode);
return written;
--
2.53.0
^ permalink raw reply [flat|nested] 9+ messages in thread