[PATCH v3 8/8] exfat: publish i_size and valid_size on direct I/O completion

From: Chi Zhiling

Date: Thu Oct 15 2026 - 23:40:17 EST


From: Chi Zhiling <chizhiling@xxxxxxxxxx>

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@xxxxxxxxxx>
---
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