From 09e4d8701614d2c524d96a88c4ff694f42b9a180 Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:30:26 +0800 Subject: [PATCH] ext4: submit and wait for disksize-grow I/O in fallocate paths 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. Note that EXT4_STATE_DISKSIZE_GROW_PENDING is not set for now, and it will be set after everything is done. Signed-off-by: Zhang Yi Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103033.2920530-26-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/ext4.h | 2 ++ fs/ext4/extents.c | 50 ++++++++++++++++++++++++++++++++++++++++------- fs/ext4/inode.c | 41 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 7 deletions(-) diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h index 5bb13a0cfeb5..2fecf0bb5cf6 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 74e50716f6f4..7193429a05e1 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -4857,6 +4857,17 @@ static long ext4_zero_range(struct file *file, loff_t offset, if (ret) 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; + } + /* Finish zeroing out if it doesn't contain partial block */ if (IS_ALIGNED(offset | end, blocksize)) return ret; @@ -4928,10 +4939,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, @@ -5673,6 +5694,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 @@ -5722,7 +5751,7 @@ static int ext4_collapse_range(struct file *file, loff_t offset, loff_t len) 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); @@ -5775,6 +5804,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. @@ -5794,8 +5831,7 @@ 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; + 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 e586d5eed3f0..61ccb3c5390a 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -4836,6 +4836,39 @@ 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_write_and_wait_range(inode->i_mapping, pstart, + pstart + plen - 1); + if (ret) + return ret; + + /* + * The pending bit should have been cleared by the ioend worker, + * which runs before the folio writeback flag is cleared. The + * caller holds i_rwsem, so no concurrent ext4_block_zero_eof() + * can re-set it. + */ + WARN_ON_ONCE(ext4_test_inode_state(inode, + EXT4_STATE_DISKSIZE_GROW_PENDING)); + 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. @@ -4994,6 +5027,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)