From: Zhang Yi When the disksize-grow-pending zeroed EOF I/O completes, i_disksize has to be advanced. Advancing it only to the end of that specific I/O would discard any later i_disksize updates from concurrent fallocate or similar operations, causing filesystem inconsistency. Scanning dirty or writeback folios beyond the current position to compute a safe advance target is expensive and racy with concurrent fallocate, so instead advance i_disksize directly to i_size. This may expose zeroed data (not stale data) after crash recovery when dirty data in the range is not yet on disk, but only for unaligned append writes, which is deemed acceptable. To support this, teach ext4_iomap_wb_update_disksize() to take an is_disksize_grow flag and advance i_disksize to i_size when set, and have ext4_iomap_finish_ioend() pass the flag based on the EXT4_IOMAP_IOEND_DISKSIZE_GROW_IO tag of the completing ioend. Suggested-by: Jan Kara Signed-off-by: Zhang Yi --- fs/ext4/page-io.c | 44 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/fs/ext4/page-io.c b/fs/ext4/page-io.c index 955ff88045db..4f1176b9332f 100644 --- a/fs/ext4/page-io.c +++ b/fs/ext4/page-io.c @@ -578,9 +578,9 @@ static void ext4_iomap_wb_disksize_pending_wait(struct inode *inode, } static int ext4_iomap_wb_update_disksize(handle_t *handle, struct inode *inode, - loff_t end) + loff_t end, bool is_disksize_grow) { - loff_t new_disksize = end; + loff_t new_disksize, i_size; struct ext4_inode_info *ei = EXT4_I(inode); int ret; @@ -589,9 +589,36 @@ static int ext4_iomap_wb_update_disksize(handle_t *handle, struct inode *inode, * i_data_sem. */ down_write(&ei->i_data_sem); - new_disksize = min(new_disksize, i_size_read(inode)); + i_size = i_size_read(inode); + + /* + * EXT4_STATE_DISKSIZE_GROW_PENDING is cleared when the pending + * I/O completes. However, another thread may have re-set the bit + * between that point and here, meaning i_disksize has already + * been advanced and a new EOF zeroing has been initiated. In that + * case, do not advance i_disksize to i_size; leave it to the + * next pending grow ioend. + */ + if (is_disksize_grow && + ext4_test_inode_state(inode, EXT4_STATE_DISKSIZE_GROW_PENDING)) + is_disksize_grow = false; + + /* + * Update i_disksize to i_size when EXT4_IOMAP_IOEND_DISKSIZE_GROW_IO + * completes. This is safe because we never directly allocate written + * blocks during buffered writes. + * + * This ensures that i_disksize is correctly advanced during + * truncate-up or append fallocate on a block-unaligned file, + * preventing it from remaining stale. The tradeoff is that zeroed + * data may be exposed after crash recovery if dirty data in this + * range is not yet on disk, but stale data will never be exposed. + * This is because the extent is only converted to written state + * after the data has been persisted. + */ + new_disksize = is_disksize_grow ? i_size : min(end, i_size); if (new_disksize > ei->i_disksize) - ei->i_disksize = new_disksize; + WRITE_ONCE(ei->i_disksize, new_disksize); up_write(&ei->i_data_sem); ret = ext4_mark_inode_dirty(handle, inode); if (ret) @@ -607,6 +634,8 @@ static void ext4_iomap_finish_ioend(struct iomap_ioend *ioend) loff_t pos = ioend->io_offset; size_t size = ioend->io_size; loff_t end = pos + size; + unsigned long io_mode = (unsigned long)ioend->io_private; + bool is_disksize_grow = (io_mode == EXT4_IOMAP_IOEND_DISKSIZE_GROW_IO); handle_t *handle; int credits; int ret, err; @@ -619,7 +648,7 @@ static void ext4_iomap_finish_ioend(struct iomap_ioend *ioend) } if (!(ioend->io_flags & IOMAP_IOEND_UNWRITTEN) && - end <= READ_ONCE(EXT4_I(inode)->i_disksize)) + end <= READ_ONCE(EXT4_I(inode)->i_disksize) && !is_disksize_grow) goto out; /* Wait for disksize-pending zeroed data to be written out. */ @@ -638,8 +667,9 @@ static void ext4_iomap_finish_ioend(struct iomap_ioend *ioend) } /* Update on-disk size after I/O is completed. */ - if (end > READ_ONCE(EXT4_I(inode)->i_disksize)) { - ret = ext4_iomap_wb_update_disksize(handle, inode, end); + if (end > READ_ONCE(EXT4_I(inode)->i_disksize) || is_disksize_grow) { + ret = ext4_iomap_wb_update_disksize(handle, inode, end, + is_disksize_grow); if (ret) goto out_journal; } -- 2.52.0