diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h index c5d3f1b04c9c..5bb13a0cfeb5 100644 --- a/fs/ext4/ext4.h +++ b/fs/ext4/ext4.h @@ -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; } diff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index 30fa1ce09f0c..74e50716f6f4 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 a271bb2550b0..e586d5eed3f0 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -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); }