[PATCH -next v5 26/32] ext4: submit and wait for disksize-grow I/O in fallocate paths
From: Zhang Yi
Date: Fri Aug 14 2026 - 05:49:26 EST
From: Zhang Yi <yi.zhang@xxxxxxxxxx>
Collapse range and insert range update i_disksize directly under
i_data_sem. If the operation runs while the zeroed EOF block is still
awaiting writeback, i_disksize could advance past the zeroed boundary
before the zeroed data is persisted, exposing stale data on crash.
Deferring i_disksize updates like fallocate and zero_range is not an
option here because the shift would move written extents beyond the
current i_disksize. So flush and wait for the pending zeroed EOF block
before these operations advance i_disksize. Since these operations
already perform writeback, the extra flush does not add significant
overhead.
In addition, for ext4_update_disksize_before_punch(), if the punch
discards the pending block, the zeroed data will never be written back
before advancing i_disksize, so it is also necessary to sync the
pending EOF range there.
Finally, for the SYNC variants of zero_range and fallocate, this also
guarantees the i_disksize update is persisted on the synchronous
return.
Signed-off-by: Zhang Yi <yi.zhang@xxxxxxxxxx>
---
fs/ext4/ext4.h | 2 ++
fs/ext4/extents.c | 53 ++++++++++++++++++++++++++++++++++++++++-------
fs/ext4/inode.c | 34 ++++++++++++++++++++++++++++++
3 files changed, 82 insertions(+), 7 deletions(-)
diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h
index b26ce3183bac..504dce9fdc6b 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -3226,6 +3226,8 @@ void ext4_iomap_clear_disksize_pending(struct inode *inode);
void ext4_iomap_wait_disksize_pending(struct inode *inode);
unsigned int ext4_iomap_get_disksize_pending_range(struct inode *inode,
loff_t *start);
+extern int ext4_iomap_sync_zeroed_eof(struct inode *inode,
+ loff_t offset, loff_t end);
extern int ext4_block_zero_eof(struct inode *inode, loff_t from, loff_t end);
#define EXT4_PARTIAL_ZERO_START 0x1
diff --git a/fs/ext4/extents.c b/fs/ext4/extents.c
index fc5aa2dbefcf..dda6d50e96e3 100644
--- a/fs/ext4/extents.c
+++ b/fs/ext4/extents.c
@@ -4876,6 +4876,16 @@ static long ext4_zero_range(struct file *file, loff_t offset,
return ret;
}
+ /*
+ * In SYNC mode, sync the pending zeroed EOF block to ensure the
+ * i_disksize update is persisted.
+ */
+ if (((file->f_flags & O_SYNC) || IS_SYNC(inode)) && new_size) {
+ ret = ext4_iomap_sync_zeroed_eof(inode, 0, LLONG_MAX);
+ if (ret)
+ return ret;
+ }
+
handle = ext4_journal_start(inode, EXT4_HT_MISC, 1);
if (IS_ERR(handle)) {
ret = PTR_ERR(handle);
@@ -4928,10 +4938,20 @@ static long ext4_do_fallocate(struct file *file, loff_t offset,
if (ret)
goto out;
- if (((file->f_flags & O_SYNC) || IS_SYNC(inode)) &&
- EXT4_SB(inode->i_sb)->s_journal) {
- ret = ext4_fc_commit(EXT4_SB(inode->i_sb)->s_journal,
- EXT4_I(inode)->i_sync_tid);
+ if ((file->f_flags & O_SYNC) || IS_SYNC(inode)) {
+ /*
+ * Sync the pending zeroed EOF block to ensure the
+ * i_disksize update is persisted.
+ */
+ if (new_size) {
+ ret = ext4_iomap_sync_zeroed_eof(inode, 0, LLONG_MAX);
+ if (ret)
+ goto out;
+ }
+ if (EXT4_SB(inode->i_sb)->s_journal) {
+ ret = ext4_fc_commit(EXT4_SB(inode->i_sb)->s_journal,
+ EXT4_I(inode)->i_sync_tid);
+ }
}
out:
trace_ext4_fallocate_exit(inode, offset,
@@ -5668,6 +5688,14 @@ static int ext4_collapse_range(struct file *file, loff_t offset, loff_t len)
if (end >= inode->i_size)
return -EINVAL;
+ /*
+ * Persist the pending zeroed EOF block to ensure i_disksize
+ * can be safely updated thereafter.
+ */
+ ret = ext4_iomap_sync_zeroed_eof(inode, 0, LLONG_MAX);
+ if (ret)
+ return ret;
+
/*
* Write tail of the last page before removed range and data that
* will be shifted since they will get removed from the page cache
@@ -5715,9 +5743,11 @@ static int ext4_collapse_range(struct file *file, loff_t offset, loff_t len)
goto out_handle;
}
+ WARN_ON_ONCE(ext4_test_inode_state(inode,
+ EXT4_STATE_DISKSIZE_GROW_PENDING));
new_size = inode->i_size - len;
i_size_write(inode, new_size);
- EXT4_I(inode)->i_disksize = new_size;
+ __ext4_set_i_disksize(inode, new_size);
up_write(&EXT4_I(inode)->i_data_sem);
ret = ext4_mark_inode_dirty(handle, inode);
@@ -5770,6 +5800,14 @@ static int ext4_insert_range(struct file *file, loff_t offset, loff_t len)
if (len > inode->i_sb->s_maxbytes - inode->i_size)
return -EFBIG;
+ /*
+ * Persist the pending zeroed EOF block to ensure i_disksize
+ * can be safely updated thereafter.
+ */
+ ret = ext4_iomap_sync_zeroed_eof(inode, 0, LLONG_MAX);
+ if (ret)
+ return ret;
+
/*
* Write out all dirty pages. Need to round down to align start offset
* to page size boundary for page size > block size.
@@ -5789,8 +5827,9 @@ static int ext4_insert_range(struct file *file, loff_t offset, loff_t len)
ext4_fc_mark_ineligible(sb, EXT4_FC_REASON_FALLOC_RANGE, handle);
/* Expand file to avoid data loss if there is error while shifting */
- inode->i_size += len;
- EXT4_I(inode)->i_disksize += len;
+ WARN_ON_ONCE(ext4_test_inode_state(inode,
+ EXT4_STATE_DISKSIZE_GROW_PENDING));
+ ext4_update_inode_size(inode, inode->i_size + len);
ret = ext4_mark_inode_dirty(handle, inode);
if (ret)
goto out_handle;
diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index 0fdc31b21be5..056937e27859 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -4823,6 +4823,32 @@ static int ext4_block_zero_range(struct inode *inode,
zero_written);
}
+/*
+ * Submit and wait for the pending zeroed EOF block range to complete
+ * if the given range [@offset, @end) fully covers it. Must be called
+ * outside the context of an active journal handle and hold the i_rwsem.
+ */
+int ext4_iomap_sync_zeroed_eof(struct inode *inode, loff_t offset, loff_t end)
+{
+ loff_t pstart, plen;
+ int ret;
+
+ if (!ext4_inode_buffered_iomap(inode))
+ return 0;
+
+ plen = ext4_iomap_get_disksize_pending_range(inode, &pstart);
+ if (!plen || offset > pstart || end < pstart + plen)
+ return 0;
+
+ ret = filemap_fdatawrite_range(inode->i_mapping, pstart,
+ pstart + plen - 1);
+ if (ret)
+ return ret;
+
+ ext4_iomap_wait_disksize_pending(inode);
+ return 0;
+}
+
/*
* Zero out a mapping from file offset 'from' up to the end of the block
* which corresponds to 'from' or to the given 'end' inside this block.
@@ -4988,6 +5014,14 @@ int ext4_update_disksize_before_punch(struct inode *inode, loff_t offset,
if (offset > size)
return 0;
+ /*
+ * We are going to punch the pending zeroed EOF block, persist
+ * it to ensure i_disksize can be safely updated thereafter.
+ */
+ ret = ext4_iomap_sync_zeroed_eof(inode, offset, offset + len);
+ if (ret)
+ return ret;
+
if (offset + len < size)
size = offset + len;
if (EXT4_I(inode)->i_disksize >= size)
--
2.52.0