FS/XFS testing framework
 help / color / mirror / Atom feed
From: Zorro Lang <zlang@kernel.org>
To: "Darrick J. Wong" <djwong@kernel.org>
Cc: fstests <fstests@vger.kernel.org>,
	xfs <linux-xfs@vger.kernel.org>,
	 Christoph Hellwig <hch@infradead.org>
Subject: Re: [PATCH] xfs: test unlinked inode list checking and repair with loops
Date: Tue, 25 Aug 2026 01:14:31 +0800	[thread overview]
Message-ID: <aoxxL4KKsjA6tr2Q@zlang-mailbox> (raw)
In-Reply-To: <20260819155225.GB839663@frogsfrogsfrogs>

On Wed, Aug 19, 2026 at 08:52:25AM -0700, Darrick J. Wong wrote:
> From: Darrick J. Wong <djwong@kernel.org>
> 
> Simple test of various weird ways we can screw up unlinked inode list
> reconstruction.
> 
> Signed-off-by: "Darrick J. Wong" <djwong@kernel.org>
> Reviewed-by: Christoph Hellwig <hch@lst.de>
> ---
>  tests/xfs/1907     |  212 ++++++++++++++++++++++++++++++++++++++++++++++++++++
>  tests/xfs/1907.out |   17 ++++
>  2 files changed, 229 insertions(+)
>  create mode 100755 tests/xfs/1907
>  create mode 100644 tests/xfs/1907.out
> 
> diff --git a/tests/xfs/1907 b/tests/xfs/1907
> new file mode 100755
> index 00000000000000..eb817eddd8a22f
> --- /dev/null
> +++ b/tests/xfs/1907
> @@ -0,0 +1,212 @@
> +#! /bin/bash
> +# SPDX-License-Identifier: GPL-2.0
> +# Copyright (c) 2026 Oracle.  All Rights Reserved.
> +#
> +# FS QA Test No. 1907
> +#
> +# Test using online fsck code to fix unlinked inodes on a clean filesystem that
> +# never got cleaned up.
> +#
> +. ./common/preamble
> +_begin_fstest auto quick unlink
> +
> +. ./common/filter
> +. ./common/fuzzy
> +. ./common/quota
> +
> +_require_command "$TIMEOUT_PROG" timeout
> +_require_xfs_db_command iunlink
> +_require_scratch_nocheck	# we'll run repair ourselves
> +
> +_scratch_mount
> +$XFS_IO_PROG -x -c 'repair -R agi 0' $SCRATCH_MNT 2>&1 | \
> +	grep -q 'Operation not supported' && \
> +	_notrun "cannot repair agi"
> +_scratch_unmount
> +
> +# From the AGI definition
> +XFS_AGI_UNLINKED_BUCKETS=64
> +NULLAGINO="0xffffffff"
> +
> +# Try to make each iunlink bucket have this many inodes in it.
> +IUNLINK_BUCKETLEN=5
> +
> +# Disable quota since quotacheck will break this test
> +orig_mount_options="$MOUNT_OPTIONS"
> +_qmount_option 'noquota'

OK, quota is disabled, but "orig_mount_options" seems unused throughout
the rest of the test?

> +
> +format_scratch() {
> +	_scratch_mkfs -d agcount=1 | _filter_mkfs 2> "${tmp}.mkfs" >> $seqres.full
> +	source "${tmp}.mkfs"
> +	test "${agcount}" -eq 1 || _notrun "test requires 1 AG for error injection"
> +
> +	local nr_iunlinks="$((IUNLINK_BUCKETLEN * XFS_AGI_UNLINKED_BUCKETS))"
> +	readarray -t BADINODES < <(_scratch_xfs_db -x -c "iunlink -n $nr_iunlinks" | awk '{print $4}')
> +
> +	ROOTINO="$(_scratch_xfs_get_sb_field rootino)"
> +
> +	BUCKET_23=()
> +	BUCKET_24=()
> +	for badinode in "${BADINODES[@]}"; do
> +		(( (badinode % 64) == 23 )) && BUCKET_23+=("${badinode}")
> +		(( (badinode % 64) == 24 )) && BUCKET_24+=("${badinode}")
> +	done
> +	test "${#BUCKET_23[@]}" -ge "$IUNLINK_BUCKETLEN" || \
> +		echo "bucket 23 should have at least $IUNLINK_BUCKETLEN inodes, has ${#BUCKET_23[@]}"
> +	test "${#BUCKET_24[@]}" -ge "$IUNLINK_BUCKETLEN" || \
> +		echo "bucket 24 should have at least $IUNLINK_BUCKETLEN inodes, has ${#BUCKET_23[@]}"
                                                                                      ^^
BUCKET_24 ?

> +
> +	# Log what we think the bucket 23 unlinked list will look like
> +	printf "ROOTINO 0x%x\n" "$ROOTINO" >> $seqres.full
> +	for badinode in "${BUCKET_23[@]}"; do
> +		printf "0x%x <- " "${badinode}" >> $seqres.full
> +	done
> +	echo " AGI.iunlinked[23]" >> $seqres.full
> +	for badinode in "${BUCKET_24[@]}"; do
> +		printf "0x%x <- " "${badinode}" >> $seqres.full
> +	done
> +	echo " AGI.iunlinked[24]" >> $seqres.full
> +
> +	# Log what the actual bucket 23 unlinked list ended up looking like
> +	local subcommands=()
> +	for badinode in "${BUCKET_23[@]}"; do
> +		subcommands+=(-c "inode ${badinode}" -c "print next_unlinked")
> +	done
> +	_scratch_xfs_db -x "${subcommands[@]}" >> $seqres.full
> +}
> +
> +__repair_check_scratch() {

__repair_check_scratch() looks like an internal fstests common helper due to
the leading underscores. If it's a test-local helper, how about dropping the
underscores and renaming it?

Thanks,
Zorro

> +	_scratch_xfs_repair -o force_geometry -n 2>&1 | \
> +		tee -a $seqres.full | \
> +		grep -E '(disconnected inode.*would move|next_unlinked in inode|unlinked bucket.*is.*in ag)'
> +	return "${PIPESTATUS[0]}"
> +}
> +
> +exercise_scratch() {
> +	# Create a bunch of files...
> +	declare -A inums
> +	for ((i = 0; i < (XFS_AGI_UNLINKED_BUCKETS * 2); i++)); do
> +		touch "${SCRATCH_MNT}/${i}" || break
> +		inums["${i}"]="$(stat -c %i "${SCRATCH_MNT}/${i}")"
> +	done
> +
> +	# ...then delete them to exercise the unlinked buckets
> +	for ((i = 0; i < (XFS_AGI_UNLINKED_BUCKETS * 2); i++)); do
> +		if ! rm -f "${SCRATCH_MNT}/${i}"; then
> +			echo "rm failed on inum ${inums[$i]}"
> +			break
> +		fi
> +	done
> +}
> +
> +test_body() {
> +	_scratch_mount
> +	timeout $((30 * TIME_FACTOR))s $XFS_IO_PROG -x -c 'scrub agi 0' -c 'repair agi 0' $SCRATCH_MNT
> +	$XFS_IO_PROG -x -c 'repair fscounters' $SCRATCH_MNT >> $seqres.full
> +	exercise_scratch
> +	_scratch_unmount
> +	final_check_scratch
> +}
> +
> +# Offline repair should not find anything
> +final_check_scratch() {
> +	__repair_check_scratch
> +	res=$?
> +	if [ $res -eq 2 ]; then
> +		echo "scratch fs went offline?"
> +		_scratch_mount
> +		_scratch_unmount
> +		__repair_check_scratch

Shouldn't "res" be updated at here?

> +	fi
> +	test "$res" -ne 0 && echo "repair returned $res?"
> +}
> +
> +echo "+ Part 1: Fix a correct unlinked list" | tee -a $seqres.full
> +_kernlog "part 1"
> +format_scratch
> +test_body
> +
> +echo "+ Part 2: Fix a loop between 1 and 3" | tee -a $seqres.full
> +_kernlog "part 2"
> +format_scratch
> +# BUCKET_23 is in reverse order of the ondisk list, so we make
> +# inode 1 point back to inode 3.
> +_scratch_xfs_db -x \
> +	-c "inode ${BUCKET_23[1]}" \
> +	-c "print next_unlinked" \
> +	-c "write -d next_unlinked ${BUCKET_23[3]}" \
> +	>> $seqres.full
> +test_body
> +
> +echo "+ Part 3: Fix a truncated bucket" | tee -a $seqres.full
> +_kernlog "part 3"
> +format_scratch
> +_scratch_xfs_db -x \
> +	-c "agi 0" \
> +	-c "print" \
> +	-c "write -d unlinked[23] ${NULLAGINO}" \
> +	>> $seqres.full
> +test_body
> +
> +echo "+ Part 4: Fix a loop at the end" | tee -a $seqres.full
> +_kernlog "part 4"
> +format_scratch
> +# BUCKET_23 is in reverse order of the ondisk list, so we make
> +# inode 0 point back to inode 0.
> +_scratch_xfs_db -x \
> +	-c "inode ${BUCKET_23[0]}" \
> +	-c "print next_unlinked" \
> +	-c "write -d next_unlinked ${BUCKET_23[0]}" \
> +	>> $seqres.full
> +test_body
> +
> +echo "+ Part 5: Fix an inode in the wrong bucket" | tee -a $seqres.full
> +_kernlog "part 5"
> +format_scratch
> +_scratch_xfs_db -x \
> +	-c "inode ${BUCKET_23[0]}" \
> +	-c "print next_unlinked" \
> +	-c "write -d next_unlinked ${BUCKET_24[1]}" \
> +	-c "inode ${BUCKET_24[2]}" \
> +	-c "print next_unlinked" \
> +	-c "write -d next_unlinked ${NULLAGINO}" \
> +	>> $seqres.full
> +test_body
> +
> +echo "+ Part 6: Fix an inode that isn't free and truncates list" | tee -a $seqres.full
> +_kernlog "part 6"
> +format_scratch
> +_scratch_xfs_db -x \
> +	-c "inode ${BUCKET_23[2]}" \
> +	-c "print next_unlinked" \
> +	-c "write -d next_unlinked ${ROOTINO}" \
> +	>> $seqres.full
> +test_body
> +
> +echo "+ Part 7: Fix an inode that isn't free" | tee -a $seqres.full
> +_kernlog "part 7"
> +format_scratch
> +_scratch_xfs_db -x \
> +	-c "inode ${BUCKET_23[2]}" \
> +	-c "print next_unlinked" \
> +	-c "write -d next_unlinked ${ROOTINO}" \
> +	-c "inode ${ROOTINO}" \
> +	-c "print next_unlinked" \
> +	-c "write -d next_unlinked ${BUCKET_23[1]}" \
> +	>> $seqres.full
> +test_body
> +
> +echo "+ Part 8: Fix an totally unallocated inode" | tee -a $seqres.full
                       ^^
                       a?

> +_kernlog "part 8"
> +format_scratch
> +target=$(( BADINODES[-1] + 256 ))
> +_scratch_xfs_db -x \
> +	-c "inode ${BUCKET_23[2]}" \
> +	-c "print next_unlinked" \
> +	-c "write -d next_unlinked $target" \
> +	>> $seqres.full
> +test_body
> +
> +# success, all done
> +status=0
> +exit
> diff --git a/tests/xfs/1907.out b/tests/xfs/1907.out
> new file mode 100644
> index 00000000000000..83746ad602b21d
> --- /dev/null
> +++ b/tests/xfs/1907.out
> @@ -0,0 +1,17 @@
> +QA output created by 1907
> ++ Part 1: Fix a correct unlinked list
> +Corruption detected.
> ++ Part 2: Fix a loop between 1 and 3
> +Corruption detected.
> ++ Part 3: Fix a truncated bucket
> +Corruption detected.
> ++ Part 4: Fix a loop at the end
> +Corruption detected.
> ++ Part 5: Fix an inode in the wrong bucket
> +Corruption detected.
> ++ Part 6: Fix an inode that isn't free and truncates list
> +Corruption detected.
> ++ Part 7: Fix an inode that isn't free
> +Corruption detected.
> ++ Part 8: Fix an totally unallocated inode
> +Corruption detected.

  reply	other threads:[~2026-08-24 17:14 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-19 15:52 [PATCH] xfs: test unlinked inode list checking and repair with loops Darrick J. Wong
2026-08-24 17:14 ` Zorro Lang [this message]
2026-08-24 17:42   ` Darrick J. Wong

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=aoxxL4KKsjA6tr2Q@zlang-mailbox \
    --to=zlang@kernel.org \
    --cc=djwong@kernel.org \
    --cc=fstests@vger.kernel.org \
    --cc=hch@infradead.org \
    --cc=linux-xfs@vger.kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox