From 981eec6c300bd4e84ffac8f3c28b762d00f13e9c Mon Sep 17 00:00:00 2001 From: guzebing Date: Mon, 10 Aug 2026 23:12:00 +0800 Subject: [PATCH 01/41] ext4: only update tune mount options when requested EXT4_IOC_SET_TUNE_SB_PARAM uses set_flags to indicate which superblock fields the caller wants to update. ext4_sb_setparams() checks set_flags before updating the other supported fields, but always copied mount_opts into the on-disk s_mount_opts field. As a result, callers that do not set EXT4_TUNE_FL_MOUNT_OPTS can still clear or alter the on-disk default mount options through their unused mount_opts buffer. This can affect subsequent mounts by changing the filesystem's stored default mount options. Only copy mount_opts into s_mount_opts when EXT4_TUNE_FL_MOUNT_OPTS is set, matching the set_flags semantics used by the other tuneable superblock fields. Cc: stable@kernel.org Fixes: 04a91570ac67 ("ext4: implemet new ioctls to set and get superblock parameters") Signed-off-by: guzebing Reviewed-by: "Darrick J. Wong" Link: https://patch.msgid.link/20260810151200.1405562-1-guzebing1612@gmail.com Signed-off-by: Theodore Ts'o --- fs/ext4/ioctl.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fs/ext4/ioctl.c b/fs/ext4/ioctl.c index c8387e6a2c6e..1f6d7afb62ec 100644 --- a/fs/ext4/ioctl.c +++ b/fs/ext4/ioctl.c @@ -1362,7 +1362,8 @@ static void ext4_sb_setparams(struct ext4_sb_info *sbi, es->s_encoding = cpu_to_le16(params->encoding); if (params->set_flags & EXT4_TUNE_FL_ENCODING_FLAGS) es->s_encoding_flags = cpu_to_le16(params->encoding_flags); - strscpy_pad(es->s_mount_opts, params->mount_opts); + if (params->set_flags & EXT4_TUNE_FL_MOUNT_OPTS) + strscpy_pad(es->s_mount_opts, params->mount_opts); if (params->set_flags & EXT4_TUNE_FL_EDIT_FEATURES) { es->s_feature_compat |= cpu_to_le32(params->set_feature_compat_mask); From e1a881ea4674876721cab97680d1d8308f90e9a4 Mon Sep 17 00:00:00 2001 From: Hsiu-Hsien Lee Date: Wed, 12 Aug 2026 12:43:53 +0800 Subject: [PATCH 02/41] ext4: fix fast commit replay failing on a read-only mount A filesystem with fast_commit that needs recovery cannot be mounted read-only: EXT4-fs (dm-0): INFO: recovery required on readonly filesystem EXT4-fs (dm-0): write access will be enabled during recovery WARNING: CPU: 22 PID: 5544 at fs/ext4/ext4_jbd2.c:73 ext4_journal_check_start __ext4_journal_start_sb __ext4_unlink ext4_fc_replay do_one_pass jbd2_journal_recover jbd2_journal_load __ext4_fill_super JBD2: journal recovery failed EXT4-fs (dm-0): error loading journal Fast commit replay runs ext4 metadata operations instead of writing blocks through the buffer cache: ext4_fc_replay_{unlink,link,create}() reach __ext4_unlink() and __ext4_link(), which start a handle. ext4_journal_check_start() returns -EROFS on a read-only sb, and as that is not -ENOENT it propagates out of jbd2_journal_recover() and kills the whole recovery. The EXT4_FC_REPLAY check that would hand out a no-journal handle sits after the sb_rdonly() test, so replay can never complete read-only. ext4_load_journal() has already promised that write access will be enabled during recovery, so make that true for the superblock as well: clear SB_RDONLY across jbd2_journal_load() when recovery is needed on a read-only mount and the devices are writable, as ext4_orphan_cleanup() does. Unlike ext4_handle_error(), which avoids SB_RDONLY because it would need s_umount, the sb here is still inside ext4_fill_super() and not published, so nothing can observe it. The failure is not clean either: the replay handlers passing a NULL handle (ext4_fc_replay_inode(), _add_range(), _del_range()) never hit ext4_journal_check_start() and do write, leaving a partially applied fast commit behind. Reproducer, where the unlink only ever reaches the fast commit area: mke2fs -q -F -t ext4 -O fast_commit -b 4096 /dev/sdb3 262144 mount /dev/sdb3 /mnt dd if=/dev/zero of=/mnt/victim bs=4k count=1 conv=fsync sync # victim now in a full commit rm /mnt/victim dd if=/dev/zero of=/mnt/trigger bs=4k count=1 conv=fsync mount -o ro /dev/sdb3 /mnt Without this patch that mount fails; with it recovery completes and victim is gone, i.e. the UNLINK record was really replayed. Fixes: 8016e29f4362 ("ext4: fast commit recovery path") Cc: stable@vger.kernel.org Signed-off-by: Hsiu-Hsien Lee Reviewed-by: Jan Kara Link: https://patch.msgid.link/20260812044353.1018268-1-swinds24@gmail.com Signed-off-by: Theodore Ts'o --- fs/ext4/super.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/fs/ext4/super.c b/fs/ext4/super.c index bca0dc87d0b7..ab243a02a493 100644 --- a/fs/ext4/super.c +++ b/fs/ext4/super.c @@ -6123,6 +6123,7 @@ static int ext4_load_journal(struct super_block *sb, int err = 0; int really_read_only; int journal_dev_ro; + bool enable_write = false; if (WARN_ON_ONCE(!ext4_has_feature_journal(sb))) return -EFSCORRUPTED; @@ -6179,6 +6180,7 @@ static int ext4_load_journal(struct super_block *sb, } ext4_msg(sb, KERN_INFO, "write access will " "be enabled during recovery"); + enable_write = true; } } @@ -6195,7 +6197,19 @@ static int ext4_load_journal(struct super_block *sb, if (save) memcpy(save, ((char *) es) + EXT4_S_ERR_START, EXT4_S_ERR_LEN); + /* + * Fast commit replay performs regular ext4 metadata updates + * (see ext4_fc_replay()) which refuse to run on a read-only + * superblock. We promised write access above, so make that + * true for the duration of the recovery, the same way + * ext4_orphan_cleanup() does. The superblock is not published + * yet, so nothing can observe the transient state. + */ + if (enable_write) + sb->s_flags &= ~SB_RDONLY; err = jbd2_journal_load(journal); + if (enable_write) + sb->s_flags |= SB_RDONLY; if (save && memcmp(((char *) es) + EXT4_S_ERR_START, save, EXT4_S_ERR_LEN)) { memcpy(((char *) es) + EXT4_S_ERR_START, From 26a3ff309c91986dbedb05d0e0f0050ea0aa629c Mon Sep 17 00:00:00 2001 From: guzebing Date: Fri, 14 Aug 2026 11:48:55 +0800 Subject: [PATCH 03/41] ext4: reject delalloc to nodelalloc before applying remount options Commit 97f5ec3b166d ("ext4: prevent delalloc to nodelalloc on remount") rejects switching a mounted filesystem from delalloc to nodelalloc. However, it performs the check after ext4_apply_options() has already cleared EXT4_MOUNT_DELALLOC in the live superblock. The failure path eventually restores the bit, but leaves a window where other CPUs can observe nodelalloc. test_opt() reads s_mount_opt directly without a lock shared with remount. If a concurrent truncate's __es_remove_extent() hits this window and observes nodelalloc, it sets count_reserved to false. Delayed extent status entries are then removed without calculating their cluster reservations, leaving reserved at zero. ext4_es_remove_extent() therefore calls ext4_da_release_space() with zero, leaving i_reserved_data_blocks and s_dirtyclusters_counter elevated. When the inode is later evicted after unlink, ext4_destroy_inode() reports: i_reserved_data_blocks (...) not cleared! CPU 0 CPU 1 ksys_truncate() ... ext4_es_remove_extent() ext4_reconfigure() ext4_check_opt_consistency() __ext4_remount() ext4_apply_options() clear EXT4_MOUNT_DELALLOC __es_remove_extent() test_opt() sees !DELALLOC count_reserved = false reject delalloc -> nodelalloc restore EXT4_MOUNT_DELALLOC remove delayed ES ext4_da_release_space(0) Follow the pre-apply validation approach used by ext4_check_quota_consistency() and reject the transition in ext4_check_opt_consistency(), before ext4_apply_options() changes live state. Use mask_s_mount_opt to determine whether delalloc/nodelalloc was specified and ctx_test_mount_opt() to check the final parsed value. Fixes: 97f5ec3b166d ("ext4: prevent delalloc to nodelalloc on remount") Cc: stable@vger.kernel.org Signed-off-by: guzebing Reviewed-by: Ojaswin Mujoo Reviewed-by: Jan Kara Link: https://patch.msgid.link/20260814034855.1573759-1-guzebing1612@gmail.com Signed-off-by: Theodore Ts'o --- fs/ext4/super.c | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/fs/ext4/super.c b/fs/ext4/super.c index ab243a02a493..fbd808f54054 100644 --- a/fs/ext4/super.c +++ b/fs/ext4/super.c @@ -2832,6 +2832,14 @@ static int ext4_check_opt_consistency(struct fs_context *fc, } if (is_remount) { + if (test_opt(sb, DELALLOC) && + (ctx->mask_s_mount_opt & EXT4_MOUNT_DELALLOC) && + !ctx_test_mount_opt(ctx, EXT4_MOUNT_DELALLOC)) { + ext4_msg(sb, KERN_ERR, + "can't disable delalloc during remount"); + return -EINVAL; + } + if (!sbi->s_journal && ctx_test_mount_opt(ctx, EXT4_MOUNT_DATA_ERR_ABORT)) { ext4_msg(NULL, KERN_WARNING, @@ -6702,13 +6710,6 @@ static int __ext4_remount(struct fs_context *fc, struct super_block *sb) goto restore_opts; } - if ((old_opts.s_mount_opt & EXT4_MOUNT_DELALLOC) && - !test_opt(sb, DELALLOC)) { - ext4_msg(sb, KERN_ERR, "can't disable delalloc during remount"); - err = -EINVAL; - goto restore_opts; - } - sb->s_flags = (sb->s_flags & ~SB_POSIXACL) | (test_opt(sb, POSIX_ACL) ? SB_POSIXACL : 0); From 9f359c031885bb50add7d23c7a5a0e9755b134c9 Mon Sep 17 00:00:00 2001 From: Daejun Park Date: Wed, 19 Aug 2026 15:09:39 +0900 Subject: [PATCH 04/41] jbd2: don't advance j_fc_off before the buffer is recorded jbd2_fc_get_buf() increments journal->j_fc_off before jbd2_journal_bmap() and __getblk() have had a chance to fail. When either does, the slot j_fc_wbuf[fc_off] is never assigned, but j_fc_off already counts it. ext4 then fails the fast commit and falls back, and the fallback path reaches jbd2_fc_release_bufs() via ext4_fc_commit -> jbd2_fc_end_commit_fallback -> __jbd2_fc_end_commit -> ext4_fc_cleanup. That walks down from j_fc_off - 1 and put_bh()es every slot until it sees NULL, so it also touches the slot that was never written. j_fc_wbuf comes from a plain kmalloc() and is never zeroed, so a slot used for the first time holds uninitialised heap data. Advance j_fc_off only after the buffer head has been stored. That restores the invariant that j_fc_off covers exactly the filled slots, which is what jbd2_fc_release_bufs() relies on: the live buffers of the current fast commit sit above the NULLs the previous one released, so stopping at the first NULL is correct. Reproduced on a KASAN kernel with an ext4 fast_commit filesystem on a loop device, truncating the backing file under the live mount so that only the journal's fast-commit region falls past the end of the device. __getblk() then fails for the first fast-commit block while the rest of the journal is still addressable, so the journal is not aborted and the fast commit reaches the fallback path. j_fc_wbuf was poisoned to make the read of the never-written slot visible: BUG: KASAN: wild-memory-access in jbd2_fc_release_bufs+0x6b/0xd0 Write of size 4 at addr 5a5a5a5a5a5a5ab2 by task sync/961 jbd2_fc_release_bufs+0x6b/0xd0 ext4_fc_cleanup+0x97/0x830 __jbd2_fc_end_commit+0x37/0xc0 ext4_fc_commit+0x524/0x560 ext4_sync_file+0x3c4/0x4b0 __x64_sys_fsync+0x20/0x30 With this patch the same run leaves j_fc_off at 0 across repeated __getblk() failures and completes cleanly. Fixes: ff780b91efe9 ("jbd2: add fast commit machinery") Cc: stable@kernel.org Reported-by: Yu Junzhe Closes: https://lore.kernel.org/linux-ext4/7e4b109d-64c3-444f-a5dc-93c2ca4576cc@gmail.com/ Signed-off-by: Daejun Park Reviewed-by: Jan Kara Link: https://patch.msgid.link/20260819060939epcms2p37bb8589b2f8a52a2b14bef38fadcf69d@epcms2p3 Signed-off-by: Theodore Ts'o --- fs/jbd2/journal.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/jbd2/journal.c b/fs/jbd2/journal.c index 00f5a98f3d4f..55b4ce010228 100644 --- a/fs/jbd2/journal.c +++ b/fs/jbd2/journal.c @@ -843,7 +843,6 @@ int jbd2_fc_get_buf(journal_t *journal, struct buffer_head **bh_out) fc_off = journal->j_fc_off; blocknr = journal->j_fc_first + fc_off; - journal->j_fc_off++; ret = jbd2_journal_bmap(journal, blocknr, &pblock); if (ret) return ret; @@ -853,6 +852,7 @@ int jbd2_fc_get_buf(journal_t *journal, struct buffer_head **bh_out) return -ENOMEM; journal->j_fc_wbuf[fc_off] = bh; + journal->j_fc_off++; *bh_out = bh; From 418adcde99032194416248168df810535a1a93f9 Mon Sep 17 00:00:00 2001 From: Hemanth Selam Date: Mon, 24 Aug 2026 15:15:12 +0530 Subject: [PATCH 05/41] ext4: skip extra isize expansion while unmounting syzbot reports a WARN from ext4_xattr_inode_create() reached through the unmount path: EXT4-fs warning (device loop0): ext4_xattr_inode_create:1485: refuse to create EA inode when umounting WARNING: fs/ext4/xattr.c:1486 at ext4_xattr_inode_lookup_create ext4_xattr_block_set ext4_expand_extra_isize_ea __ext4_expand_extra_isize __ext4_mark_inode_dirty ext4_dirty_inode __mark_inode_dirty sync_lazytime iput dentry_kill shrink_dentry_list shrink_dcache_for_umount generic_shutdown_super kill_block_super ext4_kill_sb shrink_dcache_for_umount() clears s_root before generic_shutdown_super() clears SB_ACTIVE, so during the dcache shrink the last iput() of a lazytime inode still redirties it and reaches the isize expansion. The expansion can move xattrs out to a block, and creating the EA inode for them needs s_root, which ext4_xattr_inode_create() refuses without. ext4_try_to_expand_extra_isize() already declines to expand when the superblock is not active, but that test does not cover this window. Decline while s_root is gone as well. The expansion is best effort and __ext4_mark_inode_dirty() ignores its return value, so nothing else changes; the inode can be expanded on a later mount. Running the syzbot reproducer for 60 seconds produced 3583 splats before this change and none after it, with the same number of mount cycles. Cc: stable@kernel.org Reported-by: syzbot+4b03894b6ec5753ddf24@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=4b03894b6ec5753ddf24 Fixes: f31173c19901 ("ext4: refuse to create ea block when umounted") Signed-off-by: Hemanth Selam Reviewed-by: Jan Kara Reviewed-by: Jun Nie Link: https://patch.msgid.link/20260824094512.2436628-1-hemanth.selam@gmail.com Signed-off-by: Theodore Ts'o --- fs/ext4/inode.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index 26f0f9714f03..8dd3dc9e0217 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -6603,8 +6603,14 @@ static int ext4_try_to_expand_extra_isize(struct inode *inode, * When !SB_ACTIVE, iput triggers write_inode_now() which acquires * s_writepages_rwsem, causing a deadlock with the caller's active * jbd2 handle (lock order: s_writepages_rwsem -> jbd2_handle). + * + * Skip it while unmounting as well. shrink_dcache_for_umount() + * clears s_root before generic_shutdown_super() clears SB_ACTIVE, and + * the last iput() of a lazytime inode in that window redirties it and + * lands here. Moving xattrs out to a block then needs a new EA inode, + * which ext4_xattr_inode_create() refuses without s_root. */ - if (unlikely(!(inode->i_sb->s_flags & SB_ACTIVE))) + if (unlikely(!(inode->i_sb->s_flags & SB_ACTIVE) || !inode->i_sb->s_root)) return -EBUSY; /* From aed43f47e1edeee1454eea86006a8c09480e5ed3 Mon Sep 17 00:00:00 2001 From: Daejun Park Date: Thu, 27 Aug 2026 17:48:29 +0900 Subject: [PATCH 06/41] ext4: don't report delalloc data as a hole on indirect-mapped inodes When a plain lookup finds no block, ext4_ind_map_blocks() sizes the hole it reports by counting the empty subtrees under 'partial' in the on-disk indirect tree. That count knows nothing about delayed allocation, so a range holding delalloc data that has not been written back yet is reported as a plain hole. The extent-mapped path does not have this problem: it trims the hole it found at the first delayed extent before returning it. ext4_map_blocks() consults the extent status tree before it calls into the mapping layer, so a query that starts exactly on the delayed block still finds it. A query that starts earlier does not, because the hole reported for the earlier block already spans the delayed one. On a 4k-block filesystem, lseek(SEEK_DATA) from offset 0 on a file whose only data block is at logical block N and is still dirty returns: N = 0..11 direct blocks N << 12 N = 12 first single-indirect block N << 12 N = 13..1035 inside that indirect block -1 ENXIO N = 1036 first double-indirect block N << 12 Blocks 12 and 1036 survive because the query lands on the delayed block itself. Blocks 13..1035 are swallowed by the 1024-block hole reported for block 12. fiemap loses the same data for the same reason. An ext3 filesystem mounted as ext4 hits this during ordinary use: ext3 inodes stay indirect mapped, while mount -t ext4 turns on delayed allocation even though mounting ext3 with -o delalloc is explicitly rejected. install(1) from coreutils uses SEEK_DATA to locate data in its source file, so installing a sparse file that has not been written back silently produces a destination of the right size holding none of the data: truncate -s 1G img && mkfs.ext3 -F img && mount -t ext4 img mnt echo | dd of=mnt/src bs=1 count=1 seek=64K install mnt/src mnt/dst cmp mnt/src mnt/dst # differ: char 65537 Reconciling a hole with the extent status tree does not depend on how the hole was found, so instead of open coding it a second time, split the delalloc handling out of ext4_ext_determine_insert_hole() into ext4_determine_insert_hole(), which takes the hole the caller located, and call that from ext4_ind_map_blocks() as well. All the special cases and the comments explaining them stay in one place. Two things change for indirect-mapped inodes. They now cache the holes they report, as the extent-mapped path already does: ext4_punch_hole() already puts EXTENT_STATUS_HOLE on such inodes, ext4_map_blocks() and ext4_da_map_blocks() already handle a cached hole, the latter by replacing it with the delayed extent, and consecutive hole entries merge, so the tree does not grow with the length of a scan. And when a delayed extent covers the queried block itself - a race, since ext4_map_blocks() found nothing in the tree just before - the hole is now reported only up to the end of that extent instead of in full. The indirect path passes hole_start == lblk, so the "delalloc extent in front of the queried range" case cannot be reached there: __es_tree_search() never returns an extent ending before the block it was searched for. With this the sweep above returns N << 12 for every N, while the nodelalloc and extent-mapped controls are unchanged. generic/225, generic/285, generic/448 and generic/490 pass on ext4 made both with and without the extent feature. generic/286 and generic/436 pass on extent-mapped ext4 and are skipped without it, since they need fallocate. Fixes: facab4d9711e ("ext4: return hole from ext4_map_blocks()") Reported-by: Alexander Monakov Closes: https://lore.kernel.org/linux-ext4/594c17d9-c00f-e485-96fb-cedf27ce3aa3@ispras.ru/ Suggested-by: Jan Kara Cc: stable@vger.kernel.org Signed-off-by: Daejun Park Reviewed-by: Jan Kara Reviewed-by: Zhang Yi Link: https://patch.msgid.link/20260827084829epcms2p6ecc0c39a71d0c3eebf9cb1ade77314f6@epcms2p6 Signed-off-by: Theodore Ts'o --- fs/ext4/ext4.h | 3 +++ fs/ext4/extents.c | 26 +++++++++++++------------- fs/ext4/indirect.c | 17 +++++++++++++++++ 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h index 724a27e8be61..1edc4a94bb7d 100644 --- a/fs/ext4/ext4.h +++ b/fs/ext4/ext4.h @@ -3905,6 +3905,9 @@ extern void ext4_ext_tree_init(handle_t *handle, struct inode *inode); extern int ext4_ext_index_trans_blocks(struct inode *inode, int extents); extern int ext4_ext_map_blocks(handle_t *handle, struct inode *inode, struct ext4_map_blocks *map, int flags); +ext4_lblk_t ext4_determine_insert_hole(struct inode *inode, ext4_lblk_t lblk, + ext4_lblk_t hole_start, + ext4_lblk_t hole_len); extern int ext4_ext_truncate(handle_t *, struct inode *); extern int ext4_ext_remove_space(struct inode *inode, ext4_lblk_t start, ext4_lblk_t end); diff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index 76038b6c3655..5409b4c07c3c 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -4190,21 +4190,19 @@ static int get_implied_cluster_alloc(struct super_block *sb, } /* - * Determine hole length around the given logical block, first try to - * locate and expand the hole from the given @path, and then adjust it - * if it's partially or completely converted to delayed extents, insert - * it into the extent cache tree if it's indeed a hole, finally return - * the length of the determined extent. + * Adjust the hole [@hole_start, @hole_start + @hole_len) the caller found + * around the queried block @lblk if it's partially or completely converted + * to delayed extents, insert it into the extent cache tree if it's indeed a + * hole, finally return the length of the determined extent starting at + * @lblk. @hole_start must not be behind @lblk. */ -static ext4_lblk_t ext4_ext_determine_insert_hole(struct inode *inode, - struct ext4_ext_path *path, - ext4_lblk_t lblk) +ext4_lblk_t ext4_determine_insert_hole(struct inode *inode, ext4_lblk_t lblk, + ext4_lblk_t hole_start, + ext4_lblk_t hole_len) { - ext4_lblk_t hole_start, len; + ext4_lblk_t len = hole_len; struct extent_status es; - hole_start = lblk; - len = ext4_ext_find_hole(inode, path, &hole_start); again: ext4_es_find_extent_range(inode, &ext4_es_is_delayed, hole_start, hole_start + len - 1, &es); @@ -4371,9 +4369,11 @@ int ext4_ext_map_blocks(handle_t *handle, struct inode *inode, * we couldn't try to create block if flags doesn't contain EXT4_GET_BLOCKS_CREATE */ if ((flags & EXT4_GET_BLOCKS_CREATE) == 0) { - ext4_lblk_t len; + ext4_lblk_t hole_start = map->m_lblk, len; - len = ext4_ext_determine_insert_hole(inode, path, map->m_lblk); + len = ext4_ext_find_hole(inode, path, &hole_start); + len = ext4_determine_insert_hole(inode, map->m_lblk, + hole_start, len); map->m_pblk = 0; map->m_len = min_t(unsigned int, map->m_len, len); diff --git a/fs/ext4/indirect.c b/fs/ext4/indirect.c index 5aec759eed70..a495080df710 100644 --- a/fs/ext4/indirect.c +++ b/fs/ext4/indirect.c @@ -586,6 +586,23 @@ int ext4_ind_map_blocks(handle_t *handle, struct inode *inode, for (i = partial - chain + 1; i < depth; i++) count = count * epb + (epb - offsets[i] - 1); count++; + + /* + * The count knows nothing about delayed allocation, so let + * the common helper reconcile it with the extent status + * tree. Only when the lookup really did find a hole: + * ext4_get_branch() also returns a non-NULL partial when it + * failed, and caching a hole for a range it could not read + * would answer every later read from that cache instead of + * retrying. Clamp the count first, a subtree can be bigger + * than the logical block space when the block size is large. + */ + if (!err) { + count = umin(count, EXT_MAX_BLOCKS - map->m_lblk); + count = ext4_determine_insert_hole(inode, map->m_lblk, + map->m_lblk, count); + } + /* Fill in size of a hole we found */ map->m_pblk = 0; map->m_len = umin(map->m_len, count); From 19e16597f9baf1b5719a3a6c710486446f6808fd Mon Sep 17 00:00:00 2001 From: Sergey Senozhatsky Date: Thu, 27 Aug 2026 21:50:20 +0900 Subject: [PATCH 07/41] ext4: fix ABBA deadlock in ext4_rmdir() We observe the following ABBA deadlock (casefolding enabled): INFO: task jbd2/dm-13-8:447 blocked for more than 720 seconds. task:jbd2/dm-13-8 state:D stack:0 pid:447 tgid:447 ppid:2 Call Trace: __schedule+0xc2c/0xe60 schedule+0x40/0xe0 jbd2_journal_wait_updates+0x8f/0xf0 jbd2_journal_commit_transaction+0x321/0x1850 kjournald2+0xa9/0x230 kthread+0x226/0x2a0 ret_from_fork+0x101/0x1e0 ret_from_fork_asm+0x1a/0x30 INFO: task Thread-10:4161 blocked for more than 720 seconds. task:Thread-10 state:D stack:0 pid:4161 tgid:3848 ppid:540 Call Trace: __schedule+0xc2c/0xe60 schedule+0x40/0xe0 wait_transaction_locked+0x8c/0xd0 start_this_handle+0x252/0x870 jbd2__journal_start+0x120/0x280 __ext4_journal_start_sb+0x11a/0x250 ext4_evict_inode+0x208/0x760 iput+0x222/0x5c0 dput+0x293/0x690 ____fput+0x145/0x2b0 task_work_run+0x7a/0xb0 exit_to_user_mode_loop+0xc0/0xd0 do_syscall_64+0x14b/0xf10 entry_SYSCALL_64_after_hwframe+0x76/0x7e NMI backtrace for cpu 0 CPU: 0 UID: 1010216 PID: 4160 Comm: Thread-7 RIP: 0010:d_walk+0x0/0x290 [..] Call Trace: shrink_dcache_parent+0xb2/0x100 d_invalidate+0x50/0x110 ext4_rmdir+0x3a2/0x3d0 vfs_rmdir+0x9d/0x1d0 do_rmdir+0xf0/0x330 __x64_sys_unlinkat+0x34/0x50 do_syscall_64+0x61/0xf10 The problem is that ext4_rmdir() calls d_invalidate() before stopping the jbd2 transaction handle. Because d_invalidate() traverses child dentries it can encounter dying or in-use dentries. If a concurrent thread executing __dentry_kill() on a child dentry invokes ext4_evict_inode(), it requests a new transaction handle via ext4_journal_start(), which waits for all running handles to close. However, the thread executing ext4_rmdir() still holds its active transaction handle while looping in shrink_dcache_parent() waiting for the dying child dentry to complete eviction. Stop transaction handle in ext4_rmdir() before calling d_invalidate(). Cc: stable@kernel.org Fixes: b886ee3e778e ("ext4: Support case-insensitive file name lookups") Co-developed-by: Sarthak Kukreti Signed-off-by: Sarthak Kukreti Signed-off-by: Sergey Senozhatsky Reviewed-by: Jan Kara Reviewed-by: Baokun Li Reviewed-by: Zhang Yi Link: https://patch.msgid.link/20260827125158.4030598-1-senozhatsky@chromium.org Signed-off-by: Theodore Ts'o --- fs/ext4/namei.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c index a6386c1d237f..231a274b814e 100644 --- a/fs/ext4/namei.c +++ b/fs/ext4/namei.c @@ -3209,19 +3209,20 @@ static int ext4_rmdir(struct inode *dir, struct dentry *dentry) ext4_fc_track_unlink(handle, dentry); retval = ext4_mark_inode_dirty(handle, dir); +end_rmdir: + brelse(bh); + if (handle) + ext4_journal_stop(handle); + /* VFS negative dentries are incompatible with Encoding and * Case-insensitiveness. Eventually we'll want avoid * invalidating the dentries here, alongside with returning the * negative dentries at ext4_lookup(), when it is better * supported by the VFS for the CI case. */ - if (IS_ENABLED(CONFIG_UNICODE) && IS_CASEFOLDED(dir)) + if (!retval && IS_ENABLED(CONFIG_UNICODE) && IS_CASEFOLDED(dir)) d_invalidate(dentry); -end_rmdir: - brelse(bh); - if (handle) - ext4_journal_stop(handle); return retval; } From b067d3c1f90b536e9aa9f48b19580475a0b34f0f Mon Sep 17 00:00:00 2001 From: Jinjie Ruan Date: Wed, 2 Sep 2026 15:48:04 +0800 Subject: [PATCH 08/41] ext4: fix out-of-bounds read in ext4_get_group_info() A plain read of s_groups_count in ext4_get_group_info() allows CPU load-load reordering. On weak memory models, speculative prefetch of s_group_info prior to the boundary check could lead to an out-of-bounds read if a concurrent online resize expands the array and increments s_groups_count. The data race occurs between the ioctl configuration path (holding the resize lock via ext4_resize_begin) and the lockless metadata lookup path: CPU 0 (Writer, Resize Lock) CPU 1 (Reader, Lockless) --------------------------- ------------------------ ext4_ioctl() [EXT4_IOC_GROUP_ADD] ext4_ioctl_group_add() ext4_resize_begin() // Takes lock ext4_group_add() ext4_mb_alloc_groupinfo() // Publishes expanded array via RCU rcu_assign_pointer(s_group_info, ...) ext4_flex_group_add() ext4_update_super() ext4_get_group_info() // Speculative / out-of-order read [Loads old/smaller s_group_info pointer] [Plain C store / smp_wmb()] sbi->s_groups_count += ...; // Reads new s_groups_count, // boundary check passes if (group >= s_groups_count) // Out-of-bounds array access! sbi_array_rcu_deref(..., s_group_info) Fix this by using ext4_get_groups_count() to enforce acquire semantics. Cc: stable@vger.kernel.org Cc: "Theodore Ts'o" Cc: Andreas Dilger Cc: Baokun Li Cc: Jan Kara Cc: Ojaswin Mujoo Cc: "Ritesh Harjani (IBM)" Cc: Zhang Yi Fixes: 5354b2af3406 ("ext4: allow ext4_get_group_info() to fail") Link: https://sashiko.dev/#/patchset/20260825095422.3166067-1-ruanjinjie%40huawei.com Reviewed-by: Zhang Yi Reviewed-by: Jan Kara Signed-off-by: Jinjie Ruan Link: https://patch.msgid.link/20260902074805.398540-8-ruanjinjie@huawei.com Signed-off-by: Theodore Ts'o --- fs/ext4/balloc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/ext4/balloc.c b/fs/ext4/balloc.c index 52f4c5169f91..778fe8788f06 100644 --- a/fs/ext4/balloc.c +++ b/fs/ext4/balloc.c @@ -329,7 +329,7 @@ struct ext4_group_info *ext4_get_group_info(struct super_block *sb, struct ext4_group_info **grp_info; long indexv, indexh; - if (unlikely(group >= EXT4_SB(sb)->s_groups_count)) + if (unlikely(group >= ext4_get_groups_count(sb))) return NULL; if (unlikely(!EXT4_SB(sb)->s_group_info)) return NULL; From 4a87cfb9b45ae07cd28bdc1ab9059ea73eab312e Mon Sep 17 00:00:00 2001 From: Jinjie Ruan Date: Wed, 2 Sep 2026 15:48:05 +0800 Subject: [PATCH 09/41] ext4: convert group-count barrier protocol to acquire/release During an online resize, reading s_groups_count and s_blockfile_groups requires strict ordering against subsequent initialized block group metadata. Replace the historical smp_wmb()/smp_rmb() pairs with smp_store_release() and smp_load_acquire(). This formalizes the publish-subscribe pattern and allows weakly-ordered architectures (e.g., arm64) to utilize native STLR/LDAR instructions instead of full DMB fences. Update the documentation in resize.c and ext4.h accordingly. Cc: "Theodore Ts'o" Cc: Andreas Dilger Cc: Baokun Li Cc: Jan Kara Cc: Ojaswin Mujoo Cc: "Ritesh Harjani (IBM)" Cc: Zhang Yi Assisted-by: Gemini:Gemini-3.1 Assisted-by: DeepSeek:DeepSeek-V3 Reviewed-by: Zhang Yi Reviewed-by: Jan Kara Signed-off-by: Jinjie Ruan Link: https://patch.msgid.link/20260902074805.398540-9-ruanjinjie@huawei.com Signed-off-by: Theodore Ts'o --- fs/ext4/ext4.h | 10 +++------- fs/ext4/mballoc.c | 6 ++---- fs/ext4/resize.c | 19 +++++++++++-------- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h index 1edc4a94bb7d..9d06ddd865d4 100644 --- a/fs/ext4/ext4.h +++ b/fs/ext4/ext4.h @@ -3542,16 +3542,12 @@ static inline void ext4_isize_set(struct ext4_inode *raw_inode, loff_t i_size) } /* - * Reading s_groups_count requires using smp_rmb() afterwards. See - * the locking protocol documented in the comments of ext4_group_add() - * in resize.c + * Reading s_groups_count uses acquire semantics. */ static inline ext4_group_t ext4_get_groups_count(struct super_block *sb) { - ext4_group_t ngroups = EXT4_SB(sb)->s_groups_count; - - smp_rmb(); - return ngroups; + /* Pairs with smp_store_release() in ext4_update_super() */ + return smp_load_acquire(&EXT4_SB(sb)->s_groups_count); } static inline ext4_group_t ext4_flex_group(struct ext4_sb_info *sbi, diff --git a/fs/ext4/mballoc.c b/fs/ext4/mballoc.c index 06171a11db12..a15285224cdf 100644 --- a/fs/ext4/mballoc.c +++ b/fs/ext4/mballoc.c @@ -899,10 +899,8 @@ static ext4_group_t ext4_get_allocation_groups_count( /* non-extent files are limited to low blocks/groups */ if (!(ext4_test_inode_flag(ac->ac_inode, EXT4_INODE_EXTENTS))) - ngroups = EXT4_SB(ac->ac_sb)->s_blockfile_groups; - - /* Pairs with smp_wmb() in ext4_update_super() */ - smp_rmb(); + /* Pairs with smp_store_release() in ext4_update_super() */ + ngroups = smp_load_acquire(&EXT4_SB(ac->ac_sb)->s_blockfile_groups); return ngroups; } diff --git a/fs/ext4/resize.c b/fs/ext4/resize.c index 2c5b851c552a..5e3ad8393cf8 100644 --- a/fs/ext4/resize.c +++ b/fs/ext4/resize.c @@ -1462,11 +1462,11 @@ static void ext4_update_super(struct super_block *sb, * * The precise rules we use are: * - * * Writers must perform a smp_wmb() after updating all - * dependent data and before modifying the groups count + * * Writers must use a release store when updating the groups count + * after all dependent data has been updated * - * * Readers must perform an smp_rmb() after reading the groups - * count and before reading any dependent data. + * * Readers must use an acquire load when reading the groups + * count before reading any dependent data. * * NB. These rules can be relaxed when checking the group count * while freeing data, as we can only allocate from a block @@ -1474,12 +1474,15 @@ static void ext4_update_super(struct super_block *sb, * only then free after serialising in turn against that * allocation. */ - smp_wmb(); /* Update the global fs size fields */ - sbi->s_groups_count += flex_gd->count; - sbi->s_blockfile_groups = min(sbi->s_groups_count, - (EXT4_MAX_BLOCK_FILE_PHYS / EXT4_BLOCKS_PER_GROUP(sb))); + /* Pairs with smp_load_acquire() in ext4_get_groups_count() */ + smp_store_release(&sbi->s_groups_count, + sbi->s_groups_count + flex_gd->count); + /* Pairs with smp_load_acquire() in ext4_get_allocation_groups_count() */ + smp_store_release(&sbi->s_blockfile_groups, + min(sbi->s_groups_count, + (EXT4_MAX_BLOCK_FILE_PHYS / EXT4_BLOCKS_PER_GROUP(sb)))); /* Update the reserved block counts only once the new group is * active. */ From b5501f970dbc039df1f7c720b7a481b62de125cd Mon Sep 17 00:00:00 2001 From: JonasZhou-oc Date: Wed, 2 Sep 2026 15:03:31 +0800 Subject: [PATCH 10/41] jbd2: avoid false sharing with j_state_lock is_journal_aborted() reads journal->j_flags from ext4 and JBD2 hot paths. At the same time, transaction start and stop paths repeatedly acquire and release j_state_lock. read_lock() and read_unlock() update the lock word, so this traffic invalidates the cache line containing j_flags. On the tested x86-64 build, j_flags, j_errno, and j_state_lock occupy the same 64-byte cache line. j_flags starts at offset 0, j_errno at offset 8, and j_state_lock at offset 56. Align j_state_lock to a cacheline boundary on SMP builds. This moves it to offset 64 and prevents lock traffic from invalidating j_flags and j_errno. Tests were run on Linux 7.3-rc1. On a two-socket Intel Xeon Silver 4208 system with SMT disabled, CPUs fixed at 2 GHz, and 16 stress-ng workers, ten-run average bogo-ops/s for stress-ng.utime, stress-ng.access, stress-ng.file-ioctl, and stress-ng.open increased by 9.60%, 5.42%, 2.05%, and 4.65%, respectively. In separate five-run perf c2c measurements, average Remote HITM samples for the affected j_flags/j_errno line decreased from 1996, 2041, 1555, and 383 to zero, respectively. Signed-off-by: JonasZhou-oc Link: https://patch.msgid.link/20260902070331.820322-1-jonaszhou-oc@zhaoxin.com Signed-off-by: Theodore Ts'o --- include/linux/jbd2.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/linux/jbd2.h b/include/linux/jbd2.h index 1b42fe47c26b..e3886963ba00 100644 --- a/include/linux/jbd2.h +++ b/include/linux/jbd2.h @@ -795,7 +795,7 @@ struct journal_s /** * @j_state_lock: Protect the various scalars in the journal. */ - rwlock_t j_state_lock; + rwlock_t j_state_lock ____cacheline_aligned_in_smp; /** * @j_barrier_count: From e9941268bacab2cf13cf40d115eb0d058c897cef Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:30:02 +0800 Subject: [PATCH 11/41] ext4: simplify size updating in ext4_setattr() The logic for updating the file size in ext4_setattr() is currently somewhat messy. By directly entering the error-handling path after failing to add an orphan inode, the unnecessary recovery process involving old_disksize and the file size can be avoided. Signed-off-by: Zhang Yi Reviewed-by: Jan Kara Reviewed-by: Ojaswin Mujoo Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103033.2920530-2-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/inode.c | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index 8dd3dc9e0217..1118c72ad7ad 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -6080,7 +6080,6 @@ int ext4_setattr(struct mnt_idmap *idmap, struct dentry *dentry, if (attr->ia_valid & ATTR_SIZE) { handle_t *handle; loff_t oldsize = inode->i_size; - loff_t old_disksize; int shrink = (attr->ia_size < inode->i_size); if (!(ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS))) { @@ -6164,6 +6163,8 @@ int ext4_setattr(struct mnt_idmap *idmap, struct dentry *dentry, if (ext4_handle_valid(handle) && shrink) { error = ext4_orphan_add(handle, inode); orphan = 1; + if (error) + goto out_handle; } if (shrink) @@ -6179,23 +6180,18 @@ int ext4_setattr(struct mnt_idmap *idmap, struct dentry *dentry, (attr->ia_size > 0 ? attr->ia_size - 1 : 0) >> inode->i_sb->s_blocksize_bits); - down_write(&EXT4_I(inode)->i_data_sem); - old_disksize = EXT4_I(inode)->i_disksize; - EXT4_I(inode)->i_disksize = attr->ia_size; - /* * We have to update i_size under i_data_sem together * with i_disksize to avoid races with writeback code - * running ext4_wb_update_i_disksize(). + * updating disksize in mpage_map_and_submit_extent(). */ - if (!error) - i_size_write(inode, attr->ia_size); - else - EXT4_I(inode)->i_disksize = old_disksize; + down_write(&EXT4_I(inode)->i_data_sem); + i_size_write(inode, attr->ia_size); + EXT4_I(inode)->i_disksize = attr->ia_size; up_write(&EXT4_I(inode)->i_data_sem); - rc = ext4_mark_inode_dirty(handle, inode); - if (!error) - error = rc; + + error = ext4_mark_inode_dirty(handle, inode); +out_handle: ext4_journal_stop(handle); if (error) goto out_mmap_sem; From bbe1fc8a29c052237493cd8f56ef1d81f81865dc Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:30:03 +0800 Subject: [PATCH 12/41] ext4: factor out ext4_truncate_[up|down]() Refactor ext4_setattr() by introducing two helper functions, ext4_truncate_up() and ext4_truncate_down(), to handle size changes. The current ATTR_SIZE processing consolidates checks for both shrinking and non-shrinking cases, leading to cluttered code. Separating the truncation paths improves readability. Signed-off-by: Zhang Yi Reviewed-by: Ojaswin Mujoo Reviewed-by: Jan Kara Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103033.2920530-3-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/inode.c | 199 +++++++++++++++++++++++++++--------------------- 1 file changed, 112 insertions(+), 87 deletions(-) diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index 1118c72ad7ad..c36290d8761c 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -5982,6 +5982,112 @@ 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. + */ +static inline void ext4_set_inode_size(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); + i_size_write(inode, newsize); + EXT4_I(inode)->i_disksize = newsize; + up_write(&EXT4_I(inode)->i_data_sem); +} + +static int ext4_truncate_up(struct inode *inode, loff_t oldsize, loff_t newsize) +{ + ext4_lblk_t old_lblk, new_lblk; + handle_t *handle; + int ret; + + if (!IS_ALIGNED(oldsize | newsize, i_blocksize(inode))) { + ret = ext4_inode_attach_jinode(inode); + if (ret) + return ret; + } + + inode_set_mtime_to_ts(inode, inode_set_ctime_current(inode)); + if (!IS_ALIGNED(oldsize, i_blocksize(inode))) { + ret = ext4_block_zero_eof(inode, oldsize, LLONG_MAX); + if (ret) + return ret; + } + + handle = ext4_journal_start(inode, EXT4_HT_INODE, 3); + if (IS_ERR(handle)) + return PTR_ERR(handle); + + old_lblk = oldsize > 0 ? (oldsize - 1) >> inode->i_blkbits : 0; + new_lblk = newsize > 0 ? (newsize - 1) >> inode->i_blkbits : 0; + ext4_fc_track_range(handle, inode, old_lblk, new_lblk); + + ext4_set_inode_size(inode, newsize); + + ret = ext4_mark_inode_dirty(handle, inode); + ext4_journal_stop(handle); + if (ret) + return ret; + /* + * isize extend must be called outside an active handle due to + * the lock ordering of transaction start and folio lock in the + * iomap buffered I/O path (folio lock -> transaction start). + */ + pagecache_isize_extended(inode, oldsize, newsize); + return 0; +} + +static int ext4_truncate_down(struct inode *inode, loff_t oldsize, + loff_t newsize, int *orphan) +{ + ext4_lblk_t start_lblk; + handle_t *handle; + int ret; + + /* Do not change i_size. */ + if (newsize == oldsize) + goto truncate; + + /* Shrink. */ + handle = ext4_journal_start(inode, EXT4_HT_INODE, 3); + if (IS_ERR(handle)) + return PTR_ERR(handle); + + if (ext4_handle_valid(handle)) { + ret = ext4_orphan_add(handle, inode); + *orphan = 1; + if (ret) { + ext4_journal_stop(handle); + return ret; + } + } + + start_lblk = newsize > 0 ? (newsize - 1) >> inode->i_blkbits : 0; + ext4_fc_track_range(handle, inode, start_lblk, EXT_MAX_BLOCKS - 1); + + ext4_set_inode_size(inode, newsize); + + ret = ext4_mark_inode_dirty(handle, inode); + ext4_journal_stop(handle); + if (ret) + return ret; + + if (ext4_should_journal_data(inode)) + ext4_wait_for_tail_page_commit(inode); +truncate: + /* + * Truncate pagecache after we've waited for commit in data=journal + * mode to make pages freeable. Call ext4_truncate() even if + * i_size didn't change to truncate possible preallocated blocks. + */ + truncate_pagecache(inode, newsize); + return ext4_truncate(inode); +} + /* * ext4_setattr() * @@ -6078,7 +6184,6 @@ int ext4_setattr(struct mnt_idmap *idmap, struct dentry *dentry, } if (attr->ia_valid & ATTR_SIZE) { - handle_t *handle; loff_t oldsize = inode->i_size; int shrink = (attr->ia_size < inode->i_size); @@ -6130,94 +6235,14 @@ int ext4_setattr(struct mnt_idmap *idmap, struct dentry *dentry, goto err_out; } - if (attr->ia_size != inode->i_size) { - /* attach jbd2 jinode for EOF folio tail zeroing */ - if (attr->ia_size & (inode->i_sb->s_blocksize - 1) || - oldsize & (inode->i_sb->s_blocksize - 1)) { - error = ext4_inode_attach_jinode(inode); - if (error) - goto out_mmap_sem; - } - - /* - * Update c/mtime and tail zero the EOF folio on - * truncate up. ext4_truncate() handles the shrink case - * below. - */ - if (!shrink) { - inode_set_mtime_to_ts(inode, - inode_set_ctime_current(inode)); - if (oldsize & (inode->i_sb->s_blocksize - 1)) { - error = ext4_block_zero_eof(inode, - oldsize, LLONG_MAX); - if (error) - goto out_mmap_sem; - } - } - - handle = ext4_journal_start(inode, EXT4_HT_INODE, 3); - if (IS_ERR(handle)) { - error = PTR_ERR(handle); - goto out_mmap_sem; - } - if (ext4_handle_valid(handle) && shrink) { - error = ext4_orphan_add(handle, inode); - orphan = 1; - if (error) - goto out_handle; - } - - if (shrink) - ext4_fc_track_range(handle, inode, - (attr->ia_size > 0 ? attr->ia_size - 1 : 0) >> - inode->i_sb->s_blocksize_bits, - EXT_MAX_BLOCKS - 1); - else - ext4_fc_track_range( - handle, inode, - (oldsize > 0 ? oldsize - 1 : oldsize) >> - inode->i_sb->s_blocksize_bits, - (attr->ia_size > 0 ? attr->ia_size - 1 : 0) >> - inode->i_sb->s_blocksize_bits); - - /* - * We have to update i_size under i_data_sem together - * with i_disksize to avoid races with writeback code - * updating disksize in mpage_map_and_submit_extent(). - */ - down_write(&EXT4_I(inode)->i_data_sem); - i_size_write(inode, attr->ia_size); - EXT4_I(inode)->i_disksize = attr->ia_size; - up_write(&EXT4_I(inode)->i_data_sem); - - error = ext4_mark_inode_dirty(handle, inode); -out_handle: - ext4_journal_stop(handle); - if (error) - goto out_mmap_sem; - if (!shrink) { - pagecache_isize_extended(inode, oldsize, - inode->i_size); - } else if (ext4_should_journal_data(inode)) { - ext4_wait_for_tail_page_commit(inode); - } + if (attr->ia_size > oldsize) + error = ext4_truncate_up(inode, oldsize, attr->ia_size); + else { + /* Shrink or do not change i_size. */ + error = ext4_truncate_down(inode, oldsize, + attr->ia_size, &orphan); } - /* - * Truncate pagecache after we've waited for commit - * in data=journal mode to make pages freeable. - */ - truncate_pagecache(inode, inode->i_size); - /* - * Call ext4_truncate() even if i_size didn't change to - * truncate possible preallocated blocks. - */ - if (attr->ia_size <= oldsize) { - rc = ext4_truncate(inode); - if (rc) - error = rc; - } -out_mmap_sem: filemap_invalidate_unlock(inode->i_mapping); } From 5b7a0efa8590929e44ae6b3777502314bd6a07a0 Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:30:04 +0800 Subject: [PATCH 13/41] ext4: set EXT4_MAP_NEW flag for delayed allocated blocks Set EXT4_MAP_NEW in ext4_da_map_blocks() to properly indicate that a new delayed allocation block has been inserted, allowing callers to distinguish newly created delayed extents from existing ones. Currently, the buffer_head caller, ext4_da_get_block_prep(), does not consume this flag. It intercepts EXT4_MAP_DELAYED and returns early, and unconditionally calls set_buffer_new(bh) so EXT4_MAP_NEW is not used. The flag is prepared for the iomap buffered I/O path added later. Reported-by: Ojaswin Mujoo Link: https://lore.kernel.org/linux-ext4/cc05c17d-163e-4251-b2c9-aa3a6f9555d7@huaweicloud.com/ Signed-off-by: Zhang Yi Reviewed-by: Ojaswin Mujoo Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103033.2920530-4-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/inode.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index c36290d8761c..309c656247e0 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -1990,7 +1990,7 @@ static int ext4_da_map_blocks(struct inode *inode, struct ext4_map_blocks *map) } } - map->m_flags |= EXT4_MAP_DELAYED; + map->m_flags |= EXT4_MAP_DELAYED | EXT4_MAP_NEW; retval = ext4_insert_delayed_blocks(inode, map->m_lblk, map->m_len); if (!retval) map->m_seq = READ_ONCE(EXT4_I(inode)->i_es_seq); From ecabe6bd6175dda07b68bf47ec29f4e422edd9c2 Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:30:05 +0800 Subject: [PATCH 14/41] ext4: recheck extent status tree before block allocation After acquiring i_data_sem in write mode, recheck that the mapping found via the extent status tree or disk query has not changed. A racing truncate may have trimmed the extent between the earlier lookup and the write lock acquisition, since writeback does not hold i_rwsem or the folio locks covering the full extent. This could cause ext4_map_create_blocks() to allocate blocks beyond the truncated range, potentially leading to quota leaks in the upcomming iomap buffered writeback path, because the iomap writeback infrastructure caches extents beyond the folio range. Therefore, if we find a valid extent and the sequence number has changed, retry the entire lookup to obtain the correct trimmed mapping. Note that a retry only happens when i_es_seq has actually changed, which requires another thread to hold i_data_sem in write mode, so each retry implies real lock contention, and the writer yields on the rwsem slowpath rather than spinning. Moreover, the read-write lock also tries its best to guarantee fairness, so even though no retry limit is added here, livelock should not occur in theory. Also, the retry here mirrors the existing write_ops->iomap_valid() + IOMAP_F_STALE mechanism in iomap, which also rechecks the sequence number and retries without any upper bound, and has not shown livelock problems in practice. In addition, if ext4_map_query_blocks() fails, return the error immediately. Otherwise the following sequence number comparison would use a map->m_seq that may not have been updated, and silently continuing on an error is not advisable. Suggested-by: Jan Kara Link: https://lore.kernel.org/linux-ext4/b0781809-4759-4e12-be17-71555b764f48@gmail.com/ Signed-off-by: Zhang Yi Reviewed-by: Ojaswin Mujoo Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103033.2920530-5-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/inode.c | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index 309c656247e0..86d1e027e543 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -734,6 +734,7 @@ int ext4_map_blocks(handle_t *handle, struct inode *inode, else ext4_check_map_extents_env(inode); +create_retry: /* Lookup extent status tree firstly */ if (ext4_es_lookup_extent(inode, map->m_lblk, NULL, &es, &map->m_seq)) { if (ext4_es_is_written(&es) || ext4_es_is_unwritten(&es)) { @@ -784,6 +785,8 @@ int ext4_map_blocks(handle_t *handle, struct inode *inode, down_read(&EXT4_I(inode)->i_data_sem); retval = ext4_map_query_blocks(handle, inode, map, flags); up_read((&EXT4_I(inode)->i_data_sem)); + if (retval < 0) + return retval; found: if (retval > 0 && map->m_flags & EXT4_MAP_MAPPED) { @@ -820,6 +823,21 @@ int ext4_map_blocks(handle_t *handle, struct inode *inode, * with create == 1 flag. */ down_write(&EXT4_I(inode)->i_data_sem); + + /* + * Check the validity of the mapping found via the extent status + * tree or the disk query. A racing truncate may have changed the + * extent, since writeback may not hold i_rwsem or the folio locks + * covering the full extent(e.g., the iomap writeback path may + * allocate an extent that extends beyond the length of the locked + * folio in advance). + */ + if (map->m_seq != READ_ONCE(EXT4_I(inode)->i_es_seq)) { + up_write(&EXT4_I(inode)->i_data_sem); + map->m_flags = 0; + map->m_len = orig_mlen; + goto create_retry; + } retval = ext4_map_create_blocks(handle, inode, map, flags); up_write((&EXT4_I(inode)->i_data_sem)); From 3ec0eefc07cb9e2bb1193f1fa3d8157302a737f5 Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:30:06 +0800 Subject: [PATCH 15/41] ext4: fix orig_mlen initialization in ext4_map_blocks() Save orig_mlen after clamping map->m_len to INT_MAX. Otherwise, the unclamped value may be passed below, bypassing its overflow protection. Fixes: 5bb12b1837c0 ("ext4: Add support for EXT4_GET_BLOCKS_QUERY_LEAF_BLOCKS") Signed-off-by: Zhang Yi Reviewed-by: Ojaswin Mujoo Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103033.2920530-6-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/inode.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index 86d1e027e543..c861d4683bcd 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -703,7 +703,7 @@ int ext4_map_blocks(handle_t *handle, struct inode *inode, struct extent_status es; int retval; int ret = 0; - unsigned int orig_mlen = map->m_len; + unsigned int orig_mlen; #ifdef ES_AGGRESSIVE_TEST struct ext4_map_blocks orig_map; @@ -719,6 +719,7 @@ int ext4_map_blocks(handle_t *handle, struct inode *inode, */ if (unlikely(map->m_len > INT_MAX)) map->m_len = INT_MAX; + orig_mlen = map->m_len; /* We can handle the block number less than EXT_MAX_BLOCKS */ if (unlikely(map->m_lblk >= EXT_MAX_BLOCKS)) From 6585cea3cf58b8e8bbfb2a7befecb7ceeb0792ef Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:30:07 +0800 Subject: [PATCH 16/41] ext4: allow ext4_map_blocks() to start its own transaction handle Make ext4_map_blocks() start its own transaction handle when the caller does not provide one. The handle is started after the lookup path confirms that allocation is actually needed, and is stopped at the unified out exit path. This avoids unnecessarily starting a handle for pure mapping queries. This prepares for the buffered iomap writeback conversion, which improves performance for fragile overwrite cases. Suggested-by: Jan Kara Link: https://lore.kernel.org/linux-ext4/i536qqwj5eyulec3r2ki2ycnelqdd4bkpat2drn7t72t6p622k@ktueynysgo3j/ Signed-off-by: Zhang Yi Reviewed-by: Ojaswin Mujoo Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103033.2920530-7-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/inode.c | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index c861d4683bcd..0109fb6b60b7 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -686,6 +686,11 @@ int ext4_map_create_blocks(handle_t *handle, struct inode *inode, * Otherwise, call with ext4_ind_map_blocks() to handle indirect mapping * based files * + * ext4_map_blocks() may also be called with a NULL @handle. In that case + * it starts its own transaction handle only when block allocation is + * actually needed, i.e. after the lookup confirms the blocks are not yet + * mapped, and stops it before returning. + * * On success, it returns the number of blocks being mapped or allocated. * If flags doesn't contain EXT4_GET_BLOCKS_CREATE the blocks are * pre-allocated and unwritten, the resulting @map is marked as unwritten. @@ -703,6 +708,7 @@ int ext4_map_blocks(handle_t *handle, struct inode *inode, struct extent_status es; int retval; int ret = 0; + bool internal_handle = false; unsigned int orig_mlen; #ifdef ES_AGGRESSIVE_TEST struct ext4_map_blocks orig_map; @@ -787,13 +793,13 @@ int ext4_map_blocks(handle_t *handle, struct inode *inode, retval = ext4_map_query_blocks(handle, inode, map, flags); up_read((&EXT4_I(inode)->i_data_sem)); if (retval < 0) - return retval; + goto out; found: if (retval > 0 && map->m_flags & EXT4_MAP_MAPPED) { ret = check_block_validity(inode, map); if (ret != 0) - return ret; + goto out; } /* If it is only a block(s) look up */ @@ -813,8 +819,17 @@ int ext4_map_blocks(handle_t *handle, struct inode *inode, * ext4_ext_map_blocks() */ if (!(flags & EXT4_GET_BLOCKS_CONVERT_UNWRITTEN)) - return retval; + goto out; + if (!handle) { + handle = ext4_journal_start(inode, EXT4_HT_MAP_BLOCKS, + ext4_chunk_trans_blocks(inode, orig_mlen)); + if (IS_ERR(handle)) { + ret = PTR_ERR(handle); + goto out; + } + internal_handle = true; + } ext4_fc_track_inode(handle, inode); /* @@ -845,12 +860,12 @@ int ext4_map_blocks(handle_t *handle, struct inode *inode, if (retval < 0) ext_debug(inode, "failed with err %d\n", retval); if (retval <= 0) - return retval; + goto out; if (map->m_flags & EXT4_MAP_MAPPED) { ret = check_block_validity(inode, map); if (ret != 0) - return ret; + goto out; /* * Inodes with freshly allocated blocks where contents will be @@ -872,12 +887,16 @@ int ext4_map_blocks(handle_t *handle, struct inode *inode, ret = ext4_jbd2_inode_add_write(handle, inode, start_byte, length); if (ret) - return ret; + goto out; } } ext4_fc_track_range(handle, inode, map->m_lblk, map->m_lblk + map->m_len - 1); - return retval; +out: + if (internal_handle) + ext4_journal_stop(handle); + + return ret ? ret : retval; } /* From 7f373083b45757b19f36a939f255239d40ccfb24 Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:30:08 +0800 Subject: [PATCH 17/41] ext4: avoid unnecessary transaction in ext4_map_blocks() for unwritten extents When ext4_map_blocks() finds an unwritten extent in the extent cache and the caller is willing to accept unwritten extents without conversion, there is no need to start a journal transaction since no metadata update is required. This avoids unnecessary transaction overhead in the upcoming iomap writeback path when overwriting already-allocated unwritten extents. One thing to be careful about, as the comment in ext4_map_blocks() states, if the flags contain EXT4_GET_BLOCKS_CREATE, the function will mark @map as mapped. Signed-off-by: Zhang Yi Reviewed-by: Ojaswin Mujoo Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103033.2920530-8-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/inode.c | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index 0109fb6b60b7..6a00e0fe48bc 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -812,14 +812,25 @@ int ext4_map_blocks(handle_t *handle, struct inode *inode, * Note that if blocks have been preallocated * ext4_ext_map_blocks() returns with buffer head unmapped */ - if (retval > 0 && map->m_flags & EXT4_MAP_MAPPED) + if (retval > 0) { /* - * If we need to convert extent to unwritten - * we continue and do the actual work in - * ext4_ext_map_blocks() + * If we need to convert written extent to unwritten or + * convert unwritten extent to written, continue and do + * the actual work in ext4_ext_map_blocks(). */ - if (!(flags & EXT4_GET_BLOCKS_CONVERT_UNWRITTEN)) + if (map->m_flags & EXT4_MAP_MAPPED && + !(flags & EXT4_GET_BLOCKS_CONVERT_UNWRITTEN)) goto out; + if (map->m_flags & EXT4_MAP_UNWRITTEN && + (flags & EXT4_GET_BLOCKS_UNWRIT_EXT) && + !(flags & EXT4_GET_BLOCKS_CONVERT)) { + /* Contains EXT4_GET_BLOCKS_CREATE - mark mapped. */ + ret = check_block_validity(inode, map); + if (!ret) + map->m_flags |= EXT4_MAP_MAPPED; + goto out; + } + } if (!handle) { handle = ext4_journal_start(inode, EXT4_HT_MAP_BLOCKS, From 244392197bb95955586fbb47f068b2432ed81317 Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:30:09 +0800 Subject: [PATCH 18/41] ext4: skip block allocation for holes in the data submission path 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 Reviewed-by: Ojaswin Mujoo Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103033.2920530-9-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/inode.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index 6a00e0fe48bc..89f8246ae540 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)) { From a304299a40d92c668c5983d57edc512e4e878686 Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:30:10 +0800 Subject: [PATCH 19/41] ext4: add iomap address space operations for buffered I/O Introduce initial support for iomap in the buffered I/O path for regular files on ext4. - Add a new inode state flag EXT4_STATE_BUFFERED_IOMAP to indicate the inode uses iomap instead of buffer_head for buffered I/O - Add helper ext4_inode_buffered_iomap() to check the flag - Add new address space operations ext4_iomap_aops with callbacks that will use generic iomap implementations - Add ext4_iomap_aops to ext4_set_aops() when the flag is set The following callbacks(read_folio(), readahead(), writepages()) are provided as placeholders and will be implemented in later patches. Signed-off-by: Zhang Yi Reviewed-by: Jan Kara Reviewed-by: Ojaswin Mujoo Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103033.2920530-10-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/ext4.h | 7 +++++++ fs/ext4/inode.c | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h index 9d06ddd865d4..04aa05e26897 100644 --- a/fs/ext4/ext4.h +++ b/fs/ext4/ext4.h @@ -2049,6 +2049,7 @@ enum { EXT4_STATE_FC_FLUSHING_DATA, /* Fast commit flushing data */ EXT4_STATE_ORPHAN_FILE, /* Inode orphaned in orphan file */ EXT4_STATE_FC_REQUEUE, /* Inode modified during fast commit */ + EXT4_STATE_BUFFERED_IOMAP, /* Inode use iomap for buffered IO */ }; #define EXT4_INODE_BIT_FNS(name, field, offset) \ @@ -2148,6 +2149,12 @@ static inline struct mapping_metadata_bhs *ext4_i_metadata_bhs( return READ_ONCE(EXT4_I(inode)->i_metadata_bhs); } +/* Whether the inode pass through the iomap infrastructure for buffered I/O */ +static inline bool ext4_inode_buffered_iomap(struct inode *inode) +{ + return ext4_test_inode_state(inode, EXT4_STATE_BUFFERED_IOMAP); +} + /* * Codes for operating systems */ diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index 89f8246ae540..0c9163e47d15 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -3969,6 +3969,22 @@ const struct iomap_ops ext4_iomap_report_ops = { .iomap_next = ext4_iomap_next_report, }; +static int ext4_iomap_read_folio(struct file *file, struct folio *folio) +{ + return 0; +} + +static void ext4_iomap_readahead(struct readahead_control *rac) +{ + +} + +static int ext4_iomap_writepages(struct address_space *mapping, + struct writeback_control *wbc) +{ + return 0; +} + /* * For data=journal mode, folio should be marked dirty only when it was * writeably mapped. When that happens, it was already attached to the @@ -4055,6 +4071,20 @@ static const struct address_space_operations ext4_da_aops = { .swap_activate = ext4_iomap_swap_activate, }; +static const struct address_space_operations ext4_iomap_aops = { + .read_folio = ext4_iomap_read_folio, + .readahead = ext4_iomap_readahead, + .writepages = ext4_iomap_writepages, + .dirty_folio = iomap_dirty_folio, + .bmap = ext4_bmap, + .invalidate_folio = iomap_invalidate_folio, + .release_folio = iomap_release_folio, + .migrate_folio = filemap_migrate_folio, + .is_partially_uptodate = iomap_is_partially_uptodate, + .error_remove_folio = generic_error_remove_folio, + .swap_activate = ext4_iomap_swap_activate, +}; + static const struct address_space_operations ext4_dax_aops = { .writepages = ext4_dax_writepages, .dirty_folio = noop_dirty_folio, @@ -4076,6 +4106,8 @@ void ext4_set_aops(struct inode *inode) } if (IS_DAX(inode)) inode->i_mapping->a_ops = &ext4_dax_aops; + else if (ext4_inode_buffered_iomap(inode)) + inode->i_mapping->a_ops = &ext4_iomap_aops; else if (test_opt(inode->i_sb, DELALLOC)) inode->i_mapping->a_ops = &ext4_da_aops; else From 331838d93b65f4bcac2ffec8e3da639847fa241c Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:30:11 +0800 Subject: [PATCH 20/41] ext4: implement buffered read path using iomap Implement the iomap read path for ext4 by introducing a new ext4_iomap_buffered_read_ops instance. This provides the read_folio() and readahead() callbacks for ext4_iomap_aops. The implementation introduces: - ext4_iomap_map_blocks(): Helper function to query extent mappings for a given read range using ext4_map_blocks() and convert the mapping information to iomap type - ext4_iomap_buffered_read_begin(): The iomap_begin callbacks that maps blocks, validates filesystem state, and populates the iomap. It returns -ERANGE for inline data which is not yet supported. Signed-off-by: Zhang Yi Reviewed-by: Jan Kara Reviewed-by: Ojaswin Mujoo Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103033.2920530-11-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/inode.c | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index 0c9163e47d15..f96f3576629c 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -3969,14 +3969,60 @@ const struct iomap_ops ext4_iomap_report_ops = { .iomap_next = ext4_iomap_next_report, }; +static int ext4_iomap_map_blocks(struct inode *inode, loff_t offset, + loff_t length, struct ext4_map_blocks *map) +{ + u8 blkbits = inode->i_blkbits; + + if ((offset >> blkbits) > EXT4_MAX_LOGICAL_BLOCK) + return -EINVAL; + + /* Calculate the first and last logical blocks respectively. */ + map->m_lblk = offset >> blkbits; + map->m_len = min_t(loff_t, (offset + length - 1) >> blkbits, + EXT4_MAX_LOGICAL_BLOCK) - map->m_lblk + 1; + + return ext4_map_blocks(NULL, inode, map, 0); +} + +static int ext4_iomap_buffered_read_begin(struct inode *inode, loff_t offset, + loff_t length, unsigned int flags, struct iomap *iomap, + struct iomap *srcmap) +{ + struct ext4_map_blocks map; + int ret; + + if (unlikely(ext4_forced_shutdown(inode->i_sb))) + return -EIO; + + /* Inline data support is not yet available. */ + if (WARN_ON_ONCE(ext4_has_inline_data(inode))) + return -ERANGE; + + ret = ext4_iomap_map_blocks(inode, offset, length, &map); + if (ret < 0) + return ret; + + ext4_set_iomap(inode, iomap, &map, offset, length, flags); + return 0; +} + +static DEFINE_IOMAP_ITER_NEXT(ext4_iomap_buffered_read_next, + ext4_iomap_buffered_read_begin); + +static const struct iomap_ops ext4_iomap_buffered_read_ops = { + .iomap_next = ext4_iomap_buffered_read_next, +}; + static int ext4_iomap_read_folio(struct file *file, struct folio *folio) { + iomap_bio_read_folio(folio, &ext4_iomap_buffered_read_ops); return 0; } static void ext4_iomap_readahead(struct readahead_control *rac) { - + iomap_bio_readahead(rac, &ext4_iomap_buffered_read_ops); } static int ext4_iomap_writepages(struct address_space *mapping, From 935442e001c2570e738cad776662d1c0558cbb52 Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:30:12 +0800 Subject: [PATCH 21/41] ext4: pass out extent seq counter when mapping da blocks The iomap buffered write path does not hold the folio lock between mapping the inode extent and copying data. Therefore, it can race with writeback that modifies the extent type (e.g., from unwritten to written). This can lead to data corruption on partial writes, as iomap_block_needs_zeroing() may return a false positive based on a stale extent. The iomap infrastructure uses the sequence counter stored in the inode to detect such stale mappings. Commit 07c440e8da8f ("ext4: pass out extent seq counter when mapping blocks") added the m_seq field to ext4_map_blocks to pass out extent sequence numbers, but it missed two callsites within ext4_da_map_blocks(). These callsites are on the delayed allocation path, which is needed in the iomap buffered write path. Pass out the sequence counter to ensure stale mappings can be detected. Signed-off-by: Zhang Yi Reviewed-by: Jan Kara Reviewed-by: Ojaswin Mujoo Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103033.2920530-12-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/inode.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index f96f3576629c..e96a65b8d478 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -1977,7 +1977,7 @@ static int ext4_da_map_blocks(struct inode *inode, struct ext4_map_blocks *map) ext4_check_map_extents_env(inode); /* Lookup extent status tree firstly */ - if (ext4_es_lookup_extent(inode, map->m_lblk, NULL, &es, NULL)) { + if (ext4_es_lookup_extent(inode, map->m_lblk, NULL, &es, &map->m_seq)) { map->m_len = min_t(unsigned int, map->m_len, es.es_len - (map->m_lblk - es.es_lblk)); @@ -2030,7 +2030,7 @@ static int ext4_da_map_blocks(struct inode *inode, struct ext4_map_blocks *map) * is held in write mode, before inserting a new da entry in * the extent status tree. */ - if (ext4_es_lookup_extent(inode, map->m_lblk, NULL, &es, NULL)) { + if (ext4_es_lookup_extent(inode, map->m_lblk, NULL, &es, &map->m_seq)) { map->m_len = min_t(unsigned int, map->m_len, es.es_len - (map->m_lblk - es.es_lblk)); From 3e28cc4432fe59f0957de775fe4903a860f7b6ef Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:30:13 +0800 Subject: [PATCH 22/41] ext4: do not use data=ordered mode for inodes using buffered iomap path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The data=ordered mode introduces two fundamental conflicts with the iomap buffered write path, leading to potential deadlocks. 1) Lock ordering conflict In the iomap writeback path, each folio is processed sequentially: the folio lock is acquired first, followed by starting a transaction to create block mappings. In data=ordered mode, writeback triggered by the journal commit process may attempt to acquire a folio lock that is already held by iomap background writeback process. Meanwhile, iomap, under that same folio lock, may start a new transaction to map other blocks on this folio and wait for the currently committing transaction to finish, resulting in a deadlock. Trans N commit background writeback(via iomap) journal_submit_data_buffers() ext4_journal_submit_inode_data_buffers() iomap_writepages() iomap_writepages() folio_lock() folio_lock() -- wait iomap_writeback_folio() iomap_writeback_range() ext4_journal_start() start new transaction -- wait for trans N commit, DEADLOCK ext4_map_blocks() Currently, in the buffer_head writeback path, this is handled by starting the transaction before taking any folio locks for writeback. 2) Partial folio submission not supported When block size < folio size, a folio may contain both mapped and unmapped blocks. In data=ordered mode, a deadlock can occur if the journal waits (pure JI_WAIT_DATA) for such a folio to be written back while background writeback has already started on it (with the writeback flag set). The problem is that mapping the remaining delalloc blocks can deadlock because the writeback flag is not cleared until the entire folio is processed and committed. T0: Assume we have a folio contains four blocks, from front to back, they are A, B, C, D. The block B and C are holes, and the last block D is written in delalloc mode (the block is not allocated yet). T1: The background writeback process starts to write back data, set writeback flag on the folio, allocates block D, and adds it to transaction N's order list of jbd2 in pure JI_WAIT_DATA mode. T2: This folio completes the writeback and clears the writeback flag. T3: Before transaction N commit, we buffered write block A to C. T4: Transaction N commit and folio writeback are running concurrently. Trans N commit background writeback(via iomap) iomap_writeback_folio() folio_start_writeback() -- set writeback flag jbd2_journal_finish_inode_data_buffers() __filemap_fdatawait_range() -- wait writeback flag to clear iomap_writeback_range() ext4_journal_start() start new transaction -- wait for trans N commit, DEADLOCK ext4_map_block() (B, C) Currently, in the buffer_head writeback path, this is handled by: 1. Partial folio submission — already-allocated buffers can be submitted first. The writeback flag is cleared after I/O completes, preventing block allocation while the writeback flag is set. 2. Allocation order — the transaction is started first, then blocks are allocated, the writeback flag is set, and finally the allocated buffers submission begins. To support data=ordered mode, the iomap core would need two invasive changes: - Acquire the transaction handle before locking any folio for writeback. - Support partial folio submission. Both changes are complicated and risk performance regressions. Therefore, we must avoid using data=ordered mode when converting to the iomap path. Currently, data=ordered mode is used in three scenarios: - Append write - Post-EOF partial block truncate-up followed by append write - Online defragmentation We can address the first two without data=ordered mode: - For append write: always allocate unwritten blocks (i.e. always enable dioread_nolock), preserving the behavior of current extent-type inodes. - For post-EOF truncate-up + append write: postpone updating i_disksize until after the zeroed partial block has been written back. Online defragmentation does not yet support iomap; this can be resolved separately in the future. Signed-off-by: Zhang Yi Reviewed-by: Jan Kara Reviewed-by: Ojaswin Mujoo Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103033.2920530-13-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/ext4_jbd2.h | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/fs/ext4/ext4_jbd2.h b/fs/ext4/ext4_jbd2.h index 2fbf48b3dfe2..be54e93bde0b 100644 --- a/fs/ext4/ext4_jbd2.h +++ b/fs/ext4/ext4_jbd2.h @@ -379,7 +379,12 @@ static inline int ext4_should_journal_data(struct inode *inode) static inline int ext4_should_order_data(struct inode *inode) { - return ext4_inode_journal_mode(inode) & EXT4_INODE_ORDERED_DATA_MODE; + /* + * inodes using the iomap buffered I/O path do not use the + * data=ordered mode. + */ + return !ext4_inode_buffered_iomap(inode) && + (ext4_inode_journal_mode(inode) & EXT4_INODE_ORDERED_DATA_MODE); } static inline int ext4_should_writeback_data(struct inode *inode) From 0a6f584e91f4a65f54d4d4c4876e3e6e12977955 Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:30:14 +0800 Subject: [PATCH 23/41] ext4: implement buffered write path using iomap Introduce two new iomap_ops instances for ext4 buffered writes: - ext4_iomap_buffered_da_write_ops: for delayed allocation mode, using ext4_da_map_blocks() to map delalloc extents. - ext4_iomap_buffered_write_ops: for non-delayed allocation mode, using ext4_map_blocks() to directly allocate blocks. Also add ext4_iomap_valid() for the iomap infrastructure to check extent validity. This is useful for the iomap buffered write path because it does not hold the folio lock between mapping the inode extent and copying data. So it can race with writeback, which may modify or stale the extent type. Key changes and considerations: - Unwritten extents for new blocks (dioread_nolock always on) Since data=ordered mode is not used to prevent stale data exposure in the non-delayed allocation path, new blocks are always allocated as unwritten extents. - Short write and write failure handling a. Delalloc path: On short write or failure, the stale delalloc range must be dropped and its space reservation released. Otherwise, a clean folio may cover leftover delalloc extents, causing inaccurate space reservation accounting. b. Non-delalloc path: No cleanup of allocated blocks is needed on short write since leaving the preallocated blocks behind doesn't cause any real issue. These blocks are visible to users and are not permanently leaked. - Adjust the call frequency of ext4_nonda_switch() so that it is no longer called once per folio in the buffer_head path, but instead once per write. For code simplicity, ext4_iomap_buffered_da_write_ops will be shared with the later page-fault path. Since ext4_nonda_switch() -> try_to_writeback_inodes_sb() synchronously waits for writeback, which could lead to deadlock under the same folio lock acquired by iomap_page_mkwrite(), we can no longer call ext4_nonda_switch() inside ext4_iomap_buffered_da_write_ops. This may increase the probability of a writeback-time allocation failure, but the expected real-world impact is small, so it can be implemented this way for now. - Lock ordering reversal The folio lock and transaction start ordering is reversed compared to the buffer_head buffered write path. To handle this, the journal handle must be stopped in iomap_begin() callbacks. The lock ordering documentation in super.c has been updated accordingly. Signed-off-by: Zhang Yi Reviewed-by: Ojaswin Mujoo Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103033.2920530-14-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/ext4.h | 4 ++ fs/ext4/file.c | 20 +++++++- fs/ext4/inode.c | 130 ++++++++++++++++++++++++++++++++++++++++++++++-- fs/ext4/super.c | 10 ++-- 4 files changed, 156 insertions(+), 8 deletions(-) diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h index 04aa05e26897..c7f2fd60a1ab 100644 --- a/fs/ext4/ext4.h +++ b/fs/ext4/ext4.h @@ -3165,6 +3165,7 @@ int ext4_walk_page_buffers(handle_t *handle, int do_journal_get_write_access(handle_t *handle, struct inode *inode, struct buffer_head *bh); void ext4_set_inode_mapping_order(struct inode *inode); +int ext4_nonda_switch(struct super_block *sb); #define FALL_BACK_TO_NONDELALLOC 1 #define EXT4_WRITE_DATA_INLINE 2 @@ -4046,6 +4047,9 @@ static inline void ext4_clear_io_unwritten_flag(ext4_io_end_t *io_end) extern const struct iomap_ops ext4_iomap_ops; extern const struct iomap_ops ext4_iomap_report_ops; +extern const struct iomap_ops ext4_iomap_buffered_write_ops; +extern const struct iomap_ops ext4_iomap_buffered_da_write_ops; +extern const struct iomap_write_ops ext4_iomap_write_ops; int ext4_iomap_begin(struct inode *inode, loff_t offset, loff_t length, unsigned flags, struct iomap *iomap, struct iomap *srcmap); diff --git a/fs/ext4/file.c b/fs/ext4/file.c index 374b4bc25bd5..50d3c92709c8 100644 --- a/fs/ext4/file.c +++ b/fs/ext4/file.c @@ -330,6 +330,21 @@ static ssize_t ext4_write_checks(struct kiocb *iocb, struct iov_iter *from) return count; } +static ssize_t ext4_iomap_buffered_write(struct kiocb *iocb, + struct iov_iter *from) +{ + struct inode *inode = file_inode(iocb->ki_filp); + const struct iomap_ops *iomap_ops; + + if (test_opt(inode->i_sb, DELALLOC) && !ext4_nonda_switch(inode->i_sb)) + iomap_ops = &ext4_iomap_buffered_da_write_ops; + else + iomap_ops = &ext4_iomap_buffered_write_ops; + + return iomap_file_buffered_write(iocb, from, iomap_ops, + &ext4_iomap_write_ops, NULL); +} + static ssize_t ext4_buffered_write_iter(struct kiocb *iocb, struct iov_iter *from) { @@ -351,7 +366,10 @@ static ssize_t ext4_buffered_write_iter(struct kiocb *iocb, if (ret <= 0) goto out; - ret = generic_perform_write(iocb, from); + if (ext4_inode_buffered_iomap(inode)) + ret = ext4_iomap_buffered_write(iocb, from); + else + ret = generic_perform_write(iocb, from); out: inode_unlock(inode); diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index e96a65b8d478..2d2db4207438 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -3149,7 +3149,7 @@ static int ext4_dax_writepages(struct address_space *mapping, return ret; } -static int ext4_nonda_switch(struct super_block *sb) +int ext4_nonda_switch(struct super_block *sb) { s64 free_clusters, dirty_clusters; struct ext4_sb_info *sbi = EXT4_SB(sb); @@ -3521,6 +3521,15 @@ static bool ext4_inode_datasync_dirty(struct inode *inode) return inode_state_read_once(inode) & I_DIRTY_DATASYNC; } +static bool ext4_iomap_valid(struct inode *inode, const struct iomap *iomap) +{ + return iomap->validity_cookie == READ_ONCE(EXT4_I(inode)->i_es_seq); +} + +const struct iomap_write_ops ext4_iomap_write_ops = { + .iomap_valid = ext4_iomap_valid, +}; + static void ext4_set_iomap(struct inode *inode, struct iomap *iomap, struct ext4_map_blocks *map, loff_t offset, loff_t length, unsigned int flags) @@ -3555,6 +3564,8 @@ static void ext4_set_iomap(struct inode *inode, struct iomap *iomap, !ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS)) iomap->flags |= IOMAP_F_MERGED; + iomap->validity_cookie = map->m_seq; + /* * Flags passed to ext4_map_blocks() for direct I/O writes can result * in m_flags having both EXT4_MAP_MAPPED and EXT4_MAP_UNWRITTEN bits @@ -3970,7 +3981,8 @@ const struct iomap_ops ext4_iomap_report_ops = { }; static int ext4_iomap_map_blocks(struct inode *inode, loff_t offset, - loff_t length, struct ext4_map_blocks *map) + loff_t length, struct ext4_map_blocks *map, + int flags) { u8 blkbits = inode->i_blkbits; @@ -3982,7 +3994,10 @@ static int ext4_iomap_map_blocks(struct inode *inode, loff_t offset, map->m_len = min_t(loff_t, (offset + length - 1) >> blkbits, EXT4_MAX_LOGICAL_BLOCK) - map->m_lblk + 1; - return ext4_map_blocks(NULL, inode, map, 0); + if (flags & EXT4_GET_BLOCKS_DELALLOC_RESERVE) + return ext4_da_map_blocks(inode, map); + + return ext4_map_blocks(NULL, inode, map, flags); } static int ext4_iomap_buffered_read_begin(struct inode *inode, loff_t offset, @@ -3999,7 +4014,7 @@ static int ext4_iomap_buffered_read_begin(struct inode *inode, loff_t offset, if (WARN_ON_ONCE(ext4_has_inline_data(inode))) return -ERANGE; - ret = ext4_iomap_map_blocks(inode, offset, length, &map); + ret = ext4_iomap_map_blocks(inode, offset, length, &map, 0); if (ret < 0) return ret; @@ -4007,6 +4022,113 @@ static int ext4_iomap_buffered_read_begin(struct inode *inode, loff_t offset, return 0; } +static int ext4_iomap_buffered_do_write_begin(struct inode *inode, + loff_t offset, loff_t length, unsigned int flags, + struct iomap *iomap, struct iomap *srcmap, bool delalloc) +{ + int ret, retries = 0; + struct ext4_map_blocks map; + int map_flags; + + ret = ext4_emergency_state(inode->i_sb); + if (unlikely(ret)) + return ret; + + /* Inline data and non-extent are not supported. */ + if (WARN_ON_ONCE(ext4_has_inline_data(inode))) + return -ERANGE; + if (WARN_ON_ONCE(!ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS))) + return -EINVAL; + if (WARN_ON_ONCE(!(flags & IOMAP_WRITE))) + return -EINVAL; + + map_flags = delalloc ? EXT4_GET_BLOCKS_DELALLOC_RESERVE : + EXT4_GET_BLOCKS_CREATE_UNWRIT_EXT; +retry: + ret = ext4_iomap_map_blocks(inode, offset, length, &map, map_flags); + if (ret == -ENOSPC && ext4_should_retry_alloc(inode->i_sb, &retries)) + goto retry; + if (ret < 0) + return ret; + + ext4_set_iomap(inode, iomap, &map, offset, length, flags); + return 0; +} + +static int ext4_iomap_buffered_write_begin(struct inode *inode, + loff_t offset, loff_t length, unsigned int flags, + struct iomap *iomap, struct iomap *srcmap) +{ + return ext4_iomap_buffered_do_write_begin(inode, offset, length, flags, + iomap, srcmap, false); +} + +static int ext4_iomap_buffered_da_write_begin(struct inode *inode, + loff_t offset, loff_t length, unsigned int flags, + struct iomap *iomap, struct iomap *srcmap) +{ + return ext4_iomap_buffered_do_write_begin(inode, offset, length, flags, + iomap, srcmap, true); +} + +/* + * On write failure, drop the stale delayed allocation range and release + * its reserved space for both start and end blocks. Otherwise, we may + * leave a range of delayed extents covered by a clean folio, which can + * result in inaccurate space reservation accounting. + */ +static void ext4_iomap_punch_delalloc(struct inode *inode, loff_t offset, + loff_t length, struct iomap *iomap) +{ + down_write(&EXT4_I(inode)->i_data_sem); + ext4_es_remove_extent(inode, offset >> inode->i_blkbits, + DIV_ROUND_UP_ULL(length, EXT4_BLOCK_SIZE(inode->i_sb))); + up_write(&EXT4_I(inode)->i_data_sem); +} + +static int ext4_iomap_buffered_da_write_end(struct inode *inode, loff_t offset, + loff_t length, ssize_t written, + unsigned int flags, + struct iomap *iomap) +{ + loff_t start_byte, end_byte; + + /* If we didn't reserve the blocks, we're not allowed to punch them. */ + if (iomap->type != IOMAP_DELALLOC || !(iomap->flags & IOMAP_F_NEW)) + return 0; + + /* Nothing to do if we've written the entire delalloc extent */ + start_byte = iomap_last_written_block(inode, offset, written); + end_byte = round_up(offset + length, i_blocksize(inode)); + if (start_byte >= end_byte) + return 0; + + filemap_invalidate_lock(inode->i_mapping); + iomap_write_delalloc_release(inode, start_byte, end_byte, flags, + iomap, ext4_iomap_punch_delalloc); + filemap_invalidate_unlock(inode->i_mapping); + return 0; +} + +/* + * Since we always allocate unwritten extents, there is no need for + * iomap_end to clean up allocated blocks on a short write. + */ +static DEFINE_IOMAP_ITER_NEXT(ext4_iomap_buffered_write_next, + ext4_iomap_buffered_write_begin); + +const struct iomap_ops ext4_iomap_buffered_write_ops = { + .iomap_next = ext4_iomap_buffered_write_next, +}; + +static DEFINE_IOMAP_ITER_NEXT_END(ext4_iomap_buffered_da_write_next, + ext4_iomap_buffered_da_write_begin, + ext4_iomap_buffered_da_write_end); + +const struct iomap_ops ext4_iomap_buffered_da_write_ops = { + .iomap_next = ext4_iomap_buffered_da_write_next, +}; + static DEFINE_IOMAP_ITER_NEXT(ext4_iomap_buffered_read_next, ext4_iomap_buffered_read_begin); diff --git a/fs/ext4/super.c b/fs/ext4/super.c index fbd808f54054..22738ed3d3aa 100644 --- a/fs/ext4/super.c +++ b/fs/ext4/super.c @@ -104,9 +104,13 @@ static const struct fs_parameter_spec ext4_param_specs[]; * -> page lock -> i_data_sem (rw) * * buffered write path: - * sb_start_write -> i_mutex -> mmap_lock - * sb_start_write -> i_mutex -> transaction start -> page lock -> - * i_data_sem (rw) + * sb_start_write -> i_rwsem (w) -> mmap_lock + * - buffer_head path: + * sb_start_write -> i_rwsem (w) -> transaction start -> folio lock -> + * i_data_sem (rw) + * - iomap path: + * sb_start_write -> i_rwsem (w) -> transaction start -> i_data_sem (rw) + * sb_start_write -> i_rwsem (w) -> folio lock (not under an active handle) * * truncate: * sb_start_write -> i_mutex -> invalidate_lock (w) -> i_mmap_rwsem (w) -> From 10660a0ca7ed601df7f2708136ca529cb3492920 Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:30:15 +0800 Subject: [PATCH 24/41] ext4: rework handle credit accounting for unwritten extent conversion While converting the buffered I/O path to iomap, we noticed that the writeback completion can hand over an ioend which was merged across an arbitrary number of extents because iomap_ioend_try_merge() doesn't limit the length of the merged ioends, while ext4_convert_unwritten_extents() converts all of them with the handle provided by the caller, and callers calculate the credits of that handle without knowing the exact number of extents. Converting more extents than that within one transaction can exhaust its credits. Prepare for the iomap path nevertheless by making the conversion loop check the remaining credits before each extent with ext4_journal_ensure_credits(), so that the transaction is extended, or restarted once it is full, as needed. Also estimate the credits with ext4_meta_trans_blocks(..., alloc_extents = 0), since no data blocks are allocated here, and manage an internally started transaction as a whole instead of starting and stopping one for each extent. The buffer_head writeback path is safe, as it passes a handle whose credits are reserved by ext4_meta_trans_blocks(inode, bpf, bpf, 0), which is enough to convert one extent per block of a folio, so the reservation is sufficient and the handle is never restarted there. This also helps the fallocate path, where ext4_alloc_file_blocks() sizes the handle for the conversion with ext4_chunk_trans_blocks() as well, i.e. for a single extent only, while a concurrent modification of the extent tree can make the conversion cover more extents than the handle has credits for. Signed-off-by: Zhang Yi Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103033.2920530-15-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/extents.c | 53 ++++++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index 5409b4c07c3c..30fa1ce09f0c 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -5137,27 +5137,36 @@ int ext4_convert_unwritten_extents(handle_t *handle, struct inode *inode, int ret = 0, ret2 = 0, ret3 = 0; struct ext4_map_blocks map; unsigned int blkbits = inode->i_blkbits; - unsigned int credits = 0; + unsigned int credits; + bool internal_handle = !handle; map.m_lblk = offset >> blkbits; map.m_len = max_blocks = EXT4_MAX_BLOCKS(len, offset, blkbits); - if (!handle) { - /* - * credits to insert 1 extent into extent tree - */ - credits = ext4_chunk_trans_blocks(inode, max_blocks); + /* Credits to convert one extent in this range to written state. */ + credits = ext4_meta_trans_blocks(inode, max_blocks, 1, 0); + + if (internal_handle) { + handle = ext4_journal_start(inode, EXT4_HT_MAP_BLOCKS, credits); + if (IS_ERR(handle)) { + ret = PTR_ERR(handle); + goto out; + } } while (max_blocks) { - if (credits) { - handle = ext4_journal_start(inode, EXT4_HT_MAP_BLOCKS, - credits); - if (IS_ERR(handle)) { - ret = PTR_ERR(handle); - break; - } - } + /* + * The caller cannot know how many extents the range covers, + * so make sure the transaction can take one more extent, + * extending or restarting it once it is full. The reserved + * handle of the buffer_head writeback path is safe as it has + * enough credits for the conversion, so it is not restarted + * here. + */ + ret = ext4_journal_ensure_credits(handle, credits, 0); + if (ret < 0) + break; + /* * Do not cache any unrelated extents, as it does not hold the * i_rwsem or invalidate_lock, which could corrupt the extent @@ -5177,11 +5186,6 @@ int ext4_convert_unwritten_extents(handle_t *handle, struct inode *inode, } ret2 = ext4_mark_inode_dirty(handle, inode); - if (credits) { - ret3 = ext4_journal_stop(handle); - if (unlikely(ret3)) - ret2 = ret3; - } ret = ret < 0 ? ret : ret2; if (ret) break; @@ -5189,6 +5193,13 @@ int ext4_convert_unwritten_extents(handle_t *handle, struct inode *inode, map.m_lblk += map.m_len; map.m_len = (max_blocks -= map.m_len); } + + if (internal_handle) { + ret3 = ext4_journal_stop(handle); + if (!ret) + ret = ret3; + } +out: /* Converted some or all blocks successfully? */ if (converted) *converted = conv_blocks; @@ -5203,8 +5214,8 @@ int ext4_convert_unwritten_io_end_vec(handle_t *handle, ext4_io_end_t *io_end) /* * This is somewhat ugly but the idea is clear: When transaction is - * reserved, everything goes into it. Otherwise we rather start several - * smaller transactions for conversion of each extent separately. + * reserved, everything goes into it. Otherwise the conversion runs + * its own transaction, extending or restarting it as it goes. */ if (handle) { handle = ext4_journal_start_reserved(handle, From a2e096bf59634fe2364548d59a99085680040518 Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:30:16 +0800 Subject: [PATCH 25/41] ext4: implement writeback path using iomap Add the iomap writeback path for ext4 buffered I/O. This introduces: - ext4_iomap_writepages(): the main writeback entry point. - ext4_writeback_ops: a new iomap_writeback_ops instance to handle block mapping and I/O submission. - A new end I/O work handler ext4_iomap_finish_ioend() is added for converting unwritten extents, updating file size, and handling DATA_ERR_ABORT after I/O completion. Core implementation details: - ->writeback_range() callback Calls ext4_iomap_map_writeback_range() to map and allocate blocks using the enhanced ext4_map_blocks(). ext4_map_blocks() now starts its own transaction internally when it needs to allocate blocks. For performance, when a block range is not yet allocated, it allocates based on the writeback length and delalloc extent length (ext4_map_blocks() will trim the allocated length), rather than allocating a single folio at a time. The folio is then added to an iomap_ioend instance. - ->writeback_submit() callback Registers ext4_iomap_end_bio() as the end bio callback. This callback schedules a worker to handle: - Unwritten extent conversion. - i_disksize update after data is written back. - Journal abort on writeback I/O failure. - The new end I/O work handler ext4_iomap_finish_ioend() This reuses i_rsv_conversion_work and i_rsv_conversion_list, which are now used in the buffer_head writeback path for memory saving, because an inode can never go through both buffer_head and iomap paths. Those are not initialized in this patch but will be when the iomap path is formally enabled later. Key changes and considerations: - Append write and unwritten extents Since data=ordered mode is not used to prevent stale data exposure during append writebacks, new blocks are always allocated as unwritten extents (i.e. always enable dioread_nolock), and i_disksize update is postponed until I/O completion. Additionally, the deadlock that the reserve handle was expected to resolve does not occur anymore. Therefore, the end I/O worker can start a normal journal handle instead of a reserve handle when converting unwritten extents. - Lock ordering The ->writeback_range() callback runs under the folio lock, requiring the journal handle to be started under that same lock. This reverses the order compared to the buffer_head writeback path. The lock ordering documentation in super.c has been updated accordingly. - Don't cache writes The iomap infrastructure sets the BIO_COMPLETE_IN_TASK flag when submitting I/O, so the ioend will be processed in task context. However, if a private defer worker is to be started, this flag must be cleared explicitly to avoid double deferral. In the future, all private defer work should be moved to the generic bio complete in task framework. Signed-off-by: Zhang Yi Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103033.2920530-16-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/ext4.h | 8 ++- fs/ext4/inode.c | 147 +++++++++++++++++++++++++++++++++++++++++++++- fs/ext4/page-io.c | 122 ++++++++++++++++++++++++++++++++++++++ fs/ext4/super.c | 5 +- 4 files changed, 278 insertions(+), 4 deletions(-) diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h index c7f2fd60a1ab..00164d7420fc 100644 --- a/fs/ext4/ext4.h +++ b/fs/ext4/ext4.h @@ -1208,8 +1208,10 @@ struct ext4_inode_info { /* Lock protecting lists below */ spinlock_t i_completed_io_lock; /* - * Completed IOs that need unwritten extents handling and have - * transaction reserved + * Completed IOs that need unwritten extents handling and have a + * transaction reserved for the buffer_head writeback path, and + * also used by the iomap writeback path to queue ioends needing + * unwritten extents conversion, i_disksize update, etc. */ struct list_head i_rsv_conversion_list; struct work_struct i_rsv_conversion_work; @@ -3990,6 +3992,8 @@ void ext4_bio_write_folio(struct ext4_io_submit *io, struct folio *page, size_t len); extern struct ext4_io_end_vec *ext4_alloc_io_end_vec(ext4_io_end_t *io_end); extern struct ext4_io_end_vec *ext4_last_io_end_vec(ext4_io_end_t *io_end); +extern void ext4_iomap_end_io(struct work_struct *work); +extern void ext4_iomap_end_bio(struct bio *bio); /* mmp.c */ extern int ext4_multi_mount_protect(struct super_block *, ext4_fsblk_t); diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index 2d2db4207438..e9a8aef79bee 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -44,6 +44,7 @@ #include #include "ext4_jbd2.h" +#include "ext4_extents.h" #include "xattr.h" #include "acl.h" #include "truncate.h" @@ -4147,10 +4148,154 @@ static void ext4_iomap_readahead(struct readahead_control *rac) iomap_bio_readahead(rac, &ext4_iomap_buffered_read_ops); } + +static int ext4_iomap_map_writeback_range(struct iomap_writepage_ctx *wpc, + loff_t offset, unsigned int dirty_len) +{ + struct inode *inode = wpc->inode; + struct super_block *sb = inode->i_sb; + struct journal_s *journal = EXT4_SB(sb)->s_journal; + struct ext4_map_blocks map; + ext4_lblk_t index = offset >> inode->i_blkbits; + unsigned int blk_len, blk_end; + int retries = 0; + int ret; + + ret = ext4_emergency_state(sb); + if (unlikely(ret)) + return ret; + + /* Check validity of the cached writeback mapping. */ + if (offset >= wpc->iomap.offset && + offset < wpc->iomap.offset + wpc->iomap.length && + ext4_iomap_valid(inode, &wpc->iomap)) + return 0; + + blk_len = dirty_len >> inode->i_blkbits; + blk_end = umin(wpc->wbc->range_end >> inode->i_blkbits, UINT_MAX - 1); + if (blk_end > index + blk_len) + blk_len = blk_end - index + 1; + +retry: + map.m_lblk = index; + map.m_len = min_t(unsigned int, MAX_WRITEPAGES_EXTENT_LEN, blk_len); + ret = ext4_map_blocks(NULL, inode, &map, + EXT4_GET_BLOCKS_CREATE_UNWRIT_EXT | + EXT4_GET_BLOCKS_METADATA_NOFAIL | + EXT4_GET_BLOCKS_IO_SUBMIT | + EXT4_EX_NOCACHE); + if (ret < 0) { + if (ext4_emergency_state(sb)) + return ret; + + /* + * Retry transient ENOSPC errors, if + * ext4_count_free_blocks() is non-zero, a commit + * should free up blocks. + */ + if (ret == -ENOSPC && journal && ext4_count_free_clusters(sb) && + ++retries <= 3) { + jbd2_journal_force_commit_nested(journal); + goto retry; + } + + ext4_msg(sb, KERN_CRIT, + "Delayed block allocation failed for inode %llu at logical offset %llu with max blocks %u with error %d", + inode->i_ino, (unsigned long long)map.m_lblk, + (unsigned int)map.m_len, -ret); + ext4_msg(sb, KERN_CRIT, + "This should not happen!! Data will be lost\n"); + if (ret == -ENOSPC) + ext4_print_free_blocks(inode); + return ret; + } + + ext4_set_iomap(inode, &wpc->iomap, &map, offset, dirty_len, 0); + return 0; +} + +static void ext4_iomap_discard_folio(struct folio *folio, loff_t pos) +{ + struct inode *inode = folio->mapping->host; + loff_t length = folio_pos(folio) + folio_size(folio) - pos; + + ext4_iomap_punch_delalloc(inode, pos, length, NULL); +} + +static ssize_t ext4_iomap_writeback_range(struct iomap_writepage_ctx *wpc, + struct folio *folio, u64 offset, + unsigned int len, u64 end_pos) +{ + ssize_t ret; + + ret = ext4_iomap_map_writeback_range(wpc, offset, len); + if (!ret) + ret = iomap_add_to_ioend(wpc, folio, offset, end_pos, len); + if (ret < 0) + ext4_iomap_discard_folio(folio, offset); + return ret; +} + +static int ext4_iomap_writeback_submit(struct iomap_writepage_ctx *wpc, + int error) +{ + struct iomap_ioend *ioend = wpc->wb_ctx; + struct ext4_inode_info *ei = EXT4_I(ioend->io_inode); + + /* + * After I/O completion, a worker needs to be scheduled when: + * 1) Unwritten extents require conversion. + * 2) The file size needs to be extended. + * 3) The journal needs to be aborted due to an I/O error. + */ + if ((ioend->io_flags & IOMAP_IOEND_UNWRITTEN) || + (ioend->io_offset + ioend->io_size > READ_ONCE(ei->i_disksize)) || + test_opt(ioend->io_inode->i_sb, DATA_ERR_ABORT)) + ioend->io_bio.bi_end_io = ext4_iomap_end_bio; + + /* + * ext4_iomap_end_bio() always defers endio processing, disable + * generic BIO in task to avoid double deferral since we will use + * a private defer endio handler in process context. + * + * TODO: Switch all defer handlers to the generic bio complete + * in task framework. + */ + if (ioend->io_bio.bi_end_io) + bio_clear_flag(&ioend->io_bio, BIO_COMPLETE_IN_TASK); + + return iomap_ioend_writeback_submit(wpc, error); +} + +static const struct iomap_writeback_ops ext4_writeback_ops = { + .writeback_range = ext4_iomap_writeback_range, + .writeback_submit = ext4_iomap_writeback_submit, +}; + static int ext4_iomap_writepages(struct address_space *mapping, struct writeback_control *wbc) { - return 0; + struct inode *inode = mapping->host; + struct super_block *sb = inode->i_sb; + long nr = wbc->nr_to_write; + int alloc_ctx, ret; + struct iomap_writepage_ctx wpc = { + .inode = inode, + .wbc = wbc, + .ops = &ext4_writeback_ops, + }; + + ret = ext4_emergency_state(sb); + if (unlikely(ret)) + return ret; + + alloc_ctx = ext4_writepages_down_read(sb); + trace_ext4_writepages(inode, wbc); + ret = iomap_writepages(&wpc); + trace_ext4_writepages_result(inode, wbc, ret, nr - wbc->nr_to_write); + ext4_writepages_up_read(sb, alloc_ctx); + + return ret; } /* diff --git a/fs/ext4/page-io.c b/fs/ext4/page-io.c index 0236b6b9785a..9b0e12b5463c 100644 --- a/fs/ext4/page-io.c +++ b/fs/ext4/page-io.c @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -547,3 +548,124 @@ void ext4_bio_write_folio(struct ext4_io_submit *io, struct folio *folio, io_submit_add_bh(io, inode, folio, bh); } while ((bh = bh->b_this_page) != head); } + +static int ext4_iomap_wb_update_disksize(handle_t *handle, struct inode *inode, + loff_t end) +{ + loff_t new_disksize = end; + struct ext4_inode_info *ei = EXT4_I(inode); + int ret; + + /* + * Races with truncate are avoided by checking i_size under + * i_data_sem. + */ + down_write(&ei->i_data_sem); + new_disksize = min(new_disksize, i_size_read(inode)); + if (new_disksize > ei->i_disksize) + WRITE_ONCE(ei->i_disksize, new_disksize); + up_write(&ei->i_data_sem); + ret = ext4_mark_inode_dirty(handle, inode); + if (ret) + EXT4_ERROR_INODE_ERR(inode, -ret, "Failed to mark inode dirty"); + + return ret; +} + +static void ext4_iomap_finish_ioend(struct iomap_ioend *ioend) +{ + struct inode *inode = ioend->io_inode; + struct super_block *sb = inode->i_sb; + loff_t pos = ioend->io_offset; + size_t size = ioend->io_size; + loff_t end = pos + size; + handle_t *handle; + int credits; + int ret, err; + + ret = blk_status_to_errno(ioend->io_bio.bi_status); + if (unlikely(ret)) { + if (test_opt(sb, DATA_ERR_ABORT) && !ext4_emergency_state(sb)) + jbd2_journal_abort(EXT4_SB(sb)->s_journal, ret); + goto out; + } + + if (!(ioend->io_flags & IOMAP_IOEND_UNWRITTEN) && + end <= READ_ONCE(EXT4_I(inode)->i_disksize)) + goto out; + + /* + * We may need to convert one extent, update the i_disksize and + * dirty the inode. + */ + credits = ext4_chunk_trans_blocks(inode, + EXT4_MAX_BLOCKS(size, pos, inode->i_blkbits)); + handle = ext4_journal_start(inode, EXT4_HT_EXT_CONVERT, credits); + if (IS_ERR(handle)) { + ret = PTR_ERR(handle); + goto out_err; + } + + /* 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 (ret) + goto out_journal; + } + + if (ioend->io_flags & IOMAP_IOEND_UNWRITTEN) + ret = ext4_convert_unwritten_extents(handle, inode, pos, + size, NULL); + +out_journal: + err = ext4_journal_stop(handle); + if (!ret) + ret = err; +out_err: + if (ret < 0 && !ext4_emergency_state(sb)) { + ext4_msg(sb, KERN_EMERG, + "failed to convert unwritten extents to written extents or update inode size -- potential data loss! (inode %llu, error %d)", + inode->i_ino, ret); + } +out: + iomap_finish_ioends(ioend, ret); +} + +/* + * Work on buffered iomap completed IO, to convert unwritten extents to + * mapped extents + */ +void ext4_iomap_end_io(struct work_struct *work) +{ + struct ext4_inode_info *ei = container_of(work, struct ext4_inode_info, + i_rsv_conversion_work); + struct iomap_ioend *ioend; + struct list_head ioend_list; + unsigned long flags; + + spin_lock_irqsave(&ei->i_completed_io_lock, flags); + list_replace_init(&ei->i_rsv_conversion_list, &ioend_list); + spin_unlock_irqrestore(&ei->i_completed_io_lock, flags); + + iomap_sort_ioends(&ioend_list); + while (!list_empty(&ioend_list)) { + ioend = list_entry(ioend_list.next, struct iomap_ioend, io_list); + list_del_init(&ioend->io_list); + iomap_ioend_try_merge(ioend, &ioend_list); + ext4_iomap_finish_ioend(ioend); + } +} + +void ext4_iomap_end_bio(struct bio *bio) +{ + struct iomap_ioend *ioend = iomap_ioend_from_bio(bio); + struct ext4_inode_info *ei = EXT4_I(ioend->io_inode); + unsigned long flags; + + spin_lock_irqsave(&ei->i_completed_io_lock, flags); + if (list_empty(&ei->i_rsv_conversion_list)) + queue_work(EXT4_SB(ioend->io_inode->i_sb)->rsv_conversion_wq, + &ei->i_rsv_conversion_work); + list_add_tail(&ioend->io_list, &ei->i_rsv_conversion_list); + spin_unlock_irqrestore(&ei->i_completed_io_lock, flags); +} diff --git a/fs/ext4/super.c b/fs/ext4/super.c index 22738ed3d3aa..abfacb5b8ce7 100644 --- a/fs/ext4/super.c +++ b/fs/ext4/super.c @@ -123,7 +123,10 @@ static const struct fs_parameter_spec ext4_param_specs[]; * sb_start_write -> i_mutex -> transaction start -> i_data_sem (rw) * * writepages: - * transaction start -> page lock(s) -> i_data_sem (rw) + * - buffer_head path: + * transaction start -> folio lock(s) -> i_data_sem (rw) + * - iomap path: + * folio lock -> transaction start -> i_data_sem (rw) */ static const struct fs_context_operations ext4_context_ops = { From e055578dc19256e44a774eb04a24c8d22465a698 Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:30:17 +0800 Subject: [PATCH 26/41] ext4: implement mmap path using iomap Introduce ext4_iomap_page_mkwrite() to implement the mmap iomap path for ext4. The heavy lifting is delegated to iomap_page_mkwrite(), which only requires ext4_iomap_buffered_write_ops and ext4_iomap_buffered_da_write_ops to allocate and map blocks. Note that the lock ordering between folio lock and transaction start in this path is reversed compared to the buffer_head buffered write path. The lock ordering documentation in super.c has been updated accordingly. Signed-off-by: Zhang Yi Reviewed-by: Ojaswin Mujoo Reviewed-by: Jan Kara Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103033.2920530-17-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/inode.c | 32 +++++++++++++++++++++++++++++++- fs/ext4/super.c | 8 ++++++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index e9a8aef79bee..62886ccf3eca 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -4040,7 +4040,7 @@ static int ext4_iomap_buffered_do_write_begin(struct inode *inode, return -ERANGE; if (WARN_ON_ONCE(!ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS))) return -EINVAL; - if (WARN_ON_ONCE(!(flags & IOMAP_WRITE))) + if (WARN_ON_ONCE(!(flags & (IOMAP_WRITE | IOMAP_FAULT)))) return -EINVAL; map_flags = delalloc ? EXT4_GET_BLOCKS_DELALLOC_RESERVE : @@ -4098,6 +4098,14 @@ static int ext4_iomap_buffered_da_write_end(struct inode *inode, loff_t offset, if (iomap->type != IOMAP_DELALLOC || !(iomap->flags & IOMAP_F_NEW)) return 0; + /* + * iomap_page_mkwrite() will never fail in a way that requires delalloc + * extents that it allocated to be revoked. Hence never try to release + * them here. + */ + if (flags & IOMAP_FAULT) + return 0; + /* Nothing to do if we've written the entire delalloc extent */ start_byte = iomap_last_written_block(inode, offset, written); end_byte = round_up(offset + length, i_blocksize(inode)); @@ -7315,6 +7323,23 @@ static int ext4_block_page_mkwrite(struct inode *inode, struct folio *folio, return ret; } +static vm_fault_t ext4_iomap_page_mkwrite(struct vm_fault *vmf) +{ + struct inode *inode = file_inode(vmf->vma->vm_file); + const struct iomap_ops *iomap_ops; + + /* + * ext4_nonda_switch() could writeback this folio, so have to + * call it before lock folio. + */ + if (test_opt(inode->i_sb, DELALLOC) && !ext4_nonda_switch(inode->i_sb)) + iomap_ops = &ext4_iomap_buffered_da_write_ops; + else + iomap_ops = &ext4_iomap_buffered_write_ops; + + return iomap_page_mkwrite(vmf, iomap_ops, NULL); +} + vm_fault_t ext4_page_mkwrite(struct vm_fault *vmf) { struct vm_area_struct *vma = vmf->vma; @@ -7341,6 +7366,11 @@ vm_fault_t ext4_page_mkwrite(struct vm_fault *vmf) if (err) goto out_ret; + if (ext4_inode_buffered_iomap(inode)) { + ret = ext4_iomap_page_mkwrite(vmf); + goto out; + } + /* * On data journalling we skip straight to the transaction handle: * there's no delalloc; page truncated will be checked later; the diff --git a/fs/ext4/super.c b/fs/ext4/super.c index abfacb5b8ce7..23dbdcd598fa 100644 --- a/fs/ext4/super.c +++ b/fs/ext4/super.c @@ -100,8 +100,12 @@ static const struct fs_parameter_spec ext4_param_specs[]; * Lock ordering * * page fault path: - * mmap_lock -> sb_start_pagefault -> invalidate_lock (r) -> transaction start - * -> page lock -> i_data_sem (rw) + * - buffer_head path: + * mmap_lock -> sb_start_pagefault -> invalidate_lock (r) -> + * transaction start -> folio lock -> i_data_sem (rw) + * - iomap path: + * mmap_lock -> sb_start_pagefault -> invalidate_lock (r) -> + * folio lock -> transaction start -> i_data_sem (rw) * * buffered write path: * sb_start_write -> i_rwsem (w) -> mmap_lock From 68738d77ce86cd4028e23ac2fbc45c56683bea7a Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:30:18 +0800 Subject: [PATCH 27/41] 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 Reviewed-by: Ojaswin Mujoo Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103033.2920530-18-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/inode.c | 110 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index 62886ccf3eca..614b327372cc 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -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)) { From 3c612a572094291a752fe983af2868cde634ba02 Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:30:19 +0800 Subject: [PATCH 28/41] ext4: drain writeback before removing extents on the iomap path Because the iomap infrastructure does not always create an ifs to manage sub-folio state when folio size is larger than blocksize, invalidating a partial dirty folio during punch hole may fail to clear the dirty state of the affected range. As a result, writeback of that folio may observe a hole. At writeback submit time, ext4_map_blocks() already handles this case and will not allocate blocks. However, when punch hole races with writeback, the following scenario can cause I/O completion to encounter a hole. punch hole writeback ---------- --------- ext4_punch_hole() ext4_truncate_page_cache_block_range() iomap_invalidate_folio() [partial folio] iomap_clear_range_dirty() -- no ifs, sub-block dirty bits NOT cleared ext4_iomap_writepages() iomap_writepages() ext4_iomap_writeback_submit() ext4_iomap_map_writeback_range() ext4_map_blocks(IO_SUBMIT) -> extent exists, not a hole submit_io() -> bio in flight down_write(&i_data_sem) ext4_es_remove_extent() ext4_ext_remove_space() -> extent removed, hole inserted up_write(&i_data_sem) [ transaction commits, freed blocks released to buddy ] [ allocator reuses freed blocks for another inode ] [bio completes] ext4_iomap_finish_ioend() ext4_convert_unwritten_extents() ext4_map_blocks(IO_CONVERT_EXT) -> returns 0 (hole found) [bio writes to reallocated blocks ] -> silent data corruption If the bio completes after the freed blocks have been reallocated to another inode, it silently overwrites them and causes data corruption. Therefore, on the iomap path, drain writeback in ext4_punch_hole() after partial zeroing and before starting the freeing transaction, so any bio in flight on a partial folio completes against blocks still owned by this inode before extent removal. Once extent removal begins, the ES hole insert and the existing IO_SUBMIT short-circuit prevent further bio submission. This is a workaround. The proper fix is for the iomap infrastructure to always maintain an ifs when folio size is larger than blocksize, so that partial invalidate reliably clears sub-block dirty bits and no draining is needed. Signed-off-by: Zhang Yi Reviewed-by: Ojaswin Mujoo Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103033.2920530-19-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/inode.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index 614b327372cc..04ce1f74fcdd 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -5028,7 +5028,14 @@ int ext4_punch_hole(struct file *file, loff_t offset, loff_t length) ret = ext4_zero_partial_blocks(inode, offset, length, &partial_zeroed); if (ret) return ret; - if (((file->f_flags & O_SYNC) || IS_SYNC(inode)) && partial_zeroed) { + /* + * On the iomap path, partial invalidate of a folio without an ifs + * leaves sub-block dirty bits uncleared. Drain writeback before + * removing extents so that any bio in flight on a partial folio + * completes against blocks still owned by this inode. + */ + if (ext4_inode_buffered_iomap(inode) || + (((file->f_flags & O_SYNC) || IS_SYNC(inode)) && partial_zeroed)) { ret = filemap_write_and_wait_range(inode->i_mapping, offset, end - 1); if (ret) From 008f019ab559b90171b5ecf7d8f5f6f22cd35687 Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:30:20 +0800 Subject: [PATCH 29/41] ext4: add block mapping tracepoints for iomap buffered I/O path Add tracepoints for iomap buffered read, write, partial block zeroing, and writeback operations to help debug the iomap buffered I/O path. Signed-off-by: Zhang Yi Reviewed-by: Ojaswin Mujoo Reviewed-by: Jan Kara Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103033.2920530-20-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/inode.c | 6 +++++ include/trace/events/ext4.h | 47 +++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index 04ce1f74fcdd..8a77c5acbecb 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -4019,6 +4019,8 @@ static int ext4_iomap_buffered_read_begin(struct inode *inode, loff_t offset, if (ret < 0) return ret; + trace_ext4_iomap_buffered_read_begin(inode, &map, offset, length, + flags); ext4_set_iomap(inode, iomap, &map, offset, length, flags); return 0; } @@ -4052,6 +4054,8 @@ static int ext4_iomap_buffered_do_write_begin(struct inode *inode, if (ret < 0) return ret; + trace_ext4_iomap_buffered_write_begin(inode, &map, offset, length, + flags); ext4_set_iomap(inode, iomap, &map, offset, length, flags); return 0; } @@ -4168,6 +4172,7 @@ static int ext4_iomap_zero_begin(struct inode *inode, goto again; } + trace_ext4_iomap_zero_begin(inode, &map, offset, length, flags); ext4_set_iomap(inode, iomap, &map, offset, length, flags); iomap->flags |= iomap_flags; @@ -4279,6 +4284,7 @@ static int ext4_iomap_map_writeback_range(struct iomap_writepage_ctx *wpc, return ret; } + trace_ext4_iomap_map_writeback_range(inode, &map, offset, dirty_len, 0); ext4_set_iomap(inode, &wpc->iomap, &map, offset, dirty_len, 0); return 0; } diff --git a/include/trace/events/ext4.h b/include/trace/events/ext4.h index 7028a28316fa..bcf80f518e61 100644 --- a/include/trace/events/ext4.h +++ b/include/trace/events/ext4.h @@ -3157,6 +3157,53 @@ TRACE_EVENT(ext4_move_extent_exit, __entry->ret) ); +DECLARE_EVENT_CLASS(ext4_set_iomap_class, + TP_PROTO(struct inode *inode, struct ext4_map_blocks *map, + loff_t offset, loff_t length, unsigned int flags), + TP_ARGS(inode, map, offset, length, flags), + TP_STRUCT__entry( + __field(dev_t, dev) + __field(u64, ino) + __field(ext4_lblk_t, m_lblk) + __field(ext4_fsblk_t, pblk) + __field(unsigned int, m_len) + __field(unsigned int, m_flags) + __field(u64, m_seq) + __field(loff_t, offset) + __field(loff_t, length) + __field(unsigned int, iomap_flags) + ), + TP_fast_assign( + __entry->dev = inode->i_sb->s_dev; + __entry->ino = inode->i_ino; + __entry->m_lblk = map->m_lblk; + __entry->pblk = map->m_pblk; + __entry->m_len = map->m_len; + __entry->m_flags = map->m_flags; + __entry->m_seq = map->m_seq; + __entry->offset = offset; + __entry->length = length; + __entry->iomap_flags = flags; + + ), + TP_printk("dev %d:%d ino %llu m_lblk %u pblk %llu m_len %u m_flags %s m_seq %llu orig_off 0x%llx orig_len 0x%llx iomap_flags 0x%x", + MAJOR(__entry->dev), MINOR(__entry->dev), + __entry->ino, __entry->m_lblk, __entry->pblk, __entry->m_len, + show_mflags(__entry->m_flags), __entry->m_seq, + __entry->offset, __entry->length, __entry->iomap_flags) +) + +#define DEFINE_SET_IOMAP_EVENT(name) \ +DEFINE_EVENT(ext4_set_iomap_class, name, \ + TP_PROTO(struct inode *inode, struct ext4_map_blocks *map, \ + loff_t offset, loff_t length, unsigned int flags), \ + TP_ARGS(inode, map, offset, length, flags)) + +DEFINE_SET_IOMAP_EVENT(ext4_iomap_buffered_read_begin); +DEFINE_SET_IOMAP_EVENT(ext4_iomap_buffered_write_begin); +DEFINE_SET_IOMAP_EVENT(ext4_iomap_map_writeback_range); +DEFINE_SET_IOMAP_EVENT(ext4_iomap_zero_begin); + #endif /* _TRACE_EXT4_H */ /* This part must be outside protection */ From 7a40905e58f05671f366bd67ea55e55d5f59a35c Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:30:21 +0800 Subject: [PATCH 30/41] ext4: disable online defrag when inode using iomap buffered I/O path Online defragmentation does not currently support inodes using the iomap buffered I/O path. The existing implementation relies on buffer_head for sub-folio block management and data=ordered mode for data consistency, both of which are incompatible with the iomap path. Signed-off-by: Zhang Yi Reviewed-by: Ojaswin Mujoo Reviewed-by: Jan Kara Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103033.2920530-21-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/move_extent.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/fs/ext4/move_extent.c b/fs/ext4/move_extent.c index 3329b7ad5dbd..948ef3f44df5 100644 --- a/fs/ext4/move_extent.c +++ b/fs/ext4/move_extent.c @@ -476,6 +476,17 @@ static int mext_check_validity(struct inode *orig_inode, return -EOPNOTSUPP; } + /* + * TODO: support online defrag for inodes that use the buffered + * I/O iomap path. + */ + if (ext4_inode_buffered_iomap(orig_inode) || + ext4_inode_buffered_iomap(donor_inode)) { + ext4_msg(sb, KERN_ERR, + "Online defrag not supported for inode with iomap buffered IO path"); + return -EOPNOTSUPP; + } + if (donor_inode->i_mode & (S_ISUID|S_ISGID)) { ext4_debug("ext4 move extent: suid or sgid is set to donor file [ino:orig %llu, donor %llu]\n", orig_inode->i_ino, donor_inode->i_ino); From 16852f1d3c39e15feeced92c6c99d59add2dd1d2 Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:30:22 +0800 Subject: [PATCH 31/41] ext4: add EXT4_STATE_DISKSIZE_GROW_PENDING state bit and helpers Inodes using the iomap buffered I/O path do not use data=ordered mode, so the zeroed EOF block that straddles i_disksize needs explicit tracking to ensure it is written back before i_disksize is advanced. Add the EXT4_STATE_DISKSIZE_GROW_PENDING inode state bit and three helpers: ext4_iomap_clear_disksize_pending() to atomically clear the bit and wake waiters, ext4_iomap_wait_disksize_pending() to block until the bit is cleared, and ext4_iomap_get_disksize_pending_range() to compute the pending range from i_disksize. These will be used by subsequent patches to serialize i_disksize updates with the writeback of the zeroed EOF block. Suggested-by: Jan Kara Signed-off-by: Zhang Yi Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103033.2920530-22-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/ext4.h | 7 ++++++ fs/ext4/inode.c | 57 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h index 00164d7420fc..cc8b878bb71a 100644 --- a/fs/ext4/ext4.h +++ b/fs/ext4/ext4.h @@ -2052,6 +2052,9 @@ enum { EXT4_STATE_ORPHAN_FILE, /* Inode orphaned in orphan file */ EXT4_STATE_FC_REQUEUE, /* Inode modified during fast commit */ EXT4_STATE_BUFFERED_IOMAP, /* Inode use iomap for buffered IO */ + EXT4_STATE_DISKSIZE_GROW_PENDING, + /* Has zeroed EOF block straddles + * i_disksize awaiting writeback */ }; #define EXT4_INODE_BIT_FNS(name, field, offset) \ @@ -3219,6 +3222,10 @@ extern int ext4_chunk_trans_blocks(struct inode *, int nrblocks); extern int ext4_chunk_trans_extent(struct inode *inode, int nrblocks); extern int ext4_meta_trans_blocks(struct inode *inode, int lblocks, int pextents, int alloc_extents); +void ext4_iomap_clear_disksize_pending(struct inode *inode); +void ext4_iomap_wait_disksize_pending(struct inode *inode); +unsigned int ext4_iomap_get_disksize_pending_range(struct inode *inode, + loff_t *start); extern int ext4_block_zero_eof(struct inode *inode, loff_t from, loff_t end); #define EXT4_PARTIAL_ZERO_START 0x1 diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index 8a77c5acbecb..ba49c8683ab5 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -126,6 +126,63 @@ void ext4_inode_csum_set(struct inode *inode, struct ext4_inode *raw, raw->i_checksum_hi = cpu_to_le16(csum >> 16); } +/* + * Clear the disksize-grow-pending state and wake up all waiters. + * Called when the pending zeroed EOF block which straddles i_disksize + * has completed writeback or its folio is discarded. + */ +void ext4_iomap_clear_disksize_pending(struct inode *inode) +{ + clear_and_wake_up_bit( + ext4_inode_state_wait_bit(EXT4_STATE_DISKSIZE_GROW_PENDING), + ext4_inode_state_wait_word(inode)); +} + +/* + * Wait for the disksize-grow-pending zeroed EOF block which straddles + * i_disksize to be written back or cleared. + */ +void ext4_iomap_wait_disksize_pending(struct inode *inode) +{ + wait_on_bit(ext4_inode_state_wait_word(inode), + ext4_inode_state_wait_bit(EXT4_STATE_DISKSIZE_GROW_PENDING), + TASK_UNINTERRUPTIBLE); +} + +/* + * Get the range of the disksize-grow-pending zeroed EOF block range + * which straddles i_disksize if the EXT4_STATE_DISKSIZE_GROW_PENDING + * bit is set. + * + * Return the pending range, or zero if the BIT has already been cleared. + */ +unsigned int ext4_iomap_get_disksize_pending_range(struct inode *inode, + loff_t *start) +{ + unsigned int blocksize = i_blocksize(inode); + loff_t disksize; + + if (!ext4_test_inode_state(inode, EXT4_STATE_DISKSIZE_GROW_PENDING)) + return 0; + + /* + * The pending bit should be set only when i_disksize is not + * block-size aligned. While set, i_disksize must not be advanced, + * and the bit must be cleared when i_disksize is shrunk. + */ + down_read(&EXT4_I(inode)->i_data_sem); + disksize = READ_ONCE(EXT4_I(inode)->i_disksize); + if (!ext4_test_inode_state(inode, EXT4_STATE_DISKSIZE_GROW_PENDING) || + WARN_ON_ONCE(IS_ALIGNED(disksize, blocksize))) { + up_read(&EXT4_I(inode)->i_data_sem); + return 0; + } + + up_read(&EXT4_I(inode)->i_data_sem); + *start = disksize; + return blocksize - (disksize & (blocksize - 1)); +} + static inline int ext4_begin_ordered_truncate(struct inode *inode, loff_t new_size) { From 53fe71a6bcad0f67b80046086c03422c42a92b78 Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:30:23 +0800 Subject: [PATCH 32/41] ext4: submit and wait for pending disksize-grow I/O on writeback When the current writeback pass begins beyond the disksize-grow-pending zeroed EOF block, the ioend worker would otherwise have to wait for the pending EOF block to complete before it can advance i_disksize. Otherwise the old EOF block could be exposed as stale data once i_disksize advances past it. Therefore, introduce the ioend mechanism for the pending range, tag ioends that cover the pending zeroed EOF block which straddles i_disksize with EXT4_IOMAP_IOEND_DISKSIZE_GROW_IO in ext4_iomap_writeback_submit(), and clear the bit and wake up all waiters in ext4_iomap_end_bio() when such an ioend completes. Clearing EXT4_IOMAP_IOEND_DISKSIZE_GROW_IO does not depend on whether the disksize grow I/O succeeds. That is, even if the I/O fails, we still allow subsequent writes in the range to update i_disksize. This is consistent with the previous behavior, and we rely on data_err=abort to prevent metadata updates when data write failures occur. In order to avoid the ioend that passes the pending range waiting for a long time, proactively submit the pending range first in ext4_iomap_writepages() so it completes in parallel with the rest of the writeback. Note that the handling of discarding the zeroed EOF folio will be processed later, otherwise the bit will be set forever. EXT4_STATE_DISKSIZE_GROW_PENDING will be set after everthing is done. Signed-off-by: Zhang Yi Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103033.2920530-23-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/ext4.h | 6 +++++ fs/ext4/inode.c | 56 ++++++++++++++++++++++++++++++++++++++++++++++- fs/ext4/page-io.c | 40 +++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 1 deletion(-) diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h index cc8b878bb71a..c5d3f1b04c9c 100644 --- a/fs/ext4/ext4.h +++ b/fs/ext4/ext4.h @@ -3985,6 +3985,12 @@ extern int ext4_move_extents(struct file *o_filp, struct file *d_filp, __u64 len, __u64 *moved_len); /* page-io.c */ +/* + * The I/O range covers the zeroed EOF block that straddles i_disksize + * and will advance it upon completion. + */ +#define EXT4_IOMAP_IOEND_DISKSIZE_GROW_IO 1UL + extern int __init ext4_init_pageio(void); extern void ext4_exit_pageio(void); extern ext4_io_end_t *ext4_init_io_end(struct inode *inode, gfp_t flags); diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index ba49c8683ab5..a271bb2550b0 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -4372,7 +4372,10 @@ static int ext4_iomap_writeback_submit(struct iomap_writepage_ctx *wpc, int error) { struct iomap_ioend *ioend = wpc->wb_ctx; - struct ext4_inode_info *ei = EXT4_I(ioend->io_inode); + struct inode *inode = ioend->io_inode; + struct ext4_inode_info *ei = EXT4_I(inode); + unsigned int blocksize = i_blocksize(inode); + loff_t pstart, plen; /* * After I/O completion, a worker needs to be scheduled when: @@ -4385,6 +4388,21 @@ static int ext4_iomap_writeback_submit(struct iomap_writepage_ctx *wpc, test_opt(ioend->io_inode->i_sb, DATA_ERR_ABORT)) ioend->io_bio.bi_end_io = ext4_iomap_end_bio; + /* + * Mark the I/O as DISKSIZE_GROW_IO by setting io_private to + * EXT4_IOMAP_IOEND_DISKSIZE_GROW_IO if it covers the pending range. + * Such I/O will allow or trigger i_disksize advancement in the + * ioend worker. + */ + plen = ext4_iomap_get_disksize_pending_range(inode, &pstart); + if (plen && + round_down(ioend->io_offset, blocksize) <= pstart && + round_up(ioend->io_offset + ioend->io_size, blocksize) >= + pstart + plen) { + ioend->io_bio.bi_end_io = ext4_iomap_end_bio; + ioend->io_private = (void *)EXT4_IOMAP_IOEND_DISKSIZE_GROW_IO; + } + /* * ext4_iomap_end_bio() always defers endio processing, disable * generic BIO in task to avoid double deferral since we will use @@ -4404,6 +4422,36 @@ static const struct iomap_writeback_ops ext4_writeback_ops = { .writeback_submit = ext4_iomap_writeback_submit, }; +/* + * If the current writeback range begins after the pending zeroed EOF + * block range which straddles i_disksize, issue a separate writeback to + * flush it first, so as to avoid prolonged waiting. + */ +static void ext4_iomap_wb_submit_zeroed_eof(struct inode *inode, + struct writeback_control *wbc) +{ + struct address_space *mapping = inode->i_mapping; + loff_t pstart, plen, range_start; + + if (wbc->range_cyclic) + range_start = (loff_t)mapping->writeback_index << PAGE_SHIFT; + else + range_start = wbc->range_start; + + plen = ext4_iomap_get_disksize_pending_range(inode, &pstart); + if (!plen) + return; + if (round_down(range_start, mapping_min_folio_nrbytes(mapping)) < + pstart + plen) + return; + + /* Keep the caller's sync mode to avoid stalling the background flusher. */ + if (wbc->sync_mode == WB_SYNC_ALL) + filemap_fdatawrite_range(mapping, pstart, pstart + plen - 1); + else + filemap_flush_range(mapping, pstart, pstart + plen - 1); +} + static int ext4_iomap_writepages(struct address_space *mapping, struct writeback_control *wbc) { @@ -4421,6 +4469,12 @@ static int ext4_iomap_writepages(struct address_space *mapping, if (unlikely(ret)) return ret; + /* + * Submit the pending zeroed EOF block range if the entire + * writeback range lies beyond it. + */ + ext4_iomap_wb_submit_zeroed_eof(inode, wbc); + alloc_ctx = ext4_writepages_down_read(sb); trace_ext4_writepages(inode, wbc); ret = iomap_writepages(&wpc); diff --git a/fs/ext4/page-io.c b/fs/ext4/page-io.c index 9b0e12b5463c..697e12a54a49 100644 --- a/fs/ext4/page-io.c +++ b/fs/ext4/page-io.c @@ -549,6 +549,34 @@ void ext4_bio_write_folio(struct ext4_io_submit *io, struct folio *folio, } while ((bh = bh->b_this_page) != head); } +/* + * If the current writeback range starts beyond the zeroed EOF pending + * range that straddles i_disksize, wait for the zeroed data from + * ext4_block_zero_eof() to be written out first. Otherwise, extending + * i_disksize may expose stale data in the old EOF block. + */ +static void ext4_iomap_wb_disksize_pending_wait(struct inode *inode, + loff_t pos, size_t size) +{ + loff_t disksize = READ_ONCE(EXT4_I(inode)->i_disksize); + loff_t pstart, plen; + + /* + * Overwrite I/Os and I/Os covering the EOF block do not need to + * wait: the former do not advance i_disksize past the pending + * boundary, and the latter are the pending I/O itself (cleared in + * the bio completion path). + */ + if (pos < round_up(disksize, i_blocksize(inode))) + return; + + plen = ext4_iomap_get_disksize_pending_range(inode, &pstart); + if (!plen || pos < pstart + plen) + return; + + ext4_iomap_wait_disksize_pending(inode); +} + static int ext4_iomap_wb_update_disksize(handle_t *handle, struct inode *inode, loff_t end) { @@ -594,6 +622,9 @@ static void ext4_iomap_finish_ioend(struct iomap_ioend *ioend) end <= READ_ONCE(EXT4_I(inode)->i_disksize)) goto out; + /* Wait for disksize-pending zeroed data to be written out. */ + ext4_iomap_wb_disksize_pending_wait(inode, pos, size); + /* * We may need to convert one extent, update the i_disksize and * dirty the inode. @@ -660,8 +691,17 @@ void ext4_iomap_end_bio(struct bio *bio) { struct iomap_ioend *ioend = iomap_ioend_from_bio(bio); struct ext4_inode_info *ei = EXT4_I(ioend->io_inode); + unsigned long io_mode = (unsigned long)ioend->io_private; unsigned long flags; + /* + * This is a disksize-pending I/O: clear the disksize-pending + * state set in ext4_block_zero_eof() and wake up all waiters + * that will update the inode i_disksize. + */ + if (io_mode == EXT4_IOMAP_IOEND_DISKSIZE_GROW_IO) + ext4_iomap_clear_disksize_pending(ioend->io_inode); + spin_lock_irqsave(&ei->i_completed_io_lock, flags); if (list_empty(&ei->i_rsv_conversion_list)) queue_work(EXT4_SB(ioend->io_inode->i_sb)->rsv_conversion_wq, From 02a59b693ea2e37ac121546abbe685c1d8b697e0 Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:30:24 +0800 Subject: [PATCH 33/41] ext4: advance i_disksize to i_size upon disksize-grow I/O completion 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. Note that EXT4_STATE_DISKSIZE_GROW_PENDING is not set for now, and it will be set after everthing is done. Suggested-by: Jan Kara Signed-off-by: Zhang Yi Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103033.2920530-24-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/page-io.c | 42 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/fs/ext4/page-io.c b/fs/ext4/page-io.c index 697e12a54a49..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,7 +589,34 @@ 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) WRITE_ONCE(ei->i_disksize, new_disksize); up_write(&ei->i_data_sem); @@ -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; } From ee6539fc9eb0f156652c6a8710e5db1dca56ecc3 Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:30:25 +0800 Subject: [PATCH 34/41] ext4: defer i_disksize update while DISKSIZE_GROW_PENDING is set 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. Note that in the O_SYNC mode of ext4_do_fallocate() or ext4_zero_range(), we need to flush out pending blocks that can update i_disksize synchronously. This will be handled later, and EXT4_STATE_DISKSIZE_GROW_PENDING will also be set after everything is done. Suggested-by: Jan Kara Signed-off-by: Zhang Yi Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103033.2920530-25-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- 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 c5d3f1b04c9c..5bb13a0cfeb5 100644 --- a/fs/ext4/ext4.h +++ b/fs/ext4/ext4.h @@ -3601,30 +3601,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 30fa1ce09f0c..74e50716f6f4 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 a271bb2550b0..e586d5eed3f0 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -6635,8 +6635,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) { @@ -6644,7 +6646,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); } From 09e4d8701614d2c524d96a88c4ff694f42b9a180 Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:30:26 +0800 Subject: [PATCH 35/41] ext4: submit and wait for disksize-grow I/O in fallocate paths Collapse range and insert range update i_disksize directly under i_data_sem. If the operation runs while the zeroed EOF block is still awaiting writeback, i_disksize could advance past the zeroed boundary before the zeroed data is persisted, exposing stale data on crash. Deferring i_disksize updates like fallocate and zero_range is not an option here because the shift would move written extents beyond the current i_disksize. So flush and wait for the pending zeroed EOF block before these operations advance i_disksize. Since these operations already perform writeback, the extra flush does not add significant overhead. In addition, for ext4_update_disksize_before_punch(), if the punch discards the pending block, the zeroed data will never be written back before advancing i_disksize, so it is also necessary to sync the pending EOF range there. Finally, for the SYNC variants of zero_range and fallocate, this also guarantees the i_disksize update is persisted on the synchronous return. Note that EXT4_STATE_DISKSIZE_GROW_PENDING is not set for now, and it will be set after everything is done. Signed-off-by: Zhang Yi Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103033.2920530-26-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/ext4.h | 2 ++ fs/ext4/extents.c | 50 ++++++++++++++++++++++++++++++++++++++++------- fs/ext4/inode.c | 41 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 7 deletions(-) diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h index 5bb13a0cfeb5..2fecf0bb5cf6 100644 --- a/fs/ext4/ext4.h +++ b/fs/ext4/ext4.h @@ -3226,6 +3226,8 @@ void ext4_iomap_clear_disksize_pending(struct inode *inode); void ext4_iomap_wait_disksize_pending(struct inode *inode); unsigned int ext4_iomap_get_disksize_pending_range(struct inode *inode, loff_t *start); +extern int ext4_iomap_sync_zeroed_eof(struct inode *inode, + loff_t offset, loff_t end); extern int ext4_block_zero_eof(struct inode *inode, loff_t from, loff_t end); #define EXT4_PARTIAL_ZERO_START 0x1 diff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index 74e50716f6f4..7193429a05e1 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -4857,6 +4857,17 @@ static long ext4_zero_range(struct file *file, loff_t offset, if (ret) return ret; } + + /* + * In SYNC mode, sync the pending zeroed EOF block to ensure the + * i_disksize update is persisted. + */ + if (((file->f_flags & O_SYNC) || IS_SYNC(inode)) && new_size) { + ret = ext4_iomap_sync_zeroed_eof(inode, 0, LLONG_MAX); + if (ret) + return ret; + } + /* Finish zeroing out if it doesn't contain partial block */ if (IS_ALIGNED(offset | end, blocksize)) return ret; @@ -4928,10 +4939,20 @@ static long ext4_do_fallocate(struct file *file, loff_t offset, if (ret) goto out; - if (((file->f_flags & O_SYNC) || IS_SYNC(inode)) && - EXT4_SB(inode->i_sb)->s_journal) { - ret = ext4_fc_commit(EXT4_SB(inode->i_sb)->s_journal, - EXT4_I(inode)->i_sync_tid); + if ((file->f_flags & O_SYNC) || IS_SYNC(inode)) { + /* + * Sync the pending zeroed EOF block to ensure the + * i_disksize update is persisted. + */ + if (new_size) { + ret = ext4_iomap_sync_zeroed_eof(inode, 0, LLONG_MAX); + if (ret) + goto out; + } + if (EXT4_SB(inode->i_sb)->s_journal) { + ret = ext4_fc_commit(EXT4_SB(inode->i_sb)->s_journal, + EXT4_I(inode)->i_sync_tid); + } } out: trace_ext4_fallocate_exit(inode, offset, @@ -5673,6 +5694,14 @@ static int ext4_collapse_range(struct file *file, loff_t offset, loff_t len) if (end >= inode->i_size) return -EINVAL; + /* + * Persist the pending zeroed EOF block to ensure i_disksize + * can be safely updated thereafter. + */ + ret = ext4_iomap_sync_zeroed_eof(inode, 0, LLONG_MAX); + if (ret) + return ret; + /* * Write tail of the last page before removed range and data that * will be shifted since they will get removed from the page cache @@ -5722,7 +5751,7 @@ static int ext4_collapse_range(struct file *file, loff_t offset, loff_t len) new_size = inode->i_size - len; i_size_write(inode, new_size); - EXT4_I(inode)->i_disksize = new_size; + __ext4_set_i_disksize(inode, new_size); up_write(&EXT4_I(inode)->i_data_sem); ret = ext4_mark_inode_dirty(handle, inode); @@ -5775,6 +5804,14 @@ static int ext4_insert_range(struct file *file, loff_t offset, loff_t len) if (len > inode->i_sb->s_maxbytes - inode->i_size) return -EFBIG; + /* + * Persist the pending zeroed EOF block to ensure i_disksize + * can be safely updated thereafter. + */ + ret = ext4_iomap_sync_zeroed_eof(inode, 0, LLONG_MAX); + if (ret) + return ret; + /* * Write out all dirty pages. Need to round down to align start offset * to page size boundary for page size > block size. @@ -5794,8 +5831,7 @@ static int ext4_insert_range(struct file *file, loff_t offset, loff_t len) ext4_fc_mark_ineligible(sb, EXT4_FC_REASON_FALLOC_RANGE, handle); /* Expand file to avoid data loss if there is error while shifting */ - inode->i_size += len; - EXT4_I(inode)->i_disksize += len; + ext4_update_inode_size(inode, inode->i_size + len); ret = ext4_mark_inode_dirty(handle, inode); if (ret) goto out_handle; diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index e586d5eed3f0..61ccb3c5390a 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -4836,6 +4836,39 @@ static int ext4_block_zero_range(struct inode *inode, zero_written); } +/* + * Submit and wait for the pending zeroed EOF block range to complete + * if the given range [@offset, @end) fully covers it. Must be called + * outside the context of an active journal handle and hold the i_rwsem. + */ +int ext4_iomap_sync_zeroed_eof(struct inode *inode, loff_t offset, loff_t end) +{ + loff_t pstart, plen; + int ret; + + if (!ext4_inode_buffered_iomap(inode)) + return 0; + + plen = ext4_iomap_get_disksize_pending_range(inode, &pstart); + if (!plen || offset > pstart || end < pstart + plen) + return 0; + + ret = filemap_write_and_wait_range(inode->i_mapping, pstart, + pstart + plen - 1); + if (ret) + return ret; + + /* + * The pending bit should have been cleared by the ioend worker, + * which runs before the folio writeback flag is cleared. The + * caller holds i_rwsem, so no concurrent ext4_block_zero_eof() + * can re-set it. + */ + WARN_ON_ONCE(ext4_test_inode_state(inode, + EXT4_STATE_DISKSIZE_GROW_PENDING)); + return 0; +} + /* * Zero out a mapping from file offset 'from' up to the end of the block * which corresponds to 'from' or to the given 'end' inside this block. @@ -4994,6 +5027,14 @@ int ext4_update_disksize_before_punch(struct inode *inode, loff_t offset, if (offset > size) return 0; + /* + * We are going to punch the pending zeroed EOF block, persist + * it to ensure i_disksize can be safely updated thereafter. + */ + ret = ext4_iomap_sync_zeroed_eof(inode, offset, offset + len); + if (ret) + return ret; + if (offset + len < size) size = offset + len; if (EXT4_I(inode)->i_disksize >= size) From 2309d96c93ce5c4eb243757eb6b5805fb57d831f Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:30:27 +0800 Subject: [PATCH 36/41] ext4: clear DISKSIZE_GROW_PENDING on truncate or error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The disksize-grow-pending state is set when a zeroed EOF block is queued for writeback and cleared by the ioend completion path once writeback finishes. However, the zeroed block may be discarded before writeback completes — through folio discard, truncate, or unlink and inode eviction. In any of these cases, leaving the bit set would block subsequent writeback indefinitely. Therefore, we must clear it on all paths that invalidate the pending block before writeback completes: - In ext4_iomap_discard_folio(), on folio discard. - In ext4_evict_inode(), when an unlinked inode is destroyed (don't bother with an inode that has nlink > 0, since it is only evicted after its dirty pages have been written out, and also don't bother with bad inodes, since they can never have the bit set). In ext4_truncate_down(), truncating past the pending zeroed EOF block also invalidates the pending disksize update, so the bit must be cleared there as well. Finally, add a WARN_ON in ext4_destroy_inode() to catch any inode destroyed with the bit still set. The check is skipped when the filesystem is in an error or emergency state, as the pending block may not have been written back in those cases. Note that EXT4_STATE_DISKSIZE_GROW_PENDING is not set for now, and it will be set after everthing is done. Signed-off-by: Zhang Yi Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103033.2920530-27-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/inode.c | 24 +++++++++++++++++++++++- fs/ext4/super.c | 8 ++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index 61ccb3c5390a..1fb020167323 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -266,6 +266,8 @@ void ext4_evict_inode(struct inode *inode) if (ext4_should_order_data(inode)) ext4_begin_ordered_truncate(inode, 0); + if (ext4_inode_buffered_iomap(inode)) + ext4_iomap_clear_disksize_pending(inode); truncate_inode_pages_final(&inode->i_data); /* @@ -4350,8 +4352,17 @@ static void ext4_iomap_discard_folio(struct folio *folio, loff_t pos) { struct inode *inode = folio->mapping->host; loff_t length = folio_pos(folio) + folio_size(folio) - pos; + loff_t pstart, plen; ext4_iomap_punch_delalloc(inode, pos, length, NULL); + + /* + * Clear the disksize-grow-pending state if the zeroed EOF block + * fails to write back and is discarded. + */ + plen = ext4_iomap_get_disksize_pending_range(inode, &pstart); + if (plen && pos <= pstart && folio_next_pos(folio) >= pstart + plen) + ext4_iomap_clear_disksize_pending(inode); } static ssize_t ext4_iomap_writeback_range(struct iomap_writepage_ctx *wpc, @@ -6761,7 +6772,18 @@ static int ext4_truncate_down(struct inode *inode, loff_t oldsize, start_lblk = newsize > 0 ? (newsize - 1) >> inode->i_blkbits : 0; ext4_fc_track_range(handle, inode, start_lblk, EXT_MAX_BLOCKS - 1); - ext4_set_inode_size(inode, newsize); + down_write(&EXT4_I(inode)->i_data_sem); + /* + * Truncate the zeroed EOF block invalidates the pending disksize + * update, so clear the disksize-grow-pending state. + */ + if (ext4_test_inode_state(inode, EXT4_STATE_DISKSIZE_GROW_PENDING) && + (newsize <= EXT4_I(inode)->i_disksize)) + ext4_iomap_clear_disksize_pending(inode); + + i_size_write(inode, newsize); + __ext4_set_i_disksize(inode, newsize); + up_write(&EXT4_I(inode)->i_data_sem); ret = ext4_mark_inode_dirty(handle, inode); ext4_journal_stop(handle); diff --git a/fs/ext4/super.c b/fs/ext4/super.c index 23dbdcd598fa..56bde3e439a5 100644 --- a/fs/ext4/super.c +++ b/fs/ext4/super.c @@ -1497,6 +1497,14 @@ static void ext4_destroy_inode(struct inode *inode) "Inode %llu (%p): i_reserved_data_blocks (%u) not cleared!", inode->i_ino, EXT4_I(inode), EXT4_I(inode)->i_reserved_data_blocks); + + if (!(EXT4_SB(inode->i_sb)->s_mount_state & EXT4_ERROR_FS) && + !ext4_emergency_state(inode->i_sb) && + WARN_ON_ONCE(ext4_test_inode_state(inode, + EXT4_STATE_DISKSIZE_GROW_PENDING))) + ext4_msg(inode->i_sb, KERN_ERR, + "Inode %llu (%p): EXT4_STATE_DISKSIZE_GROW_PENDING not cleared!", + inode->i_ino, EXT4_I(inode)); } static void ext4_shutdown(struct super_block *sb) From e766cff471218142abfb0fba2ad3b16bbecce833 Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:30:28 +0800 Subject: [PATCH 37/41] ext4: set DISKSIZE_GROW_PENDING after zeroing unaligned EOF block 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 Signed-off-by: Zhang Yi Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103033.2920530-28-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/inode.c | 106 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 94 insertions(+), 12 deletions(-) diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index 1fb020167323..7820e6d8126f 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -4847,6 +4847,89 @@ 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)); + + /* + * If the zeroed range does not overlap the existing on-disk tail + * block, the zeroed data lies beyond the currently on-disk data + * and will be written back before i_disksize is advanced past it, + * so no stale data can be exposed. + * + * Checking i_disksize here (after folio_wait_writeback()) is + * necessary and safe. If a delalloc writeback of this folio was + * in-flight, it could be raced by a concurrent mmap write which + * corrupts the tail block beyond i_size but the i_disksize is not + * advanced. folio_wait_writeback() has waited for its completion + * and the ioend has advanced i_disksize accordingly. If writeback + * had not started, we don't need to mark any pending state because + * any future writeback will carry the pagecache content that now + * includes the zeroed data, so no stale data can appear on disk + * even without the pending tracking. + */ + if (from >= round_up(READ_ONCE(EXT4_I(inode)->i_disksize), + i_blocksize(inode))) + goto out; + + /* + * 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 discarded. + */ + 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); +out: + 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 @@ -4923,21 +5006,20 @@ 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)) { - handle_t *handle; + if (did_zero && zero_written && !IS_DAX(inode)) { + 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; } From b7e23e0583541f96cd795f67a132397b44451d8f Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:30:29 +0800 Subject: [PATCH 38/41] ext4: add tracepoints for DISKSIZE_GROW_PENDING set, clear, and wait Add trace events ext4_iomap_mark_disksize_pending(), ext4_iomap_clear_disksize_pending(), and ext4_iomap_wait_disksize_pending() to track disksize-grow-pending state changes and waiting. Signed-off-by: Zhang Yi Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103033.2920530-29-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/inode.c | 15 ++++++++++++--- include/trace/events/ext4.h | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index 7820e6d8126f..cb2b089d4419 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -133,9 +133,12 @@ void ext4_inode_csum_set(struct inode *inode, struct ext4_inode *raw, */ void ext4_iomap_clear_disksize_pending(struct inode *inode) { - clear_and_wake_up_bit( + if (!test_and_clear_wake_up_bit( ext4_inode_state_wait_bit(EXT4_STATE_DISKSIZE_GROW_PENDING), - ext4_inode_state_wait_word(inode)); + ext4_inode_state_wait_word(inode))) + return; + + trace_ext4_iomap_clear_disksize_pending(inode); } /* @@ -144,6 +147,10 @@ void ext4_iomap_clear_disksize_pending(struct inode *inode) */ void ext4_iomap_wait_disksize_pending(struct inode *inode) { + /* Only emit the trace when the bit is actually set */ + if (ext4_test_inode_state(inode, EXT4_STATE_DISKSIZE_GROW_PENDING)) + trace_ext4_iomap_wait_disksize_pending(inode); + wait_on_bit(ext4_inode_state_wait_word(inode), ext4_inode_state_wait_bit(EXT4_STATE_DISKSIZE_GROW_PENDING), TASK_UNINTERRUPTIBLE); @@ -4922,8 +4929,10 @@ static int ext4_iomap_mark_disksize_pending(struct inode *inode, loff_t from) */ if (likely(folio_test_dirty(folio) && !ext4_test_inode_state(inode, - EXT4_STATE_DISKSIZE_GROW_PENDING))) + EXT4_STATE_DISKSIZE_GROW_PENDING))) { + trace_ext4_iomap_mark_disksize_pending(inode); ext4_set_inode_state(inode, EXT4_STATE_DISKSIZE_GROW_PENDING); + } out: folio_unlock(folio); folio_put(folio); diff --git a/include/trace/events/ext4.h b/include/trace/events/ext4.h index bcf80f518e61..d33456d06b00 100644 --- a/include/trace/events/ext4.h +++ b/include/trace/events/ext4.h @@ -3204,6 +3204,39 @@ DEFINE_SET_IOMAP_EVENT(ext4_iomap_buffered_write_begin); DEFINE_SET_IOMAP_EVENT(ext4_iomap_map_writeback_range); DEFINE_SET_IOMAP_EVENT(ext4_iomap_zero_begin); +DECLARE_EVENT_CLASS(ext4_iomap_disksize_pending, + TP_PROTO(struct inode *inode), + TP_ARGS(inode), + TP_STRUCT__entry( + __field(dev_t, dev) + __field(u64, ino) + __field(loff_t, i_disksize) + ), + TP_fast_assign( + __entry->dev = inode->i_sb->s_dev; + __entry->ino = inode->i_ino; + __entry->i_disksize = READ_ONCE(EXT4_I(inode)->i_disksize); + ), + TP_printk("dev %d:%d ino %llu i_disksize %lld", + MAJOR(__entry->dev), MINOR(__entry->dev), + __entry->ino, __entry->i_disksize) +); + +DEFINE_EVENT(ext4_iomap_disksize_pending, ext4_iomap_mark_disksize_pending, + TP_PROTO(struct inode *inode), + TP_ARGS(inode) +); + +DEFINE_EVENT(ext4_iomap_disksize_pending, ext4_iomap_clear_disksize_pending, + TP_PROTO(struct inode *inode), + TP_ARGS(inode) +); + +DEFINE_EVENT(ext4_iomap_disksize_pending, ext4_iomap_wait_disksize_pending, + TP_PROTO(struct inode *inode), + TP_ARGS(inode) +); + #endif /* _TRACE_EXT4_H */ /* This part must be outside protection */ From 1b15129d9af7723878d433d66b9a3fe2acffabc4 Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:38:29 +0800 Subject: [PATCH 39/41] ext4: add tracepoints for EOF block zeroing and disksize-grow I/O Add tracepoints to track the disksize-grow-pending lifecycle in the writeback path and the block-zero-EOF entry point: - ext4_iomap_wb_disksize_pending_submit: ioend is marked as DISKSIZE_GROW_IO in ext4_iomap_writeback_submit(). - ext4_iomap_wb_disksize_pending_complete: ioend of type DISKSIZE_GROW_IO completes in ext4_iomap_finish_ioend(). - ext4_iomap_wb_disksize_pending_wait: ioend worker waits for the pending zeroed EOF block to complete. - ext4_iomap_wb_update_disksize: i_disksize is advanced in ext4_iomap_wb_update_disksize, including the new value and whether the update is a disksize-grow completion. - ext4_block_zero_eof: ext4_block_zero_eof is called with the range and zeroing outcome, capturing the producer-side entry point. Together with the previous mark/clear/wait tracepoints, these cover the full lifetime of the disksize-grow-pending state. Signed-off-by: Zhang Yi Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103831.2978767-1-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/inode.c | 3 + fs/ext4/page-io.c | 11 ++- include/trace/events/ext4.h | 129 ++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 1 deletion(-) diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index cb2b089d4419..8cb6551684b3 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -4417,6 +4417,8 @@ static int ext4_iomap_writeback_submit(struct iomap_writepage_ctx *wpc, round_down(ioend->io_offset, blocksize) <= pstart && round_up(ioend->io_offset + ioend->io_size, blocksize) >= pstart + plen) { + trace_ext4_iomap_wb_disksize_pending_submit(inode, + ioend->io_offset, ioend->io_size); ioend->io_bio.bi_end_io = ext4_iomap_end_bio; ioend->io_private = (void *)EXT4_IOMAP_IOEND_DISKSIZE_GROW_IO; } @@ -5033,6 +5035,7 @@ int ext4_block_zero_eof(struct inode *inode, loff_t from, loff_t end) return err; } + trace_ext4_block_zero_eof(inode, from, length, did_zero, zero_written); return 0; } diff --git a/fs/ext4/page-io.c b/fs/ext4/page-io.c index 4f1176b9332f..734d856a6d47 100644 --- a/fs/ext4/page-io.c +++ b/fs/ext4/page-io.c @@ -31,6 +31,8 @@ #include "xattr.h" #include "acl.h" +#include + static struct kmem_cache *io_end_cachep; static struct kmem_cache *io_end_vec_cachep; @@ -574,6 +576,7 @@ static void ext4_iomap_wb_disksize_pending_wait(struct inode *inode, if (!plen || pos < pstart + plen) return; + trace_ext4_iomap_wb_disksize_pending_wait(inode, pos, size); ext4_iomap_wait_disksize_pending(inode); } @@ -617,8 +620,11 @@ static int ext4_iomap_wb_update_disksize(handle_t *handle, struct inode *inode, * after the data has been persisted. */ new_disksize = is_disksize_grow ? i_size : min(end, i_size); - if (new_disksize > ei->i_disksize) + if (new_disksize > ei->i_disksize) { + trace_ext4_iomap_wb_update_disksize(inode, end, i_size, + ei->i_disksize, new_disksize, is_disksize_grow); WRITE_ONCE(ei->i_disksize, new_disksize); + } up_write(&ei->i_data_sem); ret = ext4_mark_inode_dirty(handle, inode); if (ret) @@ -641,6 +647,9 @@ static void ext4_iomap_finish_ioend(struct iomap_ioend *ioend) int ret, err; ret = blk_status_to_errno(ioend->io_bio.bi_status); + if (is_disksize_grow) + trace_ext4_iomap_wb_disksize_pending_complete(ioend->io_inode, + ioend->io_offset, ioend->io_size, ret); if (unlikely(ret)) { if (test_opt(sb, DATA_ERR_ABORT) && !ext4_emergency_state(sb)) jbd2_journal_abort(EXT4_SB(sb)->s_journal, ret); diff --git a/include/trace/events/ext4.h b/include/trace/events/ext4.h index d33456d06b00..92048da9dae4 100644 --- a/include/trace/events/ext4.h +++ b/include/trace/events/ext4.h @@ -3237,6 +3237,135 @@ DEFINE_EVENT(ext4_iomap_disksize_pending, ext4_iomap_wait_disksize_pending, TP_ARGS(inode) ); +/* disksize pending I/O tracepoints for iomap Buffered I/O path */ +DECLARE_EVENT_CLASS(ext4_iomap_wb_disksize_pending, + TP_PROTO(struct inode *inode, loff_t io_offset, size_t io_size), + TP_ARGS(inode, io_offset, io_size), + TP_STRUCT__entry( + __field(dev_t, dev) + __field(u64, ino) + __field(loff_t, io_offset) + __field(size_t, io_size) + __field(loff_t, i_size) + __field(loff_t, i_disksize) + ), + TP_fast_assign( + __entry->dev = inode->i_sb->s_dev; + __entry->ino = inode->i_ino; + __entry->io_offset = io_offset; + __entry->io_size = io_size; + __entry->i_size = i_size_read(inode); + __entry->i_disksize = READ_ONCE(EXT4_I(inode)->i_disksize); + ), + TP_printk("dev %d:%d ino %llu io_offset %lld io_size %zu i_size %lld i_disksize %lld", + MAJOR(__entry->dev), MINOR(__entry->dev), + __entry->ino, __entry->io_offset, __entry->io_size, + __entry->i_size, __entry->i_disksize) +); + +DEFINE_EVENT(ext4_iomap_wb_disksize_pending, + ext4_iomap_wb_disksize_pending_submit, + TP_PROTO(struct inode *inode, loff_t io_offset, size_t io_size), + TP_ARGS(inode, io_offset, io_size) +); + +DEFINE_EVENT(ext4_iomap_wb_disksize_pending, + ext4_iomap_wb_disksize_pending_wait, + TP_PROTO(struct inode *inode, loff_t io_offset, size_t io_size), + TP_ARGS(inode, io_offset, io_size) +); + +TRACE_EVENT(ext4_iomap_wb_disksize_pending_complete, + TP_PROTO(struct inode *inode, loff_t io_offset, size_t io_size, + int ret), + TP_ARGS(inode, io_offset, io_size, ret), + TP_STRUCT__entry( + __field(dev_t, dev) + __field(u64, ino) + __field(loff_t, io_offset) + __field(size_t, io_size) + __field(loff_t, i_size) + __field(loff_t, i_disksize) + __field(int, ret) + ), + TP_fast_assign( + __entry->dev = inode->i_sb->s_dev; + __entry->ino = inode->i_ino; + __entry->io_offset = io_offset; + __entry->io_size = io_size; + __entry->i_size = i_size_read(inode); + __entry->i_disksize = READ_ONCE(EXT4_I(inode)->i_disksize); + __entry->ret = ret; + ), + TP_printk("dev %d:%d ino %llu io_offset %lld io_size %zu ret %d i_size %lld i_disksize %lld", + MAJOR(__entry->dev), MINOR(__entry->dev), + __entry->ino, __entry->io_offset, __entry->io_size, + __entry->ret, __entry->i_size, __entry->i_disksize) +); + +/* i_disksize update tracepoint */ +TRACE_EVENT(ext4_iomap_wb_update_disksize, + TP_PROTO(struct inode *inode, loff_t end, loff_t i_size, + loff_t i_disksize, loff_t new_disksize, + bool is_disksize_grow), + TP_ARGS(inode, end, i_size, i_disksize, new_disksize, + is_disksize_grow), + TP_STRUCT__entry( + __field(dev_t, dev) + __field(u64, ino) + __field(loff_t, end) + __field(loff_t, i_size) + __field(loff_t, i_disksize) + __field(loff_t, new_disksize) + __field(bool, is_disksize_grow) + ), + TP_fast_assign( + __entry->dev = inode->i_sb->s_dev; + __entry->ino = inode->i_ino; + __entry->end = end; + __entry->i_size = i_size; + __entry->i_disksize = i_disksize; + __entry->new_disksize = new_disksize; + __entry->is_disksize_grow = is_disksize_grow; + ), + TP_printk("dev %d:%d ino %llu end %lld i_size %lld i_disksize %lld new_disksize %lld is_disksize_grow %d", + MAJOR(__entry->dev), MINOR(__entry->dev), + __entry->ino, __entry->end, __entry->i_size, + __entry->i_disksize, __entry->new_disksize, + __entry->is_disksize_grow) +); + +/* Block zero EOF tracepoint */ +TRACE_EVENT(ext4_block_zero_eof, + TP_PROTO(struct inode *inode, loff_t from, loff_t length, + bool did_zero, bool zero_written), + TP_ARGS(inode, from, length, did_zero, zero_written), + TP_STRUCT__entry( + __field(dev_t, dev) + __field(u64, ino) + __field(loff_t, from) + __field(loff_t, length) + __field(loff_t, i_size) + __field(loff_t, i_disksize) + __field(bool, did_zero) + __field(bool, zero_written) + ), + TP_fast_assign( + __entry->dev = inode->i_sb->s_dev; + __entry->ino = inode->i_ino; + __entry->from = from; + __entry->length = length; + __entry->i_size = inode->i_size; + __entry->i_disksize = READ_ONCE(EXT4_I(inode)->i_disksize); + __entry->did_zero = did_zero; + __entry->zero_written = zero_written; + ), + TP_printk("dev %d:%d ino %llu zero EOF from %lld length %lld i_size %lld i_disksize %lld did_zero %d zero_written %d", + MAJOR(__entry->dev), MINOR(__entry->dev), __entry->ino, + __entry->from, __entry->length, __entry->i_size, + __entry->i_disksize, __entry->did_zero, __entry->zero_written) +); + #endif /* _TRACE_EXT4_H */ /* This part must be outside protection */ From bdcbb0f19e921c7ea3241f55922032dff23d1d26 Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:38:30 +0800 Subject: [PATCH 40/41] ext4: partially enable iomap for the buffered I/O path of regular files Introduce ext4_enable_buffered_iomap() to determine whether a regular file inode should use the iomap buffered I/O path. We now support the default filesystem features, mount options, and the bigalloc feature. However, inline data, fsverity, fscrypt, indirect inode type, and data=journal mode are not fully supported. The decision is made at inode initialization time in __ext4_new_inode() and __ext4_iget() by setting the EXT4_STATE_BUFFERED_IOMAP state flag. If any of these unsupported features are met, the inode silently falls back to the traditional buffer_head path. Switching the buffered I/O path on an active inode is not supported, with the exception of changing a per-inode journal flag. For features like encryption, verity, and inline data that can be dynamically enabled at the superblock level, checking the global feature flag avoids the complexity of toggling the path on individual inodes. Additionally: - Extend ext4_inode_journal_mode() to force ordered mode for inodes using the iomap path under a data=journal mount. For the global data journal mode (EXT4_MOUNT_JOURNAL_DATA), dynamic enablement is deferred until the next inode re-initialization. For the per-inode data journal mode (EXT4_INODE_JOURNAL_DATA), dynamic changes take effect immediately, as it is safe to switch address_space operations and drop all page cache under i_rwsem and filemap_invalidate_lock. - Add a WARN_ON_ONCE() guard in _ext4_get_block() to catch inodes using the iomap path from accidentally entering the legacy buffer_head writeback path. - Place silent BUFFERED_IOMAP checks in ext4_do_writepages() and ext4_iomap_writepages() under the writepages rwsem. Unlike _ext4_get_block(), the writeback path does not hold i_rwsem or invalidate_lock and can race with ext4_change_inode_journal_flag() switching the inode's a_ops. - Reject extent-to-indirect migration via ext4_ind_migrate() for inodes on the iomap path. Signed-off-by: Zhang Yi Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103831.2978767-2-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/ext4.h | 1 + fs/ext4/ext4_jbd2.c | 8 +++- fs/ext4/ialloc.c | 1 + fs/ext4/inode.c | 105 +++++++++++++++++++++++++++++++++++++++++++- fs/ext4/migrate.c | 2 + 5 files changed, 113 insertions(+), 4 deletions(-) diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h index 2fecf0bb5cf6..64eaf6c64ccd 100644 --- a/fs/ext4/ext4.h +++ b/fs/ext4/ext4.h @@ -3170,6 +3170,7 @@ int ext4_walk_page_buffers(handle_t *handle, int do_journal_get_write_access(handle_t *handle, struct inode *inode, struct buffer_head *bh); void ext4_set_inode_mapping_order(struct inode *inode); +void ext4_enable_buffered_iomap(struct inode *inode); int ext4_nonda_switch(struct super_block *sb); #define FALL_BACK_TO_NONDELALLOC 1 #define EXT4_WRITE_DATA_INLINE 2 diff --git a/fs/ext4/ext4_jbd2.c b/fs/ext4/ext4_jbd2.c index 53ddedb52a6f..a4664ddecdcd 100644 --- a/fs/ext4/ext4_jbd2.c +++ b/fs/ext4/ext4_jbd2.c @@ -17,8 +17,12 @@ int ext4_inode_journal_mode(struct inode *inode) test_opt(inode->i_sb, DATA_FLAGS) == EXT4_MOUNT_JOURNAL_DATA || (ext4_test_inode_flag(inode, EXT4_INODE_JOURNAL_DATA) && !test_opt(inode->i_sb, DELALLOC))) { - /* We do not support data journalling for encrypted data */ - if (S_ISREG(inode->i_mode) && IS_ENCRYPTED(inode)) + /* + * We do not support data journalling for encrypted data + * and buffered IOMAP path. + */ + if (S_ISREG(inode->i_mode) && + (IS_ENCRYPTED(inode) || ext4_inode_buffered_iomap(inode))) return EXT4_INODE_ORDERED_DATA_MODE; /* ordered */ return EXT4_INODE_JOURNAL_DATA_MODE; /* journal data */ } diff --git a/fs/ext4/ialloc.c b/fs/ext4/ialloc.c index a5831fc536db..f97a2f4904eb 100644 --- a/fs/ext4/ialloc.c +++ b/fs/ext4/ialloc.c @@ -1346,6 +1346,7 @@ struct inode *__ext4_new_inode(struct mnt_idmap *idmap, } } + ext4_enable_buffered_iomap(inode); ext4_set_inode_mapping_order(inode); ext4_update_inode_fsync_trans(handle, inode, 1); diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index 8cb6551684b3..12106bcb077e 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -1039,6 +1039,9 @@ static int _ext4_get_block(struct inode *inode, sector_t iblock, if (ext4_has_inline_data(inode)) return -ERANGE; + /* inode using the iomap buffered I/O path should not go here. */ + if (WARN_ON_ONCE(ext4_inode_buffered_iomap(inode))) + return -EINVAL; map.m_lblk = iblock; map.m_len = bh->b_size >> inode->i_blkbits; @@ -2915,6 +2918,13 @@ static int ext4_do_writepages(struct mpage_da_data *mpd) if (!mapping->nrpages || !mapping_tagged(mapping, PAGECACHE_TAG_DIRTY)) goto out_writepages; + /* + * Does ext4_change_inode_journal_flag() change the inode's + * buffered I/O path? + */ + if (ext4_inode_buffered_iomap(inode)) + goto out_writepages; + /* * If the filesystem has aborted, it is read-only, so return * right away instead of dumping stack traces later on that @@ -4053,6 +4063,9 @@ static int ext4_iomap_map_blocks(struct inode *inode, loff_t offset, { u8 blkbits = inode->i_blkbits; + /* inode using the buffer_head buffered I/O path should not go here. */ + if (WARN_ON_ONCE(!ext4_inode_buffered_iomap(inode))) + return -EINVAL; if ((offset >> blkbits) > EXT4_MAX_LOGICAL_BLOCK) return -EINVAL; @@ -4496,6 +4509,15 @@ static int ext4_iomap_writepages(struct address_space *mapping, ext4_iomap_wb_submit_zeroed_eof(inode, wbc); alloc_ctx = ext4_writepages_down_read(sb); + /* + * Does ext4_change_inode_journal_flag() change the inode's + * buffered I/O path? + */ + if (!ext4_inode_buffered_iomap(inode)) { + ext4_writepages_up_read(sb, alloc_ctx); + return ret; + } + trace_ext4_writepages(inode, wbc); ret = iomap_writepages(&wpc); trace_ext4_writepages_result(inode, wbc, ret, nr - wbc->nr_to_write); @@ -6076,6 +6098,81 @@ static int check_igot_inode(struct inode *inode, ext4_iget_flags flags, return -EFSCORRUPTED; } +/* + * Determine whether an inode should use the iomap buffered I/O path. + * EXT4_STATE_BUFFERED_IOMAP is generally set at inode initialization + * time. Online switching of the buffered I/O path on an active inode is + * NOT supported, with the exception of changing a per-inode journal + * flag. + * + * For features like inline data, fsverity, and encryption that can be + * dynamically enabled or disabled, we check the superblock-level + * feature flags. If any of these is globally enabled, no inode is + * allowed into the iomap buffered I/O path. This avoids the complexity + * of dynamic toggling. + * + * For the global data journal mode (EXT4_MOUNT_JOURNAL_DATA), dynamic + * change through remount is deferred. It will only become available + * after the inode is re-initialized (i.e., after the last reference + * drops and the inode is re-read from disk with the journal flag + * cleared). + * + * For the per-inode data journal mode (EXT4_INODE_JOURNAL_DATA), + * dynamic changes take effect immediately. This is safe because + * address_space operations can be switched and all page cache can be + * dropped under i_rwsem and filemap_invalidate_lock. + * + * For extent-to-indirect block migration (via EXT4_IOC_SETFLAGS + * clearing EXT4_EXTENTS_FL), this operation is directly rejected for + * inodes using the iomap path. + */ +void ext4_enable_buffered_iomap(struct inode *inode) +{ + struct super_block *sb = inode->i_sb; + + if (!S_ISREG(inode->i_mode)) + return; + if (ext4_test_inode_flag(inode, EXT4_INODE_EA_INODE)) + return; + + /* Unsupported Features */ + if (ext4_has_feature_inline_data(sb)) + return; + if (ext4_has_feature_verity(sb)) + return; + if (ext4_has_feature_encrypt(sb)) + return; + if (test_opt(sb, DATA_FLAGS) == EXT4_MOUNT_JOURNAL_DATA || + ext4_test_inode_flag(inode, EXT4_INODE_JOURNAL_DATA)) + return; + if (!(ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS))) + return; + + ext4_set_inode_state(inode, EXT4_STATE_BUFFERED_IOMAP); + + /* + * Install the iomap end_io handler on the shared conversion + * work. This is safe at inode initialization and during the + * buffered I/O path changes where we flush all pending + * writebacks and drop page cache under i_rwsem and + * filemap_invalidate_lock. + */ + INIT_WORK(&EXT4_I(inode)->i_rsv_conversion_work, ext4_iomap_end_io); +} + +static void ext4_disable_buffered_iomap(struct inode *inode) +{ + ext4_clear_inode_state(inode, EXT4_STATE_BUFFERED_IOMAP); + + /* + * Reinstall the buffer_head end_io handler on the shared + * conversion work. This is safe during the buffered I/O path + * changes where we flush all pending writebacks and drop page + * cache under i_rwsem and filemap_invalidate_lock. + */ + INIT_WORK(&EXT4_I(inode)->i_rsv_conversion_work, ext4_end_io_rsv_work); +} + void ext4_set_inode_mapping_order(struct inode *inode) { struct super_block *sb = inode->i_sb; @@ -6390,6 +6487,8 @@ struct inode *__ext4_iget(struct super_block *sb, unsigned long ino, if (ret) goto bad_inode; + ext4_enable_buffered_iomap(inode); + if (S_ISREG(inode->i_mode)) { inode->i_op = &ext4_file_inode_operations; inode->i_fop = &ext4_file_operations; @@ -7626,9 +7725,10 @@ int ext4_change_inode_journal_flag(struct inode *inode, int val) * the inode's in-core data-journaling state flag now. */ - if (val) + if (val) { ext4_set_inode_flag(inode, EXT4_INODE_JOURNAL_DATA); - else { + ext4_disable_buffered_iomap(inode); + } else { err = jbd2_journal_flush(journal, 0); if (err < 0) { jbd2_journal_unlock_updates(journal); @@ -7637,6 +7737,7 @@ int ext4_change_inode_journal_flag(struct inode *inode, int val) return err; } ext4_clear_inode_flag(inode, EXT4_INODE_JOURNAL_DATA); + ext4_enable_buffered_iomap(inode); } ext4_set_aops(inode); ext4_set_inode_mapping_order(inode); diff --git a/fs/ext4/migrate.c b/fs/ext4/migrate.c index 5d60ef10fe11..09931d3ba2c6 100644 --- a/fs/ext4/migrate.c +++ b/fs/ext4/migrate.c @@ -621,6 +621,8 @@ int ext4_ind_migrate(struct inode *inode) if (ext4_has_feature_bigalloc(inode->i_sb)) return -EOPNOTSUPP; + if (ext4_inode_buffered_iomap(inode)) + return -EOPNOTSUPP; /* * In order to get correct extent info, force all delayed allocation From 8c4f6e3376348fc3162eed86a05b933921c8f9ed Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Fri, 9 Oct 2026 18:38:31 +0800 Subject: [PATCH 41/41] ext4: introduce a mount option for iomap buffered I/O path Since the iomap buffered I/O path does not yet support all existing ext4 features, it cannot be enabled by default. Introduce the 'buffered_iomap' and 'nobuffered_iomap' mount options to explicitly enable or disable the iomap buffered I/O path for regular files. Toggling this option via remount is allowed. The change of I/O path will not take effect immediately. It will be deferred. The new setting will only take effect after the inode is re-initialized (i.e., after the last reference is dropped and the inode is re-read from disk). Signed-off-by: Zhang Yi Tested-by: syzbot@syzkaller.appspotmail.com Link: https://patch.msgid.link/20261009103831.2978767-3-yi.zhang@huaweicloud.com Signed-off-by: Theodore Ts'o --- fs/ext4/ext4.h | 1 + fs/ext4/inode.c | 6 ++++++ fs/ext4/super.c | 7 +++++++ 3 files changed, 14 insertions(+) diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h index 64eaf6c64ccd..8d3af062a830 100644 --- a/fs/ext4/ext4.h +++ b/fs/ext4/ext4.h @@ -1321,6 +1321,7 @@ struct ext4_inode_info { * scanning in mballoc */ #define EXT4_MOUNT2_ABORT 0x00000100 /* Abort filesystem */ +#define EXT4_MOUNT2_BUFFERED_IOMAP 0x00000200 /* Use iomap for buffered I/O */ #define clear_opt(sb, opt) EXT4_SB(sb)->s_mount_opt &= \ ~EXT4_MOUNT_##opt diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index 12106bcb077e..6ede3644a738 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -6125,11 +6125,17 @@ static int check_igot_inode(struct inode *inode, ext4_iget_flags flags, * For extent-to-indirect block migration (via EXT4_IOC_SETFLAGS * clearing EXT4_EXTENTS_FL), this operation is directly rejected for * inodes using the iomap path. + * + * When remounting to toggle the buffered_iomap mount option, the change + * of I/O path is deferred as well, it will be available after the inode + * is re-initialized. */ void ext4_enable_buffered_iomap(struct inode *inode) { struct super_block *sb = inode->i_sb; + if (!test_opt2(sb, BUFFERED_IOMAP)) + return; if (!S_ISREG(inode->i_mode)) return; if (ext4_test_inode_flag(inode, EXT4_INODE_EA_INODE)) diff --git a/fs/ext4/super.c b/fs/ext4/super.c index 56bde3e439a5..7d8aedfbbb87 100644 --- a/fs/ext4/super.c +++ b/fs/ext4/super.c @@ -1747,6 +1747,7 @@ enum { Opt_discard, Opt_nodiscard, Opt_init_itable, Opt_noinit_itable, Opt_max_dir_size_kb, Opt_nojournal_checksum, Opt_nombcache, Opt_no_prefetch_block_bitmaps, Opt_mb_optimize_scan, + Opt_buffered_iomap, Opt_nobuffered_iomap, Opt_errors, Opt_data, Opt_data_err, Opt_jqfmt, Opt_dax_type, #ifdef CONFIG_EXT4_DEBUG Opt_fc_debug_max_replay, Opt_fc_debug_force @@ -1885,6 +1886,8 @@ static const struct fs_parameter_spec ext4_param_specs[] = { fsparam_flag ("no_prefetch_block_bitmaps", Opt_no_prefetch_block_bitmaps), fsparam_s32 ("mb_optimize_scan", Opt_mb_optimize_scan), + fsparam_flag ("buffered_iomap", Opt_buffered_iomap), + fsparam_flag ("nobuffered_iomap", Opt_nobuffered_iomap), fsparam_string ("check", Opt_removed), /* mount option from ext2/3 */ fsparam_flag ("nocheck", Opt_removed), /* mount option from ext2/3 */ fsparam_flag ("reservation", Opt_removed), /* mount option from ext2/3 */ @@ -1978,6 +1981,10 @@ static const struct mount_opts { {Opt_nombcache, EXT4_MOUNT_NO_MBCACHE, MOPT_SET}, {Opt_no_prefetch_block_bitmaps, EXT4_MOUNT_NO_PREFETCH_BLOCK_BITMAPS, MOPT_SET}, + {Opt_buffered_iomap, EXT4_MOUNT2_BUFFERED_IOMAP, + MOPT_SET | MOPT_2 | MOPT_EXT4_ONLY}, + {Opt_nobuffered_iomap, EXT4_MOUNT2_BUFFERED_IOMAP, + MOPT_CLEAR | MOPT_2 | MOPT_EXT4_ONLY}, #ifdef CONFIG_EXT4_DEBUG {Opt_fc_debug_force, EXT4_MOUNT2_JOURNAL_FAST_COMMIT, MOPT_SET | MOPT_2 | MOPT_EXT4_ONLY},