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 <yi.zhang@huawei.com>
Tested-by: syzbot@syzkaller.appspotmail.com
Link: https://patch.msgid.link/20261009103033.2920530-26-yi.zhang@huaweicloud.com
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
This commit is contained in:
Zhang Yi 2026-10-09 18:30:26 +08:00 committed by Theodore Ts'o
parent ee6539fc9e
commit 09e4d87016
3 changed files with 86 additions and 7 deletions

View File

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

View File

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

View File

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