Linux EXT4 FS development
 help / color / mirror / Atom feed
* [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path
@ 2026-08-14  9:32 Zhang Yi
  2026-08-14  9:33 ` [PATCH -next v5 01/32] ext4: simplify size updating in ext4_setattr() Zhang Yi
                   ` (31 more replies)
  0 siblings, 32 replies; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:32 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

Hi, all

This is v5 of the series converting ext4's buffered I/O path from
buffer_head to iomap. This iteration extends ext4_map_blocks() with
flexible journal handle management, reworks the buffered write path and
writeback path to avoid open-coded mapping block operations, rewrites
the ordering logic between EOF zeroing and i_disksize updates, postpones
submission of zeroed EOF blocks for better concurrency, and includes
several fixes for stale delalloc extents, partial block zeroing
failures, and ordered I/O wait issues. Rebased on next-20260810 and
collected RVB from Ojaswin and Jan.


You can get commits here:

 https://github.com/zhangyi089/linux/commits/ext4_buffered_iomap_v5/

Patch 01-03: Simplify truncate operations and drop unnecessary ordered
             I/O when zeroing EOF blocks. This prepares for the iomap
	     conversion.
Patch 04-21: Extend ext4_map_blocks() and implement core iomap buffered
             read/write, writeback, mmap, and partial block zeroing.
Patch 22-30: Handle ordered sequence for zeroing the EOF partial block
             and updating i_disksize when performing file extending on
	     unaligned EOF files.
Patch 31-32: Enable iomap buffered I/O path.


Tests and Performance:

Tested with xfstests-bld using -g auto, fast_commit, and 64k
configurations. No new test failures were observed, aside from the
known occasional failure of generic/127, which is an issue in the
MM large folio split logic and warrants a separate fix.

 https://lore.kernel.org/linux-fsdevel/a638a8fb-c184-4069-ae33-379ec12cd514@huaweicloud.com/

Performance was tested with FIO on a 4-core virtual machine with an
Intel(R) Xeon(R) Platinum 8380 CPU, backed by a 150GB RAM-backed
virtual-io block device.

Write perf tests (MiB/s)
========================

  Sync  : 1 = fsync() after each write
  OW    : 1 = overwrite existing blocks
  Dnc   : 1 = RWF_DONTCACHE used

  #1    : only write page cache, no writeback
  #2    : writeback triggered (set dirty_background_bytes = 100MB and
          dirty_bytes = 200MB)


  Config                   BS       ext4+bh   ext4+iomap    delta
  ---------------------------------------------------------------
  Sync=0 OW=0 Dnc=0 (#1)   1k          41.8         42.0    ~0%
  Sync=0 OW=0 Dnc=0 (#1)   4k           170          176    +4%
  Sync=0 OW=0 Dnc=0 (#1)   64k         1816         1981    +9%
  Sync=0 OW=0 Dnc=0 (#1)   1m          4295         5780   +35%

  Sync=0 OW=0 Dnc=0 (#2)   1k          42.6         43.2    +2%
  Sync=0 OW=0 Dnc=0 (#2)   4k           137          140    +2%
  Sync=0 OW=0 Dnc=0 (#2)   64k          734          957   +30%
  Sync=0 OW=0 Dnc=0 (#2)   1m          1460         1683   +15%

  Sync=0 OW=0 Dnc=1        1k          14.6         15.0    +3%
  Sync=0 OW=0 Dnc=1        4k          67.9         61.9    -9%
  Sync=0 OW=0 Dnc=1        64k          415          698   +68%
  Sync=0 OW=0 Dnc=1        1m          1404         2883  +105%

  Sync=1 OW=0 Dnc=0        1k          0.71         0.60   -15%
  Sync=1 OW=0 Dnc=0        4k          2.04         1.89    -7%
  Sync=1 OW=0 Dnc=0        64k         32.6         32.4    -1%
  Sync=1 OW=0 Dnc=0        1m           253          277   +10%

  Sync=0 OW=1 Dnc=0 (#1)   1k          19.1         21.0   +10%
  Sync=0 OW=1 Dnc=0 (#1)   4k           164          179    +9%
  Sync=0 OW=1 Dnc=0 (#1)   64k         1776         1867    +5%
  Sync=0 OW=1 Dnc=0 (#1)   1m          4105         4879   +19%

  Sync=0 OW=1 Dnc=0 (#2)   1k          19.9         21.6    +8%
  Sync=0 OW=1 Dnc=0 (#2)   4k           148          157    +6%
  Sync=0 OW=1 Dnc=0 (#2)   64k          843          840    ~0%
  Sync=0 OW=1 Dnc=0 (#2)   1m          1579         1806   +14%

  Sync=0 OW=1 Dnc=1        1k          17.4         17.9    +3%
  Sync=0 OW=1 Dnc=1        4k          80.4         87.7    +9%
  Sync=0 OW=1 Dnc=1        64k          806          923   +14%
  Sync=0 OW=1 Dnc=1        1m          1070         1122    +5%

  Sync=1 OW=1 Dnc=0        1k          0.82         0.82     0%
  Sync=1 OW=1 Dnc=0        4k          3.55         3.56    ~0%
  Sync=1 OW=1 Dnc=0        64k         48.4         49.8    +3%
  Sync=1 OW=1 Dnc=0        1m           325          336    +4%


Read tests (MiB/s)
==================

  READ HOLE           : read of a hole
  READ DATA           : read of file data without pre-exists page cache
  READ PRECACHED DATA : read of file data with warm page cache

  Scenario                 BS       ext4+bh   ext4+iomap    delta
  ---------------------------------------------------------------
  READ HOLE                1k          46.6         47.1    +1%
  READ HOLE                4k           189          198    +5%
  READ HOLE                64k         2011         2116    +5%
  READ HOLE                1m          4538         4684    +3%

  READ DATA                1k          44.6         50.4   +13%
  READ DATA                4k           178          182    +2%
  READ DATA                64k          741          768    +4%
  READ DATA                1m          1143         1210    +6%

  READ PRECACHED DATA      1k          50.8         50.7    ~0%
  READ PRECACHED DATA      4k           179          173    -3%
  READ PRECACHED DATA      64k         2009         1989    -1%
  READ PRECACHED DATA      1m          6296         6294    ~0%

Large I/O scenarios show significant write performance improvement,
while small I/O in synchronous and non-cached scenarios experience a
slight regression. Read performance shows no significant difference.


Changes since v4:
 - Rebase on next-20260810 and collects RVB from Ojaswin and Jan
   (exclude patches with substantial changes).
 - Add patch 04: skip ordered I/O wait when zeroing beyond the
   i_disksize block (suggested by Ojaswin).
 - Add patch 05: Set EXT4_MAP_NEW in ext4_da_map_blocks() to properly
   indicate that a new delayed allocation block has been inserted, fix
   the issue of stale delalloc extent on short iomap buffered write
   (pointed out by Ojaswin).
 - Add patch 06-10: expand ext4_map_blocks(), make it can 1) recheck
   extent status tree before allocating blocks, 2) allow starting it's
   own journal handle if the caller doesn't provide one, callers can
   query first on overwrite and start handle if necessary, 3) avoid
   unnecessary journal handle when callers query pre-existing unwritten
   extents, 4) skip allocating block for holes in data submitting path.
   Preparing for implementing the iomap writeback path (suggested by
   Jan).
 - In patch 13: update the commit message to correct the reason of
   passing out extent seq counter (pointed out by Jan).
 - In patch 14: add function graph to explain the issues encountered
   when trying to support data=ordered mode during iomap conversion
   (suggested by Ojaswin).
 - In patch 15, drop ext4_iomap_get_blocks(), which open-coded mapping
   block logic in the non-delalloc buffered write path, and use the
   extended ext4_map_blocks() directly instead.
 - In patch 16, First rework the writeback block mapping path, directly
   call extended ext4_map_block() instead of open coding. Then Ignore
   the data_error=abort case for now, and always queue the ioend worker
   in ext4_iomap_end_bio(). This simplifies the logic and makes it
   easier to switch to the new ioend infrastructure in the future. Then
   fix the double deferrals for RWF_DONTCACHE writes after the new
   generic bio complete in task framework merged. Finally, reuse the
   i_rsv_conversion_work and i_rsv_conversion_list instead of creat two
   new ones (suggested by Jan).
 - In patch 18, clarify the TODO comments in
   ext4_block_iomap_zero_range() and fix a race condition that could
   cause partial block zeroing to fail.
 - Add patch 19: Clarify and resolve the issue where endio fails to
   convert extents when punch hole is raced by writeback.
 - In patch 22-30, rework the ordering of EOF block zeroing and
   i_disksize updating. Use EXT4_STATE_DISKSIZE_GROW_PENDING state bit
   to check whether a pending EOF zeroing block is still awaiting
   submit, and query i_disksize when writeback dirty folios, only
   updating i_disksize when this block is written back or discarded
   (suggested by Jan). Besides, do not submit zeroed EOF block
   immediately after write zeroes, postpone it to improve concurrency
   (suggested by Ojaswin). Then, handle the cases of fallocate
   operations in delay updating i_disksize when the EOF zeroed block is
   still pending. Finally, add more tracepoints to track the state of
   the EOF block and I/Os.

v4:     https://lore.kernel.org/linux-ext4/20260511072344.191271-1-yi.zhang@huaweicloud.com/
v3:     https://lore.kernel.org/linux-ext4/20260422021042.4157510-1-yi.zhang@huaweicloud.com/
v2:     https://lore.kernel.org/linux-ext4/20260203062523.3869120-1-yi.zhang@huawei.com/
v1:     https://lore.kernel.org/linux-ext4/20241022111059.2566137-1-yi.zhang@huaweicloud.com/
RFC v4: https://lore.kernel.org/linux-ext4/20240410142948.2817554-1-yi.zhang@huaweicloud.com/
RFC v3: https://lore.kernel.org/linux-ext4/20240127015825.1608160-1-yi.zhang@huaweicloud.com/
RFC v2: https://lore.kernel.org/linux-ext4/20240102123918.799062-1-yi.zhang@huaweicloud.com/
RFC v1: https://lore.kernel.org/linux-ext4/20231123125121.4064694-1-yi.zhang@huaweicloud.com/

Comments and suggestions are welcome!

Thanks,
Yi.


Original Cover-letter
=====================

This series adds the iomap buffered I/O path support for regular files.
It implements the core iomap APIs on ext4 and introduces the
'buffered_iomap' mount option to enable the iomap buffered I/O path.
This supports default features, default mount options and bigalloc
feature. However, this does not support online defragmentation, inline
data, fsverify, fscrypt, non-extent inodes, and data=journal mode, we
will fall to buffered_head I/O path automatically if these features and
options are used.

This iomap buffered I/O path is not enabled by default because the
preceding features are not supported. Users can explicitly enable or
disable it via 'buffered_iomap' and 'nobuffered_iomap' mount options.

Key considerations

1. Lock ordering difference

   The lock ordering of folio lock and transaction start in the iomap
   path is the opposite of that in the buffer_head path.

2. data=ordered mode is not used

   Two main reasons:
   a) The lock ordering of folio lock and transaction start for
      data=ordered mode is opposite to the iomap path, which would cause
      a deadlock.
   b) The iomap writeback path does not support partial folio submission
      (required by data=ordered mode when block size < folio size, and
      it is currently handled by ext4_bio_write_folio()), which would
      also cause a deadlock.

   To replace data=ordered mode functionality:

   - For append write: Always allocate unwritten extents (dioread_nolock
     behavior) to prevent stale data exposure.

   - For post-EOF partial block zeroing, mark the inode as
     DISKSIZE_GROW_PENDING after zeroing the EOF block, and postpone
     updating i_disksize to i_size until the zeroed block has been
     written to disk or discarded (Suggested by Jan).

     Tradeoffs:
     1) i_disksize must be updated from io_end processing, so this
        mechanism is restricted to the buffered I/O iomap path due to
        locking constraints. This limitation will eventually go away
	once everything is converted to iomap.
     2) After a crash, i_disksize may be updated while the file content
        shows zeros. This is acceptable for simplicity and does not
	break any existing guarantees.

   - For online defragmentation: Not supported yet, needs further
     consideration.

3. Always enable dioread_nolock

   Two main reasons:
   a) Since data=ordered mode cannot be used, allocating written blocks
      directly would expose stale data.
   b) To optimize writeback, we should allocate blocks based on writeback
      length rather than per-folio mapping. Direct written allocation
      would over-allocate blocks.

   dioread_nolock has been the default mount option for many years, and
   Jan pointed out that we may no longer need to disable it, so gradually
   remove this mount option in the future.



Zhang Yi (32):
  ext4: simplify size updating in ext4_setattr()
  ext4: factor out ext4_truncate_[up|down]()
  ext4: simplify error handling in ext4_setattr()
  ext4: skip ordered I/O wait when zeroing beyond i_disksize block
  ext4: set EXT4_MAP_NEW flag for delayed allocated blocks
  ext4: recheck extent status tree before block allocation
  ext4: fix orig_mlen initialization in ext4_map_blocks()
  ext4: allow ext4_map_blocks() to start its own transaction handle
  ext4: avoid unnecessary transaction in ext4_map_blocks() for unwritten
    extents
  ext4: skip block allocation for holes in the data submission path
  ext4: add iomap address space operations for buffered I/O
  ext4: implement buffered read path using iomap
  ext4: pass out extent seq counter when mapping da blocks
  ext4: do not use data=ordered mode for inodes using buffered iomap
    path
  ext4: implement buffered write path using iomap
  ext4: implement writeback path using iomap
  ext4: implement mmap path using iomap
  ext4: implement partial block zero range path using iomap
  ext4: tolerate unexpected holes in ext4_convert_unwritten_extents()
  ext4: add block mapping tracepoints for iomap buffered I/O path
  ext4: disable online defrag when inode using iomap buffered I/O path
  ext4: add EXT4_STATE_DISKSIZE_GROW_PENDING state bit and helpers
  ext4: submit and wait for pending disksize-grow I/O on writeback
  ext4: advance i_disksize to i_size upon disksize-grow I/O completion
  ext4: defer i_disksize update while DISKSIZE_GROW_PENDING is set
  ext4: submit and wait for disksize-grow I/O in fallocate paths
  ext4: clear DISKSIZE_GROW_PENDING on truncate or error
  ext4: set DISKSIZE_GROW_PENDING after zeroing unaligned EOF block
  ext4: add tracepoints for DISKSIZE_GROW_PENDING set, clear, and wait
  ext4: add tracepoints for EOF block zeroing and disksize-grow I/O
  ext4: partially enable iomap for the buffered I/O path of regular
    files
  ext4: introduce a mount option for iomap buffered I/O path

 fs/ext4/ext4.h              |   83 ++-
 fs/ext4/ext4_jbd2.c         |    8 +-
 fs/ext4/ext4_jbd2.h         |    7 +-
 fs/ext4/extents.c           |   71 ++-
 fs/ext4/file.c              |   20 +-
 fs/ext4/ialloc.c            |    1 +
 fs/ext4/inode.c             | 1183 +++++++++++++++++++++++++++++++----
 fs/ext4/migrate.c           |    2 +
 fs/ext4/move_extent.c       |   11 +
 fs/ext4/page-io.c           |  201 ++++++
 fs/ext4/super.c             |   49 +-
 include/trace/events/ext4.h |  187 ++++++
 12 files changed, 1651 insertions(+), 172 deletions(-)

-- 
2.52.0


^ permalink raw reply	[flat|nested] 65+ messages in thread

* [PATCH -next v5 01/32] ext4: simplify size updating in ext4_setattr()
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
@ 2026-08-14  9:33 ` Zhang Yi
  2026-08-14 10:02   ` sashiko-bot
  2026-08-14  9:33 ` [PATCH -next v5 02/32] ext4: factor out ext4_truncate_[up|down]() Zhang Yi
                   ` (30 subsequent siblings)
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:33 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

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 <yi.zhang@huawei.com>
Reviewed-by: Jan Kara <jack@suse.cz>
Reviewed-by: Ojaswin Mujoo <ojaswin@linux.ibm.com>
---
 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 bd4b778df9eb..14eab46f4750 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;
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 02/32] ext4: factor out ext4_truncate_[up|down]()
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
  2026-08-14  9:33 ` [PATCH -next v5 01/32] ext4: simplify size updating in ext4_setattr() Zhang Yi
@ 2026-08-14  9:33 ` Zhang Yi
  2026-08-14 10:04   ` sashiko-bot
  2026-08-14  9:33 ` [PATCH -next v5 03/32] ext4: simplify error handling in ext4_setattr() Zhang Yi
                   ` (29 subsequent siblings)
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:33 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

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 <yi.zhang@huawei.com>
Reviewed-by: Ojaswin Mujoo <ojaswin@linux.ibm.com>
Reviewed-by: Jan Kara <jack@suse.cz>
---
 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 14eab46f4750..8654006a57ef 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);
 	}
 
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 03/32] ext4: simplify error handling in ext4_setattr()
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
  2026-08-14  9:33 ` [PATCH -next v5 01/32] ext4: simplify size updating in ext4_setattr() Zhang Yi
  2026-08-14  9:33 ` [PATCH -next v5 02/32] ext4: factor out ext4_truncate_[up|down]() Zhang Yi
@ 2026-08-14  9:33 ` Zhang Yi
  2026-08-14 10:12   ` sashiko-bot
  2026-08-14  9:33 ` [PATCH -next v5 04/32] ext4: skip ordered I/O wait when zeroing beyond i_disksize block Zhang Yi
                   ` (28 subsequent siblings)
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:33 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

Refactor the error handling in ext4_setattr() for better clarity:

 - Return directly on ext4_break_layouts() failure.
 - Propagate ext4_truncate() errors using the existing error variable
   and jump to the common 'err_out' label.
 - Propagate posix_acl_chmod() errors also through the error variable,
   as it theoretically does not return a non-fatal error.

With these changes, every error path either returns immediately or jumps
to err_out. Consequently, the "if (!error)" condition guarding
setattr_copy() and mark_inode_dirty() becomes unreachable for error
cases. Remove this redundant check and the unused rc variable can be
removed as well.

Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
Reviewed-by: Ojaswin Mujoo <ojaswin@linux.ibm.com>
Reviewed-by: Jan Kara <jack@suse.cz>
---
 fs/ext4/inode.c | 32 +++++++++++++++-----------------
 1 file changed, 15 insertions(+), 17 deletions(-)

diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index 8654006a57ef..76bf0e944ebe 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -6116,7 +6116,7 @@ int ext4_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
 		 struct iattr *attr)
 {
 	struct inode *inode = d_inode(dentry);
-	int error, rc = 0;
+	int error;
 	int orphan = 0;
 	const unsigned int ia_valid = attr->ia_valid;
 	bool inc_ivers = true;
@@ -6229,10 +6229,10 @@ int ext4_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
 
 		filemap_invalidate_lock(inode->i_mapping);
 
-		rc = ext4_break_layouts(inode);
-		if (rc) {
+		error = ext4_break_layouts(inode);
+		if (error) {
 			filemap_invalidate_unlock(inode->i_mapping);
-			goto err_out;
+			return error;
 		}
 
 		if (attr->ia_size > oldsize)
@@ -6244,15 +6244,19 @@ int ext4_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
 		}
 
 		filemap_invalidate_unlock(inode->i_mapping);
+		if (error)
+			goto err_out;
 	}
 
-	if (!error) {
-		if (inc_ivers)
-			inode_inc_iversion(inode);
-		setattr_copy(idmap, inode, attr);
-		mark_inode_dirty(inode);
-	}
+	if (inc_ivers)
+		inode_inc_iversion(inode);
+	setattr_copy(idmap, inode, attr);
+	mark_inode_dirty(inode);
 
+	if (ia_valid & ATTR_MODE)
+		error = posix_acl_chmod(idmap, dentry, inode->i_mode);
+
+err_out:
 	/*
 	 * If the call to ext4_truncate failed to get a transaction handle at
 	 * all, we need to clean up the in-core orphan list manually.
@@ -6260,14 +6264,8 @@ int ext4_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
 	if (orphan && inode->i_nlink)
 		ext4_orphan_del(NULL, inode);
 
-	if (!error && (ia_valid & ATTR_MODE))
-		rc = posix_acl_chmod(idmap, dentry, inode->i_mode);
-
-err_out:
-	if  (error)
+	if (error)
 		ext4_std_error(inode->i_sb, error);
-	if (!error)
-		error = rc;
 	return error;
 }
 
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 04/32] ext4: skip ordered I/O wait when zeroing beyond i_disksize block
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
                   ` (2 preceding siblings ...)
  2026-08-14  9:33 ` [PATCH -next v5 03/32] ext4: simplify error handling in ext4_setattr() Zhang Yi
@ 2026-08-14  9:33 ` Zhang Yi
  2026-08-14 10:14   ` sashiko-bot
  2026-08-14  9:33 ` [PATCH -next v5 05/32] ext4: set EXT4_MAP_NEW flag for delayed allocated blocks Zhang Yi
                   ` (27 subsequent siblings)
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:33 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

ext4_block_zero_eof() zeros the tail of a partial block beyond EOF.
After zeroing, it waits for ordered I/O completion to prevent stale
data exposure from concurrent post-EOF mmap writes during folio
writeback.

However, if the zeroed range lies entirely beyond the block containing
i_disksize, no stale data can be exposed because the zeroed region is
beyond existing on-disk data. The zeroed pages will be written out
before i_disksize is later extended past i_size, so the ordered I/O
wait is unnecessary. Add a condition to skip it.

Suggested-by: Ojaswin Mujoo <ojaswin@linux.ibm.com>
Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
---
 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 76bf0e944ebe..7601fe3618b1 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -4241,9 +4241,16 @@ 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.
+	 *
+	 * Ordered I/O is required only when zeroing the tail of a block that
+	 * overlaps with i_disksize. If the zeroed range falls outside that
+	 * block, the zeroed data lies beyond the existing on-disk data. It
+	 * will be written out before i_disksize is later extended past
+	 * i_size, so no stale data can be exposed.
 	 */
 	if (ext4_should_order_data(inode) &&
-	    did_zero && zero_written && !IS_DAX(inode)) {
+	    did_zero && zero_written && !IS_DAX(inode) &&
+	    from < round_up(READ_ONCE(EXT4_I(inode)->i_disksize), blocksize)) {
 		handle_t *handle;
 
 		handle = ext4_journal_start(inode, EXT4_HT_MISC, 1);
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 05/32] ext4: set EXT4_MAP_NEW flag for delayed allocated blocks
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
                   ` (3 preceding siblings ...)
  2026-08-14  9:33 ` [PATCH -next v5 04/32] ext4: skip ordered I/O wait when zeroing beyond i_disksize block Zhang Yi
@ 2026-08-14  9:33 ` Zhang Yi
  2026-08-14 10:08   ` sashiko-bot
  2026-08-14  9:33 ` [PATCH -next v5 06/32] ext4: recheck extent status tree before block allocation Zhang Yi
                   ` (26 subsequent siblings)
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:33 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

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.

Reported-by: Ojaswin Mujoo <ojaswin@linux.ibm.com>
Link: https://lore.kernel.org/linux-ext4/cc05c17d-163e-4251-b2c9-aa3a6f9555d7@huaweicloud.com/
Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
---
 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 7601fe3618b1..9dbece14ae56 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);
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 06/32] ext4: recheck extent status tree before block allocation
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
                   ` (4 preceding siblings ...)
  2026-08-14  9:33 ` [PATCH -next v5 05/32] ext4: set EXT4_MAP_NEW flag for delayed allocated blocks Zhang Yi
@ 2026-08-14  9:33 ` Zhang Yi
  2026-08-14 10:15   ` sashiko-bot
  2026-08-14  9:33 ` [PATCH -next v5 07/32] ext4: fix orig_mlen initialization in ext4_map_blocks() Zhang Yi
                   ` (25 subsequent siblings)
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:33 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

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 since 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.

Suggested-by: Jan Kara <jack@suse.cz>
Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
---
 fs/ext4/inode.c | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index 9dbece14ae56..548a3968c5a7 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)) {
@@ -820,6 +821,19 @@ 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 does not hold i_rwsem or the folio locks
+	 * covering the full extent.
+	 */
+	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));
 
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 07/32] ext4: fix orig_mlen initialization in ext4_map_blocks()
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
                   ` (5 preceding siblings ...)
  2026-08-14  9:33 ` [PATCH -next v5 06/32] ext4: recheck extent status tree before block allocation Zhang Yi
@ 2026-08-14  9:33 ` Zhang Yi
  2026-08-14 10:08   ` sashiko-bot
  2026-08-14  9:33 ` [PATCH -next v5 08/32] ext4: allow ext4_map_blocks() to start its own transaction handle Zhang Yi
                   ` (24 subsequent siblings)
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:33 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

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 <yi.zhang@huawei.com>
---
 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 548a3968c5a7..bb4f1079d989 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))
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 08/32] ext4: allow ext4_map_blocks() to start its own transaction handle
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
                   ` (6 preceding siblings ...)
  2026-08-14  9:33 ` [PATCH -next v5 07/32] ext4: fix orig_mlen initialization in ext4_map_blocks() Zhang Yi
@ 2026-08-14  9:33 ` Zhang Yi
  2026-08-14 10:00   ` sashiko-bot
  2026-08-14  9:33 ` [PATCH -next v5 09/32] ext4: avoid unnecessary transaction in ext4_map_blocks() for unwritten extents Zhang Yi
                   ` (23 subsequent siblings)
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:33 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

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_handle 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 <jack@suse.cz>
Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
---
 fs/ext4/inode.c | 34 ++++++++++++++++++++++++++--------
 1 file changed, 26 insertions(+), 8 deletions(-)

diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index bb4f1079d989..c9904c274347 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -703,6 +703,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;
@@ -790,8 +791,10 @@ int ext4_map_blocks(handle_t *handle, struct inode *inode,
 found:
 	if (retval > 0 && map->m_flags & EXT4_MAP_MAPPED) {
 		ret = check_block_validity(inode, map);
-		if (ret != 0)
-			return ret;
+		if (ret != 0) {
+			retval = ret;
+			goto out_handle;
+		}
 	}
 
 	/* If it is only a block(s) look up */
@@ -811,8 +814,15 @@ 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_handle;
 
+	if (!handle) {
+		handle = ext4_journal_start(inode, EXT4_HT_MAP_BLOCKS,
+				ext4_chunk_trans_blocks(inode, orig_mlen));
+		if (IS_ERR(handle))
+			return PTR_ERR(handle);
+		internal_handle = true;
+	}
 
 	ext4_fc_track_inode(handle, inode);
 	/*
@@ -841,12 +851,14 @@ 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_handle;
 
 	if (map->m_flags & EXT4_MAP_MAPPED) {
 		ret = check_block_validity(inode, map);
-		if (ret != 0)
-			return ret;
+		if (ret != 0) {
+			retval = ret;
+			goto out_handle;
+		}
 
 		/*
 		 * Inodes with freshly allocated blocks where contents will be
@@ -867,12 +879,18 @@ int ext4_map_blocks(handle_t *handle, struct inode *inode,
 			else
 				ret = ext4_jbd2_inode_add_write(handle, inode,
 						start_byte, length);
-			if (ret)
-				return ret;
+			if (ret) {
+				retval = ret;
+				goto out_handle;
+			}
 		}
 	}
 	ext4_fc_track_range(handle, inode, map->m_lblk, map->m_lblk +
 			    map->m_len - 1);
+
+out_handle:
+	if (internal_handle)
+		ext4_journal_stop(handle);
 	return retval;
 }
 
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 09/32] ext4: avoid unnecessary transaction in ext4_map_blocks() for unwritten extents
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
                   ` (7 preceding siblings ...)
  2026-08-14  9:33 ` [PATCH -next v5 08/32] ext4: allow ext4_map_blocks() to start its own transaction handle Zhang Yi
@ 2026-08-14  9:33 ` Zhang Yi
  2026-08-14 10:15   ` sashiko-bot
  2026-08-14  9:33 ` [PATCH -next v5 10/32] ext4: skip block allocation for holes in the data submission path Zhang Yi
                   ` (22 subsequent siblings)
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:33 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

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 <yi.zhang@huawei.com>
---
 fs/ext4/inode.c | 19 ++++++++++++++-----
 1 file changed, 14 insertions(+), 5 deletions(-)

diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index c9904c274347..5dcc3f7b2ffd 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -807,14 +807,23 @@ 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_handle;
+		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. */
+			map->m_flags |= EXT4_MAP_MAPPED;
+			goto out_handle;
+		}
+	}
 
 	if (!handle) {
 		handle = ext4_journal_start(inode, EXT4_HT_MAP_BLOCKS,
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 10/32] ext4: skip block allocation for holes in the data submission path
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
                   ` (8 preceding siblings ...)
  2026-08-14  9:33 ` [PATCH -next v5 09/32] ext4: avoid unnecessary transaction in ext4_map_blocks() for unwritten extents Zhang Yi
@ 2026-08-14  9:33 ` Zhang Yi
  2026-08-14 10:15   ` sashiko-bot
  2026-08-14  9:33 ` [PATCH -next v5 11/32] ext4: add iomap address space operations for buffered I/O Zhang Yi
                   ` (21 subsequent siblings)
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:33 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

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 mirrors the existing
buffer_head writeback path, where mpage_add_bh_to_extent() skips
unmapped buffers and ext4_bio_write_folio() clears their dirty bits.

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. In this case, to avoid losing data beyond the hole, do not
stop conversion, continue on the remaining ranges. This prepares for the
buffered iomap writeback conversion.

Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
---
 fs/ext4/extents.c | 8 ++++++--
 fs/ext4/inode.c   | 7 +++++++
 2 files changed, 13 insertions(+), 2 deletions(-)

diff --git a/fs/ext4/extents.c b/fs/ext4/extents.c
index 76038b6c3655..0d62d9312284 100644
--- a/fs/ext4/extents.c
+++ b/fs/ext4/extents.c
@@ -5167,11 +5167,15 @@ int ext4_convert_unwritten_extents(handle_t *handle, struct inode *inode,
 				      EXT4_GET_BLOCKS_IO_CONVERT_EXT |
 				      EXT4_EX_NOCACHE);
 		if (ret <= 0) {
+			/*
+			 * If the ret is zero, an unexpected hole may cause
+			 * conversion to fail.  To avoid data loss during I/O
+			 * end conversion, skip the hole and continue
+			 * converting subsequent blocks.
+			 */
 			ext4_warning(inode->i_sb,
 				     "inode #%llu: block %u: len %u: ext4_map_blocks returned %d",
 				     inode->i_ino, map.m_lblk, map.m_len, ret);
-			if (unlikely(ret == 0))
-				ret = -EINVAL;
 		} else {
 			conv_blocks += map.m_len;
 		}
diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index 5dcc3f7b2ffd..d8c3e5e13b8a 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -823,6 +823,13 @@ int ext4_map_blocks(handle_t *handle, struct inode *inode,
 			map->m_flags |= EXT4_MAP_MAPPED;
 			goto out_handle;
 		}
+	} 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_handle;
 	}
 
 	if (!handle) {
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 11/32] ext4: add iomap address space operations for buffered I/O
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
                   ` (9 preceding siblings ...)
  2026-08-14  9:33 ` [PATCH -next v5 10/32] ext4: skip block allocation for holes in the data submission path Zhang Yi
@ 2026-08-14  9:33 ` Zhang Yi
  2026-08-14 10:04   ` sashiko-bot
  2026-08-14  9:33 ` [PATCH -next v5 12/32] ext4: implement buffered read path using iomap Zhang Yi
                   ` (20 subsequent siblings)
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:33 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

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 <yi.zhang@huawei.com>
Reviewed-by: Jan Kara <jack@suse.cz>
Reviewed-by: Ojaswin Mujoo <ojaswin@linux.ibm.com>
---
 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 724a27e8be61..24ec205da2d7 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 d8c3e5e13b8a..dda78cf1f68d 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -3956,6 +3956,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
@@ -4042,6 +4058,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,
@@ -4063,6 +4093,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
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 12/32] ext4: implement buffered read path using iomap
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
                   ` (10 preceding siblings ...)
  2026-08-14  9:33 ` [PATCH -next v5 11/32] ext4: add iomap address space operations for buffered I/O Zhang Yi
@ 2026-08-14  9:33 ` Zhang Yi
  2026-08-14 10:15   ` sashiko-bot
  2026-08-14  9:33 ` [PATCH -next v5 13/32] ext4: pass out extent seq counter when mapping da blocks Zhang Yi
                   ` (19 subsequent siblings)
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:33 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

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 <yi.zhang@huawei.com>
Reviewed-by: Jan Kara <jack@suse.cz>
Reviewed-by: Ojaswin Mujoo <ojaswin@linux.ibm.com>
---
 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 dda78cf1f68d..376cb9783835 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -3956,14 +3956,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);
+
+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,
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 13/32] ext4: pass out extent seq counter when mapping da blocks
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
                   ` (11 preceding siblings ...)
  2026-08-14  9:33 ` [PATCH -next v5 12/32] ext4: implement buffered read path using iomap Zhang Yi
@ 2026-08-14  9:33 ` Zhang Yi
  2026-08-14 10:12   ` sashiko-bot
  2026-08-14  9:33 ` [PATCH -next v5 14/32] ext4: do not use data=ordered mode for inodes using buffered iomap path Zhang Yi
                   ` (18 subsequent siblings)
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:33 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

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 <yi.zhang@huawei.com>
Reviewed-by: Jan Kara <jack@suse.cz>
Reviewed-by: Ojaswin Mujoo <ojaswin@linux.ibm.com>
---
 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 376cb9783835..9098d9a5fc05 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -1970,7 +1970,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));
 
@@ -2023,7 +2023,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));
 
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 14/32] ext4: do not use data=ordered mode for inodes using buffered iomap path
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
                   ` (12 preceding siblings ...)
  2026-08-14  9:33 ` [PATCH -next v5 13/32] ext4: pass out extent seq counter when mapping da blocks Zhang Yi
@ 2026-08-14  9:33 ` Zhang Yi
  2026-08-14 10:12   ` sashiko-bot
  2026-08-14  9:33 ` [PATCH -next v5 15/32] ext4: implement buffered write path using iomap Zhang Yi
                   ` (17 subsequent siblings)
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:33 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

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 <yi.zhang@huawei.com>
Reviewed-by: Jan Kara <jack@suse.cz>
---
 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)
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 15/32] ext4: implement buffered write path using iomap
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
                   ` (13 preceding siblings ...)
  2026-08-14  9:33 ` [PATCH -next v5 14/32] ext4: do not use data=ordered mode for inodes using buffered iomap path Zhang Yi
@ 2026-08-14  9:33 ` Zhang Yi
  2026-08-14 10:40   ` sashiko-bot
  2026-08-14  9:33 ` [PATCH -next v5 16/32] ext4: implement writeback " Zhang Yi
                   ` (16 subsequent siblings)
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:33 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

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.

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.

 - 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 <yi.zhang@huawei.com>
Reviewed-by: Ojaswin Mujoo <ojaswin@linux.ibm.com>
---
 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 24ec205da2d7..98295ef7069a 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
 
@@ -4047,6 +4048,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 9098d9a5fc05..d831d1911a6f 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -3136,7 +3136,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);
@@ -3508,6 +3508,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)
@@ -3542,6 +3551,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
@@ -3957,7 +3968,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;
 
@@ -3969,7 +3981,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,
@@ -3986,7 +4001,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;
 
@@ -3994,6 +4009,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 bca0dc87d0b7..30150094f2a5 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) ->
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 16/32] ext4: implement writeback path using iomap
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
                   ` (14 preceding siblings ...)
  2026-08-14  9:33 ` [PATCH -next v5 15/32] ext4: implement buffered write path using iomap Zhang Yi
@ 2026-08-14  9:33 ` Zhang Yi
  2026-08-14 10:19   ` sashiko-bot
  2026-08-14  9:33 ` [PATCH -next v5 17/32] ext4: implement mmap " Zhang Yi
                   ` (15 subsequent siblings)
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:33 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

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 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,
   rather than allocating for 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.

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 <yi.zhang@huawei.com>
---
 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 98295ef7069a..03fa90d2986f 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;
@@ -3991,6 +3993,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 d831d1911a6f..0b3e54e12b78 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -44,6 +44,7 @@
 #include <linux/iversion.h>
 
 #include "ext4_jbd2.h"
+#include "ext4_extents.h"
 #include "xattr.h"
 #include "acl.h"
 #include "truncate.h"
@@ -4134,10 +4135,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;
+	unsigned int blkbits = inode->i_blkbits;
+	unsigned int index = offset >> blkbits;
+	unsigned int blk_end, blk_len;
+	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 >> blkbits;
+	blk_end = min_t(unsigned int, (wpc->wbc->range_end >> 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)) {
+			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..2888e0057561 100644
--- a/fs/ext4/page-io.c
+++ b/fs/ext4/page-io.c
@@ -22,6 +22,7 @@
 #include <linux/bio.h>
 #include <linux/workqueue.h>
 #include <linux/kernel.h>
+#include <linux/iomap.h>
 #include <linux/slab.h>
 #include <linux/mm.h>
 #include <linux/sched/mm.h>
@@ -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)
+		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 30150094f2a5..6d2d323604f9 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 = {
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 17/32] ext4: implement mmap path using iomap
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
                   ` (15 preceding siblings ...)
  2026-08-14  9:33 ` [PATCH -next v5 16/32] ext4: implement writeback " Zhang Yi
@ 2026-08-14  9:33 ` Zhang Yi
  2026-08-14 10:35   ` sashiko-bot
  2026-08-14  9:33 ` [PATCH -next v5 18/32] ext4: implement partial block zero range " Zhang Yi
                   ` (14 subsequent siblings)
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:33 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

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 <yi.zhang@huawei.com>
Reviewed-by: Ojaswin Mujoo <ojaswin@linux.ibm.com>
Reviewed-by: Jan Kara <jack@suse.cz>
---
 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 0b3e54e12b78..a05445625895 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -4027,7 +4027,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 :
@@ -4085,6 +4085,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));
@@ -7296,6 +7304,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;
@@ -7318,6 +7343,11 @@ vm_fault_t ext4_page_mkwrite(struct vm_fault *vmf)
 
 	filemap_invalidate_lock_shared(mapping);
 
+	if (ext4_inode_buffered_iomap(inode)) {
+		ret = ext4_iomap_page_mkwrite(vmf);
+		goto out;
+	}
+
 	err = ext4_convert_inline_data(inode);
 	if (err)
 		goto out_ret;
diff --git a/fs/ext4/super.c b/fs/ext4/super.c
index 6d2d323604f9..1c2395aa1d53 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
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 18/32] ext4: implement partial block zero range path using iomap
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
                   ` (16 preceding siblings ...)
  2026-08-14  9:33 ` [PATCH -next v5 17/32] ext4: implement mmap " Zhang Yi
@ 2026-08-14  9:33 ` Zhang Yi
  2026-08-14 10:25   ` sashiko-bot
  2026-08-14  9:33 ` [PATCH -next v5 19/32] ext4: tolerate unexpected holes in ext4_convert_unwritten_extents() Zhang Yi
                   ` (13 subsequent siblings)
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:33 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

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

ext4_iomap_block_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_iomap_block_zero_range() must not be called under an
active handle. In addition, for post-EOF zeroing, the caller cannot
rely on data=ordered mode to persist the zeroed data before
i_disksize is updated.

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

Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
---
 fs/ext4/inode.c | 110 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 110 insertions(+)

diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index a05445625895..d4b4153077bb 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -4106,6 +4106,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.
@@ -4569,6 +4630,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.
@@ -4595,6 +4698,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);
@@ -4649,6 +4755,10 @@ int ext4_block_zero_eof(struct inode *inode, loff_t from, loff_t end)
 	 * block, the zeroed data lies beyond the existing on-disk data. It
 	 * will be written out before i_disksize is later extended past
 	 * i_size, so no stale data can be exposed.
+	 *
+	 * TODO: In the iomap path, handle this by tracking the ordered range
+	 * and updating i_disksize to i_size after the zeroed data has been
+	 * written back.
 	 */
 	if (ext4_should_order_data(inode) &&
 	    did_zero && zero_written && !IS_DAX(inode) &&
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 19/32] ext4: tolerate unexpected holes in ext4_convert_unwritten_extents()
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
                   ` (17 preceding siblings ...)
  2026-08-14  9:33 ` [PATCH -next v5 18/32] ext4: implement partial block zero range " Zhang Yi
@ 2026-08-14  9:33 ` Zhang Yi
  2026-08-14 10:09   ` sashiko-bot
  2026-08-14  9:33 ` [PATCH -next v5 20/32] ext4: add block mapping tracepoints for iomap buffered I/O path Zhang Yi
                   ` (12 subsequent siblings)
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:33 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

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)
                                    [bio completes]
                                    ext4_iomap_finish_ioend()
                                      ext4_convert_unwritten_extents()
                                        ext4_map_blocks(IO_CONVERT_EXT)
                                          -> returns 0 (hole found)

Therefore, in ext4_convert_unwritten_extents() we need to be
careful about the case where ext4_map_blocks() returns 0. Instead
of triggering a warning, we should ignore the hole and continue
with the subsequent conversion.

Link: https://lore.kernel.org/linux-ext4/a638a8fb-c184-4069-ae33-379ec12cd514@huaweicloud.com/
Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
---
 fs/ext4/extents.c | 20 +++++++++++---------
 1 file changed, 11 insertions(+), 9 deletions(-)

diff --git a/fs/ext4/extents.c b/fs/ext4/extents.c
index 0d62d9312284..5a06259a9b5d 100644
--- a/fs/ext4/extents.c
+++ b/fs/ext4/extents.c
@@ -5166,19 +5166,21 @@ int ext4_convert_unwritten_extents(handle_t *handle, struct inode *inode,
 		ret = ext4_map_blocks(handle, inode, &map,
 				      EXT4_GET_BLOCKS_IO_CONVERT_EXT |
 				      EXT4_EX_NOCACHE);
-		if (ret <= 0) {
-			/*
-			 * If the ret is zero, an unexpected hole may cause
-			 * conversion to fail.  To avoid data loss during I/O
-			 * end conversion, skip the hole and continue
-			 * converting subsequent blocks.
-			 */
+		/*
+		 * A return value of zero means an unexpected hole was found.
+		 * This can happen when writeback races with a concurrent
+		 * punch hole in the iomap path. Because iomap may not create
+		 * ifs for folios larger than block size, the dirty bit can
+		 * be set again after punching. If writeback happens between
+		 * partial folio invalidation and extent removal, a hole is
+		 * observed at I/O completion.
+		 */
+		if (ret < 0)
 			ext4_warning(inode->i_sb,
 				     "inode #%llu: block %u: len %u: ext4_map_blocks returned %d",
 				     inode->i_ino, map.m_lblk, map.m_len, ret);
-		} else {
+		else if (ret > 0)
 			conv_blocks += map.m_len;
-		}
 
 		ret2 = ext4_mark_inode_dirty(handle, inode);
 		if (credits) {
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 20/32] ext4: add block mapping tracepoints for iomap buffered I/O path
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
                   ` (18 preceding siblings ...)
  2026-08-14  9:33 ` [PATCH -next v5 19/32] ext4: tolerate unexpected holes in ext4_convert_unwritten_extents() Zhang Yi
@ 2026-08-14  9:33 ` Zhang Yi
  2026-08-14 10:08   ` sashiko-bot
  2026-08-14  9:33 ` [PATCH -next v5 21/32] ext4: disable online defrag when inode using " Zhang Yi
                   ` (11 subsequent siblings)
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:33 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

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 <yi.zhang@huawei.com>
Reviewed-by: Ojaswin Mujoo <ojaswin@linux.ibm.com>
Reviewed-by: Jan Kara <jack@suse.cz>
---
 fs/ext4/inode.c             |  6 +++++
 include/trace/events/ext4.h | 45 +++++++++++++++++++++++++++++++++++++
 2 files changed, 51 insertions(+)

diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index d4b4153077bb..a2f060ae6cbc 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -4006,6 +4006,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;
 }
@@ -4039,6 +4041,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;
 }
@@ -4155,6 +4159,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;
 
@@ -4266,6 +4271,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..69596a216dcb 100644
--- a/include/trace/events/ext4.h
+++ b/include/trace/events/ext4.h
@@ -3157,6 +3157,51 @@ 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(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->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 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->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 */
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 21/32] ext4: disable online defrag when inode using iomap buffered I/O path
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
                   ` (19 preceding siblings ...)
  2026-08-14  9:33 ` [PATCH -next v5 20/32] ext4: add block mapping tracepoints for iomap buffered I/O path Zhang Yi
@ 2026-08-14  9:33 ` Zhang Yi
  2026-08-14 10:08   ` sashiko-bot
  2026-08-14  9:33 ` [PATCH -next v5 22/32] ext4: add EXT4_STATE_DISKSIZE_GROW_PENDING state bit and helpers Zhang Yi
                   ` (10 subsequent siblings)
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:33 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

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 <yi.zhang@huawei.com>
Reviewed-by: Ojaswin Mujoo <ojaswin@linux.ibm.com>
Reviewed-by: Jan Kara <jack@suse.cz>
---
 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);
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 22/32] ext4: add EXT4_STATE_DISKSIZE_GROW_PENDING state bit and helpers
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
                   ` (20 preceding siblings ...)
  2026-08-14  9:33 ` [PATCH -next v5 21/32] ext4: disable online defrag when inode using " Zhang Yi
@ 2026-08-14  9:33 ` Zhang Yi
  2026-08-14 10:09   ` sashiko-bot
  2026-08-14  9:33 ` [PATCH -next v5 23/32] ext4: submit and wait for pending disksize-grow I/O on writeback Zhang Yi
                   ` (9 subsequent siblings)
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:33 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

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 <jack@suse.cz>
Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
---
 fs/ext4/ext4.h  |  7 ++++++
 fs/ext4/inode.c | 64 +++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 71 insertions(+)

diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h
index 03fa90d2986f..1c3d736fb700 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 a2f060ae6cbc..e4a4396eaf87 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -126,6 +126,70 @@ 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)
+{
+	ext4_clear_inode_state(inode, EXT4_STATE_DISKSIZE_GROW_PENDING);
+	/*
+	 * Make sure clearing of EXT4_STATE_DISKSIZE_GROW_PENDING is
+	 * visible before we send the wakeup. Pairs with the implicit
+	 * barrier in prepare_to_wait() inside wait_on_bit() in
+	 * ext4_iomap_wait_disksize_pending().
+	 */
+	smp_mb();
+	wake_up_bit(ext4_inode_state_wait_word(inode),
+		    ext4_inode_state_wait_bit(EXT4_STATE_DISKSIZE_GROW_PENDING));
+}
+
+/*
+ * 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)
 {
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 23/32] ext4: submit and wait for pending disksize-grow I/O on writeback
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
                   ` (21 preceding siblings ...)
  2026-08-14  9:33 ` [PATCH -next v5 22/32] ext4: add EXT4_STATE_DISKSIZE_GROW_PENDING state bit and helpers Zhang Yi
@ 2026-08-14  9:33 ` Zhang Yi
  2026-08-14 10:25   ` sashiko-bot
  2026-08-14  9:33 ` [PATCH -next v5 24/32] ext4: advance i_disksize to i_size upon disksize-grow I/O completion Zhang Yi
                   ` (8 subsequent siblings)
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:33 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

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.

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.

Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
---
 fs/ext4/ext4.h    |  6 ++++++
 fs/ext4/inode.c   | 49 ++++++++++++++++++++++++++++++++++++++++++++++-
 fs/ext4/page-io.c | 40 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 94 insertions(+), 1 deletion(-)

diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h
index 1c3d736fb700..089dbd39c5c2 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -3986,6 +3986,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 e4a4396eaf87..a0707310b464 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -4366,7 +4366,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:
@@ -4379,6 +4382,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
@@ -4398,6 +4416,29 @@ 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 || range_start < pstart + plen)
+		return;
+
+	filemap_fdatawrite_range(mapping, pstart, pstart + plen - 1);
+}
+
 static int ext4_iomap_writepages(struct address_space *mapping,
 				 struct writeback_control *wbc)
 {
@@ -4415,6 +4456,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 2888e0057561..955ff88045db 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,
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 24/32] ext4: advance i_disksize to i_size upon disksize-grow I/O completion
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
                   ` (22 preceding siblings ...)
  2026-08-14  9:33 ` [PATCH -next v5 23/32] ext4: submit and wait for pending disksize-grow I/O on writeback Zhang Yi
@ 2026-08-14  9:33 ` Zhang Yi
  2026-08-14 10:25   ` sashiko-bot
  2026-08-14  9:33 ` [PATCH -next v5 25/32] ext4: defer i_disksize update while DISKSIZE_GROW_PENDING is set Zhang Yi
                   ` (7 subsequent siblings)
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:33 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

When the disksize-grow-pending zeroed EOF I/O completes, i_disksize has
to be advanced. Advancing it only to the end of that specific I/O would
discard any later i_disksize updates from concurrent fallocate or
similar operations, causing filesystem inconsistency.

Scanning dirty or writeback folios beyond the current position to
compute a safe advance target is expensive and racy with concurrent
fallocate, so instead advance i_disksize directly to i_size. This may
expose zeroed data (not stale data) after crash recovery when dirty data
in the range is not yet on disk, but only for unaligned append writes,
which is deemed acceptable.

To support this, teach ext4_iomap_wb_update_disksize() to take an
is_disksize_grow flag and advance i_disksize to i_size when set, and
have ext4_iomap_finish_ioend() pass the flag based on the
EXT4_IOMAP_IOEND_DISKSIZE_GROW_IO tag of the completing ioend.

Suggested-by: Jan Kara <jack@suse.cz>
Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
---
 fs/ext4/page-io.c | 44 +++++++++++++++++++++++++++++++++++++-------
 1 file changed, 37 insertions(+), 7 deletions(-)

diff --git a/fs/ext4/page-io.c b/fs/ext4/page-io.c
index 955ff88045db..4f1176b9332f 100644
--- a/fs/ext4/page-io.c
+++ b/fs/ext4/page-io.c
@@ -578,9 +578,9 @@ static void ext4_iomap_wb_disksize_pending_wait(struct inode *inode,
 }
 
 static int ext4_iomap_wb_update_disksize(handle_t *handle, struct inode *inode,
-					 loff_t end)
+					 loff_t end, bool is_disksize_grow)
 {
-	loff_t new_disksize = end;
+	loff_t new_disksize, i_size;
 	struct ext4_inode_info *ei = EXT4_I(inode);
 	int ret;
 
@@ -589,9 +589,36 @@ static int ext4_iomap_wb_update_disksize(handle_t *handle, struct inode *inode,
 	 * i_data_sem.
 	 */
 	down_write(&ei->i_data_sem);
-	new_disksize = min(new_disksize, i_size_read(inode));
+	i_size = i_size_read(inode);
+
+	/*
+	 * EXT4_STATE_DISKSIZE_GROW_PENDING is cleared when the pending
+	 * I/O completes. However, another thread may have re-set the bit
+	 * between that point and here, meaning i_disksize has already
+	 * been advanced and a new EOF zeroing has been initiated. In that
+	 * case, do not advance i_disksize to i_size; leave it to the
+	 * next pending grow ioend.
+	 */
+	if (is_disksize_grow &&
+	    ext4_test_inode_state(inode, EXT4_STATE_DISKSIZE_GROW_PENDING))
+		is_disksize_grow = false;
+
+	/*
+	 * Update i_disksize to i_size when EXT4_IOMAP_IOEND_DISKSIZE_GROW_IO
+	 * completes. This is safe because we never directly allocate written
+	 * blocks during buffered writes.
+	 *
+	 * This ensures that i_disksize is correctly advanced during
+	 * truncate-up or append fallocate on a block-unaligned file,
+	 * preventing it from remaining stale. The tradeoff is that zeroed
+	 * data may be exposed after crash recovery if dirty data in this
+	 * range is not yet on disk, but stale data will never be exposed.
+	 * This is because the extent is only converted to written state
+	 * after the data has been persisted.
+	 */
+	new_disksize = is_disksize_grow ? i_size : min(end, i_size);
 	if (new_disksize > ei->i_disksize)
-		ei->i_disksize = new_disksize;
+		WRITE_ONCE(ei->i_disksize, new_disksize);
 	up_write(&ei->i_data_sem);
 	ret = ext4_mark_inode_dirty(handle, inode);
 	if (ret)
@@ -607,6 +634,8 @@ static void ext4_iomap_finish_ioend(struct iomap_ioend *ioend)
 	loff_t pos = ioend->io_offset;
 	size_t size = ioend->io_size;
 	loff_t end = pos + size;
+	unsigned long io_mode = (unsigned long)ioend->io_private;
+	bool is_disksize_grow = (io_mode == EXT4_IOMAP_IOEND_DISKSIZE_GROW_IO);
 	handle_t *handle;
 	int credits;
 	int ret, err;
@@ -619,7 +648,7 @@ static void ext4_iomap_finish_ioend(struct iomap_ioend *ioend)
 	}
 
 	if (!(ioend->io_flags & IOMAP_IOEND_UNWRITTEN) &&
-	    end <= READ_ONCE(EXT4_I(inode)->i_disksize))
+	    end <= READ_ONCE(EXT4_I(inode)->i_disksize) && !is_disksize_grow)
 		goto out;
 
 	/* Wait for disksize-pending zeroed data to be written out. */
@@ -638,8 +667,9 @@ static void ext4_iomap_finish_ioend(struct iomap_ioend *ioend)
 	}
 
 	/* Update on-disk size after I/O is completed. */
-	if (end > READ_ONCE(EXT4_I(inode)->i_disksize)) {
-		ret = ext4_iomap_wb_update_disksize(handle, inode, end);
+	if (end > READ_ONCE(EXT4_I(inode)->i_disksize) || is_disksize_grow) {
+		ret = ext4_iomap_wb_update_disksize(handle, inode, end,
+						    is_disksize_grow);
 		if (ret)
 			goto out_journal;
 	}
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 25/32] ext4: defer i_disksize update while DISKSIZE_GROW_PENDING is set
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
                   ` (23 preceding siblings ...)
  2026-08-14  9:33 ` [PATCH -next v5 24/32] ext4: advance i_disksize to i_size upon disksize-grow I/O completion Zhang Yi
@ 2026-08-14  9:33 ` Zhang Yi
  2026-08-14 10:31   ` sashiko-bot
  2026-08-14  9:33 ` [PATCH -next v5 26/32] ext4: submit and wait for disksize-grow I/O in fallocate paths Zhang Yi
                   ` (6 subsequent siblings)
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:33 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

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.

Suggested-by: Jan Kara <jack@suse.cz>
Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
---
 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 089dbd39c5c2..b26ce3183bac 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -3605,30 +3605,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 5a06259a9b5d..fc5aa2dbefcf 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 a0707310b464..0fdc31b21be5 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -6622,8 +6622,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)
 {
@@ -6631,7 +6633,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);
 }
 
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 26/32] ext4: submit and wait for disksize-grow I/O in fallocate paths
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
                   ` (24 preceding siblings ...)
  2026-08-14  9:33 ` [PATCH -next v5 25/32] ext4: defer i_disksize update while DISKSIZE_GROW_PENDING is set Zhang Yi
@ 2026-08-14  9:33 ` Zhang Yi
  2026-08-14 10:28   ` sashiko-bot
  2026-08-14  9:33 ` [PATCH -next v5 27/32] ext4: clear DISKSIZE_GROW_PENDING on truncate or error Zhang Yi
                   ` (5 subsequent siblings)
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:33 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

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.

Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
---
 fs/ext4/ext4.h    |  2 ++
 fs/ext4/extents.c | 53 ++++++++++++++++++++++++++++++++++++++++-------
 fs/ext4/inode.c   | 34 ++++++++++++++++++++++++++++++
 3 files changed, 82 insertions(+), 7 deletions(-)

diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h
index b26ce3183bac..504dce9fdc6b 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 fc5aa2dbefcf..dda6d50e96e3 100644
--- a/fs/ext4/extents.c
+++ b/fs/ext4/extents.c
@@ -4876,6 +4876,16 @@ static long ext4_zero_range(struct file *file, loff_t offset,
 			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;
+	}
+
 	handle = ext4_journal_start(inode, EXT4_HT_MISC, 1);
 	if (IS_ERR(handle)) {
 		ret = PTR_ERR(handle);
@@ -4928,10 +4938,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,
@@ -5668,6 +5688,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
@@ -5715,9 +5743,11 @@ static int ext4_collapse_range(struct file *file, loff_t offset, loff_t len)
 		goto out_handle;
 	}
 
+	WARN_ON_ONCE(ext4_test_inode_state(inode,
+				EXT4_STATE_DISKSIZE_GROW_PENDING));
 	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);
@@ -5770,6 +5800,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.
@@ -5789,8 +5827,9 @@ 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;
+	WARN_ON_ONCE(ext4_test_inode_state(inode,
+				EXT4_STATE_DISKSIZE_GROW_PENDING));
+	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 0fdc31b21be5..056937e27859 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -4823,6 +4823,32 @@ 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_fdatawrite_range(inode->i_mapping, pstart,
+				       pstart + plen - 1);
+	if (ret)
+		return ret;
+
+	ext4_iomap_wait_disksize_pending(inode);
+	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.
@@ -4988,6 +5014,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)
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 27/32] ext4: clear DISKSIZE_GROW_PENDING on truncate or error
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
                   ` (25 preceding siblings ...)
  2026-08-14  9:33 ` [PATCH -next v5 26/32] ext4: submit and wait for disksize-grow I/O in fallocate paths Zhang Yi
@ 2026-08-14  9:33 ` Zhang Yi
  2026-08-14 10:31   ` sashiko-bot
  2026-08-14  9:33 ` [PATCH -next v5 28/32] ext4: set DISKSIZE_GROW_PENDING after zeroing unaligned EOF block Zhang Yi
                   ` (4 subsequent siblings)
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:33 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

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. Additionally, if the filesystem enters an emergency state, the
block will no longer be written back.

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:
 - ext4_iomap_discard_folio() on folio discard.
 - ext4_evict_inode() when an unlinked inode is destroyed.
 - ext4_iomap_writepages() when the filesystem is in emergency state.

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.

Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
---
 fs/ext4/inode.c | 33 +++++++++++++++++++++++++++++++--
 fs/ext4/super.c | 19 +++++++++++++------
 2 files changed, 44 insertions(+), 8 deletions(-)

diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index 056937e27859..a1dfb70127ca 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -273,6 +273,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);
 
 	/*
@@ -4344,8 +4346,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,
@@ -4453,8 +4464,15 @@ static int ext4_iomap_writepages(struct address_space *mapping,
 	};
 
 	ret = ext4_emergency_state(sb);
-	if (unlikely(ret))
+	if (unlikely(ret)) {
+		/*
+		 * The filesystem is in an emergency state and no further
+		 * writeback will occur.  Clear the disksize-grow-pending
+		 * state to avoid complaints when the inode is destroyed.
+		 */
+		ext4_iomap_clear_disksize_pending(inode);
 		return ret;
+	}
 
 	/*
 	 * Submit the pending zeroed EOF block range if the entire
@@ -6741,7 +6759,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 1c2395aa1d53..86ed5228dbe9 100644
--- a/fs/ext4/super.c
+++ b/fs/ext4/super.c
@@ -1491,12 +1491,19 @@ static void ext4_destroy_inode(struct inode *inode)
 		dump_stack();
 	}
 
-	if (!(EXT4_SB(inode->i_sb)->s_mount_state & EXT4_ERROR_FS) &&
-	    WARN_ON_ONCE(EXT4_I(inode)->i_reserved_data_blocks))
-		ext4_msg(inode->i_sb, KERN_ERR,
-			 "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)) {
+		if (WARN_ON_ONCE(EXT4_I(inode)->i_reserved_data_blocks))
+			ext4_msg(inode->i_sb, KERN_ERR,
+				 "Inode %llu (%p): i_reserved_data_blocks (%u) not cleared!",
+				 inode->i_ino, EXT4_I(inode),
+				 EXT4_I(inode)->i_reserved_data_blocks);
+
+		if (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)
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 28/32] ext4: set DISKSIZE_GROW_PENDING after zeroing unaligned EOF block
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
                   ` (26 preceding siblings ...)
  2026-08-14  9:33 ` [PATCH -next v5 27/32] ext4: clear DISKSIZE_GROW_PENDING on truncate or error Zhang Yi
@ 2026-08-14  9:33 ` Zhang Yi
  2026-08-14 10:18   ` sashiko-bot
  2026-08-14  9:33 ` [PATCH -next v5 29/32] ext4: add tracepoints for DISKSIZE_GROW_PENDING set, clear, and wait Zhang Yi
                   ` (3 subsequent siblings)
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:33 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

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 <jack@suse.cz>
Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
---
 fs/ext4/inode.c | 85 ++++++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 73 insertions(+), 12 deletions(-)

diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index a1dfb70127ca..2ec69e8abe54 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -4841,6 +4841,68 @@ static int ext4_block_zero_range(struct inode *inode,
 					zero_written);
 }
 
+/*
+ * Inodes using the iomap buffered I/O path do not use data=ordered mode.
+ * Therefore, we mark the inode as disksize-grow-pending after zeroing the
+ * EOF block. The zeroed block will be submitted before any subsequent
+ * data.
+ *
+ * In the I/O completion path, ext4_iomap_wb_disksize_pending_wait() will
+ * wait for I/O completion before advancing i_disksize if the write
+ * extends beyond the zeroed boundary.
+ *
+ * When zeroed I/O is in progress, operations that extend i_disksize are
+ * handled as follows:
+ *
+ *  - Truncate up, append fallocate and zero_range:
+ *    Defer the update. The file size will be updated to i_size by the
+ *    end_io handler once the ongoing pending I/O completes.
+ *
+ *  - Insert range and collapse range operations:
+ *    Wait synchronously for the relevant I/O to complete before updating
+ *    i_disksize.
+ */
+static int ext4_iomap_mark_disksize_pending(struct inode *inode, loff_t from)
+{
+	struct folio *folio;
+
+	folio = filemap_lock_folio(inode->i_mapping, from >> PAGE_SHIFT);
+	if (IS_ERR(folio))
+		/* Already in writeback and cleared? */
+		return PTR_ERR(folio) == -ENOENT ? 0 : PTR_ERR(folio);
+
+	/*
+	 * Ensure that in-flight writeback, possibly started after
+	 * iomap_zero_range() unlocked the folio, has completed. Without
+	 * this wait folio_test_dirty() below may miss the zeroed data
+	 * (writeback clears PG_dirty), causing us to skip the
+	 * disksize-grow-pending tracking and potentially expose stale
+	 * on-disk data.
+	 */
+	folio_wait_writeback(folio);
+	WARN_ON_ONCE(folio_test_writeback(folio));
+
+	/*
+	 * Mark the inode as disksize-grow-pending. The zeroed block will
+	 * be written out by the generic writepages cycle or any other
+	 * syncing operation.
+	 *
+	 * Multiple overlapping unaligned EOF writes should not happen,
+	 * because we only mark the pending state after zeroing the on-disk
+	 * EOF block, and i_disksize can only be updated after the previous
+	 * zeroed pending block has been written back or the dirty folio
+	 * has been discared.
+	 */
+	if (likely(folio_test_dirty(folio) &&
+		   !ext4_test_inode_state(inode,
+					  EXT4_STATE_DISKSIZE_GROW_PENDING)))
+		ext4_set_inode_state(inode, EXT4_STATE_DISKSIZE_GROW_PENDING);
+
+	folio_unlock(folio);
+	folio_put(folio);
+	return 0;
+}
+
 /*
  * Submit and wait for the pending zeroed EOF block range to complete
  * if the given range [@offset, @end) fully covers it.  Must be called
@@ -4916,22 +4978,21 @@ int ext4_block_zero_eof(struct inode *inode, loff_t from, loff_t end)
 	 * block, the zeroed data lies beyond the existing on-disk data. It
 	 * will be written out before i_disksize is later extended past
 	 * i_size, so no stale data can be exposed.
-	 *
-	 * TODO: In the iomap path, handle this by tracking the ordered range
-	 * and updating i_disksize to i_size after the zeroed data has been
-	 * written back.
 	 */
-	if (ext4_should_order_data(inode) &&
-	    did_zero && zero_written && !IS_DAX(inode) &&
+	if (did_zero && zero_written && !IS_DAX(inode) &&
 	    from < round_up(READ_ONCE(EXT4_I(inode)->i_disksize), blocksize)) {
-		handle_t *handle;
+		if (ext4_should_order_data(inode)) {
+			handle_t *handle;
 
-		handle = ext4_journal_start(inode, EXT4_HT_MISC, 1);
-		if (IS_ERR(handle))
-			return PTR_ERR(handle);
+			handle = ext4_journal_start(inode, EXT4_HT_MISC, 1);
+			if (IS_ERR(handle))
+				return PTR_ERR(handle);
 
-		err = ext4_jbd2_inode_add_write(handle, inode, from, length);
-		ext4_journal_stop(handle);
+			err = ext4_jbd2_inode_add_write(handle, inode, from,
+							length);
+			ext4_journal_stop(handle);
+		} else if (ext4_inode_buffered_iomap(inode))
+			err = ext4_iomap_mark_disksize_pending(inode, from);
 		if (err)
 			return err;
 	}
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 29/32] ext4: add tracepoints for DISKSIZE_GROW_PENDING set, clear, and wait
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
                   ` (27 preceding siblings ...)
  2026-08-14  9:33 ` [PATCH -next v5 28/32] ext4: set DISKSIZE_GROW_PENDING after zeroing unaligned EOF block Zhang Yi
@ 2026-08-14  9:33 ` Zhang Yi
  2026-08-14 10:18   ` sashiko-bot
  2026-08-14  9:33 ` [PATCH -next v5 30/32] ext4: add tracepoints for EOF block zeroing and disksize-grow I/O Zhang Yi
                   ` (2 subsequent siblings)
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:33 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

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 <yi.zhang@huawei.com>
---
 fs/ext4/inode.c             |  6 +++++-
 include/trace/events/ext4.h | 35 +++++++++++++++++++++++++++++++++++
 2 files changed, 40 insertions(+), 1 deletion(-)

diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index 2ec69e8abe54..bca7d5c33919 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -133,6 +133,7 @@ void ext4_inode_csum_set(struct inode *inode, struct ext4_inode *raw,
  */
 void ext4_iomap_clear_disksize_pending(struct inode *inode)
 {
+	trace_ext4_iomap_clear_disksize_pending(inode);
 	ext4_clear_inode_state(inode, EXT4_STATE_DISKSIZE_GROW_PENDING);
 	/*
 	 * Make sure clearing of EXT4_STATE_DISKSIZE_GROW_PENDING is
@@ -151,6 +152,7 @@ void ext4_iomap_clear_disksize_pending(struct inode *inode)
  */
 void ext4_iomap_wait_disksize_pending(struct inode *inode)
 {
+	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);
@@ -4895,8 +4897,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);
+	}
 
 	folio_unlock(folio);
 	folio_put(folio);
diff --git a/include/trace/events/ext4.h b/include/trace/events/ext4.h
index 69596a216dcb..4539ef8e5f86 100644
--- a/include/trace/events/ext4.h
+++ b/include/trace/events/ext4.h
@@ -3202,6 +3202,41 @@ 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_size)
+		__field(loff_t, i_disksize)
+	),
+	TP_fast_assign(
+		__entry->dev = inode->i_sb->s_dev;
+		__entry->ino = inode->i_ino;
+		__entry->i_size = i_size_read(inode);
+		__entry->i_disksize = READ_ONCE(EXT4_I(inode)->i_disksize);
+	),
+	TP_printk("dev %d:%d ino %llu i_size %lld i_disksize %lld",
+		  MAJOR(__entry->dev), MINOR(__entry->dev),
+		  __entry->ino, __entry->i_size, __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 */
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 30/32] ext4: add tracepoints for EOF block zeroing and disksize-grow I/O
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
                   ` (28 preceding siblings ...)
  2026-08-14  9:33 ` [PATCH -next v5 29/32] ext4: add tracepoints for DISKSIZE_GROW_PENDING set, clear, and wait Zhang Yi
@ 2026-08-14  9:33 ` Zhang Yi
  2026-08-14 10:19   ` sashiko-bot
  2026-08-14  9:46 ` [PATCH -next v5 31/32] ext4: partially enable iomap for the buffered I/O path of regular files Zhang Yi
  2026-08-14  9:46 ` [PATCH -next v5 32/32] ext4: introduce a mount option for iomap buffered I/O path Zhang Yi
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:33 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

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_end_bio.
- 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 <yi.zhang@huawei.com>
---
 fs/ext4/inode.c             |   3 +
 fs/ext4/page-io.c           |  13 ++++-
 include/trace/events/ext4.h | 107 ++++++++++++++++++++++++++++++++++++
 3 files changed, 121 insertions(+), 2 deletions(-)

diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index bca7d5c33919..ee15366422a1 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -4406,6 +4406,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;
 	}
@@ -5001,6 +5003,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..4464eb03c972 100644
--- a/fs/ext4/page-io.c
+++ b/fs/ext4/page-io.c
@@ -31,6 +31,8 @@
 #include "xattr.h"
 #include "acl.h"
 
+#include <trace/events/ext4.h>
+
 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)
@@ -729,8 +735,11 @@ void ext4_iomap_end_bio(struct bio *bio)
 	 * 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)
+	if (io_mode == EXT4_IOMAP_IOEND_DISKSIZE_GROW_IO) {
+		trace_ext4_iomap_wb_disksize_pending_complete(ioend->io_inode,
+				ioend->io_offset, ioend->io_size);
 		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))
diff --git a/include/trace/events/ext4.h b/include/trace/events/ext4.h
index 4539ef8e5f86..c9259c2a3e36 100644
--- a/include/trace/events/ext4.h
+++ b/include/trace/events/ext4.h
@@ -3237,6 +3237,113 @@ 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_complete,
+	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)
+);
+
+/* 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 */
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 31/32] ext4: partially enable iomap for the buffered I/O path of regular files
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
                   ` (29 preceding siblings ...)
  2026-08-14  9:33 ` [PATCH -next v5 30/32] ext4: add tracepoints for EOF block zeroing and disksize-grow I/O Zhang Yi
@ 2026-08-14  9:46 ` Zhang Yi
  2026-08-14 10:39   ` sashiko-bot
  2026-08-14  9:46 ` [PATCH -next v5 32/32] ext4: introduce a mount option for iomap buffered I/O path Zhang Yi
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:46 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

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 WARN_ON_ONCE() guards in _ext4_get_block() and
   ext4_do_writepages() to catch inodes using the iomap path from
   accidentally entering the legacy buffer_head writeback path.

 - Reject extent-to-indirect migration via ext4_ind_migrate() for inodes
   on the iomap path.

Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
---
 fs/ext4/ext4.h      |  1 +
 fs/ext4/ext4_jbd2.c |  8 +++-
 fs/ext4/ialloc.c    |  1 +
 fs/ext4/inode.c     | 99 ++++++++++++++++++++++++++++++++++++++++++++-
 fs/ext4/migrate.c   |  2 +
 5 files changed, 107 insertions(+), 4 deletions(-)

diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h
index 504dce9fdc6b..405e256a1802 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 ee15366422a1..c9ee78fba4d0 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -1034,6 +1034,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;
@@ -2904,6 +2907,12 @@ static int ext4_do_writepages(struct mpage_da_data *mpd)
 	if (!mapping->nrpages || !mapping_tagged(mapping, PAGECACHE_TAG_DIRTY))
 		goto out_writepages;
 
+	/* inode using the iomap buffered I/O path should not go here. */
+	if (WARN_ON_ONCE(ext4_inode_buffered_iomap(inode))) {
+		ret = -EINVAL;
+		goto out_writepages;
+	}
+
 	/*
 	 * If the filesystem has aborted, it is read-only, so return
 	 * right away instead of dumping stack traces later on that
@@ -4042,6 +4051,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;
 
@@ -4467,6 +4479,10 @@ static int ext4_iomap_writepages(struct address_space *mapping,
 		.ops = &ext4_writeback_ops,
 	};
 
+	/* inode using the buffer_head buffered I/O path should not go here. */
+	if (WARN_ON_ONCE(!ext4_inode_buffered_iomap(inode)))
+		return -EINVAL;
+
 	ret = ext4_emergency_state(sb);
 	if (unlikely(ret)) {
 		/*
@@ -6037,6 +6053,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;
@@ -6351,6 +6442,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;
@@ -7574,9 +7667,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);
@@ -7585,6 +7679,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
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH -next v5 32/32] ext4: introduce a mount option for iomap buffered I/O path
  2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
                   ` (30 preceding siblings ...)
  2026-08-14  9:46 ` [PATCH -next v5 31/32] ext4: partially enable iomap for the buffered I/O path of regular files Zhang Yi
@ 2026-08-14  9:46 ` Zhang Yi
  2026-08-14 10:25   ` sashiko-bot
  31 siblings, 1 reply; 65+ messages in thread
From: Zhang Yi @ 2026-08-14  9:46 UTC (permalink / raw)
  To: linux-ext4, linux-fsdevel
  Cc: linux-kernel, tytso, adilger.kernel, libaokun, jack, ojaswin,
	ritesh.list, djwong, hch, yi.zhang, yi.zhang, yizhang089,
	chengzhihao1, yangerkun, yukuai

From: Zhang Yi <yi.zhang@huawei.com>

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 <yi.zhang@huawei.com>
---
 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 405e256a1802..8cfe77b45e21 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 c9ee78fba4d0..c7466ecc44c2 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -6080,11 +6080,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 86ed5228dbe9..0855f9801df4 100644
--- a/fs/ext4/super.c
+++ b/fs/ext4/super.c
@@ -1746,6 +1746,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
@@ -1884,6 +1885,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 */
@@ -1977,6 +1980,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},
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 08/32] ext4: allow ext4_map_blocks() to start its own transaction handle
  2026-08-14  9:33 ` [PATCH -next v5 08/32] ext4: allow ext4_map_blocks() to start its own transaction handle Zhang Yi
@ 2026-08-14 10:00   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:00 UTC (permalink / raw)
  To: Zhang Yi; +Cc: linux-ext4, tytso

> From: Zhang Yi <yi.zhang@huawei.com>
> 
> 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_handle 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 <jack@suse.cz>
> Signed-off-by: Zhang Yi <yi.zhang@huawei.com>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=8


^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 01/32] ext4: simplify size updating in ext4_setattr()
  2026-08-14  9:33 ` [PATCH -next v5 01/32] ext4: simplify size updating in ext4_setattr() Zhang Yi
@ 2026-08-14 10:02   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:02 UTC (permalink / raw)
  To: Zhang Yi; +Cc: linux-ext4, tytso

> From: Zhang Yi <yi.zhang@huawei.com>
> 
> 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 <yi.zhang@huawei.com>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=1


^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 02/32] ext4: factor out ext4_truncate_[up|down]()
  2026-08-14  9:33 ` [PATCH -next v5 02/32] ext4: factor out ext4_truncate_[up|down]() Zhang Yi
@ 2026-08-14 10:04   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:04 UTC (permalink / raw)
  To: Zhang Yi; +Cc: linux-ext4, tytso

> From: Zhang Yi <yi.zhang@huawei.com>
> 
> 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 <yi.zhang@huawei.com>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=2


^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 11/32] ext4: add iomap address space operations for buffered I/O
  2026-08-14  9:33 ` [PATCH -next v5 11/32] ext4: add iomap address space operations for buffered I/O Zhang Yi
@ 2026-08-14 10:04   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:04 UTC (permalink / raw)
  To: Zhang Yi; +Cc: linux-ext4, tytso

> From: Zhang Yi <yi.zhang@huawei.com>
> 
> 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 <yi.zhang@huawei.com>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=11


^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 21/32] ext4: disable online defrag when inode using iomap buffered I/O path
  2026-08-14  9:33 ` [PATCH -next v5 21/32] ext4: disable online defrag when inode using " Zhang Yi
@ 2026-08-14 10:08   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:08 UTC (permalink / raw)
  To: Zhang Yi; +Cc: tytso, linux-ext4

> From: Zhang Yi <yi.zhang@huawei.com>
> 
> 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 <yi.zhang@huawei.com>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=21


^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 20/32] ext4: add block mapping tracepoints for iomap buffered I/O path
  2026-08-14  9:33 ` [PATCH -next v5 20/32] ext4: add block mapping tracepoints for iomap buffered I/O path Zhang Yi
@ 2026-08-14 10:08   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:08 UTC (permalink / raw)
  To: Zhang Yi; +Cc: tytso, linux-ext4

> From: Zhang Yi <yi.zhang@huawei.com>
> 
> 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 <yi.zhang@huawei.com>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=20


^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 05/32] ext4: set EXT4_MAP_NEW flag for delayed allocated blocks
  2026-08-14  9:33 ` [PATCH -next v5 05/32] ext4: set EXT4_MAP_NEW flag for delayed allocated blocks Zhang Yi
@ 2026-08-14 10:08   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:08 UTC (permalink / raw)
  To: Zhang Yi; +Cc: linux-ext4, tytso

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] The newly introduced EXT4_MAP_NEW flag is completely ignored by its only caller, rendering the patch ineffective (dead code) and failing to resolve the underlying bug.
--

--- Patch [5]: [PATCH -next v5 05/32] ext4: set EXT4_MAP_NEW flag for delayed allocated blocks ---
commit f71e7357d8a5fa9c83f4d4fc004cfa3640d60291
Author: Zhang Yi <yi.zhang@huawei.com>

ext4: set EXT4_MAP_NEW flag for delayed allocated blocks

This commit sets the EXT4_MAP_NEW flag in ext4_da_map_blocks() to indicate
when a new delayed allocation block has been inserted. This aims to allow
callers to distinguish newly created delayed extents from existing ones.

Link: https://lore.kernel.org/linux-ext4/cc05c17d-163e-4251-b2c9-aa3a6f9555d7@huaweicloud.com/

> diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
> index 7601fe3618b1f..9dbece14ae569 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;

[Severity: High]
Does this code actually allow callers to use the newly added flag?

Looking at ext4_da_get_block_prep(), which calls ext4_da_map_blocks(),
it unconditionally intercepts EXT4_MAP_DELAYED, sets BH_New, and returns
early without checking EXT4_MAP_NEW:

fs/ext4/inode.c:ext4_da_get_block_prep() {
    ...
	if (map.m_flags & EXT4_MAP_DELAYED) {
		map_bh(bh, inode->i_sb, invalid_block);
		set_buffer_new(bh);
		set_buffer_delay(bh);
		return 0;
	}
    ...
}

Because EXT4_MAP_DELAYED is always set alongside EXT4_MAP_NEW, this branch is
always taken, and set_buffer_new(bh) is unconditionally called. The code never
seems to evaluate map.m_flags & EXT4_MAP_NEW.

Is it possible this patch is incomplete and the underlying issue remains
unfixed because the new flag is ignored?

>  	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);

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=5

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 07/32] ext4: fix orig_mlen initialization in ext4_map_blocks()
  2026-08-14  9:33 ` [PATCH -next v5 07/32] ext4: fix orig_mlen initialization in ext4_map_blocks() Zhang Yi
@ 2026-08-14 10:08   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:08 UTC (permalink / raw)
  To: Zhang Yi; +Cc: tytso, linux-ext4

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:

Pre-existing issues:
- [High] If ext4_issue_zeroout() fails during block allocation with EXT4_GET_BLOCKS_ZERO (e.g. via DAX hole-filling), the newly allocated WRITTEN extent is not reverted, exposing uninitialized disk data.
--

--- Patch [7]: [PATCH -next v5 07/32] ext4: fix orig_mlen initialization in ext4_map_blocks() ---
commit 547294dd62d03f6ea5caef0d90e340b0af933eae
Author: Zhang Yi <yi.zhang@huawei.com>

ext4: fix orig_mlen initialization in ext4_map_blocks()

This commit fixes an overflow protection bypass by ensuring orig_mlen
is saved only after clamping map->m_len to INT_MAX.

> diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
> --- a/fs/ext4/inode.c
> +++ b/fs/ext4/inode.c

[ ... ]

> @@ -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;

[Severity: High]
This is a pre-existing issue, but while reviewing the block mapping code in
fs/ext4/inode.c, I noticed a potential data exposure when DAX hole-filling
writes fail to zero blocks.

If ext4_issue_zeroout() fails in ext4_map_create_blocks(), the error is
returned immediately without reverting the extent tree modification:

fs/ext4/inode.c:ext4_map_create_blocks() {
    ...
    if (flags & EXT4_GET_BLOCKS_ZERO &&
        map->m_flags & EXT4_MAP_MAPPED && map->m_flags & EXT4_MAP_NEW) {
        err = ext4_issue_zeroout(inode, map->m_lblk, map->m_pblk, map->m_len);
        if (err)
            return err;
    }
    ...
}

This error propagates back to ext4_iomap_alloc(), which aborts the operation
but proceeds to commit the transaction anyway:

fs/ext4/inode.c:ext4_iomap_alloc() {
    ...
    if (ret < 0) {
        ext4_journal_stop(handle);
        return ret;
    }
    ...
}

For a DAX write to a hole within EOF, the extend flag evaluates to false in
ext4_dax_write_iter(). This completely bypasses the cleanup path that would
otherwise drop the failed extent:

fs/ext4/file.c:ext4_dax_write_iter() {
    ...
    if (extend) {
        ret = ext4_handle_inode_extension(inode, offset, ret, count);
        ext4_inode_extension_cleanup(inode, ret < (ssize_t)count);
    }
    ...
}

Because the newly allocated extent was already inserted into the extent tree
as WRITTEN by ext4_ext_map_blocks() earlier in the call chain, could this
sequence leave uninitialized disk data mapped and exposed to userspace?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=7

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 19/32] ext4: tolerate unexpected holes in ext4_convert_unwritten_extents()
  2026-08-14  9:33 ` [PATCH -next v5 19/32] ext4: tolerate unexpected holes in ext4_convert_unwritten_extents() Zhang Yi
@ 2026-08-14 10:09   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:09 UTC (permalink / raw)
  To: Zhang Yi; +Cc: linux-ext4, tytso

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Critical] The patch masks a severe physical block Use-After-Free that causes silent data corruption by suppressing the warning when IO completion observes a hole.
--

--- Patch [19]: [PATCH -next v5 19/32] ext4: tolerate unexpected holes in ext4_convert_unwritten_extents() ---
commit 024b041d5cfa7f6f30a260ff0a2a3cd17336e881
Author: Zhang Yi <yi.zhang@huawei.com>
Subject: ext4: tolerate unexpected holes in ext4_convert_unwritten_extents()

This patch suppresses the warning in ext4_convert_unwritten_extents() when
ext4_map_blocks() returns 0 (indicating a hole). It aims to handle the case
where a punch hole operation races with writeback when folio sizes are larger
than the block size.

Link: https://lore.kernel.org/linux-ext4/a638a8fb-c184-4069-ae33-379ec12cd514@huaweicloud.com/

> diff --git a/fs/ext4/extents.c b/fs/ext4/extents.c
> --- a/fs/ext4/extents.c
> +++ b/fs/ext4/extents.c

[ ... ]

> @@ -5166,19 +5166,21 @@ int ext4_convert_unwritten_extents(handle_t *handle, struct inode *inode,
>  		ret = ext4_map_blocks(handle, inode, &map,
>  				      EXT4_GET_BLOCKS_IO_CONVERT_EXT |
>  				      EXT4_EX_NOCACHE);
> -		if (ret <= 0) {
> -			/*
> -			 * If the ret is zero, an unexpected hole may cause
> -			 * conversion to fail.  To avoid data loss during I/O
> -			 * end conversion, skip the hole and continue
> -			 * converting subsequent blocks.
> -			 */
> +		/*
> +		 * A return value of zero means an unexpected hole was found.
> +		 * This can happen when writeback races with a concurrent
> +		 * punch hole in the iomap path. Because iomap may not create
> +		 * ifs for folios larger than block size, the dirty bit can
> +		 * be set again after punching. If writeback happens between
> +		 * partial folio invalidation and extent removal, a hole is
> +		 * observed at I/O completion.
> +		 */
> +		if (ret < 0)
>  			ext4_warning(inode->i_sb,
>  				     "inode #%llu: block %u: len %u: ext4_map_blocks returned %d",
>  				     inode->i_ino, map.m_lblk, map.m_len, ret);
> -		} else {
> +		else if (ret > 0)
>  			conv_blocks += map.m_len;

[Severity: Critical]
Does masking the ret == 0 case here hide a physical block use-after-free?

If background writeback via ext4_iomap_writepages() submits a write bio for
the dirty folio, and a concurrent ext4_punch_hole() removes the extent, the
physical blocks are freed.

Since JBD2 does not wait for this in-flight bio during transaction commit
(due to data=ordered mode being disabled for iomap inodes), those blocks
return to the free pool and can be reallocated to another file.

When the in-flight write bio finally completes, will it silently overwrite
the newly reallocated blocks and cause data corruption?

Instead of silencing the warning during IO completion in
ext4_convert_unwritten_extents(), could the underlying race be prevented by
ensuring sub-folio dirty state is properly tracked so that punched blocks
are not submitted for writeback in the first place?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=19

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 22/32] ext4: add EXT4_STATE_DISKSIZE_GROW_PENDING state bit and helpers
  2026-08-14  9:33 ` [PATCH -next v5 22/32] ext4: add EXT4_STATE_DISKSIZE_GROW_PENDING state bit and helpers Zhang Yi
@ 2026-08-14 10:09   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:09 UTC (permalink / raw)
  To: Zhang Yi; +Cc: linux-ext4, tytso

> From: Zhang Yi <yi.zhang@huawei.com>
> 
> 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 <jack@suse.cz>
> Signed-off-by: Zhang Yi <yi.zhang@huawei.com>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=22


^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 03/32] ext4: simplify error handling in ext4_setattr()
  2026-08-14  9:33 ` [PATCH -next v5 03/32] ext4: simplify error handling in ext4_setattr() Zhang Yi
@ 2026-08-14 10:12   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:12 UTC (permalink / raw)
  To: Zhang Yi; +Cc: linux-ext4, tytso

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Critical] The patch incorrectly routes non-fatal errors from `posix_acl_chmod()` (and `ext4_truncate()`) to `ext4_std_error()`, triggering a spurious filesystem corruption response (DoS).
--

--- Patch [3]: [PATCH -next v5 03/32] ext4: simplify error handling in ext4_setattr() ---
commit f94f03ca7ff7f314c8faf61276a2cc51f78e4ce7
Author: Zhang Yi <yi.zhang@huawei.com>

ext4: simplify error handling in ext4_setattr()

Refactor the error handling in ext4_setattr() to propagate errors using
the existing error variable and jump to the common 'err_out' label. This
removes the need for the redundant rc variable and unreachable checks.

> diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
> index 8654006a57eff..76bf0e944ebee 100644
> --- a/fs/ext4/inode.c
> +++ b/fs/ext4/inode.c
[ ... ]
> @@ -6244,15 +6244,19 @@ int ext4_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
>  		}
>  
>  		filemap_invalidate_unlock(inode->i_mapping);
> +		if (error)
> +			goto err_out;
>  	}
>  
> -	if (!error) {
> -		if (inc_ivers)
> -			inode_inc_iversion(inode);
> -		setattr_copy(idmap, inode, attr);
> -		mark_inode_dirty(inode);
> -	}
> +	if (inc_ivers)
> +		inode_inc_iversion(inode);
> +	setattr_copy(idmap, inode, attr);
> +	mark_inode_dirty(inode);
>  
> +	if (ia_valid & ATTR_MODE)
> +		error = posix_acl_chmod(idmap, dentry, inode->i_mode);
> +
> +err_out:
>  	/*
>  	 * If the call to ext4_truncate failed to get a transaction handle at
>  	 * all, we need to clean up the in-core orphan list manually.
> @@ -6260,14 +6264,8 @@ int ext4_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
>  	if (orphan && inode->i_nlink)
>  		ext4_orphan_del(NULL, inode);
>  
> -	if (!error && (ia_valid & ATTR_MODE))
> -		rc = posix_acl_chmod(idmap, dentry, inode->i_mode);
> -
> -err_out:
> -	if  (error)
> +	if (error)
>  		ext4_std_error(inode->i_sb, error);

[Severity: Critical]
Does this code now incorrectly route non-fatal errors from posix_acl_chmod()
(and ext4_truncate()) to ext4_std_error()?

Passing a non-fatal error code like -ENOSPC or -EDQUOT to ext4_std_error()
can cause it to treat the condition as a critical filesystem metadata
corruption, aborting the journal and remounting read-only (or panicking).

For example, if an unprivileged user exhausts their quota or filesystem
space, and then executes chmod on a file with a POSIX ACL,
posix_acl_chmod() attempts to update the ACL, calling ext4_set_acl(), which
may return -EDQUOT or -ENOSPC. This error is now assigned to the error
variable and falls through to the err_out label, triggering
ext4_std_error() and causing a potential local Denial of Service (DoS).

> -	if (!error)
> -		error = rc;
>  	return error;
>  }
>

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=3

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 13/32] ext4: pass out extent seq counter when mapping da blocks
  2026-08-14  9:33 ` [PATCH -next v5 13/32] ext4: pass out extent seq counter when mapping da blocks Zhang Yi
@ 2026-08-14 10:12   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:12 UTC (permalink / raw)
  To: Zhang Yi; +Cc: tytso, linux-ext4

> From: Zhang Yi <yi.zhang@huawei.com>
> 
> 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 <yi.zhang@huawei.com>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=13


^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 14/32] ext4: do not use data=ordered mode for inodes using buffered iomap path
  2026-08-14  9:33 ` [PATCH -next v5 14/32] ext4: do not use data=ordered mode for inodes using buffered iomap path Zhang Yi
@ 2026-08-14 10:12   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:12 UTC (permalink / raw)
  To: Zhang Yi; +Cc: tytso, linux-ext4

> From: Zhang Yi <yi.zhang@huawei.com>
> 
> 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,
> [ ... ]
> 
> Online defragmentation does not yet support iomap; this can be resolved
> separately in the future.
> 
> Signed-off-by: Zhang Yi <yi.zhang@huawei.com>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=14


^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 04/32] ext4: skip ordered I/O wait when zeroing beyond i_disksize block
  2026-08-14  9:33 ` [PATCH -next v5 04/32] ext4: skip ordered I/O wait when zeroing beyond i_disksize block Zhang Yi
@ 2026-08-14 10:14   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:14 UTC (permalink / raw)
  To: Zhang Yi; +Cc: tytso, linux-ext4

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Torn read of the 64-bit `i_disksize` on 32-bit architectures can cause the kernel to skip ordered I/O, leading to stale data exposure.
--

--- Patch [4]: [PATCH -next v5 04/32] ext4: skip ordered I/O wait when zeroing beyond i_disksize block ---
commit bb84de10d990ded9aab4107d18dd1616342efccd
Author: Zhang Yi <yi.zhang@huawei.com>

ext4: skip ordered I/O wait when zeroing beyond i_disksize block

This commit optimizes ext4_block_zero_eof() by skipping the ordered I/O
wait when the zeroed range is entirely beyond the block containing
i_disksize. Since data beyond i_disksize hasn't been written to disk yet,
the ordered I/O wait is unnecessary and can be bypassed.

> --- a/fs/ext4/inode.c
> +++ b/fs/ext4/inode.c
[ ... ]
> @@ -4241,9 +4241,16 @@ 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.
> +	 *
> +	 * Ordered I/O is required only when zeroing the tail of a block that
> +	 * overlaps with i_disksize. If the zeroed range falls outside that
> +	 * block, the zeroed data lies beyond the existing on-disk data. It
> +	 * will be written out before i_disksize is later extended past
> +	 * i_size, so no stale data can be exposed.
>  	 */
>  	if (ext4_should_order_data(inode) &&
> -	    did_zero && zero_written && !IS_DAX(inode)) {
> +	    did_zero && zero_written && !IS_DAX(inode) &&
> +	    from < round_up(READ_ONCE(EXT4_I(inode)->i_disksize), blocksize)) {

[Severity: High]
Could this READ_ONCE() on the 64-bit i_disksize result in a torn read on
32-bit architectures?

If a background writeback thread concurrently updates i_disksize under
i_data_sem but without holding i_rwsem, and the file size is extending
across a 4GB boundary (for example, transitioning from 0x00000000FFFFFFFF
to 0x0000000100000000), a torn read might fetch 0 if the lower 32 bits are
zeroed first.

If 0 is returned here, round_up(0, blocksize) evaluates to 0, which makes
the condition 'from < 0' false. Would this incorrectly bypass the critical
ordered I/O guarantee and potentially expose stale data in the event of a
crash?

>  		handle_t *handle;
>  
>  		handle = ext4_journal_start(inode, EXT4_HT_MISC, 1);

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=4

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 12/32] ext4: implement buffered read path using iomap
  2026-08-14  9:33 ` [PATCH -next v5 12/32] ext4: implement buffered read path using iomap Zhang Yi
@ 2026-08-14 10:15   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:15 UTC (permalink / raw)
  To: Zhang Yi; +Cc: tytso, linux-ext4

> From: Zhang Yi <yi.zhang@huawei.com>
> 
> 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 <yi.zhang@huawei.com>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=12


^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 06/32] ext4: recheck extent status tree before block allocation
  2026-08-14  9:33 ` [PATCH -next v5 06/32] ext4: recheck extent status tree before block allocation Zhang Yi
@ 2026-08-14 10:15   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:15 UTC (permalink / raw)
  To: Zhang Yi; +Cc: linux-ext4, tytso

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Reading uninitialized `map->m_seq` on error paths leads to an infinite retry loop in `ext4_map_blocks()`, causing a kernel hard lockup.
--

--- Patch [6]: [PATCH -next v5 06/32] ext4: recheck extent status tree before block allocation ---
commit 6ae9b515f2027351280fce82734e7e8361a281e7
Author: Zhang Yi <yi.zhang@huawei.com>

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. If
we find a valid extent and the sequence number has changed, retry
the entire lookup to obtain the correct trimmed mapping.

> diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
> index 9dbece14ae569..548a3968c5a7d 100644
> --- a/fs/ext4/inode.c
> +++ b/fs/ext4/inode.c

[ ... ]

> @@ -820,6 +821,19 @@ 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 does not hold i_rwsem or the folio locks
> +	 * covering the full extent.
> +	 */
> +	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;
> +	}

[Severity: High]
Could this cause an infinite loop and hard lockup on disk error?

If ext4_map_query_blocks() encounters an error like -EIO or -EFSCORRUPTED,
it returns < 0 without setting map->m_seq.

However, if EXT4_GET_BLOCKS_CREATE is set, the function does not return
early on error and falls through to this TOCTOU check.

Since callers like _ext4_get_block() allocate struct ext4_map_blocks
on the stack:

fs/ext4/inode.c:_ext4_get_block() {
        struct ext4_map_blocks map;
        ...
        map.m_lblk = iblock;
        map.m_len = bh->b_size >> inode->i_blkbits;

        ret = ext4_map_blocks(..., &map, flags);
}

Will map->m_seq contain uninitialized stack garbage, causing the sequence
check to mismatch and jump to create_retry, which then encounters the exact
same disk error indefinitely?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=6

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 10/32] ext4: skip block allocation for holes in the data submission path
  2026-08-14  9:33 ` [PATCH -next v5 10/32] ext4: skip block allocation for holes in the data submission path Zhang Yi
@ 2026-08-14 10:15   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:15 UTC (permalink / raw)
  To: Zhang Yi; +Cc: linux-ext4, tytso

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] ext4_convert_unwritten_extents_atomic() silently aborts conversion if it encounters a hole, leading to data loss for the rest of the atomic write range.
--

--- Patch [10]: [PATCH -next v5 10/32] ext4: skip block allocation for holes in the data submission path ---
commit 741674f6128ebda7382e0529fd0b709e4ae876be
Author: Zhang Yi <yi.zhang@huawei.com>

ext4: skip block allocation for holes in the data submission path

This commit updates ext4_map_blocks() to avoid allocating blocks for holes
in the data submission path, returning 0 instead. To prevent data loss
during extent conversion, ext4_convert_unwritten_extents() is modified
to skip over unexpected holes and continue converting subsequent blocks.

> diff --git a/fs/ext4/extents.c b/fs/ext4/extents.c
> index 76038b6c36552..0d62d9312284e 100644
> --- a/fs/ext4/extents.c
> +++ b/fs/ext4/extents.c
> @@ -5167,11 +5167,15 @@ int ext4_convert_unwritten_extents(handle_t *handle, struct inode *inode,
>  				      EXT4_GET_BLOCKS_IO_CONVERT_EXT |
>  				      EXT4_EX_NOCACHE);
>  		if (ret <= 0) {
> +			/*
> +			 * If the ret is zero, an unexpected hole may cause
> +			 * conversion to fail.  To avoid data loss during I/O
> +			 * end conversion, skip the hole and continue
> +			 * converting subsequent blocks.
> +			 */
>  			ext4_warning(inode->i_sb,
>  				     "inode #%llu: block %u: len %u: ext4_map_blocks returned %d",
>  				     inode->i_ino, map.m_lblk, map.m_len, ret);
> -			if (unlikely(ret == 0))
> -				ret = -EINVAL;
>  		} else {
>  			conv_blocks += map.m_len;
>  		}

[Severity: High]
Since ext4_map_blocks() will now return 0 for holes when
EXT4_GET_BLOCKS_IO_SUBMIT is set, should
ext4_convert_unwritten_extents_atomic() also be updated to handle holes?

In ext4_convert_unwritten_extents_atomic():

	int flags = EXT4_GET_BLOCKS_IO_CONVERT_EXT | EXT4_EX_NOCACHE;
...
	while (ret >= 0 && ret < max_blocks) {
		map.m_lblk += ret;
		map.m_len = (max_blocks -= ret);
		ret = ext4_map_blocks(handle, inode, &map, flags);
...
		if (ret <= 0)
			break;
	}

It looks like if ext4_map_blocks() returns 0 for a hole, the loop will
break early and the function will return 0. Will this leave the remaining
blocks in the atomic write range unconverted and cause data loss?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=10

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 09/32] ext4: avoid unnecessary transaction in ext4_map_blocks() for unwritten extents
  2026-08-14  9:33 ` [PATCH -next v5 09/32] ext4: avoid unnecessary transaction in ext4_map_blocks() for unwritten extents Zhang Yi
@ 2026-08-14 10:15   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:15 UTC (permalink / raw)
  To: Zhang Yi; +Cc: linux-ext4, tytso

> From: Zhang Yi <yi.zhang@huawei.com>
> 
> 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 <yi.zhang@huawei.com>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=9


^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 28/32] ext4: set DISKSIZE_GROW_PENDING after zeroing unaligned EOF block
  2026-08-14  9:33 ` [PATCH -next v5 28/32] ext4: set DISKSIZE_GROW_PENDING after zeroing unaligned EOF block Zhang Yi
@ 2026-08-14 10:18   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:18 UTC (permalink / raw)
  To: Zhang Yi; +Cc: tytso, linux-ext4

> From: Zhang Yi <yi.zhang@huawei.com>
> 
> 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 <jack@suse.cz>
> Signed-off-by: Zhang Yi <yi.zhang@huawei.com>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=28


^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 29/32] ext4: add tracepoints for DISKSIZE_GROW_PENDING set, clear, and wait
  2026-08-14  9:33 ` [PATCH -next v5 29/32] ext4: add tracepoints for DISKSIZE_GROW_PENDING set, clear, and wait Zhang Yi
@ 2026-08-14 10:18   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:18 UTC (permalink / raw)
  To: Zhang Yi; +Cc: tytso, linux-ext4

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Sequence counter deadlock in tracepoint when called from softirq context on 32-bit SMP.
--

--- Patch [29]: [PATCH -next v5 29/32] ext4: add tracepoints for DISKSIZE_GROW_PENDING set, clear, and wait ---
commit f14aeb9661220b55cf0eeb4ff49da9fa6a2a18ca
Author: Zhang Yi <yi.zhang@huawei.com>

ext4: add tracepoints for DISKSIZE_GROW_PENDING set, clear, and wait
    
This commit adds trace events ext4_iomap_mark_disksize_pending(),
ext4_iomap_clear_disksize_pending(), and ext4_iomap_wait_disksize_pending().
These tracepoints track disksize-grow-pending state changes and waiting.

> diff --git a/include/trace/events/ext4.h b/include/trace/events/ext4.h
> index 69596a216dcb1..4539ef8e5f86c 100644
> --- a/include/trace/events/ext4.h
> +++ b/include/trace/events/ext4.h
[ ... ]
> +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_size)
> +		__field(loff_t, i_disksize)
> +	),
> +	TP_fast_assign(
> +		__entry->dev = inode->i_sb->s_dev;
> +		__entry->ino = inode->i_ino;
> +		__entry->i_size = i_size_read(inode);

[Severity: High]
Could this cause a sequence counter deadlock on 32-bit SMP architectures?

If a process executing i_size_write() is preempted by a block I/O completion
hardware interrupt, and pending softirqs are executed on return from the
interrupt, the softirq could call ext4_iomap_end_bio() in fs/ext4/page-io.c:

fs/ext4/page-io.c:ext4_iomap_end_bio() {
        ...
        if (io_mode == EXT4_IOMAP_IOEND_DISKSIZE_GROW_IO)
                ext4_iomap_clear_disksize_pending(ioend->io_inode);
        ...
}

This would trigger trace_ext4_iomap_clear_disksize_pending() which calls
i_size_read() here in TP_fast_assign.

Since i_size_write() disables preemption but not softirqs, the seqcount
would be odd. This would cause i_size_read() to spin indefinitely waiting
for the interrupted i_size_write() to complete on the same CPU.

> +		__entry->i_disksize = READ_ONCE(EXT4_I(inode)->i_disksize);
> +	),
> +	TP_printk("dev %d:%d ino %llu i_size %lld i_disksize %lld",
> +		  MAJOR(__entry->dev), MINOR(__entry->dev),
> +		  __entry->ino, __entry->i_size, __entry->i_disksize)
> +);

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=29

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 30/32] ext4: add tracepoints for EOF block zeroing and disksize-grow I/O
  2026-08-14  9:33 ` [PATCH -next v5 30/32] ext4: add tracepoints for EOF block zeroing and disksize-grow I/O Zhang Yi
@ 2026-08-14 10:19   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:19 UTC (permalink / raw)
  To: Zhang Yi; +Cc: linux-ext4, tytso

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Calling `i_size_read()` from the block I/O completion context (softirq) inside the `ext4_iomap_wb_disksize_pending` tracepoint class causes a hard lockup on 32-bit SMP systems.
--

--- Patch [30]: [PATCH -next v5 30/32] ext4: add tracepoints for EOF block zeroing and disksize-grow I/O ---
commit 9f6116e7039af778ef1701a210ea9b800dcc543c
Author: Zhang Yi <yi.zhang@huawei.com>

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.

> diff --git a/fs/ext4/page-io.c b/fs/ext4/page-io.c
> index 4f1176b9332f2..4464eb03c9728 100644
> --- a/fs/ext4/page-io.c
> +++ b/fs/ext4/page-io.c

[ ... ]

> @@ -729,8 +735,11 @@ void ext4_iomap_end_bio(struct bio *bio)
>  	 * 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)
> +	if (io_mode == EXT4_IOMAP_IOEND_DISKSIZE_GROW_IO) {
> +		trace_ext4_iomap_wb_disksize_pending_complete(ioend->io_inode,
> +				ioend->io_offset, ioend->io_size);
>  		ext4_iomap_clear_disksize_pending(ioend->io_inode);
> +	}

> diff --git a/include/trace/events/ext4.h b/include/trace/events/ext4.h
> index 4539ef8e5f86c..c9259c2a3e364 100644
> --- a/include/trace/events/ext4.h
> +++ b/include/trace/events/ext4.h

[ ... ]

> +/* 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);

[Severity: High]
Does this code introduce a deadlock on 32-bit SMP systems?

The tracepoint ext4_iomap_wb_disksize_pending_complete is called from
ext4_iomap_end_bio(), which executes in softirq context. This happens
because ext4_iomap_writeback_submit() clears the BIO_COMPLETE_IN_TASK flag.

When TP_fast_assign() evaluates i_size_read(inode), it uses a read sequence
lock on 32-bit SMP systems via read_seqcount_begin(&inode->i_size_seqcount).

If i_size_write() is executing concurrently in process context on the same
CPU, it holds the sequence lock without disabling softirqs. If the softirq
interrupts the i_size_write() section, i_size_read() will spin infinitely
waiting for the sequence count to become even.

Is there a way to avoid reading the inode size in this softirq context, or
a safer way to sample it here?

> +		__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)
> +);

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=30

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 16/32] ext4: implement writeback path using iomap
  2026-08-14  9:33 ` [PATCH -next v5 16/32] ext4: implement writeback " Zhang Yi
@ 2026-08-14 10:19   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:19 UTC (permalink / raw)
  To: Zhang Yi; +Cc: tytso, linux-ext4

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [Critical] The iomap writeback path queues `iomap_ioend` structures to the `i_rsv_conversion_list`, but the worker function is uninitialized and defaults to `ext4_end_io_rsv_work`, leading to type confusion and memory corruption.
- [Critical] Failure to update `ext4_page_mkwrite()` to use the iomap framework for iomap-enabled inodes causes AB-BA deadlocks and type confusion on `folio->private`.
- [High] The iomap writeback range mapping logic unconditionally expands the allocation length up to the end of the writeback range, incorrectly allocating up to 8MB of unwritten extents into sparse file holes.
- [Medium] Missing WRITE_ONCE() for 64-bit ei->i_disksize allows torn reads on 32-bit systems.
- [Medium] Improper use of min_t() truncates the 64-bit wbc->range_end before comparison, defeating the maximum block cap.
--

--- Patch [16]: [PATCH -next v5 16/32] ext4: implement writeback path using iomap ---
commit 5c1a016c9a25acdff3a66fb714afe96821e567b6
Author: Zhang Yi <yi.zhang@huawei.com>

ext4: implement writeback path using iomap

This commit adds the iomap writeback path for ext4 buffered I/O, introducing 
ext4_iomap_writepages() and the corresponding iomap_writeback_ops. It also 
adds an end I/O work handler to convert unwritten extents and update file 
size upon I/O completion.

> diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
> index d831d1911a6ff..0b3e54e12b78f 100644
> --- a/fs/ext4/inode.c
> +++ b/fs/ext4/inode.c
[ ... ]
> @@ -4134,10 +4135,154 @@ static void ext4_iomap_readahead(struct readahead_control *rac)
> +static int ext4_iomap_map_writeback_range(struct iomap_writepage_ctx *wpc,
> +					  loff_t offset, unsigned int dirty_len)
> +{
[ ... ]
> +	blk_len = dirty_len >> blkbits;
> +	blk_end = min_t(unsigned int, (wpc->wbc->range_end >> blkbits),
> +				      (UINT_MAX - 1));

[Severity: Medium]
Does this code truncate the 64-bit range_end before the comparison?

If wpc->wbc->range_end >> blkbits is evaluated as a 64-bit value but then
cast to unsigned int by min_t(unsigned int, ...), could this truncate the
value to 0 when the shifted value is a multiple of 2^32?

This might disable the writeback batching optimization for specific offsets
in very large files (e.g., syncing around 16TB boundaries).

> +	if (blk_end > index + blk_len)
> +		blk_len = blk_end - index + 1;

[Severity: High]
Will this logic unconditionally expand the block allocation length to the
end of the writeback range?

It looks like blk_len is expanded up to blk_end (which typically represents
the end of the VFS writeback range) without querying the delalloc
reservation length.

If a dirty folio is followed by a hole, could this allocate up to 8MB of
unwritten extents into sparse file holes? This seems like it could
improperly consume disk space and quotas.

> +retry:
> +	map.m_lblk = index;
> +	map.m_len = min_t(unsigned int, MAX_WRITEPAGES_EXTENT_LEN, blk_len);
[ ... ]
> +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)
>  {

[Severity: Critical]
Since this patch introduces the iomap writeback path, do we also need to
update the mmap fault handler?

The ext4_file_vm_ops.page_mkwrite handler remains hardcoded to
ext4_page_mkwrite(), which uses the legacy ext4_block_page_mkwrite()
logic.

That legacy path attaches a buffer_head to folio->private and starts a
transaction before taking the folio lock.

When ext4_iomap_writepages() later flushes the folio,
iomap_writeback_init() will cast folio->private to struct
iomap_folio_state.

Calling atomic_set() on this state could overwrite the b_this_page list
pointer in the buffer_head, potentially leading to memory corruption and
AB-BA deadlocks between page faulting and writeback.

> -	return 0;
> +	struct inode *inode = mapping->host;
[ ... ]
> diff --git a/fs/ext4/page-io.c b/fs/ext4/page-io.c
> index 0236b6b9785a2..2888e00575615 100644
> --- a/fs/ext4/page-io.c
> +++ b/fs/ext4/page-io.c
[ ... ]
> @@ -547,3 +548,124 @@ void ext4_bio_write_folio(struct ext4_io_submit *io, struct folio *folio,
> +static int ext4_iomap_wb_update_disksize(handle_t *handle, struct inode *inode,
> +					 loff_t end)
> +{
[ ... ]
> +	down_write(&ei->i_data_sem);
> +	new_disksize = min(new_disksize, i_size_read(inode));
> +	if (new_disksize > ei->i_disksize)
> +		ei->i_disksize = new_disksize;

[Severity: Medium]
Does this 64-bit assignment need a WRITE_ONCE() wrapper?

On 32-bit platforms, a plain 64-bit store to ei->i_disksize can be split
into two 32-bit stores.

A concurrent lockless reader calling READ_ONCE(ei->i_disksize) might
observe a torn value, resulting in wildly incorrect disk size values
during concurrent writeback and truncation.

> +	up_write(&ei->i_data_sem);
> +	ret = ext4_mark_inode_dirty(handle, inode);
[ ... ]
> +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);

[Severity: Critical]
Is the worker function for ei->i_rsv_conversion_work properly initialized
for iomap inodes?

It looks like ei->i_rsv_conversion_work is statically initialized to the
legacy ext4_end_io_rsv_work() function in ext4_alloc_inode().

This new iomap writeback path queues iomap_ioend structs to the list, but
it doesn't appear to call INIT_WORK() to reassign the worker function to
the newly introduced ext4_iomap_end_io().

When the work queue executes, wouldn't the legacy ext4_end_io_rsv_work()
iterate over this list and blindly cast iomap_ioend structs to
incompatible ext4_io_end_t structs, resulting in type confusion?

> +	list_add_tail(&ioend->io_list, &ei->i_rsv_conversion_list);
> +	spin_unlock_irqrestore(&ei->i_completed_io_lock, flags);
> +}

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=16

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 32/32] ext4: introduce a mount option for iomap buffered I/O path
  2026-08-14  9:46 ` [PATCH -next v5 32/32] ext4: introduce a mount option for iomap buffered I/O path Zhang Yi
@ 2026-08-14 10:25   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:25 UTC (permalink / raw)
  To: Zhang Yi; +Cc: linux-ext4, tytso

> From: Zhang Yi <yi.zhang@huawei.com>
> 
> 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 <yi.zhang@huawei.com>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=32


^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 18/32] ext4: implement partial block zero range path using iomap
  2026-08-14  9:33 ` [PATCH -next v5 18/32] ext4: implement partial block zero range " Zhang Yi
@ 2026-08-14 10:25   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:25 UTC (permalink / raw)
  To: Zhang Yi; +Cc: linux-ext4, tytso

> From: Zhang Yi <yi.zhang@huawei.com>
> 
> Introduce a new iomap_ops instance, ext4_iomap_zero_ops, along with
> ext4_iomap_block_zero_range() to implement block zeroing via the iomap
> infrastructure for ext4.
> 
> ext4_iomap_block_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.
> [ ... ]
> 
> Subsequent patches will address this by deferring i_disksize update
> to i_size until after the zeroed data has been written back.
> 
> Signed-off-by: Zhang Yi <yi.zhang@huawei.com>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=18


^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 24/32] ext4: advance i_disksize to i_size upon disksize-grow I/O completion
  2026-08-14  9:33 ` [PATCH -next v5 24/32] ext4: advance i_disksize to i_size upon disksize-grow I/O completion Zhang Yi
@ 2026-08-14 10:25   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:25 UTC (permalink / raw)
  To: Zhang Yi; +Cc: tytso, linux-ext4

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] The EXT4_STATE_DISKSIZE_GROW_PENDING state bit is never set anywhere in the codebase, rendering the entire disksize-grow logic and safety mechanism inert.
--

--- Patch [24]: [PATCH -next v5 24/32] ext4: advance i_disksize to i_size upon disksize-grow I/O completion ---
commit d834c25602b493451d52cc75b3ae8fd40461bce9
Author: Zhang Yi <yi.zhang@huawei.com>

ext4: advance i_disksize to i_size upon disksize-grow I/O completion

This commit updates the iomap writeback path to advance i_disksize to i_size
upon completion of a disksize-grow I/O. It introduces an is_disksize_grow flag
to ext4_iomap_wb_update_disksize() to trigger this update safely.

> diff --git a/fs/ext4/page-io.c b/fs/ext4/page-io.c
> index 955ff88045db6..4f1176b9332f2 100644
> --- a/fs/ext4/page-io.c
> +++ b/fs/ext4/page-io.c
[ ... ]
> @@ -589,9 +589,36 @@ static int ext4_iomap_wb_update_disksize(handle_t *handle, struct inode *inode,
>  	 * i_data_sem.
>  	 */
>  	down_write(&ei->i_data_sem);
> -	new_disksize = min(new_disksize, i_size_read(inode));
> +	i_size = i_size_read(inode);
> +
> +	/*
> +	 * EXT4_STATE_DISKSIZE_GROW_PENDING is cleared when the pending
> +	 * I/O completes. However, another thread may have re-set the bit
> +	 * between that point and here, meaning i_disksize has already
> +	 * been advanced and a new EOF zeroing has been initiated. In that
> +	 * case, do not advance i_disksize to i_size; leave it to the
> +	 * next pending grow ioend.
> +	 */
> +	if (is_disksize_grow &&
> +	    ext4_test_inode_state(inode, EXT4_STATE_DISKSIZE_GROW_PENDING))
> +		is_disksize_grow = false;

[Severity: High]
Is EXT4_STATE_DISKSIZE_GROW_PENDING actually set anywhere in the codebase?

It appears this state bit is never set, such as during EOF zeroing in
ext4_block_zero_eof() where there is a TODO comment about tracking the
ordered range.

If the bit is never set, ext4_test_inode_state() will always return false
and this safety check will be bypassed during concurrent fallocate and
truncate operations.

[ ... ]
> @@ -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);

[Severity: High]
Is EXT4_IOMAP_IOEND_DISKSIZE_GROW_IO ever assigned to ioend->io_private?

Because the EXT4_STATE_DISKSIZE_GROW_PENDING bit is never set, it seems
ext4_iomap_get_disksize_pending_range() will always return 0, meaning this
io_mode flag is never assigned.

Does this render the entire disksize-grow logic inert?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=24

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 23/32] ext4: submit and wait for pending disksize-grow I/O on writeback
  2026-08-14  9:33 ` [PATCH -next v5 23/32] ext4: submit and wait for pending disksize-grow I/O on writeback Zhang Yi
@ 2026-08-14 10:25   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:25 UTC (permalink / raw)
  To: Zhang Yi; +Cc: tytso, linux-ext4

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [Critical] Unconditional clearing of the disksize-grow-pending state on I/O error exposes stale data.
- [Critical] The iomap writeback path queues `struct iomap_ioend` to `ei->i_rsv_conversion_list`, but the work item executes `ext4_end_io_rsv_work` expecting `ext4_io_end_t`, causing memory corruption.
- [High] Proactive submission of the EOF block issues a WB_SYNC_ALL writeback pass, violating WB_SYNC_NONE non-blocking guarantees.
- [High] The EXT4_STATE_DISKSIZE_GROW_PENDING bit is never cleared when a file is truncated down or punched, leading to a permanent deadlock in ext4_iomap_wb_disksize_pending_wait on subsequent writes.
- [Medium] The EXT4_STATE_DISKSIZE_GROW_PENDING flag is never set anywhere in the codebase.
--

--- Patch [23]: [PATCH -next v5 23/32] ext4: submit and wait for pending disksize-grow I/O on writeback ---
commit 8550774845865922b3c12e81e62fba6b64136065
Author: Zhang Yi <yi.zhang@huawei.com>
Subject: ext4: submit and wait for pending disksize-grow I/O on writeback

This patch introduces logic to submit and wait for pending zeroed EOF blocks
that straddle i_disksize during writeback to prevent exposing stale data.
It tags ioends with EXT4_IOMAP_IOEND_DISKSIZE_GROW_IO and proactively submits
the pending ranges.

> diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
> index e4a4396eaf879..a0707310b464c 100644
> --- a/fs/ext4/inode.c
> +++ b/fs/ext4/inode.c
> @@ -4379,6 +4382,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);

[Severity: Medium]
Does this actually get triggered? A codebase search shows that the flag
EXT4_STATE_DISKSIZE_GROW_PENDING is never actually set anywhere (for example,
when zeroing the tail of the EOF block). Because the flag is never set,
ext4_iomap_get_disksize_pending_range() will always return 0, which seems to
completely bypass the newly added logic.

> +	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;
> +	}
> +

[ ... ]

> @@ -4398,6 +4416,29 @@ 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 || range_start < pstart + plen)
> +		return;
> +
> +	filemap_fdatawrite_range(mapping, pstart, pstart + plen - 1);

[Severity: High]
Does this violate the WB_SYNC_NONE guarantees when called by the background
flusher? 

filemap_fdatawrite_range() initiates a WB_SYNC_ALL writeback pass. If
ext4_iomap_writepages() is invoked with wbc->sync_mode == WB_SYNC_NONE,
this nested WB_SYNC_ALL call will synchronously block waiting on page locks
and in-flight I/O. This could stall the background flusher and potentially
cause deadlocks if the thread holding the lock is blocked in memory reclaim
waiting for the flusher to free pages.

> +}
> +

[ ... ]

> diff --git a/fs/ext4/page-io.c b/fs/ext4/page-io.c
> index 2888e00575615..955ff88045db6 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);

[Severity: High]
Could this wait indefinitely if the file is truncated down or punched?

If EXT4_STATE_DISKSIZE_GROW_PENDING is set, but no code clears it during
ext4_truncate() or ext4_punch_hole(), the zeroed EOF page will be removed
from the page cache while the bit remains set. If new data is subsequently
written past the stale pending range, the writeback completion might call
ext4_iomap_wb_disksize_pending_wait(). 

Since the original page was dropped, no I/O will ever complete to clear the
bit, causing the worker thread to hang forever in the wait_on_bit call and
permanently blocking the rsv_conversion_wq.

> +}
> +

[ ... ]

> @@ -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);

[Severity: Critical]
Does clearing the pending state here expose stale data on I/O errors?

If the writeback fails, the pending state is unconditionally cleared here,
waking up waiters in ext4_iomap_wb_disksize_pending_wait(). Because those
waiters only check their own bio status, they could proceed to advance
i_disksize past the failed EOF block in ext4_iomap_finish_ioend(). Advancing
i_disksize over the failed zeroing operation exposes the stale data previously
present in the block's tail on disk.

> +
>  	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);

[Severity: Critical]
Can this queueing lead to memory corruption due to type confusion?

A struct iomap_ioend is added to ei->i_rsv_conversion_list here and
ei->i_rsv_conversion_work is scheduled. However, this work item is
permanently bound in ext4_alloc_inode() to execute ext4_end_io_rsv_work().

When the workqueue executes ext4_end_io_rsv_work(), it iterates over the
list and improperly casts the struct iomap_ioend entries to ext4_io_end_t.
Accessing fields from this miscast pointer will retrieve garbage data,
leading to a panic or use-after-free.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=23

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 26/32] ext4: submit and wait for disksize-grow I/O in fallocate paths
  2026-08-14  9:33 ` [PATCH -next v5 26/32] ext4: submit and wait for disksize-grow I/O in fallocate paths Zhang Yi
@ 2026-08-14 10:28   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:28 UTC (permalink / raw)
  To: Zhang Yi; +Cc: tytso, linux-ext4

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] In `ext4_do_fallocate` with `O_SYNC`, the fast commit can execute before the workqueue updates `i_disksize`, resulting in lost file extension on crash.
--

--- Patch [26]: [PATCH -next v5 26/32] ext4: submit and wait for disksize-grow I/O in fallocate paths ---
commit 918bb376bb323b629f0c4b2e5c84c055d7c4c754
Author: Zhang Yi <yi.zhang@huawei.com>

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.

> diff --git a/fs/ext4/extents.c b/fs/ext4/extents.c
> index fc5aa2dbefcff..dda6d50e96e30 100644
> --- a/fs/ext4/extents.c
> +++ b/fs/ext4/extents.c
> @@ -4928,10 +4938,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);
> +		}

[Severity: High]
Can this fast commit execute before the workqueue updates i_disksize,
potentially losing the file extension on a crash?

Looking at the bio completion path in fs/ext4/page-io.c:ext4_iomap_end_bio():

	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,
		           ...);

It appears the EXT4_STATE_DISKSIZE_GROW_PENDING flag is cleared before the
workqueue is actually dispatched and executed.

If ext4_iomap_sync_zeroed_eof() wakes up immediately after the flag is
cleared, could it proceed to ext4_fc_commit() before the workqueue gets a
chance to update i_disksize?

If a crash happens right after the fast commit in this scenario, would it
persist the inode with the old i_disksize, causing a regression by
violating the requested O_SYNC guarantees?

>  	}
>  out:
>  	trace_ext4_fallocate_exit(inode, offset,

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=26

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 25/32] ext4: defer i_disksize update while DISKSIZE_GROW_PENDING is set
  2026-08-14  9:33 ` [PATCH -next v5 25/32] ext4: defer i_disksize update while DISKSIZE_GROW_PENDING is set Zhang Yi
@ 2026-08-14 10:31   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:31 UTC (permalink / raw)
  To: Zhang Yi; +Cc: tytso, linux-ext4

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] Missing State Initialization / Dead Code: EXT4_STATE_DISKSIZE_GROW_PENDING is never set.
- [High] State Leak / Data Loss: Discarding the zeroed EOF folio without writeback permanently leaks the pending bit.
- [High] Durability Loss: O_SYNC extensions via fallocate/zero_range silently persist a stale i_disksize.
--

--- Patch [25]: [PATCH -next v5 25/32] ext4: defer i_disksize update while DISKSIZE_GROW_PENDING is set ---
commit 5fa612fe6ec599be935a3aa55b36845e34d3180e
Author: Zhang Yi <yi.zhang@huawei.com>

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.

> diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h
> --- a/fs/ext4/ext4.h
> +++ b/fs/ext4/ext4.h
> @@ -3605,30 +3605,67 @@ do {								\

[ ... ]

> +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))

[Severity: High]
Could O_SYNC extensions via ext4_do_fallocate() or ext4_zero_range() silently
persist a stale i_disksize here?

If the size is extended with an unaligned old EOF, the i_disksize update is
deferred. Since O_SYNC immediately commits the journal without waiting for
the pending zeroing writeback to complete, it seems it will commit the old,
deferred i_disksize to disk.

Would this cause the file size to revert on crash despite a successful fsync,
discarding the size extension?

> +		WRITE_ONCE(EXT4_I(inode)->i_disksize, newsize);
> +}

[ ... ]

>  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)) {

[Severity: High]
Are we setting EXT4_STATE_DISKSIZE_GROW_PENDING anywhere in the codebase?

Looking at this patch series, it appears the bit is checked here but never
actually initialized or set. Does this mean the deferral condition is always
bypassed and i_disksize is always updated immediately?

[Severity: High]
If the EXT4_STATE_DISKSIZE_GROW_PENDING bit is properly set, could it leak
if the dirty folio is discarded before writeback?

For example, if a file is truncated down before writeback completes, it
seems the bit might never be cleared since the writeback completion function
wouldn't be called for that folio.

Would this leak cause all subsequent appends to skip updating i_disksize,
leading to silent data loss on crash for future writes to the file?

> +		WRITE_ONCE(EXT4_I(inode)->i_disksize, newsize);
>  		changed |= 2;
>  	}
> +	up_write(&EXT4_I(inode)->i_data_sem);
>  	return changed;
>  }

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=25

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 27/32] ext4: clear DISKSIZE_GROW_PENDING on truncate or error
  2026-08-14  9:33 ` [PATCH -next v5 27/32] ext4: clear DISKSIZE_GROW_PENDING on truncate or error Zhang Yi
@ 2026-08-14 10:31   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:31 UTC (permalink / raw)
  To: Zhang Yi; +Cc: linux-ext4, tytso

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] The EXT4_STATE_DISKSIZE_GROW_PENDING inode state bit is never set anywhere in the codebase. As a result, the entire deferred disksize update mechanism introduced in this patch series is non-functional.
- [Medium] The DISKSIZE_GROW_PENDING state is not cleared for linked inodes (i_nlink > 0) during eviction, leading to a WARN_ON_ONCE in ext4_destroy_inode() on forced shutdown.
--

--- Patch [27]: [PATCH -next v5 27/32] ext4: clear DISKSIZE_GROW_PENDING on truncate or error ---
commit a9d36fc1a5b406105120b9e8fd8d91b4a9fe12f0
Author: Zhang Yi <yi.zhang@huawei.com>

ext4: clear DISKSIZE_GROW_PENDING on truncate or error

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. This patch clears the disksize-grow-pending state on discard,
inode eviction, filesystem emergency state, or truncate to avoid blocking
writebacks and preventing warnings in ext4_destroy_inode().

> diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
> index 056937e278594..a1dfb70127ca8 100644
> --- a/fs/ext4/inode.c
> +++ b/fs/ext4/inode.c
> @@ -273,6 +273,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);

[Severity: Medium]
If a filesystem is forcibly shut down (e.g., via EXT4_IOC_SHUTDOWN without
logging an error that sets EXT4_ERROR_FS) and then unmounted, dirty linked
inodes will be evicted.

Looking earlier in ext4_evict_inode(), if inode->i_nlink is > 0, the code 
jumps to the no_delete label, completely bypassing this cleanup:

	if (inode->i_nlink) {
		...
		goto no_delete;
	}

Will this cause the newly added WARN_ON_ONCE in ext4_destroy_inode() to
trigger for linked inodes since they bypass this state clearing?

>  	truncate_inode_pages_final(&inode->i_data);
>  
>  	/*

[ ... ]

> @@ -6741,7 +6759,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);

[Severity: High]
Is the EXT4_STATE_DISKSIZE_GROW_PENDING bit ever set anywhere in the
codebase? 

A search of the tree indicates the literal EXT4_STATE_DISKSIZE_GROW_PENDING
is only ever read or cleared, never set via ext4_set_inode_state() or 
similar. 

There is a TODO comment in ext4_block_zero_eof() indicating that this state
should be set:

	/*
	 * 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 the bit is never actually set, won't this entire deferred disksize update
mechanism be non-functional, leaving the filesystem vulnerable to the stale
data exposure this patchset was intended to fix?

> +
> +	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);

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=27

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 17/32] ext4: implement mmap path using iomap
  2026-08-14  9:33 ` [PATCH -next v5 17/32] ext4: implement mmap " Zhang Yi
@ 2026-08-14 10:35   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:35 UTC (permalink / raw)
  To: Zhang Yi; +Cc: tytso, linux-ext4

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] File operations that zero partial blocks will crash on iomap-enabled files due to legacy buffer_head dereferencing.
- [Medium] Bypass of inline data conversion for iomap-enabled inodes causes failures and kernel warnings.
--

--- Patch [17]: [PATCH -next v5 17/32] ext4: implement mmap path using iomap ---
commit 355e5d2bcead3197b1566acfa637f19866c3b52c
Author: Zhang Yi <yi.zhang@huawei.com>
Subject: ext4: implement mmap path using iomap

This commit introduces ext4_iomap_page_mkwrite() to implement the mmap
iomap path for ext4. It delegates the heavy lifting to iomap_page_mkwrite(),
using ext4_iomap_buffered_write_ops and ext4_iomap_buffered_da_write_ops.
The lock ordering documentation in super.c is also updated accordingly.

> diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
> index 0b3e54e12b78f..a054456258953 100644
> --- a/fs/ext4/inode.c
> +++ b/fs/ext4/inode.c

[ ... ]

> @@ -7318,6 +7343,11 @@ vm_fault_t ext4_page_mkwrite(struct vm_fault *vmf)
>  
>  	filemap_invalidate_lock_shared(mapping);
>  
> +	if (ext4_inode_buffered_iomap(inode)) {
> +		ret = ext4_iomap_page_mkwrite(vmf);
> +		goto out;
> +	}
> +

[Severity: Medium]
Will this bypass inline data conversion for iomap-enabled inodes?

If ext4_inode_buffered_iomap(inode) is true, we route to
ext4_iomap_page_mkwrite() and skip ext4_convert_inline_data(inode).

However, the iomap implementation explicitly forbids inline data, triggering
a warning and returning -ERANGE:

fs/ext4/inode.c:ext4_iomap_buffered_do_write_begin() {
    ...
    /* Inline data and non-extent are not supported. */
    if (WARN_ON_ONCE(ext4_has_inline_data(inode)))
        return -ERANGE;
    ...
}

Should the inline data conversion happen before checking for the iomap path?

[Severity: Medium]
Will file operations that zero partial blocks crash on iomap-enabled files?

When an application performs an extending write or truncate up on a file
with EXT4_STATE_BUFFERED_IOMAP enabled, ext4_write_checks() unconditionally
calls ext4_block_zero_eof() without checking if the inode uses iomap:

fs/ext4/file.c:ext4_write_checks() {
    ...
        ret = ext4_block_zero_eof(inode, old_size, iocb->ki_pos);
    ...
}

This proceeds to ext4_block_do_zero_range() and ext4_load_tail_bh(), which
calls folio_buffers(). For an iomap folio, folio->private points to an
iomap_folio_state, not a buffer_head:

fs/ext4/inode.c:ext4_load_tail_bh() {
    ...
    bh = folio_buffers(folio);
    if (!bh)
        bh = create_empty_buffers(folio, blocksize, 0);

    /* Find the buffer that contains "offset" */
    offset = offset_in_folio(folio, from);
    pos = blocksize;
    while (offset >= pos) {
        bh = bh->b_this_page;
    ...
}

Treating iomap_folio_state as a buffer_head leads to a crash when iterating
bh->b_this_page. Since there are no callers setting EXT4_STATE_BUFFERED_IOMAP
in the current codebase, this is a latent regression. How should partial block
zeroing be handled for iomap-enabled files?

>  	err = ext4_convert_inline_data(inode);
>  	if (err)
>  		goto out_ret;

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=17

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 31/32] ext4: partially enable iomap for the buffered I/O path of regular files
  2026-08-14  9:46 ` [PATCH -next v5 31/32] ext4: partially enable iomap for the buffered I/O path of regular files Zhang Yi
@ 2026-08-14 10:39   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:39 UTC (permalink / raw)
  To: Zhang Yi; +Cc: linux-ext4, tytso

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Critical] Encrypted and verity inodes on corrupted or downgraded filesystems bypass fscrypt and fsverity, leading to plaintext data writes and unverified reads.
- [Medium] Race condition in background writeback causing spurious WARN_ON_ONCE in ext4_iomap_writepages().
--

--- Patch [31]: [PATCH -next v5 31/32] ext4: partially enable iomap for the buffered I/O path of regular files ---
commit 385d0947c4a6c1e02718f1b42a8ef1dc35bdbf03
Author: Zhang Yi <yi.zhang@huawei.com>
Subject: ext4: partially enable iomap for the buffered I/O path of regular files

This commit introduces ext4_enable_buffered_iomap() to determine whether a
regular file inode should use the iomap buffered I/O path. Inodes silently
fall back to the traditional buffer_head path if unsupported features like
encryption, verity, inline data, or data=journal mode are met.

> diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
> index ee15366422a1e..c9ee78fba4d0c 100644
> --- a/fs/ext4/inode.c
> +++ b/fs/ext4/inode.c

[ ... ]

> @@ -4467,6 +4479,10 @@ static int ext4_iomap_writepages(struct address_space *mapping,
>  		.ops = &ext4_writeback_ops,
>  	};
>  
> +	/* inode using the buffer_head buffered I/O path should not go here. */
> +	if (WARN_ON_ONCE(!ext4_inode_buffered_iomap(inode)))
> +		return -EINVAL;
> +

[Severity: Medium]
Can this WARN_ON_ONCE trigger spuriously due to a race with
ext4_change_inode_journal_flag()?

If background writeback via do_writepages() resolves
mapping->a_ops->writepages to ext4_iomap_writepages locklessly, the thread
might be preempted right before this check.

Meanwhile, another thread running ext4_change_inode_journal_flag() could
flush writeback, acquire s_writepages_rwsem exclusively, and clear the
EXT4_STATE_BUFFERED_IOMAP flag.

When the writeback thread resumes, it would evaluate this condition before
acquiring the s_writepages_rwsem read lock, which would trip the warning.
Should this check be moved after acquiring the read lock?

>  	ret = ext4_emergency_state(sb);
>  	if (unlikely(ret)) {

[ ... ]

> @@ -6037,6 +6053,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

[ ... ]

> +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;

[Severity: Critical]
Could checking the superblock features instead of the inode flags here lead
to a security bypass if the filesystem is manipulated offline?

If an attacker clears the encryption or verity superblock features offline,
the kernel still sets S_ENCRYPTED or S_VERITY on individual inodes that have
the flags set.

However, these superblock checks would fall through, allowing the encrypted
or verity inodes to be assigned to the iomap buffered I/O path. Since the
ext4 iomap path currently lacks fscrypt and fsverity integration, would this
cause writes to submit plaintext bios and reads to skip cryptographic
verification?

Would it be safer to check IS_ENCRYPTED(inode) and
ext4_test_inode_flag(inode, EXT4_INODE_VERITY) directly?

> +	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);

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=31

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH -next v5 15/32] ext4: implement buffered write path using iomap
  2026-08-14  9:33 ` [PATCH -next v5 15/32] ext4: implement buffered write path using iomap Zhang Yi
@ 2026-08-14 10:40   ` sashiko-bot
  0 siblings, 0 replies; 65+ messages in thread
From: sashiko-bot @ 2026-08-14 10:40 UTC (permalink / raw)
  To: Zhang Yi; +Cc: linux-ext4, tytso

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Critical] Extending a file within an already-allocated block does not update the on-disk `i_disksize` or mark the inode dirty, causing appended data to be lost on a crash or unmount.
- [High] The iomap non-delalloc buffered write path permanently leaks unwritten extents past `i_size` when a short write or failure occurs.
--

--- Patch [15]: [PATCH -next v5 15/32] ext4: implement buffered write path using iomap ---
commit 7a553e37ec457f0a485aa7c6fc378e68d6d66bfb
Author: Zhang Yi <yi.zhang@huawei.com>

ext4: implement buffered write path using iomap

This commit introduces two new iomap_ops instances for ext4 buffered writes:
one for delayed allocation mode and another for non-delayed allocation mode.
It also updates the folio lock and transaction start ordering for the new
iomap buffered write path.

> diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
> index 9098d9a5fc05b..d831d1911a6ff 100644
> --- a/fs/ext4/inode.c
> +++ b/fs/ext4/inode.c

[ ... ]

> @@ -3994,6 +4009,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);

[Severity: High]
Does this code permanently leak unwritten extents past i_size if a short
write or write failure occurs?

If the user memory copy subsequently fails (e.g. fault_in_iov_iter_readable()
returns -EFAULT), it results in a short write.

In the non-delalloc path, ext4_iomap_buffered_write_ops lacks an iomap_end
callback to clean up the allocated on-disk blocks, which could exhaust
disk space or quota.

> +	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_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;

[Severity: Critical]
Will this silently lose data for extending writes within already-allocated
blocks?

If a buffered write appends data to a file within an extent that was already
allocated, iomap_write_iter() will update the in-memory i_size and set
IOMAP_F_SIZE_CHANGED.

However, this early return skips checking IOMAP_F_SIZE_CHANGED.

Without updating EXT4_I(inode)->i_disksize and calling ext4_mark_inode_dirty()
to persist the size change, appended data can be lost on a crash.

> +
> +	/* 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,
> +};

[Severity: High]
Is it safe to omit an iomap_end callback here?

Without an iomap_end callback for the non-delalloc path, two issues appear
to occur:

1) Allocated unwritten extents past i_size are leaked if a write fails,
exhausting disk space or quota.

2) Extending writes into an already-allocated extent set IOMAP_F_SIZE_CHANGED
in the core iomap code, but without an iomap_end callback to update i_disksize
and mark the inode dirty, this size update is never persisted to disk.

> +
> +static DEFINE_IOMAP_ITER_NEXT_END(ext4_iomap_buffered_da_write_next,
> +				  ext4_iomap_buffered_da_write_begin,
> +				  ext4_iomap_buffered_da_write_end);

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814093331.1703882-1-yi.zhang@huaweicloud.com?part=15

^ permalink raw reply	[flat|nested] 65+ messages in thread

end of thread, other threads:[~2026-08-14 10:40 UTC | newest]

Thread overview: 65+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-14  9:32 [PATCH -next v5 00/32] ext4: use iomap for regular file's buffered I/O path Zhang Yi
2026-08-14  9:33 ` [PATCH -next v5 01/32] ext4: simplify size updating in ext4_setattr() Zhang Yi
2026-08-14 10:02   ` sashiko-bot
2026-08-14  9:33 ` [PATCH -next v5 02/32] ext4: factor out ext4_truncate_[up|down]() Zhang Yi
2026-08-14 10:04   ` sashiko-bot
2026-08-14  9:33 ` [PATCH -next v5 03/32] ext4: simplify error handling in ext4_setattr() Zhang Yi
2026-08-14 10:12   ` sashiko-bot
2026-08-14  9:33 ` [PATCH -next v5 04/32] ext4: skip ordered I/O wait when zeroing beyond i_disksize block Zhang Yi
2026-08-14 10:14   ` sashiko-bot
2026-08-14  9:33 ` [PATCH -next v5 05/32] ext4: set EXT4_MAP_NEW flag for delayed allocated blocks Zhang Yi
2026-08-14 10:08   ` sashiko-bot
2026-08-14  9:33 ` [PATCH -next v5 06/32] ext4: recheck extent status tree before block allocation Zhang Yi
2026-08-14 10:15   ` sashiko-bot
2026-08-14  9:33 ` [PATCH -next v5 07/32] ext4: fix orig_mlen initialization in ext4_map_blocks() Zhang Yi
2026-08-14 10:08   ` sashiko-bot
2026-08-14  9:33 ` [PATCH -next v5 08/32] ext4: allow ext4_map_blocks() to start its own transaction handle Zhang Yi
2026-08-14 10:00   ` sashiko-bot
2026-08-14  9:33 ` [PATCH -next v5 09/32] ext4: avoid unnecessary transaction in ext4_map_blocks() for unwritten extents Zhang Yi
2026-08-14 10:15   ` sashiko-bot
2026-08-14  9:33 ` [PATCH -next v5 10/32] ext4: skip block allocation for holes in the data submission path Zhang Yi
2026-08-14 10:15   ` sashiko-bot
2026-08-14  9:33 ` [PATCH -next v5 11/32] ext4: add iomap address space operations for buffered I/O Zhang Yi
2026-08-14 10:04   ` sashiko-bot
2026-08-14  9:33 ` [PATCH -next v5 12/32] ext4: implement buffered read path using iomap Zhang Yi
2026-08-14 10:15   ` sashiko-bot
2026-08-14  9:33 ` [PATCH -next v5 13/32] ext4: pass out extent seq counter when mapping da blocks Zhang Yi
2026-08-14 10:12   ` sashiko-bot
2026-08-14  9:33 ` [PATCH -next v5 14/32] ext4: do not use data=ordered mode for inodes using buffered iomap path Zhang Yi
2026-08-14 10:12   ` sashiko-bot
2026-08-14  9:33 ` [PATCH -next v5 15/32] ext4: implement buffered write path using iomap Zhang Yi
2026-08-14 10:40   ` sashiko-bot
2026-08-14  9:33 ` [PATCH -next v5 16/32] ext4: implement writeback " Zhang Yi
2026-08-14 10:19   ` sashiko-bot
2026-08-14  9:33 ` [PATCH -next v5 17/32] ext4: implement mmap " Zhang Yi
2026-08-14 10:35   ` sashiko-bot
2026-08-14  9:33 ` [PATCH -next v5 18/32] ext4: implement partial block zero range " Zhang Yi
2026-08-14 10:25   ` sashiko-bot
2026-08-14  9:33 ` [PATCH -next v5 19/32] ext4: tolerate unexpected holes in ext4_convert_unwritten_extents() Zhang Yi
2026-08-14 10:09   ` sashiko-bot
2026-08-14  9:33 ` [PATCH -next v5 20/32] ext4: add block mapping tracepoints for iomap buffered I/O path Zhang Yi
2026-08-14 10:08   ` sashiko-bot
2026-08-14  9:33 ` [PATCH -next v5 21/32] ext4: disable online defrag when inode using " Zhang Yi
2026-08-14 10:08   ` sashiko-bot
2026-08-14  9:33 ` [PATCH -next v5 22/32] ext4: add EXT4_STATE_DISKSIZE_GROW_PENDING state bit and helpers Zhang Yi
2026-08-14 10:09   ` sashiko-bot
2026-08-14  9:33 ` [PATCH -next v5 23/32] ext4: submit and wait for pending disksize-grow I/O on writeback Zhang Yi
2026-08-14 10:25   ` sashiko-bot
2026-08-14  9:33 ` [PATCH -next v5 24/32] ext4: advance i_disksize to i_size upon disksize-grow I/O completion Zhang Yi
2026-08-14 10:25   ` sashiko-bot
2026-08-14  9:33 ` [PATCH -next v5 25/32] ext4: defer i_disksize update while DISKSIZE_GROW_PENDING is set Zhang Yi
2026-08-14 10:31   ` sashiko-bot
2026-08-14  9:33 ` [PATCH -next v5 26/32] ext4: submit and wait for disksize-grow I/O in fallocate paths Zhang Yi
2026-08-14 10:28   ` sashiko-bot
2026-08-14  9:33 ` [PATCH -next v5 27/32] ext4: clear DISKSIZE_GROW_PENDING on truncate or error Zhang Yi
2026-08-14 10:31   ` sashiko-bot
2026-08-14  9:33 ` [PATCH -next v5 28/32] ext4: set DISKSIZE_GROW_PENDING after zeroing unaligned EOF block Zhang Yi
2026-08-14 10:18   ` sashiko-bot
2026-08-14  9:33 ` [PATCH -next v5 29/32] ext4: add tracepoints for DISKSIZE_GROW_PENDING set, clear, and wait Zhang Yi
2026-08-14 10:18   ` sashiko-bot
2026-08-14  9:33 ` [PATCH -next v5 30/32] ext4: add tracepoints for EOF block zeroing and disksize-grow I/O Zhang Yi
2026-08-14 10:19   ` sashiko-bot
2026-08-14  9:46 ` [PATCH -next v5 31/32] ext4: partially enable iomap for the buffered I/O path of regular files Zhang Yi
2026-08-14 10:39   ` sashiko-bot
2026-08-14  9:46 ` [PATCH -next v5 32/32] ext4: introduce a mount option for iomap buffered I/O path Zhang Yi
2026-08-14 10:25   ` sashiko-bot

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox