aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDarrick J. Wong <djwong@kernel.org>2023-11-13 09:08:30 -0800
committerZorro Lang <zlang@kernel.org>2023-11-16 22:47:09 +0800
commitb5ba77147bf6cf6766ada00b3fed4635626e550d (patch)
treeae5f977e029c6d4b9436ecbc31ede1d5286626a2
parent59bf56a1a56e8b588d18c4c39757b439342cd24d (diff)
downloadxfstests-dev-b5ba77147bf6cf6766ada00b3fed4635626e550d.tar.gz
xfs: test unlinked inode list repair on demand
Create a test to exercise recovery of unlinked inodes on a clean filesystem. This was definitely possible on old kernels that on an ro mount would clean the log without processing the iunlink list. Signed-off-by: "Darrick J. Wong" <djwong@kernel.org> Reviewed-by: Zorro Lang <zlang@redhat.com> Signed-off-by: Zorro Lang <zlang@kernel.org>
-rw-r--r--common/rc4
-rwxr-xr-xtests/xfs/602111
-rw-r--r--tests/xfs/602.out5
-rwxr-xr-xtests/xfs/603215
-rw-r--r--tests/xfs/603.out6
5 files changed, 340 insertions, 1 deletions
diff --git a/common/rc b/common/rc
index e64dea108c..cc92fe0681 100644
--- a/common/rc
+++ b/common/rc
@@ -2668,9 +2668,11 @@ _require_xfs_io_command()
param_checked="$pwrite_opts $param"
;;
"scrub"|"repair")
- testio=`$XFS_IO_PROG -x -c "$command probe" $TEST_DIR 2>&1`
+ test -z "$param" && param="probe"
+ testio=`$XFS_IO_PROG -x -c "$command $param" $TEST_DIR 2>&1`
echo $testio | grep -q "Inappropriate ioctl" && \
_notrun "xfs_io $command support is missing"
+ param_checked="$param"
;;
"startupdate"|"commitupdate"|"cancelupdate")
$XFS_IO_PROG -f -c 'pwrite -S 0x58 0 128k -b 128k' $testfile > /dev/null
diff --git a/tests/xfs/602 b/tests/xfs/602
new file mode 100755
index 0000000000..3bc2484e61
--- /dev/null
+++ b/tests/xfs/602
@@ -0,0 +1,111 @@
+#! /bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (c) 2023 Oracle. All Rights Reserved.
+#
+# FS QA Test No. 602
+#
+# Test using runtime 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
+
+# real QA test starts here
+
+_supported_fs xfs
+_require_xfs_db_command iunlink
+_require_scratch_nocheck # we'll run repair ourselves
+
+# From the AGI definition
+XFS_AGI_UNLINKED_BUCKETS=64
+
+# 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'
+
+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}')
+}
+
+__repair_check_scratch() {
+ _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
+}
+
+# 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
+ fi
+ test "$res" -ne 0 && echo "repair returned $res?"
+}
+
+echo "+ Part 0: See if runtime can recover the unlinked list" | tee -a $seqres.full
+format_scratch
+_kernlog "part 0"
+_scratch_mount
+exercise_scratch
+_scratch_unmount
+final_check_scratch
+
+echo "+ Part 1: See if bulkstat can recover the unlinked list" | tee -a $seqres.full
+format_scratch
+_kernlog "part 1"
+_scratch_mount
+$XFS_IO_PROG -c 'bulkstat' $SCRATCH_MNT > /dev/null
+exercise_scratch
+_scratch_unmount
+final_check_scratch
+
+echo "+ Part 2: See if quotacheck can recover the unlinked list" | tee -a $seqres.full
+if [ -f /proc/fs/xfs/xqmstat ]; then
+ MOUNT_OPTIONS="$orig_mount_options"
+ _qmount_option 'quota'
+ format_scratch
+ _kernlog "part 2"
+ _scratch_mount
+ exercise_scratch
+ _scratch_unmount
+ final_check_scratch
+fi
+
+# success, all done
+echo Silence is golden
+status=0
+exit
diff --git a/tests/xfs/602.out b/tests/xfs/602.out
new file mode 100644
index 0000000000..086c6715d7
--- /dev/null
+++ b/tests/xfs/602.out
@@ -0,0 +1,5 @@
+QA output created by 602
++ Part 0: See if runtime can recover the unlinked list
++ Part 1: See if bulkstat can recover the unlinked list
++ Part 2: See if quotacheck can recover the unlinked list
+Silence is golden
diff --git a/tests/xfs/603 b/tests/xfs/603
new file mode 100755
index 0000000000..444ebf40b8
--- /dev/null
+++ b/tests/xfs/603
@@ -0,0 +1,215 @@
+#! /bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (c) 2023 Oracle. All Rights Reserved.
+#
+# FS QA Test No. 603
+#
+# Functional test of using online repair to fix unlinked inodes on a clean
+# filesystem that never got cleaned up.
+#
+. ./common/preamble
+_begin_fstest auto online_repair
+
+. ./common/filter
+. ./common/fuzzy
+. ./common/quota
+
+# real QA test starts here
+
+_supported_fs xfs
+_require_xfs_db_command iunlink
+# The iunlink bucket repair code wasn't added to the AGI repair code
+# until after the directory repair code was merged
+_require_xfs_io_command repair -R directory
+_require_scratch_nocheck # repair doesn't like single-AG fs
+
+# From the AGI definition
+XFS_AGI_UNLINKED_BUCKETS=64
+
+# Try to make each iunlink bucket have this many inodes in it.
+IUNLINK_BUCKETLEN=5
+
+# Disable quota since quotacheck will break this test
+_qmount_option 'noquota'
+
+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}')
+}
+
+__repair_check_scratch() {
+ _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]}"
+}
+
+corrupt_scratch() {
+ # How far into the iunlink bucket chain do we target inodes for corruption?
+ # 1 = target the inode pointed to by the AGI
+ # 3 = middle of bucket list
+ # 5 = last element in bucket
+ local corruption_bucket_depth="$1"
+ if ((corruption_bucket_depth < 1 || corruption_bucket_depth > IUNLINK_BUCKETLEN)); then
+ echo "${corruption_bucket_depth}: Value must be between 1 and ${IUNLINK_BUCKETLEN}."
+ return 1
+ fi
+
+ # Index of the inode numbers within BADINODES
+ local bad_ino1_idx=$(( (IUNLINK_BUCKETLEN - corruption_bucket_depth) * XFS_AGI_UNLINKED_BUCKETS))
+ local bad_ino2_idx=$((bad_ino1_idx + 1))
+
+ # Inode numbers to target
+ local bad_ino1="${BADINODES[bad_ino1_idx]}"
+ local bad_ino2="${BADINODES[bad_ino2_idx]}"
+ printf "bad: 0x%x 0x%x\n" "${bad_ino1}" "${bad_ino2}" | _tee_kernlog >> $seqres.full
+
+ # Bucket within AGI 0's iunlinked array.
+ local ino1_bucket="$((bad_ino1 % XFS_AGI_UNLINKED_BUCKETS))"
+ local ino2_bucket="$((bad_ino2 % XFS_AGI_UNLINKED_BUCKETS))"
+
+ # The first bad inode stays on the unlinked list but gets a nonzero
+ # nlink; the second bad inode is removed from the unlinked list but
+ # keeps its zero nlink
+ _scratch_xfs_db -x \
+ -c "inode ${bad_ino1}" -c "write -d core.nlinkv2 5555" \
+ -c "agi 0" -c "fuzz -d unlinked[${ino2_bucket}] ones" -c "print unlinked" >> $seqres.full
+
+ local iwatch=()
+ local idx
+
+ # Make a list of the adjacent iunlink bucket inodes for the first inode
+ # that we targeted.
+ if [ "${corruption_bucket_depth}" -gt 1 ]; then
+ # Previous ino in bucket
+ idx=$(( (IUNLINK_BUCKETLEN - corruption_bucket_depth + 1) * XFS_AGI_UNLINKED_BUCKETS))
+ iwatch+=("${BADINODES[idx]}")
+ fi
+ iwatch+=("${bad_ino1}")
+ if [ "$((corruption_bucket_depth + 1))" -lt "${IUNLINK_BUCKETLEN}" ]; then
+ # Next ino in bucket
+ idx=$(( (IUNLINK_BUCKETLEN - corruption_bucket_depth - 1) * XFS_AGI_UNLINKED_BUCKETS))
+ iwatch+=("${BADINODES[idx]}")
+ fi
+
+ # Make a list of the adjacent iunlink bucket inodes for the second
+ # inode that we targeted.
+ if [ "${corruption_bucket_depth}" -gt 1 ]; then
+ # Previous ino in bucket
+ idx=$(( (IUNLINK_BUCKETLEN - corruption_bucket_depth + 1) * XFS_AGI_UNLINKED_BUCKETS))
+ iwatch+=("${BADINODES[idx + 1]}")
+ fi
+ iwatch+=("${bad_ino2}")
+ if [ "$((corruption_bucket_depth + 1))" -lt "${IUNLINK_BUCKETLEN}" ]; then
+ # Next ino in bucket
+ idx=$(( (IUNLINK_BUCKETLEN - corruption_bucket_depth - 1) * XFS_AGI_UNLINKED_BUCKETS))
+ iwatch+=("${BADINODES[idx + 1]}")
+ fi
+
+ # Construct a grep string for tracepoints.
+ GREP_STR="(xrep_attempt|xrep_done|bucket ${ino1_bucket} |bucket ${ino2_bucket} |bucket ${fuzz_bucket} "
+ GREP_STR="(xrep_attempt|xrep_done|bucket ${ino1_bucket} |bucket ${ino2_bucket} "
+ for ino in "${iwatch[@]}"; do
+ f="$(printf "|ino 0x%x" "${ino}")"
+ GREP_STR="${GREP_STR}${f}"
+ done
+ GREP_STR="${GREP_STR})"
+ echo "grep -E \"${GREP_STR}\"" >> $seqres.full
+
+ # Dump everything we did to to the full file.
+ local db_dump=(-c 'agi 0' -c 'print unlinked')
+ db_dump+=(-c 'addr root' -c 'print')
+ test "${ino1_bucket}" -gt 0 && \
+ db_dump+=(-c "dump_iunlinked -a 0 -b $((ino1_bucket - 1))")
+ db_dump+=(-c "dump_iunlinked -a 0 -b ${ino1_bucket}")
+ db_dump+=(-c "dump_iunlinked -a 0 -b ${ino2_bucket}")
+ test "${ino2_bucket}" -lt 63 && \
+ db_dump+=(-c "dump_iunlinked -a 0 -b $((ino2_bucket + 1))")
+ db_dump+=(-c "inode $bad_ino1" -c 'print core.nlinkv2 v3.inumber next_unlinked')
+ db_dump+=(-c "inode $bad_ino2" -c 'print core.nlinkv2 v3.inumber next_unlinked')
+ _scratch_xfs_db "${db_dump[@]}" >> $seqres.full
+
+ # Test run of repair to make sure we find disconnected inodes
+ __repair_check_scratch | \
+ sed -e 's/disconnected inode \([0-9]*\)/disconnected inode XXXXXX/g' \
+ -e 's/next_unlinked in inode \([0-9]*\)/next_unlinked in inode XXXXXX/g' \
+ -e 's/unlinked bucket \([0-9]*\) is \([0-9]*\) in ag \([0-9]*\) .inode=\([0-9]*\)/unlinked bucket YY is XXXXXX in ag Z (inode=AAAAAA/g' | \
+ uniq -c >> $seqres.full
+ res=${PIPESTATUS[0]}
+ test "$res" -ne 0 || echo "repair returned $res after corruption?"
+}
+
+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
+}
+
+# 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
+ fi
+ test "$res" -ne 0 && echo "repair returned $res?"
+}
+
+echo "+ Part 1: See if scrub can recover the unlinked list" | tee -a $seqres.full
+format_scratch
+_kernlog "no bad inodes"
+_scratch_mount
+_scratch_scrub >> $seqres.full
+exercise_scratch
+_scratch_unmount
+final_check_scratch
+
+echo "+ Part 2: Corrupt the first inode in the bucket" | tee -a $seqres.full
+format_scratch
+corrupt_scratch 1
+_scratch_mount
+_scratch_scrub >> $seqres.full
+exercise_scratch
+_scratch_unmount
+final_check_scratch
+
+echo "+ Part 3: Corrupt the middle inode in the bucket" | tee -a $seqres.full
+format_scratch
+corrupt_scratch 3
+_scratch_mount
+_scratch_scrub >> $seqres.full
+exercise_scratch
+_scratch_unmount
+final_check_scratch
+
+echo "+ Part 4: Corrupt the last inode in the bucket" | tee -a $seqres.full
+format_scratch
+corrupt_scratch 5
+_scratch_mount
+_scratch_scrub >> $seqres.full
+exercise_scratch
+_scratch_unmount
+final_check_scratch
+
+# success, all done
+echo Silence is golden
+status=0
+exit
diff --git a/tests/xfs/603.out b/tests/xfs/603.out
new file mode 100644
index 0000000000..0c2f40b4c2
--- /dev/null
+++ b/tests/xfs/603.out
@@ -0,0 +1,6 @@
+QA output created by 603
++ Part 1: See if scrub can recover the unlinked list
++ Part 2: Corrupt the first inode in the bucket
++ Part 3: Corrupt the middle inode in the bucket
++ Part 4: Corrupt the last inode in the bucket
+Silence is golden