ext4: implement partial block zero range path using iomap

Introduce a new iomap_ops instance, ext4_iomap_zero_ops, along with
ext4_block_iomap_zero_range() to implement block zeroing via the iomap
infrastructure for ext4.

ext4_block_iomap_zero_range() calls iomap_zero_range() with
ext4_iomap_zero_begin() as the callback. The callback locates the
range and populates the iomap mapping. If the range is mapped,
iomap_zero_iter() in the iomap core zeros the partial block
directly. If the range is an unwritten extent within EOF, the
callback collects a dirty folio batch via iomap_fill_dirty_folios()
so that iomap_zero_iter() can zero those folios directly, bypassing
a separate slow flush operation that would otherwise be needed to
convert the unwritten extent.

Note that ext4_iomap_zero_begin() can race with concurrent writeback:
after it queries an unwritten extent, writeback may convert it to
written and complete on the folio before iomap_fill_dirty_folios() scans
the range. The empty batch then makes iomap_zero_iter() skip zeroing,
leaving stale on-disk data.

  zero_range                      writeback
  ----------------------          ----------------------
  ext4_block_zero_range()
    iomap_zero_range()
      iomap_iter()
        ext4_iomap_zero_begin()
          ext4_iomap_map_blocks()
            -> extent is UNWRITTEN
                                  ext4_convert_unwritten_extents_endio()
                                    -> extent is converted to WRITTEN
                                    -> folio is clean
          iomap_fill_dirty_folios()
            filemap_get_folios_dirty()
            -> folio is clean, not added to batch
          ext4_set_iomap() -> IOMAP_UNWRITTEN
      iomap_zero_iter()
        __iomap_get_folio() -> NULL (empty batch)
        iomap_iter_advance_full()  <-- zeroing skipped

  [later read returns stale on-disk data]   <--   CORRUPTION

Therefore, we retry the extent lookup when iomap_fill_dirty_folios()
adds nothing and the i_es_seq cookie captured at the first lookup has
advanced, indicating the race actually occurred.

Other important constraints:

Zeroing out under an active journal handle can cause deadlock, as the
lock/handle ordering is inconsistent with the iomap writeback path.
Therefore, ext4_block_iomap_zero_range() must not be called under an
active handle. In addition, for post-EOF zeroing, the caller cannot
rely on data=ordered mode to persist the zeroed data before
i_disksize is updated.

Subsequent patches will address this by deferring i_disksize update
to i_size until after the zeroed data has been written back.

Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
Reviewed-by: Ojaswin Mujoo <ojaswin@linux.ibm.com>
Tested-by: syzbot@syzkaller.appspotmail.com
Link: https://patch.msgid.link/20261009103033.2920530-18-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:18 +08:00 committed by Theodore Ts'o
parent e055578dc1
commit 68738d77ce

View File

@ -4119,6 +4119,67 @@ static int ext4_iomap_buffered_da_write_end(struct inode *inode, loff_t offset,
return 0;
}
static int ext4_iomap_zero_begin(struct inode *inode,
loff_t offset, loff_t length, unsigned int flags,
struct iomap *iomap, struct iomap *srcmap)
{
struct iomap_iter *iter = container_of(iomap, struct iomap_iter, iomap);
struct ext4_map_blocks map;
u8 blkbits = inode->i_blkbits;
unsigned int iomap_flags;
int ret;
ret = ext4_emergency_state(inode->i_sb);
if (unlikely(ret))
return ret;
if (WARN_ON_ONCE(!(flags & IOMAP_ZERO)))
return -EINVAL;
again:
ret = ext4_iomap_map_blocks(inode, offset, length, &map, 0);
if (ret < 0)
return ret;
/*
* Look up dirty folios for unwritten mappings within EOF. Providing
* this bypasses the flush iomap uses to trigger extent conversion
* when unwritten mappings have dirty pagecache in need of zeroing.
*/
iomap_flags = 0;
if (map.m_flags & EXT4_MAP_UNWRITTEN) {
loff_t start = ((loff_t)map.m_lblk) << blkbits;
loff_t end = ((loff_t)map.m_lblk + map.m_len) << blkbits;
unsigned int count;
count = iomap_fill_dirty_folios(iter, &start, end,
&iomap_flags);
if ((start >> blkbits) < map.m_lblk + map.m_len)
map.m_len = (start >> blkbits) - map.m_lblk;
/*
* This can be raced by a concurrent writeback that cleans
* the folio and converts the unwritten extent to written.
* Recheck the mapping after a folio lock round in
* iomap_fill_dirty_folios().
*/
if (count == 0 &&
map.m_seq != READ_ONCE(EXT4_I(inode)->i_es_seq))
goto again;
}
ext4_set_iomap(inode, iomap, &map, offset, length, flags);
iomap->flags |= iomap_flags;
return 0;
}
static DEFINE_IOMAP_ITER_NEXT(ext4_iomap_zero_next, ext4_iomap_zero_begin);
static const struct iomap_ops ext4_iomap_zero_ops = {
.iomap_next = ext4_iomap_zero_next,
};
/*
* Since we always allocate unwritten extents, there is no need for
* iomap_end to clean up allocated blocks on a short write.
@ -4582,6 +4643,48 @@ static int ext4_block_journalled_zero_range(struct inode *inode, loff_t from,
return err;
}
static int ext4_block_iomap_zero_range(struct inode *inode, loff_t from,
loff_t length, bool *did_zero,
bool *zero_written)
{
int ret;
/*
* Zeroing out under an active handle can cause deadlock since
* the order of acquiring the folio lock and starting a handle is
* inconsistent with the iomap writeback procedure.
*/
if (WARN_ON_ONCE(ext4_handle_valid(journal_current_handle())))
return -EINVAL;
/* The zeroing scope should not extend across a block. */
if (WARN_ON_ONCE((from >> inode->i_blkbits) !=
((from + length - 1) >> inode->i_blkbits)))
return -EINVAL;
if (!(EXT4_SB(inode->i_sb)->s_mount_state & EXT4_ORPHAN_FS) &&
!(inode_state_read_once(inode) & (I_NEW | I_FREEING)))
WARN_ON_ONCE(!inode_is_locked(inode) &&
!rwsem_is_locked(&inode->i_mapping->invalidate_lock));
ret = iomap_zero_range(inode, from, length, did_zero,
&ext4_iomap_zero_ops, &ext4_iomap_write_ops,
NULL);
if (ret)
return ret;
/*
* TODO: The iomap does not distinguish between different types
* of zeroing operations. So we always set zero_written whenever
* zeroing is performed, which may cause unnecessary folio
* flushing when zeroing occurs on delayed-allocated blocks.
*/
if (did_zero && zero_written)
*zero_written = *did_zero;
return 0;
}
/*
* Zeros out a mapping of length 'length' starting from file offset
* 'from'. The range to be zero'd must be contained with in one block.
@ -4608,6 +4711,9 @@ static int ext4_block_zero_range(struct inode *inode,
} else if (ext4_should_journal_data(inode)) {
return ext4_block_journalled_zero_range(inode, from, length,
did_zero);
} else if (ext4_inode_buffered_iomap(inode)) {
return ext4_block_iomap_zero_range(inode, from, length,
did_zero, zero_written);
}
return ext4_block_do_zero_range(inode, from, length, did_zero,
zero_written);
@ -4656,6 +4762,10 @@ int ext4_block_zero_eof(struct inode *inode, loff_t from, loff_t end)
* truncating up or performing an append write, because there might be
* exposing stale on-disk data which may caused by concurrent post-EOF
* mmap write during folio writeback.
*
* 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)) {