blob: 444ebf40b8ec92c35470db72784506f4a67d0a3a (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
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
|