All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCHSET v2 2/2] xfs: LLM-inspired iunlink repair bug fixes
@ 2026-07-27  5:22 Darrick J. Wong
  2026-07-27  5:24 ` [PATCH 01/10] xfs: hoist per-bucket unlinked list check to helper Darrick J. Wong
                   ` (9 more replies)
  0 siblings, 10 replies; 13+ messages in thread
From: Darrick J. Wong @ 2026-07-27  5:22 UTC (permalink / raw)
  To: djwong, cem, hch; +Cc: stable, linux-xfs

Hi all,

These bugfixes got started as part of a series of assorted bug fixes.
However, in writing a test case to exercise weird corruption scenarios
I discovered a few more fixes, so I decided to collect them all in a
targeted series.

v2: add rvbs, fix a few minor coding errors, strengthen loop detection

If you're going to start using this code, I strongly recommend pulling
from my git trees, which are linked below.

The remaining unreviewed patches in this series are:

  [PATCH 08/10] xfs: fix another iunlink infinite loop bug in online
  [PATCH 10/10] xfs: don't ignore runtime errors in

With a bit of luck, this should all go splendidly.
Comments and questions are, as always, welcome.

--D

kernel git tree:
https://git.kernel.org/cgit/linux/kernel/git/djwong/xfs-linux.git/log/?h=llm-fixes-iunlink
---
Commits in this patchset:
 * xfs: hoist per-bucket unlinked list check to helper
 * xfs: don't livelock in scrub on a circular unlinked list
 * xfs: don't walk off the end of a null sc->sa.agi_bp in AGI repair
 * xfs: load next_agino from the correct xfarray in xrep_iunlink_relink_prev
 * xfs: pass runtime errors from xrep_iunlink_mark_ondisk_rec up to callers
 * xfs: check xfarray iteration errors when committing unlinked inode lists
 * xfs: fix allocated inodes that show up in the unlinked list
 * xfs: fix another iunlink infinite loop bug in online fsck
 * xfs: set the prev pointer when reinserting an inode on the unlinked list
 * xfs: don't ignore runtime errors in xrep_iunlink_reload_next
---
 fs/xfs/scrub/trace.h           |    2 
 fs/xfs/scrub/agheader.c        |   94 +++++++++++++++++-----
 fs/xfs/scrub/agheader_repair.c |  169 +++++++++++++++++++++++++++++++++-------
 3 files changed, 213 insertions(+), 52 deletions(-)

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

* [PATCH 01/10] xfs: hoist per-bucket unlinked list check to helper
  2026-07-27  5:22 [PATCHSET v2 2/2] xfs: LLM-inspired iunlink repair bug fixes Darrick J. Wong
@ 2026-07-27  5:24 ` Darrick J. Wong
  2026-07-27  5:24 ` [PATCH 02/10] xfs: don't livelock in scrub on a circular unlinked list Darrick J. Wong
                   ` (8 subsequent siblings)
  9 siblings, 0 replies; 13+ messages in thread
From: Darrick J. Wong @ 2026-07-27  5:24 UTC (permalink / raw)
  To: djwong, cem, hch; +Cc: linux-xfs

From: Darrick J. Wong <djwong@kernel.org>

In the next patch we're going to make this loop more exciting, so hoist
the code to a helper function to reduce clutter in the resulting code.

Signed-off-by: "Darrick J. Wong" <djwong@kernel.org>
Reviewed-by: Christoph Hellwig <hch@lst.de>
---
 fs/xfs/scrub/agheader.c |   62 +++++++++++++++++++++++++++++++----------------
 1 file changed, 41 insertions(+), 21 deletions(-)


diff --git a/fs/xfs/scrub/agheader.c b/fs/xfs/scrub/agheader.c
index 62ed5eaf08fbc1..cecf034ef989c7 100644
--- a/fs/xfs/scrub/agheader.c
+++ b/fs/xfs/scrub/agheader.c
@@ -932,6 +932,42 @@ xchk_agi_xref(
 	/* scrub teardown will take care of sc->sa for us */
 }
 
+/*
+ * Walk the incore unlinked list for a particular AGI bucket to construct
+ * the unlinked inode bitmap for later reconstruction of the unlinked list.
+ * Returns 1 if we should keep checking, or 0 to stop checking.
+ */
+static int
+xchk_iunlink_bucket(
+	struct xfs_scrub		*sc,
+	unsigned int			bucket,
+	xfs_agino_t			agino)
+{
+	while (agino != NULLAGINO) {
+		struct xfs_inode	*ip;
+
+		if (agino % XFS_AGI_UNLINKED_BUCKETS != bucket) {
+			xchk_block_set_corrupt(sc, sc->sa.agi_bp);
+			return 0;
+		}
+
+		ip = xfs_iunlink_lookup(sc->sa.pag, agino);
+		if (!ip) {
+			xchk_block_set_corrupt(sc, sc->sa.agi_bp);
+			return 0;
+		}
+
+		if (!xfs_inode_on_unlinked_list(ip)) {
+			xchk_block_set_corrupt(sc, sc->sa.agi_bp);
+			return 0;
+		}
+
+		agino = ip->i_next_unlinked;
+	}
+
+	return 1;
+}
+
 /*
  * Check the unlinked buckets for links to bad inodes.  We hold the AGI, so
  * there cannot be any threads updating unlinked list pointers in this AG.
@@ -942,30 +978,14 @@ xchk_iunlink(
 	struct xfs_agi		*agi)
 {
 	unsigned int		i;
-	struct xfs_inode	*ip;
 
 	for (i = 0; i < XFS_AGI_UNLINKED_BUCKETS; i++) {
-		xfs_agino_t	agino = be32_to_cpu(agi->agi_unlinked[i]);
+		int		ret;
 
-		while (agino != NULLAGINO) {
-			if (agino % XFS_AGI_UNLINKED_BUCKETS != i) {
-				xchk_block_set_corrupt(sc, sc->sa.agi_bp);
-				return;
-			}
-
-			ip = xfs_iunlink_lookup(sc->sa.pag, agino);
-			if (!ip) {
-				xchk_block_set_corrupt(sc, sc->sa.agi_bp);
-				return;
-			}
-
-			if (!xfs_inode_on_unlinked_list(ip)) {
-				xchk_block_set_corrupt(sc, sc->sa.agi_bp);
-				return;
-			}
-
-			agino = ip->i_next_unlinked;
-		}
+		ret = xchk_iunlink_bucket(sc, i,
+				be32_to_cpu(agi->agi_unlinked[i]));
+		if (ret < 1)
+			return;
 	}
 }
 


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

* [PATCH 02/10] xfs: don't livelock in scrub on a circular unlinked list
  2026-07-27  5:22 [PATCHSET v2 2/2] xfs: LLM-inspired iunlink repair bug fixes Darrick J. Wong
  2026-07-27  5:24 ` [PATCH 01/10] xfs: hoist per-bucket unlinked list check to helper Darrick J. Wong
@ 2026-07-27  5:24 ` Darrick J. Wong
  2026-07-27  5:25 ` [PATCH 03/10] xfs: don't walk off the end of a null sc->sa.agi_bp in AGI repair Darrick J. Wong
                   ` (7 subsequent siblings)
  9 siblings, 0 replies; 13+ messages in thread
From: Darrick J. Wong @ 2026-07-27  5:24 UTC (permalink / raw)
  To: djwong, cem, hch; +Cc: stable, linux-xfs

From: Darrick J. Wong <djwong@kernel.org>

LOLLM points out that online fsck can livelock if an unlinked inode list
contains a loop.  Use a bitmap to detect cycles.

Cc: <stable@vger.kernel.org> # v4.15
Fixes: a12890aebb8959 ("xfs: scrub the AGI")
Signed-off-by: "Darrick J. Wong" <djwong@kernel.org>
Assisted-by: LOLLM # finding obvious bugs
Reviewed-by: Christoph Hellwig <hch@lst.de>
---
 fs/xfs/scrub/agheader.c        |   44 +++++++++++++++++++++++++++++++++-------
 fs/xfs/scrub/agheader_repair.c |   17 ++++++++++++++-
 2 files changed, 51 insertions(+), 10 deletions(-)


diff --git a/fs/xfs/scrub/agheader.c b/fs/xfs/scrub/agheader.c
index cecf034ef989c7..1fa66aa68e169f 100644
--- a/fs/xfs/scrub/agheader.c
+++ b/fs/xfs/scrub/agheader.c
@@ -18,6 +18,8 @@
 #include "xfs_inode.h"
 #include "scrub/scrub.h"
 #include "scrub/common.h"
+#include "scrub/bitmap.h"
+#include "scrub/agino_bitmap.h"
 
 int
 xchk_setup_agheader(
@@ -935,7 +937,8 @@ xchk_agi_xref(
 /*
  * Walk the incore unlinked list for a particular AGI bucket to construct
  * the unlinked inode bitmap for later reconstruction of the unlinked list.
- * Returns 1 if we should keep checking, or 0 to stop checking.
+ * Returns 1 if we should keep checking, 0 to stop checking, or a negative
+ * errno.
  */
 static int
 xchk_iunlink_bucket(
@@ -943,36 +946,57 @@ xchk_iunlink_bucket(
 	unsigned int			bucket,
 	xfs_agino_t			agino)
 {
+	struct xagino_bitmap		seen;
+	int				ret;
+
+	xagino_bitmap_init(&seen);
+
 	while (agino != NULLAGINO) {
 		struct xfs_inode	*ip;
+		unsigned int		len = 1;
 
 		if (agino % XFS_AGI_UNLINKED_BUCKETS != bucket) {
 			xchk_block_set_corrupt(sc, sc->sa.agi_bp);
-			return 0;
+			goto bad;
+		}
+
+		if (xagino_bitmap_test(&seen, agino, &len)) {
+			xchk_block_set_corrupt(sc, sc->sa.agi_bp);
+			goto bad;
 		}
 
 		ip = xfs_iunlink_lookup(sc->sa.pag, agino);
 		if (!ip) {
 			xchk_block_set_corrupt(sc, sc->sa.agi_bp);
-			return 0;
+			goto bad;
 		}
 
 		if (!xfs_inode_on_unlinked_list(ip)) {
 			xchk_block_set_corrupt(sc, sc->sa.agi_bp);
-			return 0;
+			goto bad;
 		}
 
+		ret = xagino_bitmap_set(&seen, agino, 1);
+		if (ret)
+			goto out_bitmap;
+
 		agino = ip->i_next_unlinked;
 	}
+	ret = 1;
 
-	return 1;
+out_bitmap:
+	xagino_bitmap_destroy(&seen);
+	return ret;
+bad:
+	ret = 0;
+	goto out_bitmap;
 }
 
 /*
  * Check the unlinked buckets for links to bad inodes.  We hold the AGI, so
  * there cannot be any threads updating unlinked list pointers in this AG.
  */
-STATIC void
+STATIC int
 xchk_iunlink(
 	struct xfs_scrub	*sc,
 	struct xfs_agi		*agi)
@@ -985,8 +1009,10 @@ xchk_iunlink(
 		ret = xchk_iunlink_bucket(sc, i,
 				be32_to_cpu(agi->agi_unlinked[i]));
 		if (ret < 1)
-			return;
+			return ret;
 	}
+
+	return 0;
 }
 
 /* Scrub the AGI. */
@@ -1073,7 +1099,9 @@ xchk_agi(
 	if (pag->pagi_freecount != be32_to_cpu(agi->agi_freecount))
 		xchk_block_set_corrupt(sc, sc->sa.agi_bp);
 
-	xchk_iunlink(sc, agi);
+	error = xchk_iunlink(sc, agi);
+	if (error)
+		goto out;
 
 	xchk_agi_xref(sc);
 out:
diff --git a/fs/xfs/scrub/agheader_repair.c b/fs/xfs/scrub/agheader_repair.c
index 2554494847ff1b..13074d5e319cc0 100644
--- a/fs/xfs/scrub/agheader_repair.c
+++ b/fs/xfs/scrub/agheader_repair.c
@@ -1080,18 +1080,22 @@ xrep_iunlink_walk_ondisk_bucket(
 	struct xrep_agi		*ragi,
 	unsigned int		bucket)
 {
+	struct xagino_bitmap	seen;
 	struct xfs_scrub	*sc = ragi->sc;
 	struct xfs_agi		*agi = sc->sa.agi_bp->b_addr;
 	xfs_agino_t		prev_agino = NULLAGINO;
 	xfs_agino_t		next_agino;
 	int			error = 0;
 
+	xagino_bitmap_init(&seen);
+
 	next_agino = be32_to_cpu(agi->agi_unlinked[bucket]);
 	while (next_agino != NULLAGINO) {
 		xfs_agino_t	agino = next_agino;
+		unsigned int	len = 1;
 
 		if (xchk_should_terminate(ragi->sc, &error))
-			return error;
+			goto out_bitmap;
 
 		trace_xrep_iunlink_walk_ondisk_bucket(sc->sa.pag, bucket,
 				prev_agino, agino);
@@ -1099,15 +1103,24 @@ xrep_iunlink_walk_ondisk_bucket(
 		if (bucket != agino % XFS_AGI_UNLINKED_BUCKETS)
 			break;
 
+		if (xagino_bitmap_test(&seen, agino, &len))
+			break;
+
 		next_agino = xrep_iunlink_next(sc, agino);
 		if (!next_agino)
 			next_agino = xrep_iunlink_reload_next(ragi, prev_agino,
 					agino);
 
+		error = xagino_bitmap_set(&seen, agino, 1);
+		if (error)
+			goto out_bitmap;
+
 		prev_agino = agino;
 	}
 
-	return 0;
+out_bitmap:
+	xagino_bitmap_destroy(&seen);
+	return error;
 }
 
 /* Decide if this is an unlinked inode in this AG. */


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

* [PATCH 03/10] xfs: don't walk off the end of a null sc->sa.agi_bp in AGI repair
  2026-07-27  5:22 [PATCHSET v2 2/2] xfs: LLM-inspired iunlink repair bug fixes Darrick J. Wong
  2026-07-27  5:24 ` [PATCH 01/10] xfs: hoist per-bucket unlinked list check to helper Darrick J. Wong
  2026-07-27  5:24 ` [PATCH 02/10] xfs: don't livelock in scrub on a circular unlinked list Darrick J. Wong
@ 2026-07-27  5:25 ` Darrick J. Wong
  2026-07-27  5:25 ` [PATCH 04/10] xfs: load next_agino from the correct xfarray in xrep_iunlink_relink_prev Darrick J. Wong
                   ` (6 subsequent siblings)
  9 siblings, 0 replies; 13+ messages in thread
From: Darrick J. Wong @ 2026-07-27  5:25 UTC (permalink / raw)
  To: djwong, cem, hch; +Cc: stable, linux-xfs

From: Darrick J. Wong <djwong@kernel.org>

LOLLM noticed a longstanding bug where xrep_iunlink_walk_ondisk_bucket
tries to walk ragi->sc->sa.agi_bp to rebuild the unlinked inode lists.
Unfortunately, it's possible for agi_bp to be null if the buffer
verifier fails, so we have to use ragi->agi_bp (which skips verifier
checks) instead.

Cc: <stable@vger.kernel.org> # v6.10
Fixes: ab97f4b1c03075 ("xfs: repair AGI unlinked inode bucket lists")
Signed-off-by: "Darrick J. Wong" <djwong@kernel.org>
Assisted-by: LOLLM # finding obvious bugs
Reviewed-by: Christoph Hellwig <hch@lst.de>
---
 fs/xfs/scrub/agheader_repair.c |    2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)


diff --git a/fs/xfs/scrub/agheader_repair.c b/fs/xfs/scrub/agheader_repair.c
index 13074d5e319cc0..39468b8fa9863d 100644
--- a/fs/xfs/scrub/agheader_repair.c
+++ b/fs/xfs/scrub/agheader_repair.c
@@ -1082,7 +1082,7 @@ xrep_iunlink_walk_ondisk_bucket(
 {
 	struct xagino_bitmap	seen;
 	struct xfs_scrub	*sc = ragi->sc;
-	struct xfs_agi		*agi = sc->sa.agi_bp->b_addr;
+	struct xfs_agi		*agi = ragi->agi_bp->b_addr;
 	xfs_agino_t		prev_agino = NULLAGINO;
 	xfs_agino_t		next_agino;
 	int			error = 0;


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

* [PATCH 04/10] xfs: load next_agino from the correct xfarray in xrep_iunlink_relink_prev
  2026-07-27  5:22 [PATCHSET v2 2/2] xfs: LLM-inspired iunlink repair bug fixes Darrick J. Wong
                   ` (2 preceding siblings ...)
  2026-07-27  5:25 ` [PATCH 03/10] xfs: don't walk off the end of a null sc->sa.agi_bp in AGI repair Darrick J. Wong
@ 2026-07-27  5:25 ` Darrick J. Wong
  2026-07-27  5:25 ` [PATCH 05/10] xfs: pass runtime errors from xrep_iunlink_mark_ondisk_rec up to callers Darrick J. Wong
                   ` (5 subsequent siblings)
  9 siblings, 0 replies; 13+ messages in thread
From: Darrick J. Wong @ 2026-07-27  5:25 UTC (permalink / raw)
  To: djwong, cem, hch; +Cc: stable, linux-xfs

From: Darrick J. Wong <djwong@kernel.org>

LOLLM notices that xrep_iunlink_relink_prev has the comment "set the
forward pointer..." but then loads the value from the xfarray that
stores pointers to the previous inode in the unlinked list.  That's
wrong, so fix the variable access.

Cc: <stable@vger.kernel.org> # v6.10
Fixes: ab97f4b1c03075 ("xfs: repair AGI unlinked inode bucket lists")
Signed-off-by: "Darrick J. Wong" <djwong@kernel.org>
Assisted-by: LOLLM # finding obvious bugs
Reviewed-by: Christoph Hellwig <hch@lst.de>
---
 fs/xfs/scrub/agheader_repair.c |    2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)


diff --git a/fs/xfs/scrub/agheader_repair.c b/fs/xfs/scrub/agheader_repair.c
index 39468b8fa9863d..5204eab09c6450 100644
--- a/fs/xfs/scrub/agheader_repair.c
+++ b/fs/xfs/scrub/agheader_repair.c
@@ -1619,7 +1619,7 @@ xrep_iunlink_relink_prev(
 		want_rele = true;
 
 		/* Set the forward pointer since this just came off disk. */
-		error = xfarray_load(ragi->iunlink_prev, agino, &next_agino);
+		error = xfarray_load(ragi->iunlink_next, agino, &next_agino);
 		if (error)
 			goto out_rele;
 


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

* [PATCH 05/10] xfs: pass runtime errors from xrep_iunlink_mark_ondisk_rec up to callers
  2026-07-27  5:22 [PATCHSET v2 2/2] xfs: LLM-inspired iunlink repair bug fixes Darrick J. Wong
                   ` (3 preceding siblings ...)
  2026-07-27  5:25 ` [PATCH 04/10] xfs: load next_agino from the correct xfarray in xrep_iunlink_relink_prev Darrick J. Wong
@ 2026-07-27  5:25 ` Darrick J. Wong
  2026-07-27  5:25 ` [PATCH 06/10] xfs: check xfarray iteration errors when committing unlinked inode lists Darrick J. Wong
                   ` (4 subsequent siblings)
  9 siblings, 0 replies; 13+ messages in thread
From: Darrick J. Wong @ 2026-07-27  5:25 UTC (permalink / raw)
  To: djwong, cem, hch; +Cc: stable, linux-xfs

From: Darrick J. Wong <djwong@kernel.org>

LOLLM points out that the only error that xrep_iunlink_mark_ondisk_rec
returns is ENOMEM, but we ignore that, and can end up writing a garbage
AGI based on incomplete information.  We shouldn't do that, though here
we must be screen out EFSCORRUPTED/EFSBASDCRC because we haven't
checked the inobt yet.

Cc: <stable@vger.kernel.org> # v6.10
Fixes: ab97f4b1c03075 ("xfs: repair AGI unlinked inode bucket lists")
Signed-off-by: "Darrick J. Wong" <djwong@kernel.org>
Assisted-by: LOLLM # finding obvious bugs
Reviewed-by: Christoph Hellwig <hch@lst.de>
---
 fs/xfs/scrub/agheader_repair.c |   14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)


diff --git a/fs/xfs/scrub/agheader_repair.c b/fs/xfs/scrub/agheader_repair.c
index 5204eab09c6450..7f19554843038d 100644
--- a/fs/xfs/scrub/agheader_repair.c
+++ b/fs/xfs/scrub/agheader_repair.c
@@ -1309,7 +1309,7 @@ xrep_iunlink_mark_ondisk_rec(
  * iunlink_bmp.   We haven't checked the inobt yet, so we don't error out if
  * the btree is corrupt.
  */
-STATIC void
+STATIC int
 xrep_iunlink_mark_ondisk(
 	struct xrep_agi		*ragi)
 {
@@ -1321,6 +1321,14 @@ xrep_iunlink_mark_ondisk(
 	cur = xfs_inobt_init_cursor(sc->sa.pag, sc->tp, agi_bp);
 	error = xfs_btree_query_all(cur, xrep_iunlink_mark_ondisk_rec, ragi);
 	xfs_btree_del_cursor(cur, error);
+
+	/*
+	 * Don't proceed if we couldn't set a bit in the bitmap.  All other
+	 * errors we ignore because we haven't actually checked the inobt yet.
+	 */
+	if (error == -ENOMEM)
+		return -ENOMEM;
+	return 0;
 }
 
 /*
@@ -1508,7 +1516,9 @@ xrep_iunlink_rebuild_buckets(
 	 * If there are ondisk inodes that are unlinked and are not been loaded
 	 * into cache, record them in iunlink_bmp.
 	 */
-	xrep_iunlink_mark_ondisk(ragi);
+	error = xrep_iunlink_mark_ondisk(ragi);
+	if (error)
+		return error;
 
 	/*
 	 * Walk each iunlink bucket to (re)construct as much of the incore list


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

* [PATCH 06/10] xfs: check xfarray iteration errors when committing unlinked inode lists
  2026-07-27  5:22 [PATCHSET v2 2/2] xfs: LLM-inspired iunlink repair bug fixes Darrick J. Wong
                   ` (4 preceding siblings ...)
  2026-07-27  5:25 ` [PATCH 05/10] xfs: pass runtime errors from xrep_iunlink_mark_ondisk_rec up to callers Darrick J. Wong
@ 2026-07-27  5:25 ` Darrick J. Wong
  2026-07-27  5:26 ` [PATCH 07/10] xfs: fix allocated inodes that show up in the unlinked list Darrick J. Wong
                   ` (3 subsequent siblings)
  9 siblings, 0 replies; 13+ messages in thread
From: Darrick J. Wong @ 2026-07-27  5:25 UTC (permalink / raw)
  To: djwong, cem, hch; +Cc: stable, linux-xfs

From: Darrick J. Wong <djwong@kernel.org>

LOLLM noticed that we neglect to check for xfarray_iter itself returning
errors when writing a new AGI.  Fix that.

Cc: <stable@vger.kernel.org> # v6.10
Fixes: ab97f4b1c03075 ("xfs: repair AGI unlinked inode bucket lists")
Signed-off-by: "Darrick J. Wong" <djwong@kernel.org>
Assisted-by: LOLLM # finding obvious bugs
Reviewed-by: Christoph Hellwig <hch@lst.de>
---
 fs/xfs/scrub/agheader_repair.c |    4 ++++
 1 file changed, 4 insertions(+)


diff --git a/fs/xfs/scrub/agheader_repair.c b/fs/xfs/scrub/agheader_repair.c
index 7f19554843038d..8514d2d7e3cea3 100644
--- a/fs/xfs/scrub/agheader_repair.c
+++ b/fs/xfs/scrub/agheader_repair.c
@@ -1675,6 +1675,8 @@ xrep_iunlink_commit(
 		if (error)
 			return error;
 	}
+	if (error < 0)
+		return error;
 
 	/* Fix all the back links */
 	idx = XFARRAY_CURSOR_INIT;
@@ -1683,6 +1685,8 @@ xrep_iunlink_commit(
 		if (error)
 			return error;
 	}
+	if (error < 0)
+		return error;
 
 	/* Copy the staged iunlink buckets to the new AGI. */
 	for (i = 0; i < XFS_AGI_UNLINKED_BUCKETS; i++) {


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

* [PATCH 07/10] xfs: fix allocated inodes that show up in the unlinked list
  2026-07-27  5:22 [PATCHSET v2 2/2] xfs: LLM-inspired iunlink repair bug fixes Darrick J. Wong
                   ` (5 preceding siblings ...)
  2026-07-27  5:25 ` [PATCH 06/10] xfs: check xfarray iteration errors when committing unlinked inode lists Darrick J. Wong
@ 2026-07-27  5:26 ` Darrick J. Wong
  2026-07-27  5:26 ` [PATCH 08/10] xfs: fix another iunlink infinite loop bug in online fsck Darrick J. Wong
                   ` (2 subsequent siblings)
  9 siblings, 0 replies; 13+ messages in thread
From: Darrick J. Wong @ 2026-07-27  5:26 UTC (permalink / raw)
  To: djwong, cem, hch; +Cc: stable, linux-xfs

From: Darrick J. Wong <djwong@kernel.org>

If an allocated inode shows up in the unlinked list, we need to get it
completely off the list.  Set the corrected next/prev pointers such that
the inode will not look like it should be on an unlinked list at all.

Cc: <stable@vger.kernel.org> # v6.10
Fixes: ab97f4b1c03075 ("xfs: repair AGI unlinked inode bucket lists")
Signed-off-by: "Darrick J. Wong" <djwong@kernel.org>
Reviewed-by: Christoph Hellwig <hch@lst.de>
---
 fs/xfs/scrub/trace.h           |    1 +
 fs/xfs/scrub/agheader_repair.c |   63 ++++++++++++++++++++++++++++++++++++----
 2 files changed, 57 insertions(+), 7 deletions(-)


diff --git a/fs/xfs/scrub/trace.h b/fs/xfs/scrub/trace.h
index d5d39d82749e5c..00fbe1b9c2354f 100644
--- a/fs/xfs/scrub/trace.h
+++ b/fs/xfs/scrub/trace.h
@@ -3542,6 +3542,7 @@ DEFINE_REPAIR_IUNLINK_RESOLVE_EVENT(xrep_iunlink_resolve_uncached);
 DEFINE_REPAIR_IUNLINK_RESOLVE_EVENT(xrep_iunlink_resolve_wronglist);
 DEFINE_REPAIR_IUNLINK_RESOLVE_EVENT(xrep_iunlink_resolve_nolist);
 DEFINE_REPAIR_IUNLINK_RESOLVE_EVENT(xrep_iunlink_resolve_ok);
+DEFINE_REPAIR_IUNLINK_RESOLVE_EVENT(xrep_iunlink_resolve_allocated);
 
 TRACE_EVENT(xrep_iunlink_relink_next,
 	TP_PROTO(struct xfs_inode *ip, xfs_agino_t next_agino),
diff --git a/fs/xfs/scrub/agheader_repair.c b/fs/xfs/scrub/agheader_repair.c
index 8514d2d7e3cea3..41de5cf87352b4 100644
--- a/fs/xfs/scrub/agheader_repair.c
+++ b/fs/xfs/scrub/agheader_repair.c
@@ -979,6 +979,13 @@ xrep_agi_calc_from_btrees(
 	return error;
 }
 
+/*
+ * Magic value that means "not unlinked" because xfarrays don't support storing
+ * totally zeroed elements.  There can't be a cluster that starts in daddr 0 so
+ * there can't be an inode #1 either.
+ */
+#define LINKED_AGINO	(0x1)
+
 /*
  * Record a forwards unlinked chain pointer from agino -> next_agino in our
  * staging information.
@@ -1362,6 +1369,35 @@ xrep_iunlink_resolve_bucket(
 			break;
 		}
 
+		if (VFS_I(ip)->i_nlink != 0) {
+			/*
+			 * Inode is linked somewhere!  Blow out both unlinked
+			 * list pointers, advance the list, and pretend we
+			 * didn't see this inode.  Clear it from iunlink_bmp
+			 * because it's linked.
+			 */
+			trace_xrep_iunlink_resolve_allocated(sc->sa.pag,
+					bucket, prev_agino, next_agino);
+
+			error = xrep_iunlink_store_next(ragi, next_agino,
+					NULLAGINO);
+			if (error)
+				return error;
+
+			error = xrep_iunlink_store_prev(ragi, next_agino,
+					LINKED_AGINO);
+			if (error)
+				return error;
+
+			error = xagino_bitmap_clear(&ragi->iunlink_bmp,
+					next_agino, 1);
+			if (error)
+				return error;
+
+			next_agino = ip->i_next_unlinked;
+			continue;
+		}
+
 		if (next_agino % XFS_AGI_UNLINKED_BUCKETS != bucket) {
 			/*
 			 * Inode is in the wrong bucket.  Advance the list,
@@ -1540,6 +1576,24 @@ xrep_iunlink_rebuild_buckets(
 			xrep_iunlink_add_lost_inodes, ragi);
 }
 
+static inline void
+set_inode_prev_unlinked(
+	struct xfs_inode	*ip,
+	xfs_agino_t		prev_agino)
+{
+	/*
+	 * Magic value that means "not unlinked" because xfarrays don't support
+	 * storing totally zeroed elements.
+	 */
+	if (prev_agino == LINKED_AGINO)
+		prev_agino = 0;
+
+	if (ip->i_prev_unlinked != prev_agino) {
+		trace_xrep_iunlink_relink_prev(ip, prev_agino);
+		ip->i_prev_unlinked = prev_agino;
+	}
+}
+
 /* Update i_next_iunlinked for the inode @agino. */
 STATIC int
 xrep_iunlink_relink_next(
@@ -1573,8 +1627,7 @@ xrep_iunlink_relink_next(
 		if (error)
 			goto out_rele;
 
-		trace_xrep_iunlink_relink_prev(ip, prev_agino);
-		ip->i_prev_unlinked = prev_agino;
+		set_inode_prev_unlinked(ip, prev_agino);
 	}
 
 	/* Update the forward pointer. */
@@ -1641,11 +1694,7 @@ xrep_iunlink_relink_prev(
 		ip->i_next_unlinked = next_agino;
 	}
 
-	/* Update the backward pointer. */
-	if (ip->i_prev_unlinked != prev_agino) {
-		trace_xrep_iunlink_relink_prev(ip, prev_agino);
-		ip->i_prev_unlinked = prev_agino;
-	}
+	set_inode_prev_unlinked(ip, prev_agino);
 
 out_rele:
 	/*


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

* [PATCH 08/10] xfs: fix another iunlink infinite loop bug in online fsck
  2026-07-27  5:22 [PATCHSET v2 2/2] xfs: LLM-inspired iunlink repair bug fixes Darrick J. Wong
                   ` (6 preceding siblings ...)
  2026-07-27  5:26 ` [PATCH 07/10] xfs: fix allocated inodes that show up in the unlinked list Darrick J. Wong
@ 2026-07-27  5:26 ` Darrick J. Wong
  2026-07-28  3:15   ` Christoph Hellwig
  2026-07-27  5:26 ` [PATCH 09/10] xfs: set the prev pointer when reinserting an inode on the unlinked list Darrick J. Wong
  2026-07-27  5:26 ` [PATCH 10/10] xfs: don't ignore runtime errors in xrep_iunlink_reload_next Darrick J. Wong
  9 siblings, 1 reply; 13+ messages in thread
From: Darrick J. Wong @ 2026-07-27  5:26 UTC (permalink / raw)
  To: djwong, cem, hch; +Cc: stable, linux-xfs

From: Darrick J. Wong <djwong@kernel.org>

xrep_iunlink_resolve_bucket is supposed to reconstruct as much of the
incore prev and next unlinked list pointers based on what it finds on
disk and in memory before we move on to relinking the truly lost inodes
back into the unlinked list.  However, it's still vulnerable to infinite
loops that come in via the next_unlinked pointers.

Fix this problem by remembering which inodes we've already seen and
checking new agino pointers against that.  If a bit is already set,
either this is a loop or the inode has nonzero link count.  We'll deal
with the second case in a subsequent patch.

Cc: <stable@vger.kernel.org> # v6.10
Fixes: ab97f4b1c03075 ("xfs: repair AGI unlinked inode bucket lists")
Signed-off-by: "Darrick J. Wong" <djwong@kernel.org>
---
 fs/xfs/scrub/trace.h           |    1 +
 fs/xfs/scrub/agheader_repair.c |   37 ++++++++++++++++++++++++++++---------
 2 files changed, 29 insertions(+), 9 deletions(-)


diff --git a/fs/xfs/scrub/trace.h b/fs/xfs/scrub/trace.h
index 00fbe1b9c2354f..14aa0ec1f09e4a 100644
--- a/fs/xfs/scrub/trace.h
+++ b/fs/xfs/scrub/trace.h
@@ -3538,6 +3538,7 @@ DEFINE_EVENT(xrep_iunlink_resolve_class, name, \
 	TP_PROTO(const struct xfs_perag *pag, unsigned int bucket, \
 		 xfs_agino_t prev_agino, xfs_agino_t next_agino), \
 	TP_ARGS(pag, bucket, prev_agino, next_agino))
+DEFINE_REPAIR_IUNLINK_RESOLVE_EVENT(xrep_iunlink_resolve_infinite_loop);
 DEFINE_REPAIR_IUNLINK_RESOLVE_EVENT(xrep_iunlink_resolve_uncached);
 DEFINE_REPAIR_IUNLINK_RESOLVE_EVENT(xrep_iunlink_resolve_wronglist);
 DEFINE_REPAIR_IUNLINK_RESOLVE_EVENT(xrep_iunlink_resolve_nolist);
diff --git a/fs/xfs/scrub/agheader_repair.c b/fs/xfs/scrub/agheader_repair.c
index 41de5cf87352b4..65b9a8befce99b 100644
--- a/fs/xfs/scrub/agheader_repair.c
+++ b/fs/xfs/scrub/agheader_repair.c
@@ -1348,15 +1348,32 @@ xrep_iunlink_resolve_bucket(
 	struct xrep_agi		*ragi,
 	unsigned int		bucket)
 {
+	struct xagino_bitmap	seen;
 	struct xfs_scrub	*sc = ragi->sc;
 	struct xfs_inode	*ip;
 	xfs_agino_t		prev_agino = NULLAGINO;
 	xfs_agino_t		next_agino = ragi->iunlink_heads[bucket];
 	int			error = 0;
 
+	xagino_bitmap_init(&seen);
+
 	while (next_agino != NULLAGINO) {
+		unsigned int len = 1;
+
 		if (xchk_should_terminate(ragi->sc, &error))
-			return error;
+			goto out_bitmap;
+
+		/* Inode already seen?  We're stuck in a loop */
+		if (xagino_bitmap_test(&seen, next_agino, &len)) {
+			trace_xrep_iunlink_resolve_infinite_loop(sc->sa.pag,
+					bucket, prev_agino, next_agino);
+			next_agino = NULLAGINO;
+			break;
+		}
+
+		error = xagino_bitmap_set(&seen, next_agino, 1);
+		if (error)
+			goto out_bitmap;
 
 		/* Find the next inode in the chain. */
 		ip = xfs_iunlink_lookup(sc->sa.pag, next_agino);
@@ -1382,17 +1399,17 @@ xrep_iunlink_resolve_bucket(
 			error = xrep_iunlink_store_next(ragi, next_agino,
 					NULLAGINO);
 			if (error)
-				return error;
+				goto out_bitmap;
 
 			error = xrep_iunlink_store_prev(ragi, next_agino,
 					LINKED_AGINO);
 			if (error)
-				return error;
+				goto out_bitmap;
 
 			error = xagino_bitmap_clear(&ragi->iunlink_bmp,
 					next_agino, 1);
 			if (error)
-				return error;
+				goto out_bitmap;
 
 			next_agino = ip->i_next_unlinked;
 			continue;
@@ -1433,20 +1450,20 @@ xrep_iunlink_resolve_bucket(
 		 */
 		error = xagino_bitmap_clear(&ragi->iunlink_bmp, next_agino, 1);
 		if (error)
-			return error;
+			goto out_bitmap;
 
 		/* Remember the previous inode's next pointer. */
 		if (prev_agino != NULLAGINO) {
 			error = xrep_iunlink_store_next(ragi, prev_agino,
 					next_agino);
 			if (error)
-				return error;
+				goto out_bitmap;
 		}
 
 		/* Remember this inode's previous pointer. */
 		error = xrep_iunlink_store_prev(ragi, next_agino, prev_agino);
 		if (error)
-			return error;
+			goto out_bitmap;
 
 		/* Advance the list and remember this inode. */
 		prev_agino = next_agino;
@@ -1457,10 +1474,12 @@ xrep_iunlink_resolve_bucket(
 	if (prev_agino != NULLAGINO) {
 		error = xrep_iunlink_store_next(ragi, prev_agino, next_agino);
 		if (error)
-			return error;
+			goto out_bitmap;
 	}
 
-	return 0;
+out_bitmap:
+	xagino_bitmap_destroy(&seen);
+	return error;
 }
 
 /* Reinsert this unlinked inode into the head of the staged bucket list. */


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

* [PATCH 09/10] xfs: set the prev pointer when reinserting an inode on the unlinked list
  2026-07-27  5:22 [PATCHSET v2 2/2] xfs: LLM-inspired iunlink repair bug fixes Darrick J. Wong
                   ` (7 preceding siblings ...)
  2026-07-27  5:26 ` [PATCH 08/10] xfs: fix another iunlink infinite loop bug in online fsck Darrick J. Wong
@ 2026-07-27  5:26 ` Darrick J. Wong
  2026-07-27  5:26 ` [PATCH 10/10] xfs: don't ignore runtime errors in xrep_iunlink_reload_next Darrick J. Wong
  9 siblings, 0 replies; 13+ messages in thread
From: Darrick J. Wong @ 2026-07-27  5:26 UTC (permalink / raw)
  To: djwong, cem, hch; +Cc: stable, linux-xfs

From: Darrick J. Wong <djwong@kernel.org>

If we find a rogue free inode and decide to reinsert it into the
unlinked list, we need to set the prev pointer to NULLAGINO so that the
incore list gets updated.

Cc: <stable@vger.kernel.org> # v6.10
Fixes: ab97f4b1c03075 ("xfs: repair AGI unlinked inode bucket lists")
Signed-off-by: "Darrick J. Wong" <djwong@kernel.org>
Reviewed-by: Christoph Hellwig <hch@lst.de>
---
 fs/xfs/scrub/agheader_repair.c |    4 ++++
 1 file changed, 4 insertions(+)


diff --git a/fs/xfs/scrub/agheader_repair.c b/fs/xfs/scrub/agheader_repair.c
index 65b9a8befce99b..4f1f235349c5a3 100644
--- a/fs/xfs/scrub/agheader_repair.c
+++ b/fs/xfs/scrub/agheader_repair.c
@@ -1504,6 +1504,10 @@ xrep_iunlink_add_to_bucket(
 	if (error)
 		return error;
 
+	error = xrep_iunlink_store_prev(ragi, agino, NULLAGINO);
+	if (error)
+		return error;
+
 	/* Remember the head inode's previous pointer. */
 	if (current_head != NULLAGINO) {
 		error = xrep_iunlink_store_prev(ragi, current_head, agino);


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

* [PATCH 10/10] xfs: don't ignore runtime errors in xrep_iunlink_reload_next
  2026-07-27  5:22 [PATCHSET v2 2/2] xfs: LLM-inspired iunlink repair bug fixes Darrick J. Wong
                   ` (8 preceding siblings ...)
  2026-07-27  5:26 ` [PATCH 09/10] xfs: set the prev pointer when reinserting an inode on the unlinked list Darrick J. Wong
@ 2026-07-27  5:26 ` Darrick J. Wong
  2026-07-28  3:16   ` Christoph Hellwig
  9 siblings, 1 reply; 13+ messages in thread
From: Darrick J. Wong @ 2026-07-27  5:26 UTC (permalink / raw)
  To: djwong, cem, hch; +Cc: stable, linux-xfs

From: Darrick J. Wong <djwong@kernel.org>

LOLLM complained that this function ignores runtime errors being
returned by xrep_iunlink_store_*.  Rework the function signature so that
we can return runtime errors to abort the repair.

Cc: <stable@vger.kernel.org> # v6.10
Fixes: ab97f4b1c03075 ("xfs: repair AGI unlinked inode bucket lists")
Signed-off-by: "Darrick J. Wong" <djwong@kernel.org>
Assisted-by: LOLLM # finding obvious bugs
---
 fs/xfs/scrub/agheader_repair.c |   32 ++++++++++++++++++++++----------
 1 file changed, 22 insertions(+), 10 deletions(-)


diff --git a/fs/xfs/scrub/agheader_repair.c b/fs/xfs/scrub/agheader_repair.c
index 4f1f235349c5a3..2104512f1ee19a 100644
--- a/fs/xfs/scrub/agheader_repair.c
+++ b/fs/xfs/scrub/agheader_repair.c
@@ -1041,31 +1041,40 @@ xrep_iunlink_next(
  * the chain or if we should stop walking the chain due to corruption; or a
  * per-AG inode number.
  */
-STATIC xfs_agino_t
+STATIC int
 xrep_iunlink_reload_next(
 	struct xrep_agi		*ragi,
 	xfs_agino_t		prev_agino,
-	xfs_agino_t		agino)
+	xfs_agino_t		agino,
+	xfs_agino_t		*next_agino)
 {
 	struct xfs_scrub	*sc = ragi->sc;
 	struct xfs_inode	*ip;
-	xfs_agino_t		ret = NULLAGINO;
 	int			error;
 
+	*next_agino = NULLAGINO;
+
 	error = xchk_iget(ragi->sc, xfs_agino_to_ino(sc->sa.pag, agino), &ip);
 	if (error)
-		return ret;
+		return 0;
 
 	trace_xrep_iunlink_reload_next(ip, prev_agino);
 
 	/* If this is a linked inode, stop processing the chain. */
 	if (VFS_I(ip)->i_nlink != 0) {
-		xrep_iunlink_store_next(ragi, agino, NULLAGINO);
+		error = xrep_iunlink_store_next(ragi, agino, NULLAGINO);
+		if (error)
+			return error;
+
+		error = xrep_iunlink_store_prev(ragi, agino, LINKED_AGINO);
+		if (error)
+			return error;
+
 		goto rele;
 	}
 
 	ip->i_prev_unlinked = prev_agino;
-	ret = ip->i_next_unlinked;
+	*next_agino = ip->i_next_unlinked;
 
 	/*
 	 * Drop the inode reference that we just took.  We hold the AGI, so
@@ -1074,7 +1083,7 @@ xrep_iunlink_reload_next(
 	 */
 rele:
 	xchk_irele(sc, ip);
-	return ret;
+	return 0;
 }
 
 /*
@@ -1114,9 +1123,12 @@ xrep_iunlink_walk_ondisk_bucket(
 			break;
 
 		next_agino = xrep_iunlink_next(sc, agino);
-		if (!next_agino)
-			next_agino = xrep_iunlink_reload_next(ragi, prev_agino,
-					agino);
+		if (!next_agino) {
+			error = xrep_iunlink_reload_next(ragi, prev_agino,
+					agino, &next_agino);
+			if (error)
+				break;
+		}
 
 		error = xagino_bitmap_set(&seen, agino, 1);
 		if (error)


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

* Re: [PATCH 08/10] xfs: fix another iunlink infinite loop bug in online fsck
  2026-07-27  5:26 ` [PATCH 08/10] xfs: fix another iunlink infinite loop bug in online fsck Darrick J. Wong
@ 2026-07-28  3:15   ` Christoph Hellwig
  0 siblings, 0 replies; 13+ messages in thread
From: Christoph Hellwig @ 2026-07-28  3:15 UTC (permalink / raw)
  To: Darrick J. Wong; +Cc: cem, hch, stable, linux-xfs

Looks good:

Reviewed-by: Christoph Hellwig <hch@lst.de>


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

* Re: [PATCH 10/10] xfs: don't ignore runtime errors in xrep_iunlink_reload_next
  2026-07-27  5:26 ` [PATCH 10/10] xfs: don't ignore runtime errors in xrep_iunlink_reload_next Darrick J. Wong
@ 2026-07-28  3:16   ` Christoph Hellwig
  0 siblings, 0 replies; 13+ messages in thread
From: Christoph Hellwig @ 2026-07-28  3:16 UTC (permalink / raw)
  To: Darrick J. Wong; +Cc: cem, hch, stable, linux-xfs

Looks good:

Reviewed-by: Christoph Hellwig <hch@lst.de>


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

end of thread, other threads:[~2026-07-28  3:16 UTC | newest]

Thread overview: 13+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-27  5:22 [PATCHSET v2 2/2] xfs: LLM-inspired iunlink repair bug fixes Darrick J. Wong
2026-07-27  5:24 ` [PATCH 01/10] xfs: hoist per-bucket unlinked list check to helper Darrick J. Wong
2026-07-27  5:24 ` [PATCH 02/10] xfs: don't livelock in scrub on a circular unlinked list Darrick J. Wong
2026-07-27  5:25 ` [PATCH 03/10] xfs: don't walk off the end of a null sc->sa.agi_bp in AGI repair Darrick J. Wong
2026-07-27  5:25 ` [PATCH 04/10] xfs: load next_agino from the correct xfarray in xrep_iunlink_relink_prev Darrick J. Wong
2026-07-27  5:25 ` [PATCH 05/10] xfs: pass runtime errors from xrep_iunlink_mark_ondisk_rec up to callers Darrick J. Wong
2026-07-27  5:25 ` [PATCH 06/10] xfs: check xfarray iteration errors when committing unlinked inode lists Darrick J. Wong
2026-07-27  5:26 ` [PATCH 07/10] xfs: fix allocated inodes that show up in the unlinked list Darrick J. Wong
2026-07-27  5:26 ` [PATCH 08/10] xfs: fix another iunlink infinite loop bug in online fsck Darrick J. Wong
2026-07-28  3:15   ` Christoph Hellwig
2026-07-27  5:26 ` [PATCH 09/10] xfs: set the prev pointer when reinserting an inode on the unlinked list Darrick J. Wong
2026-07-27  5:26 ` [PATCH 10/10] xfs: don't ignore runtime errors in xrep_iunlink_reload_next Darrick J. Wong
2026-07-28  3:16   ` Christoph Hellwig

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.