[PATCH 22/33] xfs: convert xfs_reflink_end_cow to rolling transactions

Dave Chinner <[email protected]> Wed, 29 Jul 2026 20:02:06 +1000
Newsgroups org.kernel.vger.linux-xfs
Message-ID <[email protected]>
Convert xfs_reflink_end_cow() from per-extent transaction allocation
to a single rolling transaction that keeps the ILOCK held across the
entire COW remapping loop.

The rolling transaction keeps the ILOCK held throughout, making the
COW remapping operation atomic with respect to other ILOCK-protected
extent manipulations such as truncate, reflink remapping, and other
concurrent end_cow operations on overlapping regions.

XFS_TRANS_RENEW_BLKRES is set so that the btree split block
reservation is automatically renewed by xfs_defer_finish() after
each iteration's deferred operations are processed.

Assisted-by: LLM
Signed-off-by: Dave Chinner <[email protected]>
---
 fs/xfs/xfs_reflink.c | 75 ++++++++++++++++++++++----------------------
 1 file changed, 38 insertions(+), 37 deletions(-)

diff --git a/fs/xfs/xfs_reflink.c b/fs/xfs/xfs_reflink.c
index b175a549ee55..7e62f05499be 100644
--- a/fs/xfs/xfs_reflink.c
+++ b/fs/xfs/xfs_reflink.c
@@ -925,6 +925,13 @@ xfs_reflink_end_cow_extent(
 
 /*
  * Remap parts of a file's data fork after a successful CoW.
+ *
+ * The rolling transaction keeps the ILOCK held across the entire remapping
+ * loop, making the operation atomic with respect to other ILOCK-protected
+ * extent manipulations such as truncate, reflink remapping, and other
+ * concurrent end_cow operations on overlapping regions.
+ * XFS_TRANS_RENEW_BLKRES ensures the btree split block reservation is
+ * renewed after each xfs_defer_finish() call.
  */
 int
 xfs_reflink_end_cow(
@@ -932,54 +939,48 @@ xfs_reflink_end_cow(
 	xfs_off_t			offset,
 	xfs_off_t			count)
 {
+	struct xfs_mount		*mp = ip->i_mount;
 	xfs_fileoff_t			offset_fsb;
 	xfs_fileoff_t			end_fsb;
-	int				error = 0;
+	struct xfs_trans		*tp;
+	unsigned int			resblks;
+	int				error;
 
 	trace_xfs_reflink_end_cow(ip, offset, count);
 
-	offset_fsb = XFS_B_TO_FSBT(ip->i_mount, offset);
-	end_fsb = XFS_B_TO_FSB(ip->i_mount, offset + count);
+	offset_fsb = XFS_B_TO_FSBT(mp, offset);
+	end_fsb = XFS_B_TO_FSB(mp, offset + count);
 
-	/*
-	 * Walk forwards until we've remapped the I/O range.  The loop function
-	 * repeatedly cycles the ILOCK to allocate one transaction per remapped
-	 * extent.
-	 *
-	 * If we're being called by writeback then the folios will still
-	 * have the writeback flag set, which prevents races with reflink
-	 * remapping and truncate.  Reflink remapping prevents races with
-	 * writeback by taking the iolock and mmaplock before flushing
-	 * the folios and remapping, which means there won't be any further
-	 * writeback or page cache dirtying until the reflink completes.
-	 *
-	 * We should never have two threads issuing writeback for the same file
-	 * region.  There are also have post-eof checks in the writeback
-	 * preparation code so that we don't bother writing out folios that are
-	 * about to be truncated.
-	 *
-	 * If we're being called as part of directio write completion, the dio
-	 * count is still elevated, which reflink and truncate will wait for.
-	 * Reflink remapping takes the iolock and mmaplock and waits for
-	 * pending dio to finish, which should prevent any directio until the
-	 * remap completes.  Multiple concurrent directio writes to the same
-	 * region are handled by end_cow processing only occurring for the
-	 * threads which succeed; the outcome of multiple overlapping direct
-	 * writes is not well defined anyway.
-	 *
-	 * It's possible that a buffered write and a direct write could collide
-	 * here (the buffered write stumbles in after the dio flushes and
-	 * invalidates the page cache and immediately queues writeback), but we
-	 * have never supported this 100%.  If either disk write succeeds the
-	 * blocks will be remapped.
-	 */
-	while (end_fsb > offset_fsb && !error)
-		error = xfs_reflink_end_cow_extent(NULL, ip, &offset_fsb,
+	resblks = XFS_EXTENTADD_SPACE_RES(mp, XFS_DATA_FORK);
+	error = xfs_trans_alloc(mp, &M_RES(mp)->tr_write, resblks, 0,
+			XFS_TRANS_RESERVE | XFS_TRANS_RENEW_BLKRES, &tp);
+	if (error)
+		return error;
+	xfs_ilock(ip, XFS_ILOCK_EXCL);
+	xfs_trans_ijoin(tp, ip, 0);
+
+	while (end_fsb > offset_fsb) {
+		error = xfs_reflink_end_cow_extent(tp, ip, &offset_fsb,
 				end_fsb);
+		if (error)
+			goto out_cancel;
+
+		error = xfs_defer_finish(&tp);
+		if (error)
+			goto out_cancel;
+	}
 
+	error = xfs_trans_commit(tp);
+	xfs_iunlock(ip, XFS_ILOCK_EXCL);
 	if (error)
 		trace_xfs_reflink_end_cow_error(ip, error, _RET_IP_);
 	return error;
+
+out_cancel:
+	xfs_trans_cancel(tp);
+	xfs_iunlock(ip, XFS_ILOCK_EXCL);
+	trace_xfs_reflink_end_cow_error(ip, error, _RET_IP_);
+	return error;
 }
 
 /*
-- 
2.55.0