Skip to content

Commit 682495e

Browse files
PXB-3328: Add --check-tables option to validate InnoDB indexes during prepare
JIRA: https://perconadev.atlassian.net/browse/PXB-3328 After xtrabackup --prepare completes redo apply, there is no built-in way to verify that the B-tree indexes are structurally correct. Users must restore and run CHECK TABLE on a live server to find corruption. This patch adds --check-tables to xtrabackup --prepare which validates every committed InnoDB index after recovery. The option is only valid during the prepare phase; it is rejected with --backup, --copy-back, --move-back, and other non-prepare modes. It can be combined with --export to validate indexes and produce export artifacts in a single prepare invocation. It works with both --apply-log-only and final prepare since validation is strictly read-only (SX-latch, no writes to pages or dynamic metadata). The implementation has five parts: 1. Dictionary loading: For each tablespace (file-per-table and general), load dict_table_t objects via dict_load_from_spaces_sdi(), similar to what --export already does. 2. Parallel B-tree validation: Spawn --parallel worker threads that call btr_validate_index() on every committed index. All corruptions are reported (no fail-fast). Per-index and per-table names are printed. 3. Graceful assertion handling: InnoDB's btr_validate_level() has 11 ut_a() assertions that crash on corrupted pages. Under #ifdef XTRABACKUP these are converted to soft errors that log a message and return false. Corrupted sibling links (e.g. all-zero pages with FIL_PAGE_NEXT=0) are handled by stopping traversal to prevent infinite loops. 4. LOB corruption detection (PS-9683): Port of Percona Server fix to detect external LOB pages shared between two records, a corruption scenario found in production. Uses blob_ref_map to track LOB first pages through the validation call chain. 5. Test coverage: Two test scripts covering real-world corruption scenarios (PAGE_INDEX_ID corruption, checksum corruption, all-zero pages, multiple corrupted tables), invalid option combinations (--backup --check-tables, --copy-back --check-tables), --export combined with --check-tables, and debug-only injection tests (broken sibling links, wrong index ID, wrong min-record flag, LOB duplicates).
1 parent 5b2218e commit 682495e

9 files changed

Lines changed: 1052 additions & 25 deletions

File tree

storage/innobase/btr/btr0btr.cc

Lines changed: 178 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4096,7 +4096,8 @@ static void btr_validate_report2(
40964096
@return true if ok */
40974097
static bool btr_validate_level(
40984098
dict_index_t *index, const trx_t *trx, ulint level,
4099-
bool lockout IF_DEBUG(, Index_details &index_details)) {
4099+
bool lockout IF_DEBUG(, Index_details &index_details),
4100+
blob_ref_map *blob_map = nullptr) {
41004101
buf_block_t *block;
41014102
page_t *page;
41024103
buf_block_t *right_block = nullptr; /* remove warning */
@@ -4169,13 +4170,40 @@ static bool btr_validate_level(
41694170
ret = false;
41704171
}
41714172

4173+
#ifdef XTRABACKUP
4174+
/* In xtrabackup --check-tables we report corruption instead of crashing.
4175+
InnoDB uses ut_a here because space ID mismatch or wrong page level
4176+
during B-tree descent is unrecoverable for a running server. In
4177+
xtrabackup we only need a damage report. */
4178+
if (index->space != block->page.id.space() ||
4179+
index->space != page_get_space_id(page)) {
4180+
btr_validate_report1(index, level, block);
4181+
ib::error() << "B-tree corruption: space ID mismatch during descent."
4182+
<< " Expected " << index->space << ", got block space "
4183+
<< block->page.id.space() << ", page space "
4184+
<< page_get_space_id(page) << " in index " << index->name();
4185+
mtr_commit(&mtr);
4186+
mem_heap_free(heap);
4187+
return false;
4188+
}
4189+
if (page_is_leaf(page)) {
4190+
btr_validate_report1(index, level, block);
4191+
ib::error() << "B-tree corruption: unexpected leaf page "
4192+
<< page_get_page_no(page) << " during descent to level "
4193+
<< level << " in index " << index->name();
4194+
mtr_commit(&mtr);
4195+
mem_heap_free(heap);
4196+
return false;
4197+
}
4198+
#else
41724199
ut_a(index->space == block->page.id.space());
41734200
ut_a(index->space == page_get_space_id(page));
4201+
ut_a(!page_is_leaf(page));
4202+
#endif /* XTRABACKUP */
41744203
#ifdef UNIV_ZIP_DEBUG
41754204
page_zip = buf_block_get_page_zip(block);
41764205
ut_a(!page_zip || page_zip_validate(page_zip, page, index));
41774206
#endif /* UNIV_ZIP_DEBUG */
4178-
ut_a(!page_is_leaf(page));
41794207

41804208
page_cur_set_before_first(block, &cursor);
41814209
page_cur_move_to_next(&cursor);
@@ -4222,6 +4250,7 @@ static bool btr_validate_level(
42224250
}
42234251

42244252
do {
4253+
ulint cur_page_no;
42254254
mem_heap_empty(heap);
42264255
offsets = offsets2 = nullptr;
42274256
if (!srv_read_only_mode) {
@@ -4237,7 +4266,32 @@ static bool btr_validate_level(
42374266
ut_a(!page_zip || page_zip_validate(page_zip, page, index));
42384267
#endif /* UNIV_ZIP_DEBUG */
42394268

4269+
#ifdef XTRABACKUP
4270+
/* In xtrabackup --check-tables we report corruption instead of crashing.
4271+
Space ID mismatch in the page scan means the page does not belong to
4272+
this index's tablespace -- severe corruption. Stop traversing this
4273+
index because sibling links on a corrupted page are untrustworthy. */
4274+
if (block->page.id.space() != index->space) {
4275+
btr_validate_report1(index, level, block);
4276+
ib::error() << "B-tree corruption: page space ID "
4277+
<< block->page.id.space() << " != index space "
4278+
<< index->space << " on page " << block->page.id.page_no()
4279+
<< " in index " << index->name();
4280+
ret = false;
4281+
right_page_no = FIL_NULL;
4282+
goto node_ptr_fails;
4283+
}
4284+
#else
42404285
ut_a(block->page.id.space() == index->space);
4286+
#endif /* XTRABACKUP */
4287+
4288+
DBUG_EXECUTE_IF("check_table_wrong_index_id", {
4289+
thread_local bool injected = false;
4290+
if (!injected && btr_page_get_index_id(page) == index->id) {
4291+
mach_write_to_8(page + PAGE_HEADER + PAGE_INDEX_ID, index->id + 1);
4292+
injected = true;
4293+
}
4294+
});
42414295

42424296
if (fseg_page_is_free(seg, block->page.id.space(),
42434297
block->page.id.page_no())) {
@@ -4252,7 +4306,7 @@ static bool btr_validate_level(
42524306

42534307
ret = false;
42544308

4255-
} else if (!page_validate(page, index)) {
4309+
} else if (!page_validate(page, index, true, blob_map)) {
42564310
btr_validate_report1(index, level, block);
42574311
ret = false;
42584312

@@ -4263,17 +4317,53 @@ static bool btr_validate_level(
42634317
ret = false;
42644318
}
42654319

4320+
#ifdef XTRABACKUP
4321+
/* In xtrabackup --check-tables we report corruption instead of crashing.
4322+
Page level mismatch means the page has wrong metadata (e.g. all-zero
4323+
page from unflushed redo). Stop traversal -- sibling links are
4324+
untrustworthy on a corrupted page. */
4325+
if (btr_page_get_level(page) != level) {
4326+
btr_validate_report1(index, level, block);
4327+
ib::error() << "B-tree corruption: page level "
4328+
<< btr_page_get_level(page) << " != expected level " << level
4329+
<< " on page " << page_get_page_no(page) << " in index "
4330+
<< index->name();
4331+
ret = false;
4332+
right_page_no = FIL_NULL;
4333+
goto node_ptr_fails;
4334+
}
4335+
#else
42664336
ut_a(btr_page_get_level(page) == level);
4337+
#endif /* XTRABACKUP */
42674338
right_page_no = btr_page_get_next(page, &mtr);
42684339
left_page_no = btr_page_get_prev(page, &mtr);
4269-
const ulint cur_page_no = page_get_page_no(page);
4340+
cur_page_no = page_get_page_no(page);
42704341

42714342
#ifdef UNIV_DEBUG
42724343
index_details.add_page(cur_page_no, level);
42734344
#endif /* UNIV_DEBUG */
42744345

4346+
#ifdef XTRABACKUP
4347+
/* In xtrabackup --check-tables we report corruption instead of crashing.
4348+
An empty non-root page indicates corruption (e.g. all-zero page from
4349+
unflushed redo, or truncated tablespace). Stop traversal -- an
4350+
all-zero page has FIL_PAGE_NEXT=0 which would loop through non-B-tree
4351+
pages indefinitely. */
4352+
if (page_is_empty(page) &&
4353+
!(level == 0 && page_get_page_no(page) == dict_index_get_page(index))) {
4354+
btr_validate_report1(index, level, block);
4355+
ib::error() << "B-tree corruption: page " << page_get_page_no(page)
4356+
<< " is empty but is not the root page"
4357+
<< " in index " << index->name()
4358+
<< ". Possible all-zero (unflushed) page.";
4359+
ret = false;
4360+
right_page_no = FIL_NULL;
4361+
goto node_ptr_fails;
4362+
}
4363+
#else
42754364
ut_a(!page_is_empty(page) ||
42764365
(level == 0 && page_get_page_no(page) == dict_index_get_page(index)));
4366+
#endif /* XTRABACKUP */
42774367

42784368
if (right_page_no != FIL_NULL) {
42794369
const rec_t *right_rec;
@@ -4285,6 +4375,14 @@ static bool btr_validate_level(
42854375

42864376
right_page = buf_block_get_frame(right_block);
42874377

4378+
DBUG_EXECUTE_IF("check_table_break_sibling_link", {
4379+
thread_local bool injected = false;
4380+
if (!injected && btr_page_get_prev(right_page, &mtr) != 0) {
4381+
mach_write_to_4(right_page + FIL_PAGE_PREV, 0);
4382+
injected = true;
4383+
}
4384+
});
4385+
42884386
const auto fil_page_prev = btr_page_get_prev(right_page, &mtr);
42894387
if (fil_page_prev != cur_page_no) {
42904388
btr_validate_report2(index, level, block, right_block);
@@ -4295,9 +4393,15 @@ static bool btr_validate_level(
42954393

42964394
ret = false;
42974395
#ifdef UNIV_DEBUG
4298-
/* In debug build, it is best to fail with an assert. */
4299-
const bool siblings_link_correct = false;
4300-
ut_ad(siblings_link_correct);
4396+
{
4397+
bool skip_assert = false;
4398+
DBUG_EXECUTE_IF("check_table_break_sibling_link",
4399+
skip_assert = true;);
4400+
if (!skip_assert) {
4401+
const bool siblings_link_correct = false;
4402+
ut_ad(siblings_link_correct);
4403+
}
4404+
}
43014405
#endif /* UNIV_DEBUG */
43024406
}
43034407

@@ -4342,9 +4446,25 @@ static bool btr_validate_level(
43424446
}
43434447

43444448
if (level > 0 && left_page_no == FIL_NULL) {
4449+
#ifdef XTRABACKUP
4450+
/* In xtrabackup --check-tables we report corruption instead of
4451+
crashing. Missing min-record flag on the leftmost non-leaf page
4452+
indicates B-tree structural corruption. */
4453+
if (!(REC_INFO_MIN_REC_FLAG &
4454+
rec_get_info_bits(page_rec_get_next(page_get_infimum_rec(page)),
4455+
page_is_comp(page)))) {
4456+
btr_validate_report1(index, level, block);
4457+
ib::error() << "B-tree corruption: min-record flag missing on"
4458+
<< " leftmost page " << page_get_page_no(page)
4459+
<< " at level " << level << " in index " << index->name();
4460+
ret = false;
4461+
goto node_ptr_fails;
4462+
}
4463+
#else
43454464
ut_a(REC_INFO_MIN_REC_FLAG &
43464465
rec_get_info_bits(page_rec_get_next(page_get_infimum_rec(page)),
43474466
page_is_comp(page)));
4467+
#endif /* XTRABACKUP */
43484468
}
43494469

43504470
/* Similarly skip the father node check for spatial index for now,
@@ -4425,15 +4545,61 @@ static bool btr_validate_level(
44254545
}
44264546

44274547
if (left_page_no == FIL_NULL) {
4548+
#ifdef XTRABACKUP
4549+
/* In xtrabackup --check-tables we report corruption instead of
4550+
crashing. Wrong father pointer for leftmost child or unexpected
4551+
prev link on father page indicates structural corruption. */
4552+
if (node_ptr != page_rec_get_next(page_get_infimum_rec(father_page))) {
4553+
btr_validate_report1(index, level, block);
4554+
ib::error() << "B-tree corruption: leftmost child page "
4555+
<< page_get_page_no(page)
4556+
<< " has wrong father node pointer"
4557+
<< " in index " << index->name();
4558+
ret = false;
4559+
goto node_ptr_fails;
4560+
}
4561+
if (btr_page_get_prev(father_page, &mtr) != FIL_NULL) {
4562+
btr_validate_report1(index, level, block);
4563+
ib::error() << "B-tree corruption: father page of leftmost child"
4564+
<< " has unexpected prev link"
4565+
<< " in index " << index->name();
4566+
ret = false;
4567+
goto node_ptr_fails;
4568+
}
4569+
#else
44284570
ut_a(node_ptr == page_rec_get_next(page_get_infimum_rec(father_page)));
44294571
ut_a(btr_page_get_prev(father_page, &mtr) == FIL_NULL);
4572+
#endif /* XTRABACKUP */
44304573
}
44314574

44324575
if (right_page_no == FIL_NULL) {
44334576
const rec_t *expected_node_ptr =
44344577
page_rec_get_prev(page_get_supremum_rec(father_page));
4578+
#ifdef XTRABACKUP
4579+
/* In xtrabackup --check-tables we report corruption instead of
4580+
crashing. Wrong father pointer for rightmost child or unexpected
4581+
next link on father page indicates structural corruption. */
4582+
if (node_ptr != expected_node_ptr) {
4583+
btr_validate_report1(index, level, block);
4584+
ib::error() << "B-tree corruption: rightmost child page "
4585+
<< page_get_page_no(page)
4586+
<< " has wrong father node pointer"
4587+
<< " in index " << index->name();
4588+
ret = false;
4589+
goto node_ptr_fails;
4590+
}
4591+
if (btr_page_get_next(father_page, &mtr) != FIL_NULL) {
4592+
btr_validate_report1(index, level, block);
4593+
ib::error() << "B-tree corruption: father page of rightmost child"
4594+
<< " has unexpected next link"
4595+
<< " in index " << index->name();
4596+
ret = false;
4597+
goto node_ptr_fails;
4598+
}
4599+
#else
44354600
ut_a(node_ptr == expected_node_ptr);
44364601
ut_a(btr_page_get_next(father_page, &mtr) == FIL_NULL);
4602+
#endif /* XTRABACKUP */
44374603
} else {
44384604
const rec_t *right_node_ptr;
44394605

@@ -4584,9 +4750,10 @@ static bool btr_validate_spatial_index(
45844750
/** Checks the consistency of an index tree.
45854751
@return true if ok */
45864752
bool btr_validate_index(
4587-
dict_index_t *index, /*!< in: index */
4588-
const trx_t *trx, /*!< in: transaction or NULL */
4589-
bool lockout) /*!< in: true if X-latch index is intended */
4753+
dict_index_t *index, /*!< in: index */
4754+
const trx_t *trx, /*!< in: transaction or NULL */
4755+
bool lockout, /*!< in: true if X-latch index is intended */
4756+
blob_ref_map *blob_map) /*!< in: optional LOB reference map */
45904757
{
45914758
#ifdef UNIV_DEBUG
45924759
Index_details index_details;
@@ -4650,7 +4817,7 @@ bool btr_validate_index(
46504817

46514818
for (ulint i = 0; i <= n; ++i) {
46524819
if (!btr_validate_level(index, trx, n - i,
4653-
lockout IF_DEBUG(, index_details))) {
4820+
lockout IF_DEBUG(, index_details), blob_map)) {
46544821
ok = false;
46554822
break;
46564823
}

storage/innobase/fsp/fsp0fsp.cc

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3561,20 +3561,39 @@ bool fseg_page_is_free(fseg_header_t *seg_header, /*!< in: segment header */
35613561

35623562
fil_space_t *space = fil_space_get(space_id);
35633563

3564+
#ifdef XTRABACKUP
3565+
if (space == nullptr) {
3566+
ib::error() << "Cannot load tablespace for space_id: " << space_id
3567+
<< " page_no: " << page;
3568+
return true;
3569+
}
3570+
#endif /* XTRABACKUP */
3571+
35643572
mtr_start(&mtr);
35653573

35663574
mtr_x_lock_space(space, &mtr);
35673575

35683576
const page_size_t page_size(space->flags);
35693577

3570-
seg_inode = fseg_inode_get(seg_header, space_id, page_size, &mtr);
3578+
if (seg_header != nullptr) {
3579+
seg_inode = fseg_inode_get(seg_header, space_id, page_size, &mtr);
35713580

3572-
ut_a(seg_inode);
3573-
ut_ad(mach_read_from_4(seg_inode + FSEG_MAGIC_N) == FSEG_MAGIC_N_VALUE);
3574-
ut_ad(!((page_offset(seg_inode) - FSEG_ARR_OFFSET) % FSEG_INODE_SIZE));
3581+
ut_a(seg_inode);
3582+
ut_ad(mach_read_from_4(seg_inode + FSEG_MAGIC_N) == FSEG_MAGIC_N_VALUE);
3583+
ut_ad(!((page_offset(seg_inode) - FSEG_ARR_OFFSET) % FSEG_INODE_SIZE));
3584+
}
35753585

35763586
descr = xdes_get_descriptor(space_id, page, page_size, &mtr);
3587+
#ifdef XTRABACKUP
3588+
if (descr == nullptr) {
3589+
ib::error() << "Cannot get extent descriptor for space_id: " << space_id
3590+
<< " page_no: " << page;
3591+
mtr_commit(&mtr);
3592+
return true;
3593+
}
3594+
#else
35773595
ut_a(descr);
3596+
#endif /* XTRABACKUP */
35783597

35793598
auto is_free =
35803599
xdes_mtr_get_bit(descr, XDES_FREE_BIT, page % FSP_EXTENT_SIZE, &mtr);

storage/innobase/include/btr0btr.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ this program; if not, write to the Free Software Foundation, Inc.,
4141
#include "gis0type.h"
4242
#include "mtr0mtr.h"
4343
#include "page0cur.h"
44+
#include "page0page.h"
4445
#include "univ.i"
4546

4647
/** Maximum record size which can be stored on a page, without using the
@@ -571,7 +572,8 @@ the index.
571572
[[nodiscard]] bool btr_validate_index(
572573
dict_index_t *index, /*!< in: index */
573574
const trx_t *trx, /*!< in: transaction or 0 */
574-
bool lockout); /*!< in: true if X-latch index is intended */
575+
bool lockout, /*!< in: true if X-latch index is intended */
576+
blob_ref_map *blob_map = nullptr); /*!< in: optional LOB reference map */
575577

576578
/** Creates SDI index and stores the root page numbers in page 1 & 2
577579
@param[in] space_id tablespace id

0 commit comments

Comments
 (0)