#! /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