[PATCH -next v5 25/32] ext4: defer i_disksize update while DISKSIZE_GROW_PENDING is set
From: Zhang Yi
Date: Fri Aug 14 2026 - 05:55:18 EST
From: Zhang Yi <yi.zhang@xxxxxxxxxx>
Operations like append allocate, zero range, and truncate update
i_disksize directly. If the new i_disksize exceeds the original value
while the zeroed EOF block is still awaiting writeback, metadata may be
persisted before the zeroed data, exposing stale data on crash.
Defer i_disksize updates while EXT4_STATE_DISKSIZE_GROW_PENDING is set;
the ioend worker for the pending block will advance i_disksize to i_size
once the zeroed data is written back. The tradeoff is that i_disksize
may lag i_size transiently, but this is observable only to callers that
read i_disksize directly.
Introduce __ext4_set_i_disksize() to centralize the bit check for
callers already holding i_data_sem (ext4_ext_truncate and
ext4_set_inode_size), and refactor ext4_update_inode_size() to take
i_data_sem itself and check the bit atomically with i_size_write(), so
the ioend worker observes the latest i_size under the same lock.
Suggested-by: Jan Kara <jack@xxxxxxx>
Signed-off-by: Zhang Yi <yi.zhang@xxxxxxxxxx>
---
fs/ext4/ext4.h | 47 ++++++++++++++++++++++++++++++++++++++++++-----
fs/ext4/extents.c | 2 +-
fs/ext4/inode.c | 8 +++++---
3 files changed, 48 insertions(+), 9 deletions(-)
diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h
index 089dbd39c5c2..b26ce3183bac 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -3605,30 +3605,67 @@ do { \
#define EXT4_FREECLUSTERS_WATERMARK 0
#endif
-/* Update i_disksize. Requires i_rwsem to avoid races with truncate */
+/*
+ * Update i_disksize. Requires i_rwsem to avoid races with truncate.
+ *
+ * In the iomap buffered I/O path, the EXT4_STATE_DISKSIZE_GROW_PENDING
+ * inode state bit indicates that the zeroed EOF partial block which
+ * straddles i_disksize is still waiting writeback. In that case,
+ * i_disksize will be updated after the pending zeroed data has been
+ * written out.
+ */
static inline void ext4_update_i_disksize(struct inode *inode, loff_t newsize)
{
WARN_ON_ONCE(S_ISREG(inode->i_mode) &&
!inode_is_locked(inode));
down_write(&EXT4_I(inode)->i_data_sem);
- if (newsize > EXT4_I(inode)->i_disksize)
+ if (newsize > EXT4_I(inode)->i_disksize &&
+ !ext4_test_inode_state(inode, EXT4_STATE_DISKSIZE_GROW_PENDING))
WRITE_ONCE(EXT4_I(inode)->i_disksize, newsize);
up_write(&EXT4_I(inode)->i_data_sem);
}
-/* Update i_size, i_disksize. Requires i_rwsem to avoid races with truncate */
+static inline void __ext4_set_i_disksize(struct inode *inode, loff_t newsize)
+{
+ WARN_ON_ONCE(!rwsem_is_locked(&EXT4_I(inode)->i_data_sem));
+
+ if (newsize < EXT4_I(inode)->i_disksize ||
+ !ext4_test_inode_state(inode, EXT4_STATE_DISKSIZE_GROW_PENDING))
+ WRITE_ONCE(EXT4_I(inode)->i_disksize, newsize);
+}
+
+/*
+ * Update i_size and i_disksize to @newsize. Requires i_rwsem to avoid
+ * races with truncate.
+ *
+ * In the iomap buffered I/O path, i_disksize is updated only if no zeroed
+ * pending block straddles i_disksize (EXT4_STATE_DISKSIZE_GROW_PENDING
+ * clear), otherwise the ioend worker for the pending block will advance
+ * i_disksize once the pending block is written back. Both updates happen
+ * under i_data_sem so that the writeback ioend worker can always see the
+ * latest i_size under the same semaphore.
+ *
+ * Returns 0 if nothing changed, 1 if i_size was raised, 2 if i_disksize
+ * was raised, or 3 if both were.
+ */
static inline int ext4_update_inode_size(struct inode *inode, loff_t newsize)
{
int changed = 0;
+ if (newsize <= inode->i_size && newsize <= EXT4_I(inode)->i_disksize)
+ return 0;
+
+ down_write(&EXT4_I(inode)->i_data_sem);
if (newsize > inode->i_size) {
i_size_write(inode, newsize);
changed = 1;
}
- if (newsize > EXT4_I(inode)->i_disksize) {
- ext4_update_i_disksize(inode, newsize);
+ if (newsize > EXT4_I(inode)->i_disksize &&
+ !ext4_test_inode_state(inode, EXT4_STATE_DISKSIZE_GROW_PENDING)) {
+ WRITE_ONCE(EXT4_I(inode)->i_disksize, newsize);
changed |= 2;
}
+ up_write(&EXT4_I(inode)->i_data_sem);
return changed;
}
diff --git a/fs/ext4/extents.c b/fs/ext4/extents.c
index 5a06259a9b5d..fc5aa2dbefcf 100644
--- a/fs/ext4/extents.c
+++ b/fs/ext4/extents.c
@@ -4561,7 +4561,7 @@ int ext4_ext_truncate(handle_t *handle, struct inode *inode)
*/
/* we have to know where to truncate from in crash case */
- EXT4_I(inode)->i_disksize = inode->i_size;
+ __ext4_set_i_disksize(inode, inode->i_size);
err = ext4_mark_inode_dirty(handle, inode);
if (err)
return err;
diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index a0707310b464..0fdc31b21be5 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -6622,8 +6622,10 @@ static void ext4_wait_for_tail_page_commit(struct inode *inode)
* Set i_size and i_disksize to 'newsize'.
*
* Both i_rwsem and i_data_sem are required here to avoid races between
- * generic append writeback and concurrent truncate that also modify
- * i_size and i_disksize.
+ * generic append writeback (or zeroed pending I/O writeback) and
+ * concurrent operations (e.g., fallocate, truncate) that also modify
+ * i_size and i_disksize. This also ensures that the writeback ioend worker
+ * observes the latest i_size under the same lock protection.
*/
static inline void ext4_set_inode_size(struct inode *inode, loff_t newsize)
{
@@ -6631,7 +6633,7 @@ static inline void ext4_set_inode_size(struct inode *inode, loff_t newsize)
down_write(&EXT4_I(inode)->i_data_sem);
i_size_write(inode, newsize);
- EXT4_I(inode)->i_disksize = newsize;
+ __ext4_set_i_disksize(inode, newsize);
up_write(&EXT4_I(inode)->i_data_sem);
}
--
2.52.0