Skip to content

Commit be349de

Browse files
PXB-3804 : Detect cyclic FIL_PAGE_NEXT chains in --check-tables with a visited-page set
btr_validate_level() scans a level left to right via FIL_PAGE_NEXT. On a self-consistent cycle the back-link and bounds checks pass, the key-order check reports but does not stop, and check-tables runs with trx=nullptr so trx_is_interrupted() never fires -- so --check-tables hung forever. Track the pages already visited on the level in a std::unordered_set: a page can appear only once per level, so revisiting one means the sibling links loop. Only the page number is recorded, and only when a page becomes the current page (once per left->right step), so re-reads/re-latches during the scan are never re-inserted, and the root re-descended for other subtrees is a separate per-level pass with its own set. Detection is at the first revisit (one loop around). Test: sibling_link_cycle. https://perconadev.atlassian.net/browse/PXB-3804 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c12d5c1 commit be349de

2 files changed

Lines changed: 110 additions & 0 deletions

File tree

storage/innobase/btr/btr0btr.cc

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ this program; if not, write to the Free Software Foundation, Inc.,
3535
#include "btr0btr.h"
3636

3737
#include <sys/types.h>
38+
#include <unordered_set>
3839

3940
#ifndef UNIV_HOTBACKUP
4041
#include "btr0cur.h"
@@ -4325,9 +4326,33 @@ static bool btr_validate_level(
43254326
seg -= PAGE_BTR_SEG_TOP - PAGE_BTR_SEG_LEAF;
43264327
}
43274328

4329+
#ifdef XTRABACKUP
4330+
/* Detect a cyclic FIL_PAGE_NEXT chain on this level. A page can appear only
4331+
once on a level, so revisiting one means the sibling links loop back.
4332+
Without this the scan never terminates: on a self-consistent cycle the
4333+
back-link and bounds checks pass, the key-order check reports but does not
4334+
stop, and check-tables runs with trx=nullptr so trx_is_interrupted() never
4335+
fires. Only the page number is stored, and only when a page becomes the
4336+
current page (once per left->right step) -- re-reads/re-latches of a page
4337+
during this iteration are never re-inserted, and the root re-descended for
4338+
other subtrees belongs to a different (per-level) call with its own set. */
4339+
std::unordered_set<page_no_t> seen_pages;
4340+
#endif /* XTRABACKUP */
4341+
43284342
do {
43294343
ulint cur_page_no;
43304344
mem_heap_empty(heap);
4345+
#ifdef XTRABACKUP
4346+
if (!seen_pages.insert(block->page.id.page_no()).second) {
4347+
btr_validate_report1(index, level, block);
4348+
ib::error() << "B-tree corruption: page " << block->page.id.page_no()
4349+
<< " revisited on level " << level << " of index "
4350+
<< index->name() << " -- FIL_PAGE_NEXT chain forms a cycle";
4351+
ret = false;
4352+
right_page_no = FIL_NULL;
4353+
goto node_ptr_fails;
4354+
}
4355+
#endif /* XTRABACKUP */
43314356
offsets = offsets2 = nullptr;
43324357
if (!srv_read_only_mode) {
43334358
if (lockout) {
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 on a cyclic sibling chain.
3+
#
4+
# Theory (Gap 2): btr_validate_level() scans a level left->right via
5+
# FIL_PAGE_NEXT with NO visited tracking and NO iteration bound. The existing
6+
# guards do not stop a *consistent, in-bounds* cycle:
7+
# * btr_page_no_in_bounds -> only rejects out-of-tablespace page numbers
8+
# * page_is_empty -> only the all-zero FIL_PAGE_NEXT=0 case
9+
# * back-link check -> only inconsistent prev/next
10+
# * key-order check -> sets ret=false but does NOT stop the loop
11+
# check-tables passes trx=nullptr, so trx_is_interrupted() never breaks out.
12+
#
13+
# Repro: make the two leftmost leaves a closed cycle A<->B:
14+
# A.next=B (orig), B.prev=A (orig), and corrupt B.next=A, A.prev=B.
15+
# The scan starts at A (the descent target), so there is no entry edge whose
16+
# back-link could be checked, and A.next=B / B.next=A are mutually consistent.
17+
# Expectation BEFORE fix: infinite loop -> timeout. AFTER fix: the bounded
18+
# scan reports the cycle and exits non-zero.
19+
############################################################################
20+
21+
. inc/common.sh
22+
23+
start_server --innodb_file_per_table
24+
25+
mysql test <<'EOF'
26+
SET SESSION cte_max_recursion_depth = 20000;
27+
CREATE TABLE t1 (id INT PRIMARY KEY, pad VARCHAR(100));
28+
INSERT INTO t1
29+
WITH RECURSIVE seq(n) AS (SELECT 1 UNION ALL SELECT n + 1 FROM seq WHERE n < 5000)
30+
SELECT n, CONCAT('p', n) FROM seq;
31+
EOF
32+
33+
xtrabackup --backup --target-dir=$topdir/backup
34+
xtrabackup --prepare --apply-log-only --target-dir=$topdir/backup
35+
IBD=$topdir/backup/test/t1.ibd
36+
37+
PAGE_SIZE=$(get_page_size "$IBD")
38+
39+
# leftmost leaf A (FIL_PAGE_PREV == FIL_NULL) and its right sibling B.
40+
read A B <<< "$(find_leftmost_leaf "$IBD")"
41+
[ "$A" != "NONE" ] && [ "$B" != "4294967295" ] || \
42+
die "need a leftmost leaf with a right sibling (got '$A' '$B')"
43+
vlog "leftmost leaf A=$A, right sibling B=$B"
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: close the A<->B cycle (A.prev=B, B.next=A).
58+
# FIL_PAGE_PREV offset = 8, FIL_PAGE_NEXT offset = 12 (4 bytes each).
59+
#
60+
vlog "=== Negative: make sibling chain a cycle A<->B ==="
61+
cp -r $topdir/backup $topdir/backup_bad
62+
CIBD=$topdir/backup_bad/test/t1.ibd
63+
mach_write_n "$CIBD" "$A" 8 "$B" 4 # A.FIL_PAGE_PREV = B
64+
mach_write_n "$CIBD" "$B" 12 "$A" 4 # B.FIL_PAGE_NEXT = A
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 "sibling_link_cycle: --check-tables HUNG on a cyclic sibling chain"
77+
grep -qiE "Assertion failure|got signal|ut_error|ib::fatal triggered" $topdir/bad.log && \
78+
die "sibling_link_cycle: --check-tables ABORTED"
79+
[ "$SIZE_AFTER" = "$SIZE_BEFORE" ] || \
80+
die "sibling_link_cycle: .ibd grew during --check-tables ($SIZE_BEFORE -> $SIZE_AFTER)"
81+
[ "$RC" -ne 0 ] || die "sibling_link_cycle: --check-tables passed a cyclic sibling chain"
82+
grep -qiE "cycle|too many pages|sibling|Table check failed" $topdir/bad.log || \
83+
die "sibling_link_cycle: corruption not reported"
84+
85+
vlog "sibling_link_cycle passed: cyclic sibling chain reported gracefully (rc=$RC)"

0 commit comments

Comments
 (0)