[PATCH -next v5 25/32] ext4: defer i_disksize update while DISKSIZE_GROW_PENDING is set
Zhang Yi <[email protected]>
| Newsgroups | org.kernel.vger.linux-fsdevel,org.kernel.vger.linux-ext4,org.kernel.vger.linux-kernel |
|---|---|
| Message-ID | <[email protected]> |
From: Zhang Yi <[email protected]> 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. Suggested-by: Jan Kara <[email protected]> Signed-off-by: Zhang Yi <[email protected]> --- fs/ext4/ext4.h | 47 ++++++++++++++++++++++++++++++++++++++++++----- fs/ext4/extents.c | 2 +- fs/ext4/inode.c | 8 +++++--- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h index 089dbd39c5c2..b26ce3183bac 100644 --- a/fs/ext4/ext4.h +++ b/fs/ext4/ext4.h @@ -3605,30 +3605,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 5a06259a9b5d..fc5aa2dbefcf 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 a0707310b464..0fdc31b21be5 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -6622,8 +6622,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) { @@ -6631,7 +6633,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); } -- 2.52.0