[PATCH v7 08/31] ext4: skip block allocation for holes in the data submission path

From: Zhang Yi

Date: Fri Oct 09 2026 - 06:46:41 EST


From: Zhang Yi <yi.zhang@xxxxxxxxxx>

When ext4_map_blocks() is called from the data submission path and I/O
end extent conversion path (EXT4_GET_BLOCKS_IO_SUBMIT), it should not
allocate blocks if the lookup returns a hole.

The writeback path can legitimately encounter dirty ranges that map to
holes. For example, when a folio straddles i_size and the tail beyond
i_size is dirtied via a mmap write. Allocating blocks for such ranges is
wrong because there is no data to write back, the dirty bits should
simply be discarded without submitting I/O.

This prepares for the buffered iomap writeback conversion, mirrors the
existing buffer_head writeback path, where mpage_add_bh_to_extent() skip
unmapped buffers and ext4_bio_write_folio() clears their dirty bits.
Since ext4_map_blocks() can now return zero for a hole,
mpage_map_one_extent() must handle it as well. However, because those
holes are already filtered out by mpage_add_bh_to_extent(), it only maps
buffers that are delayed or unwritten, so a hole can never occur in
normal operation. Add a WARN_ON_ONCE() and bail out with -EINVAL rather
than proceeding with an invalid physical block number of zero.

In the ioend extent conversion path, holes are also not expected because
we should wait for folio writeback before punching hole. If one is
encountered, it likely indicates a failure in the concurrency
protection, so ext4_map_blocks() returns zero, and we keep warning and
bail out with -EINVAL to surface the failure rather than continuing
conversion on torn data. Atomic writes in
ext4_convert_unwritten_extents_atomic() already bail out similarly.

Signed-off-by: Zhang Yi <yi.zhang@xxxxxxxxxx>
Reviewed-by: Ojaswin Mujoo <ojaswin@xxxxxxxxxxxxx>
---
fs/ext4/inode.c | 13 +++++++++++++
1 file changed, 13 insertions(+)

diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index 6b0489cbb1319..778cf539f6df2 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -830,6 +830,13 @@ int ext4_map_blocks(handle_t *handle, struct inode *inode,
map->m_flags |= EXT4_MAP_MAPPED;
goto out;
}
+ } else if (retval == 0) {
+ /*
+ * Do not allocate blocks for holes in the context of
+ * data submission path.
+ */
+ if (!map->m_flags && (flags & EXT4_GET_BLOCKS_IO_SUBMIT))
+ goto out;
}

if (!handle) {
@@ -2428,6 +2435,12 @@ static int mpage_map_one_extent(handle_t *handle, struct mpage_da_data *mpd)
err = ext4_map_blocks(handle, inode, map, get_blocks_flags);
if (err < 0)
return err;
+ /*
+ * A hole? This should never happen since mpage_add_bh_to_extent()
+ * has filtered it out.
+ */
+ if (WARN_ON_ONCE(!err))
+ return -EINVAL;
if (dioread_nolock && (map->m_flags & EXT4_MAP_UNWRITTEN)) {
if (!mpd->io_submit.io_end->handle &&
ext4_handle_valid(handle)) {
--
2.52.0