[PATCH -next v5 28/32] ext4: set DISKSIZE_GROW_PENDING after zeroing unaligned EOF block
Zhang Yi <[email protected]>
| Newsgroups | org.kernel.vger.linux-ext4,org.kernel.vger.linux-fsdevel,org.kernel.vger.linux-kernel |
|---|---|
| Message-ID | <[email protected]> |
From: Zhang Yi <[email protected]> In the iomap buffered I/O path, data=ordered mode is not used, so the zeroed EOF block has no implicit ordering with later i_disksize updates. Without the pending state being set, i_disksize can be advanced past the zeroed block before writeback completes, exposing stale data after a crash. Previous patches added the consumer side of the disksize-grow-pending mechanism: the state bit, clear and wait helpers, and ioend tagging. Now add ext4_iomap_mark_disksize_pending() and call it from ext4_block_zero_eof() after zeroing the tail of the block that straddles i_disksize. The helper locks the folio, waits for any in-flight writeback on it to complete, then sets EXT4_STATE_DISKSIZE_GROW_PENDING only if the folio is still dirty. Waiting for writeback prevents folio_test_dirty() from returning false mid-writeback, which would cause us to skip the pending state while zeroed data is still in flight. The dirty check then avoids setting the bit when the data has already been written back. Suggested-by: Jan Kara <[email protected]> Signed-off-by: Zhang Yi <[email protected]> --- fs/ext4/inode.c | 85 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 73 insertions(+), 12 deletions(-) diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index a1dfb70127ca..2ec69e8abe54 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -4841,6 +4841,68 @@ static int ext4_block_zero_range(struct inode *inode, zero_written); } +/* + * Inodes using the iomap buffered I/O path do not use data=ordered mode. + * Therefore, we mark the inode as disksize-grow-pending after zeroing the + * EOF block. The zeroed block will be submitted before any subsequent + * data. + * + * In the I/O completion path, ext4_iomap_wb_disksize_pending_wait() will + * wait for I/O completion before advancing i_disksize if the write + * extends beyond the zeroed boundary. + * + * When zeroed I/O is in progress, operations that extend i_disksize are + * handled as follows: + * + * - Truncate up, append fallocate and zero_range: + * Defer the update. The file size will be updated to i_size by the + * end_io handler once the ongoing pending I/O completes. + * + * - Insert range and collapse range operations: + * Wait synchronously for the relevant I/O to complete before updating + * i_disksize. + */ +static int ext4_iomap_mark_disksize_pending(struct inode *inode, loff_t from) +{ + struct folio *folio; + + folio = filemap_lock_folio(inode->i_mapping, from >> PAGE_SHIFT); + if (IS_ERR(folio)) + /* Already in writeback and cleared? */ + return PTR_ERR(folio) == -ENOENT ? 0 : PTR_ERR(folio); + + /* + * Ensure that in-flight writeback, possibly started after + * iomap_zero_range() unlocked the folio, has completed. Without + * this wait folio_test_dirty() below may miss the zeroed data + * (writeback clears PG_dirty), causing us to skip the + * disksize-grow-pending tracking and potentially expose stale + * on-disk data. + */ + folio_wait_writeback(folio); + WARN_ON_ONCE(folio_test_writeback(folio)); + + /* + * Mark the inode as disksize-grow-pending. The zeroed block will + * be written out by the generic writepages cycle or any other + * syncing operation. + * + * Multiple overlapping unaligned EOF writes should not happen, + * because we only mark the pending state after zeroing the on-disk + * EOF block, and i_disksize can only be updated after the previous + * zeroed pending block has been written back or the dirty folio + * has been discared. + */ + if (likely(folio_test_dirty(folio) && + !ext4_test_inode_state(inode, + EXT4_STATE_DISKSIZE_GROW_PENDING))) + ext4_set_inode_state(inode, EXT4_STATE_DISKSIZE_GROW_PENDING); + + folio_unlock(folio); + folio_put(folio); + return 0; +} + /* * Submit and wait for the pending zeroed EOF block range to complete * if the given range [@offset, @end) fully covers it. Must be called @@ -4916,22 +4978,21 @@ int ext4_block_zero_eof(struct inode *inode, loff_t from, loff_t end) * block, the zeroed data lies beyond the existing on-disk data. It * will be written out before i_disksize is later extended past * i_size, so no stale data can be exposed. - * - * TODO: In the iomap path, handle this by tracking the ordered range - * and updating i_disksize to i_size after the zeroed data has been - * written back. */ - if (ext4_should_order_data(inode) && - did_zero && zero_written && !IS_DAX(inode) && + if (did_zero && zero_written && !IS_DAX(inode) && from < round_up(READ_ONCE(EXT4_I(inode)->i_disksize), blocksize)) { - handle_t *handle; + if (ext4_should_order_data(inode)) { + handle_t *handle; - handle = ext4_journal_start(inode, EXT4_HT_MISC, 1); - if (IS_ERR(handle)) - return PTR_ERR(handle); + handle = ext4_journal_start(inode, EXT4_HT_MISC, 1); + if (IS_ERR(handle)) + return PTR_ERR(handle); - err = ext4_jbd2_inode_add_write(handle, inode, from, length); - ext4_journal_stop(handle); + err = ext4_jbd2_inode_add_write(handle, inode, from, + length); + ext4_journal_stop(handle); + } else if (ext4_inode_buffered_iomap(inode)) + err = ext4_iomap_mark_disksize_pending(inode, from); if (err) return err; } -- 2.52.0