Skip to content

Commit b3379cb

Browse files
PXB-3804 : Bound the B-tree descent in --check-tables against node-pointer cycles
btr_validate_level()'s descent loop (while level != btr_page_get_level(page)) has no iteration bound and does not require the level to strictly decrease; btr_descent_level_is_sane() only checks the level is in range. A node pointer whose child-page-number is corrupted to point sideways/upward (e.g. back to the root) made the descent loop forever (check-tables runs with trx=nullptr, so trx_is_interrupted() never fires). Cap the descent at BTR_MAX_NODE_LEVEL steps -- a well-formed descent moves one level down per step -- and report corruption if exceeded. Test: descent_nodeptr_cycle. https://perconadev.atlassian.net/browse/PXB-3804
1 parent 4076c70 commit b3379cb

2 files changed

Lines changed: 107 additions & 0 deletions

File tree

storage/innobase/btr/btr0btr.cc

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4218,9 +4218,31 @@ static bool btr_validate_level(
42184218
return (false);
42194219
}
42204220

4221+
#ifdef XTRABACKUP
4222+
/* Bound the descent. Each step must move strictly one level down, so a
4223+
well-formed descent takes at most BTR_MAX_NODE_LEVEL steps. A node pointer
4224+
whose child-page-number is corrupted to point sideways/upward (e.g. back to
4225+
the root) would otherwise loop here forever -- btr_page_level_is_sane()
4226+
only checks the level is in range, not that it decreases, and check-tables
4227+
runs with trx=nullptr so trx_is_interrupted() never fires. */
4228+
ulint descent_steps = 0;
4229+
#endif /* XTRABACKUP */
42214230
while (level != btr_page_get_level(page)) {
42224231
const rec_t *node_ptr;
42234232

4233+
#ifdef XTRABACKUP
4234+
if (++descent_steps > BTR_MAX_NODE_LEVEL) {
4235+
btr_validate_report1(index, level, block);
4236+
ib::error() << "B-tree corruption: descent to level " << level
4237+
<< " of index " << index->name() << " exceeded "
4238+
<< BTR_MAX_NODE_LEVEL
4239+
<< " steps -- cyclic or non-decreasing node pointers";
4240+
mtr_commit(&mtr);
4241+
mem_heap_free(heap);
4242+
return false;
4243+
}
4244+
#endif /* XTRABACKUP */
4245+
42244246
if (fseg_page_is_free(seg, block->page.id.space(),
42254247
block->page.id.page_no())) {
42264248
btr_validate_report1(index, level, block);
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
############################################################################
2+
# PXB-3804 : --check-tables must not hang during B-tree descent.
3+
#
4+
# Theory (Gap 3): btr_validate_level()'s descent loop
5+
# while (level != btr_page_get_level(page)) { ... follow first node ptr ... }
6+
# has no iteration bound and no requirement that the level strictly DECREASES.
7+
# btr_descent_level_is_sane() only checks the level is in range (<=
8+
# BTR_MAX_NODE_LEVEL), not that descent makes progress. So a node pointer whose
9+
# child-page-number is corrupted to point to an in-bounds same/higher page
10+
# makes the descent loop forever (check-tables passes trx=nullptr, so
11+
# trx_is_interrupted() never breaks out).
12+
#
13+
# Repro (2-level tree): point the ROOT's leftmost node pointer's child-page
14+
# number back at the ROOT itself. Validating level 0 then descends
15+
# root -> (child = root) -> root -> ... forever.
16+
# Expectation BEFORE fix: timeout. AFTER fix: bounded descent reports the
17+
# cycle and exits non-zero.
18+
############################################################################
19+
20+
. inc/common.sh
21+
22+
start_server --innodb_file_per_table
23+
24+
mysql test <<'EOF'
25+
SET SESSION cte_max_recursion_depth = 20000;
26+
CREATE TABLE t1 (id INT PRIMARY KEY, pad VARCHAR(100));
27+
INSERT INTO t1
28+
WITH RECURSIVE seq(n) AS (SELECT 1 UNION ALL SELECT n + 1 FROM seq WHERE n < 5000)
29+
SELECT n, CONCAT('p', n) FROM seq;
30+
EOF
31+
32+
xtrabackup --backup --target-dir=$topdir/backup
33+
xtrabackup --prepare --apply-log-only --target-dir=$topdir/backup
34+
IBD=$topdir/backup/test/t1.ibd
35+
36+
PAGE_SIZE=$(get_page_size "$IBD")
37+
38+
# ROOT page and the in-page byte offset of the leftmost node pointer's
39+
# child-page-number field.
40+
read ROOT OFF <<< "$(find_leftmost_node_ptr "$IBD")"
41+
[ "$ROOT" != "ERR" ] && [ -n "$OFF" ] || \
42+
die "need a multi-level clustered index with node pointers ($ROOT $OFF)"
43+
vlog "root=$ROOT, leftmost node-ptr child-page field at in-page offset $OFF"
44+
45+
#
46+
# Control: clean backup passes.
47+
#
48+
vlog "=== Control: clean --check-tables ==="
49+
cp -r $topdir/backup $topdir/backup_ctrl
50+
xtrabackup --prepare --check-tables --target-dir=$topdir/backup_ctrl 2>&1 \
51+
| tee $topdir/ctrl.log
52+
grep -q "All table checks passed" $topdir/ctrl.log || \
53+
die "Control: clean backup unexpectedly failed --check-tables"
54+
vlog "Control passed"
55+
56+
#
57+
# Negative: point the root's first node pointer back at the root.
58+
#
59+
vlog "=== Negative: root node-ptr child -> root (descent self-cycle) ==="
60+
cp -r $topdir/backup $topdir/backup_bad
61+
CIBD=$topdir/backup_bad/test/t1.ibd
62+
ORIG=$(mach_read_n "$CIBD" "$ROOT" "$OFF" 4)
63+
mach_write_n "$CIBD" "$ROOT" "$OFF" "$ROOT" 4
64+
vlog "child page $ORIG -> $ROOT"
65+
66+
SIZE_BEFORE=$(stat -c %s "$CIBD")
67+
set +e
68+
timeout 90 $XB_BIN $XB_ARGS --prepare --check-tables \
69+
--innodb-checksum-algorithm=none \
70+
--target-dir=$topdir/backup_bad 2>&1 | tee $topdir/bad.log
71+
RC=${PIPESTATUS[0]}
72+
set -e
73+
SIZE_AFTER=$(stat -c %s "$CIBD")
74+
vlog "check-tables exit code: $RC"
75+
76+
[ "$RC" -ne 124 ] || die "descent_nodeptr_cycle: --check-tables HUNG during descent"
77+
grep -qiE "Assertion failure|got signal|ut_error|ib::fatal triggered" $topdir/bad.log && \
78+
die "descent_nodeptr_cycle: --check-tables ABORTED"
79+
[ "$SIZE_AFTER" = "$SIZE_BEFORE" ] || \
80+
die "descent_nodeptr_cycle: .ibd grew during --check-tables ($SIZE_BEFORE -> $SIZE_AFTER)"
81+
[ "$RC" -ne 0 ] || die "descent_nodeptr_cycle: --check-tables passed a descent cycle"
82+
grep -qiE "descent|cycle|too deep|B-tree corruption|Table check failed" $topdir/bad.log || \
83+
die "descent_nodeptr_cycle: corruption not reported"
84+
85+
vlog "descent_nodeptr_cycle passed: descent cycle reported gracefully (rc=$RC)"

0 commit comments

Comments
 (0)