mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH v2 0/7] exfat: fix valid_size update races
@ 2026-10-13  3:38 Chi Zhiling
  2026-10-13  3:38 ` [PATCH v2 1/7] exfat: advance valid_size under the folio lock for buffered writes Chi Zhiling
                   ` (7 more replies)
  0 siblings, 8 replies; 10+ messages in thread
From: Chi Zhiling @ 2026-10-13  3:38 UTC (permalink / raw)
  To: exfat, linux-kernel
  Cc: Yuezhang.Mo, dxdt, linkinjeon, sj1557.seo, 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.

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 (7):
  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: do not re-zero the valid range in 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  | 69 ++++++++++++++++++++++---------------
 fs/exfat/iomap.c | 88 +++++++++++++++++++++++++++++++++++++++---------
 fs/exfat/iomap.h |  1 +
 3 files changed, 114 insertions(+), 44 deletions(-)

-- 
2.53.0


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

* [PATCH v2 1/7] exfat: advance valid_size under the folio lock for buffered writes
  2026-10-13  3:38 [PATCH v2 0/7] exfat: fix valid_size update races Chi Zhiling
@ 2026-10-13  3:38 ` Chi Zhiling
  2026-10-13  3:38 ` [PATCH v2 2/7] exfat: advance valid_size while zeroing new ranges Chi Zhiling
                   ` (6 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: Chi Zhiling @ 2026-10-13  3:38 UTC (permalink / raw)
  To: exfat, linux-kernel
  Cc: Yuezhang.Mo, dxdt, linkinjeon, sj1557.seo, 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.

Signed-off-by: Chi Zhiling <chizhiling@kylinos.cn>
---
 fs/exfat/file.c  |  8 ++++++--
 fs/exfat/iomap.c | 16 ++++++++++++++++
 fs/exfat/iomap.h |  1 +
 3 files changed, 23 insertions(+), 2 deletions(-)

diff --git a/fs/exfat/file.c b/fs/exfat/file.c
index f106db945d8d..dc6df0b73373 100644
--- a/fs/exfat/file.c
+++ b/fs/exfat/file.c
@@ -816,7 +816,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;
 
@@ -907,11 +907,15 @@ 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 < 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 e50ecbfbfbe0..f3816509c6e5 100644
--- a/fs/exfat/iomap.c
+++ b/fs/exfat/iomap.c
@@ -252,6 +252,22 @@ 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);
+}
+
+const struct iomap_write_ops exfat_iomap_write_ops = {
+	.put_folio	= exfat_iomap_put_folio,
+};
+
 /*
  * 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] 10+ messages in thread

* [PATCH v2 2/7] exfat: advance valid_size while zeroing new ranges
  2026-10-13  3:38 [PATCH v2 0/7] exfat: fix valid_size update races Chi Zhiling
  2026-10-13  3:38 ` [PATCH v2 1/7] exfat: advance valid_size under the folio lock for buffered writes Chi Zhiling
@ 2026-10-13  3:38 ` Chi Zhiling
  2026-10-13  3:38 ` [PATCH v2 3/7] exfat: validate cached iomaps during buffered writes Chi Zhiling
                   ` (5 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: Chi Zhiling @ 2026-10-13  3:38 UTC (permalink / raw)
  To: exfat, linux-kernel
  Cc: Yuezhang.Mo, dxdt, linkinjeon, sj1557.seo, 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.

Signed-off-by: Chi Zhiling <chizhiling@kylinos.cn>
---
 fs/exfat/file.c | 34 +++++++++++++++++++---------------
 1 file changed, 19 insertions(+), 15 deletions(-)

diff --git a/fs/exfat/file.c b/fs/exfat/file.c
index dc6df0b73373..6f7b50c7a075 100644
--- a/fs/exfat/file.c
+++ b/fs/exfat/file.c
@@ -724,6 +724,7 @@ static int exfat_prepare_valid_range(struct inode *inode, loff_t start,
 				    loff_t end)
 {
 	struct address_space *mapping = inode->i_mapping;
+	struct exfat_inode_info *ei = EXFAT_I(inode);
 	loff_t next, pos = start;
 	struct folio *folio;
 	pgoff_t index;
@@ -744,12 +745,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;
 }
 
@@ -794,11 +804,6 @@ static int exfat_extend_valid_size(struct inode *inode, loff_t new_valid_size)
 			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);
 	}
 
 	return ret;
@@ -962,6 +967,7 @@ 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;
+	bool dirty = false;
 
 	if (!inode_trylock(inode))
 		return VM_FAULT_RETRY;
@@ -988,16 +994,7 @@ static vm_fault_t exfat_page_mkwrite(struct vm_fault *vmf)
 				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);
+		dirty = true;
 	}
 
 	sb_start_pagefault(inode->i_sb);
@@ -1005,6 +1002,13 @@ 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 && dirty) {
+		exfat_advance_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_shared(inode->i_mapping);
 	sb_end_pagefault(inode->i_sb);
 	inode_unlock(inode);
-- 
2.53.0


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

* [PATCH v2 3/7] exfat: validate cached iomaps during buffered writes
  2026-10-13  3:38 [PATCH v2 0/7] exfat: fix valid_size update races Chi Zhiling
  2026-10-13  3:38 ` [PATCH v2 1/7] exfat: advance valid_size under the folio lock for buffered writes Chi Zhiling
  2026-10-13  3:38 ` [PATCH v2 2/7] exfat: advance valid_size while zeroing new ranges Chi Zhiling
@ 2026-10-13  3:38 ` Chi Zhiling
  2026-10-13  3:38 ` [PATCH v2 4/7] exfat: drop i_rwsem from exfat_page_mkwrite Chi Zhiling
                   ` (4 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: Chi Zhiling @ 2026-10-13  3:38 UTC (permalink / raw)
  To: exfat, linux-kernel
  Cc: Yuezhang.Mo, dxdt, linkinjeon, sj1557.seo, 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 f3816509c6e5..26d216ef9c44 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;
@@ -252,6 +253,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)
 {
@@ -264,8 +276,32 @@ 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;
+}
+
 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,
 };
 
 /*
-- 
2.53.0


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

* [PATCH v2 4/7] exfat: drop i_rwsem from exfat_page_mkwrite
  2026-10-13  3:38 [PATCH v2 0/7] exfat: fix valid_size update races Chi Zhiling
                   ` (2 preceding siblings ...)
  2026-10-13  3:38 ` [PATCH v2 3/7] exfat: validate cached iomaps during buffered writes Chi Zhiling
@ 2026-10-13  3:38 ` Chi Zhiling
  2026-10-13  3:38 ` [PATCH v2 5/7] exfat: do not re-zero the valid range in exfat_page_mkwrite Chi Zhiling
                   ` (3 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: Chi Zhiling @ 2026-10-13  3:38 UTC (permalink / raw)
  To: exfat, linux-kernel
  Cc: Yuezhang.Mo, dxdt, linkinjeon, sj1557.seo, 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, which
serializes the fault against truncate, and advance valid_size after
iomap_page_mkwrite() has returned the locked folio.

Signed-off-by: Chi Zhiling <chizhiling@kylinos.cn>
---
 fs/exfat/file.c | 16 ++++++----------
 1 file changed, 6 insertions(+), 10 deletions(-)

diff --git a/fs/exfat/file.c b/fs/exfat/file.c
index 6f7b50c7a075..a8c1f4757678 100644
--- a/fs/exfat/file.c
+++ b/fs/exfat/file.c
@@ -969,8 +969,9 @@ static vm_fault_t exfat_page_mkwrite(struct vm_fault *vmf)
 	loff_t new_valid_size, mmap_valid_size, fault_page_start;
 	bool dirty = false;
 
-	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);
 
 	mmap_valid_size = ((loff_t)vmf->pgoff + 1) << PAGE_SHIFT;
 	fault_page_start = ((loff_t)vmf->pgoff) << PAGE_SHIFT;
@@ -990,17 +991,13 @@ static vm_fault_t exfat_page_mkwrite(struct vm_fault *vmf)
 			err = exfat_prepare_valid_range(inode, zeroed_size,
 					fault_page_start);
 			if (err < 0) {
-				inode_unlock(inode);
-				return vmf_fs_error(err);
+				ret = vmf_fs_error(err);
+				goto out;
 			}
 		}
 		dirty = true;
 	}
 
-	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 && dirty) {
 		exfat_advance_valid_size(ei, new_valid_size);
@@ -1008,10 +1005,9 @@ static vm_fault_t exfat_page_mkwrite(struct vm_fault *vmf)
 				round_up(new_valid_size, i_blocksize(inode)));
 		mark_inode_dirty(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] 10+ messages in thread

* [PATCH v2 5/7] exfat: do not re-zero the valid range in exfat_page_mkwrite
  2026-10-13  3:38 [PATCH v2 0/7] exfat: fix valid_size update races Chi Zhiling
                   ` (3 preceding siblings ...)
  2026-10-13  3:38 ` [PATCH v2 4/7] exfat: drop i_rwsem from exfat_page_mkwrite Chi Zhiling
@ 2026-10-13  3:38 ` Chi Zhiling
  2026-10-13  3:38 ` [PATCH v2 6/7] exfat: complete direct writes synchronously when they update metadata Chi Zhiling
                   ` (2 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: Chi Zhiling @ 2026-10-13  3:38 UTC (permalink / raw)
  To: exfat, linux-kernel
  Cc: Yuezhang.Mo, dxdt, linkinjeon, sj1557.seo, Chi Zhiling

From: Chi Zhiling <chizhiling@kylinos.cn>

valid_size is advanced before zeroed_size, and exfat_page_mkwrite() no
longer holds i_rwsem, so a fault can observe a zeroed_size that lags
valid_size.  Zeroing the gap from zeroed_size then reads back and dirties
folios below valid_size that already hold valid data.

Start from max(valid_size, zeroed_size) instead, as
exfat_extend_valid_size() already does.  The skipped range is either
already zeroed or already valid, so nothing that needs zeroing is missed.

Signed-off-by: Chi Zhiling <chizhiling@kylinos.cn>
---
 fs/exfat/file.c | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/fs/exfat/file.c b/fs/exfat/file.c
index a8c1f4757678..6209a991a118 100644
--- a/fs/exfat/file.c
+++ b/fs/exfat/file.c
@@ -965,8 +965,9 @@ static vm_fault_t exfat_page_mkwrite(struct vm_fault *vmf)
 {
 	struct inode *inode = file_inode(vmf->vma->vm_file);
 	struct exfat_inode_info *ei = EXFAT_I(inode);
+	loff_t mmap_valid_size, fault_page_start;
+	loff_t old_valid_size, new_valid_size;
 	vm_fault_t ret;
-	loff_t new_valid_size, mmap_valid_size, fault_page_start;
 	bool dirty = false;
 
 	sb_start_pagefault(inode->i_sb);
@@ -975,12 +976,14 @@ static vm_fault_t exfat_page_mkwrite(struct vm_fault *vmf)
 
 	mmap_valid_size = ((loff_t)vmf->pgoff + 1) << PAGE_SHIFT;
 	fault_page_start = ((loff_t)vmf->pgoff) << PAGE_SHIFT;
+	old_valid_size = exfat_get_valid_size(ei);
 	new_valid_size = min(mmap_valid_size, i_size_read(inode));
 
-	if (exfat_get_valid_size(ei) < new_valid_size) {
+	if (old_valid_size < new_valid_size) {
 		loff_t zeroed_size = exfat_get_zeroed_size(ei);
 
 		if (zeroed_size < fault_page_start) {
+			loff_t gap_start = max(old_valid_size, zeroed_size);
 			int err;
 
 			/*
@@ -988,7 +991,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, zeroed_size,
+			err = exfat_prepare_valid_range(inode, gap_start,
 					fault_page_start);
 			if (err < 0) {
 				ret = vmf_fs_error(err);
-- 
2.53.0


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

* [PATCH v2 6/7] exfat: complete direct writes synchronously when they update metadata
  2026-10-13  3:38 [PATCH v2 0/7] exfat: fix valid_size update races Chi Zhiling
                   ` (4 preceding siblings ...)
  2026-10-13  3:38 ` [PATCH v2 5/7] exfat: do not re-zero the valid range in exfat_page_mkwrite Chi Zhiling
@ 2026-10-13  3:38 ` Chi Zhiling
  2026-10-13  3:38 ` [PATCH v2 7/7] exfat: publish i_size and valid_size on direct I/O completion Chi Zhiling
  2026-10-13  4:04 ` [PATCH v2 0/7] exfat: fix valid_size update races Namjae Jeon
  7 siblings, 0 replies; 10+ messages in thread
From: Chi Zhiling @ 2026-10-13  3:38 UTC (permalink / raw)
  To: exfat, linux-kernel
  Cc: Yuezhang.Mo, dxdt, linkinjeon, sj1557.seo, 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. 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 — force synchronous
completion. This guarantees that the i_size update happens under the
inode lock.

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 6209a991a118..efcfeab750c0 100644
--- a/fs/exfat/file.c
+++ b/fs/exfat/file.c
@@ -840,10 +840,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] 10+ messages in thread

* [PATCH v2 7/7] exfat: publish i_size and valid_size on direct I/O completion
  2026-10-13  3:38 [PATCH v2 0/7] exfat: fix valid_size update races Chi Zhiling
                   ` (5 preceding siblings ...)
  2026-10-13  3:38 ` [PATCH v2 6/7] exfat: complete direct writes synchronously when they update metadata Chi Zhiling
@ 2026-10-13  3:38 ` Chi Zhiling
  2026-10-13  4:04 ` [PATCH v2 0/7] exfat: fix valid_size update races Namjae Jeon
  7 siblings, 0 replies; 10+ messages in thread
From: Chi Zhiling @ 2026-10-13  3:38 UTC (permalink / raw)
  To: exfat, linux-kernel
  Cc: Yuezhang.Mo, dxdt, linkinjeon, sj1557.seo, 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().

Signed-off-by: Chi Zhiling <chizhiling@kylinos.cn>
---
 fs/exfat/iomap.c | 34 +++++++++++++++++++---------------
 1 file changed, 19 insertions(+), 15 deletions(-)

diff --git a/fs/exfat/iomap.c b/fs/exfat/iomap.c
index 26d216ef9c44..9e25c18c148a 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. The updating of i_size and valid_size 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;
 }
 
@@ -213,15 +222,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)
@@ -229,18 +234,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] 10+ messages in thread

* Re: [PATCH v2 0/7] exfat: fix valid_size update races
  2026-10-13  3:38 [PATCH v2 0/7] exfat: fix valid_size update races Chi Zhiling
                   ` (6 preceding siblings ...)
  2026-10-13  3:38 ` [PATCH v2 7/7] exfat: publish i_size and valid_size on direct I/O completion Chi Zhiling
@ 2026-10-13  4:04 ` Namjae Jeon
  2026-10-13  6:02   ` Chi Zhiling
  7 siblings, 1 reply; 10+ messages in thread
From: Namjae Jeon @ 2026-10-13  4:04 UTC (permalink / raw)
  To: Chi Zhiling
  Cc: exfat, linux-kernel, Yuezhang.Mo, dxdt, sj1557.seo, Chi Zhiling

On Tue, Oct 13, 2026 at 12:39 PM Chi Zhiling <chizhiling@163.com> wrote:
>
> From: Chi Zhiling <chizhiling@kylinos.cn>
Hi Chi,
>
> 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.
>
> 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 (7):
>   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: do not re-zero the valid range in exfat_page_mkwrite
>   exfat: complete direct writes synchronously when they update metadata
>   exfat: publish i_size and valid_size on direct I/O completion
Can you add fixes tags to the patches ?
Thanks.

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

* Re: [PATCH v2 0/7] exfat: fix valid_size update races
  2026-10-13  4:04 ` [PATCH v2 0/7] exfat: fix valid_size update races Namjae Jeon
@ 2026-10-13  6:02   ` Chi Zhiling
  0 siblings, 0 replies; 10+ messages in thread
From: Chi Zhiling @ 2026-10-13  6:02 UTC (permalink / raw)
  To: Namjae Jeon
  Cc: exfat, linux-kernel, Yuezhang.Mo, dxdt, sj1557.seo, Chi Zhiling

On 10/13/2026 12:04 PM, Namjae Jeon wrote:
> On Tue, Oct 13, 2026 at 12:39 PM Chi Zhiling <chizhiling@163.com> wrote:
>>
>> From: Chi Zhiling <chizhiling@kylinos.cn>
> Hi Chi,

Hi Namjae,

>>
>> 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.
>>
>> 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 (7):
>>    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: do not re-zero the valid range in exfat_page_mkwrite
>>    exfat: complete direct writes synchronously when they update metadata
>>    exfat: publish i_size and valid_size on direct I/O completion
> Can you add fixes tags to the patches ?

Sure, I'll add Fixes tag in the next version.


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

end of thread, other threads:[~2026-10-13  6:03 UTC | newest]

Thread overview: 10+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-13  3:38 [PATCH v2 0/7] exfat: fix valid_size update races Chi Zhiling
2026-10-13  3:38 ` [PATCH v2 1/7] exfat: advance valid_size under the folio lock for buffered writes Chi Zhiling
2026-10-13  3:38 ` [PATCH v2 2/7] exfat: advance valid_size while zeroing new ranges Chi Zhiling
2026-10-13  3:38 ` [PATCH v2 3/7] exfat: validate cached iomaps during buffered writes Chi Zhiling
2026-10-13  3:38 ` [PATCH v2 4/7] exfat: drop i_rwsem from exfat_page_mkwrite Chi Zhiling
2026-10-13  3:38 ` [PATCH v2 5/7] exfat: do not re-zero the valid range in exfat_page_mkwrite Chi Zhiling
2026-10-13  3:38 ` [PATCH v2 6/7] exfat: complete direct writes synchronously when they update metadata Chi Zhiling
2026-10-13  3:38 ` [PATCH v2 7/7] exfat: publish i_size and valid_size on direct I/O completion Chi Zhiling
2026-10-13  4:04 ` [PATCH v2 0/7] exfat: fix valid_size update races Namjae Jeon
2026-10-13  6:02   ` 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®