ext4: defer i_disksize update while DISKSIZE_GROW_PENDING is set

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.

Note that in the O_SYNC mode of ext4_do_fallocate() or
ext4_zero_range(), we need to flush out pending blocks that can update
i_disksize synchronously. This will be handled later, and
EXT4_STATE_DISKSIZE_GROW_PENDING will also be set after everything is
done.

Suggested-by: Jan Kara <jack@suse.cz>
Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
Tested-by: syzbot@syzkaller.appspotmail.com
Link: https://patch.msgid.link/20261009103033.2920530-25-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:25 +08:00 committed by Theodore Ts'o
parent 02a59b693e
commit ee6539fc9e
3 changed files with 48 additions and 9 deletions

View File

@ -3601,30 +3601,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;
}

View File

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

View File

@ -6635,8 +6635,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)
{
@ -6644,7 +6646,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);
}