mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH v3 0/8] exfat: fix valid_size update races
@ 2026-10-16  3:33 Chi Zhiling
  2026-10-16  3:33 ` [PATCH v3 1/8] iomap: export iomap_bio_read_folio_range_sync() Chi Zhiling
                   ` (7 more replies)
  0 siblings, 8 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 uses valid_size (ValidDataLength) as the on-disk boundary of
initialised data, but the current code advances it too late or without
proper serialisation.  As a result, buffered writes can silently lose
data reclaimed before valid_size covers it, page faults and direct I/O
can race with writes, and a failed direct write still grows the file
on disk.

This series advances valid_size under the folio lock for buffered
writes, revalidates cached iomaps, drops i_rwsem from
exfat_page_mkwrite(), and publishes i_size/valid_size only on
successful direct I/O completion.

V3:
- Add Fixes: tags to some patches
- Fix two issues since v2 (p2, p6)

V2: https://lore.kernel.org/exfat/20261013033837.2748258-1-chizhiling@163.com/
V1: https://lore.kernel.org/exfat/59056131-e3bd-4003-8c30-0a356cf0bd82@163.com/
Link: https://lore.kernel.org/exfat/10029d6f-13cc-4d17-a2f9-b093a8339dde/T/#t


Chi Zhiling (8):
  iomap: export iomap_bio_read_folio_range_sync()
  exfat: zero the invalid tail of the straddle block under the folio
    lock
  exfat: advance valid_size under the folio lock for buffered writes
  exfat: advance valid_size while zeroing new ranges
  exfat: validate cached iomaps during buffered writes
  exfat: drop i_rwsem from exfat_page_mkwrite
  exfat: complete direct writes synchronously when they update metadata
  exfat: publish i_size and valid_size on direct I/O completion

 fs/exfat/file.c       | 111 +++++++++++++++++----------------
 fs/exfat/inode.c      |   9 +--
 fs/exfat/iomap.c      | 139 +++++++++++++++++++++++++++++++-----------
 fs/exfat/iomap.h      |   1 +
 fs/iomap/bio.c        |   1 +
 fs/iomap/internal.h   |  12 ----
 include/linux/iomap.h |   9 +++
 7 files changed, 176 insertions(+), 106 deletions(-)

-- 
2.53.0


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [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

end of thread, other threads:[~2026-10-16  3:35 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
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 ` [PATCH v3 3/8] exfat: advance valid_size under the folio lock for buffered writes Chi Zhiling
2026-10-16  3:33 ` [PATCH v3 4/8] exfat: advance valid_size while zeroing new ranges Chi Zhiling
2026-10-16  3:33 ` [PATCH v3 5/8] exfat: validate cached iomaps during buffered writes Chi Zhiling
2026-10-16  3:33 ` [PATCH v3 6/8] exfat: drop i_rwsem from exfat_page_mkwrite 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

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®