Skip to content

Commit 9979f60

Browse files
PXB-3807 : Verify system and undo tablespace checksums during --check-tables
https://perconadev.atlassian.net/browse/PXB-3807 Problem: -------- --check-tables only walks fsp_is_ibd_tablespace() B-trees, so the system tablespace (ibdata*, which also holds the legacy rollback segments) and the undo tablespaces -- rollback segments, change buffer, undo logs, allocation/ free pages -- received no integrity check at all. Fix: ---- Add xb_checksum_system_undo_spaces(), a single-threaded, engine-native pass run at the top of the check-tables phase over space 0 (TRX_SYS_SPACE) and every undo::spaces entry. Each page is read with fil_io() into a private aligned buffer (ut_unique_ptr) -- NOT into the buffer pool -- which maps the page to the right ibdata file and decrypts/decompresses it, while deliberately bypassing buf_page_io_complete()'s abort-on-corruption policy. Each page is then run through BlockReporter::is_corrupted() (the same checksum core innochecksum uses) and corruption is reported gracefully (non-zero exit), not aborted. Legacy doublewrite pages in the system tablespace are skipped. The buffer pool is flushed first (buf_flush_sync_all_buf_pools()) so the raw on-disk image read here equals the recovered state -- post-recovery undo/ trx-sys/purge init can leave system pages dirty, which would otherwise read as a stale/torn page and report a false positive. Flushing does not advance the redo checkpoint, so it is safe under --apply-log-only. Tests: ------ suites/check_tables/system_undo_checksum.sh corrupts a cold page of an undo tablespace and of ibdata1 and asserts --check-tables reports a checksum mismatch and exits non-zero, with a clean-run control; system_undo_checksum_ debug.sh uses the check_system_inject_corruption DBUG point plus a clean control. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent cdad211 commit 9979f60

3 files changed

Lines changed: 282 additions & 0 deletions

File tree

storage/innobase/xtrabackup/src/xtrabackup.cc

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ Place, Suite 330, Boston, MA 02111-1307 USA
102102

103103
#include "backup_copy.h"
104104
#include "backup_mysql.h"
105+
#include "buf0flu.h"
105106
#include "changed_page_tracking.h"
106107
#include "crc_glue.h"
107108
#include "ddl_tracker.h"
@@ -114,6 +115,7 @@ Place, Suite 330, Boston, MA 02111-1307 USA
114115
#include "read_filt.h"
115116
#include "redo_log.h"
116117
#include "space_map.h"
118+
#include "trx0purge.h"
117119
#include "utils.h"
118120
#include "write_filt.h"
119121
#include "wsrep.h"
@@ -6794,6 +6796,89 @@ Threads share a datafiles_iter and each grabs the next tablespace to check.
67946796
On corruption, sets *ctxt->error to true but does NOT stop -- all threads
67956797
run to completion so the operator gets a full damage report.
67966798
@param[in] ctxt shared context used by all threads */
6799+
/* PXB-3807: verify the per-page checksums of the system tablespace (ibdata*,
6800+
which also holds the legacy rollback segments) and every undo tablespace during
6801+
--check-tables. The B-tree validation below (check_tables_thread_func) only
6802+
visits fsp_is_ibd_tablespace() spaces, so without this pass the system and undo
6803+
pages -- rollback segments, change buffer, undo logs, allocation/free pages --
6804+
get no integrity check at all.
6805+
6806+
Single-threaded and engine-native: each page is read with fil_io(), which maps
6807+
the page number to the right ibdata file and decrypts/decompresses it from the
6808+
space's encryption metadata, into a private buffer -- NOT into the buffer pool.
6809+
Reading outside the buffer pool deliberately avoids buf_page_io_complete(),
6810+
whose corrupt-page policy is to abort (or, under srv_force_recovery, silently
6811+
ignore the corruption); instead we run each page through
6812+
BlockReporter::is_corrupted() ourselves (the same checksum core innochecksum
6813+
uses) and report gracefully. The caller flushes the buffer pool first
6814+
(buf_flush_sync_all_buf_pools) so the on-disk image read here equals the
6815+
recovered state.
6816+
6817+
@return true if every page passed, false if any page is corrupt or unreadable.
6818+
*/
6819+
static bool xb_checksum_system_undo_spaces() {
6820+
/* Targets are the system tablespace (ibdata*, space 0) and every undo
6821+
tablespace. Collect their ids directly -- space 0 plus the global undo list
6822+
-- instead of scanning every fil space. This runs at the top level of
6823+
--check-tables, where no other thread registers or drops undo spaces, so
6824+
reading undo::spaces without taking its latch is safe. */
6825+
std::vector<space_id_t> space_ids;
6826+
space_ids.push_back(TRX_SYS_SPACE);
6827+
for (size_t i = 0; i < undo::spaces->size(); ++i) {
6828+
space_ids.push_back(undo::spaces->at(i)->id());
6829+
}
6830+
6831+
/* One aligned page buffer, reused for every read (O_DIRECT needs alignment).
6832+
ut_unique_ptr frees it automatically when this function returns. */
6833+
ut_unique_ptr buf_holder = ut_make_unique_ptr_nokey(2 * UNIV_PAGE_SIZE);
6834+
byte *buf = static_cast<byte *>(ut_align(buf_holder.get(), UNIV_PAGE_SIZE));
6835+
6836+
bool ok = true;
6837+
6838+
for (const space_id_t space_id : space_ids) {
6839+
const page_size_t page_size(fil_space_get_flags(space_id));
6840+
const page_no_t n_pages = fil_space_get_size(space_id);
6841+
const bool is_system = fsp_is_system_tablespace(space_id);
6842+
6843+
xb::info() << "check-tables: verifying checksums of tablespace " << space_id
6844+
<< " (" << n_pages << " pages).";
6845+
6846+
for (page_no_t page_no = 0; page_no < n_pages; ++page_no) {
6847+
/* Legacy doublewrite-buffer pages in the system tablespace carry
6848+
non-standard checksums; skip them (as innochecksum does). */
6849+
if (is_system && page_no >= FSP_EXTENT_SIZE &&
6850+
page_no < FSP_EXTENT_SIZE * 3) {
6851+
continue;
6852+
}
6853+
6854+
const page_id_t page_id(space_id, page_no);
6855+
const dberr_t err =
6856+
fil_io(IORequest(IORequest::READ), true, page_id, page_size, 0,
6857+
page_size.physical(), buf, nullptr);
6858+
if (err != DB_SUCCESS) {
6859+
xb::error() << "check-tables: cannot read page " << space_id << ":"
6860+
<< page_no
6861+
<< " for checksum verification: " << ut_strerr(err) << ".";
6862+
ok = false;
6863+
continue;
6864+
}
6865+
6866+
BlockReporter reporter(false /* check_lsn */, buf, page_size,
6867+
fsp_is_checksum_disabled(space_id));
6868+
bool corrupt = reporter.is_corrupted();
6869+
DBUG_EXECUTE_IF("check_system_inject_corruption",
6870+
corrupt = (page_no > 0););
6871+
if (corrupt) {
6872+
xb::error() << "check-tables: checksum mismatch on page " << space_id
6873+
<< ":" << page_no << "; the tablespace is corrupted.";
6874+
ok = false;
6875+
}
6876+
}
6877+
}
6878+
6879+
return ok;
6880+
}
6881+
67976882
static void check_tables_thread_func(data_thread_ctxt_t *ctxt) {
67986883
fil_node_t *node;
67996884

@@ -7229,11 +7314,26 @@ static void xtrabackup_prepare_func(int argc, char **argv) {
72297314
uint ct_count;
72307315
ib_mutex_t ct_count_mutex;
72317316

7317+
/* PXB-3807: redo recovery has finished, but some system/undo pages may
7318+
still be dirty in the buffer pool (post-recovery undo/trx-sys/purge init
7319+
re-dirty them). Flush once so the on-disk image read by the checksum scan
7320+
below equals the recovered state -- otherwise a raw read could see a stale
7321+
or torn page and report a false positive. Flushing pages does not advance
7322+
the redo checkpoint, so this is safe under --apply-log-only too. */
7323+
buf_flush_sync_all_buf_pools();
7324+
72327325
/* --check-tables only reads; redo has already been applied. Forbid the
72337326
Fil_shard::do_io() out-of-bounds auto-extend (PXB-1819) for the duration
72347327
of validation so a corrupt page pointer cannot grow a backup file. */
72357328
fil_check_tables_no_extend.store(true);
72367329

7330+
/* PXB-3807: verify the per-page checksums of the system tablespace
7331+
(ibdata*) and all undo tablespaces, which the B-tree validation below
7332+
skips entirely. */
7333+
if (!xb_checksum_system_undo_spaces()) {
7334+
check_failed = true;
7335+
}
7336+
72377337
it = datafiles_iter_new(nullptr);
72387338
if (it == NULL) {
72397339
xb::error() << "datafiles_iter_new() failed.";
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
############################################################################
2+
# PXB-3807 : --check-tables verifies the per-page checksums of the system
3+
# tablespace (ibdata*) and all undo tablespaces. These spaces are skipped by
4+
# the B-tree validation, so without this pass their pages get no integrity
5+
# check at all.
6+
#
7+
# Real on-disk corruption (no debug build required): corrupt a COLD page -- one
8+
# InnoDB does not read during startup -- near the end of an undo tablespace and
9+
# of ibdata1, leaving the stored checksum stale. --prepare --check-tables must
10+
# detect the mismatch, report it, and exit non-zero, WITHOUT extending the file
11+
# or hanging. A clean-run control proves there are no false positives (the
12+
# buffer-pool flush before the scan makes the on-disk image authoritative).
13+
#
14+
# Cold pages are chosen near the high end of each file (free/unused pages that
15+
# recovery does not touch) so the corruption is caught by the read-only
16+
# checksum scan rather than by a crashing buffer-pool read at startup.
17+
############################################################################
18+
19+
. inc/common.sh
20+
21+
PAGE_SZ=16384
22+
23+
# corrupt_last_page <file> -- flip a body byte of the file's last page, leaving
24+
# the stored checksum stale. A free (all-zero) last page becomes a non-empty
25+
# page with a zero checksum field, which is a checksum mismatch.
26+
corrupt_last_page() {
27+
local F="$1"
28+
local SZ LASTPAGE
29+
SZ=$(stat -c %s "$F")
30+
LASTPAGE=$(( SZ / PAGE_SZ - 1 ))
31+
vlog "corrupting $F : last page $LASTPAGE at byte offset 200"
32+
mach_write_8 "$F" "$LASTPAGE" 200 0xDEADBEEFDEADBEEF
33+
}
34+
35+
start_server
36+
37+
vlog "Create a table and generate undo (rollback) so undo pages are allocated"
38+
mysql test <<EOF
39+
CREATE TABLE t1 (a INT PRIMARY KEY AUTO_INCREMENT, b VARCHAR(200));
40+
EOF
41+
for i in $(seq 1 200); do
42+
run_cmd $MYSQL $MYSQL_ARGS test -e \
43+
"INSERT INTO t1 (b) VALUES (REPEAT('x', 200));"
44+
done
45+
run_cmd $MYSQL $MYSQL_ARGS test -e \
46+
"BEGIN; UPDATE t1 SET b = REPEAT('y', 200); ROLLBACK;"
47+
48+
vlog "Take backup"
49+
xtrabackup --backup --target-dir=$topdir/backup
50+
51+
#
52+
# Control: a healthy backup must pass, and the new checksum pass must run.
53+
#
54+
vlog "=== Control: clean system/undo checksum pass ==="
55+
cp -r $topdir/backup $topdir/backup_ok
56+
xtrabackup --prepare --apply-log-only --target-dir=$topdir/backup_ok
57+
xtrabackup --prepare --check-tables --target-dir=$topdir/backup_ok 2>&1 \
58+
| tee $topdir/ok.log
59+
grep -q "verifying checksums of tablespace" $topdir/ok.log || \
60+
die "Control: system/undo checksum pass did not run"
61+
grep -q "All table checks passed" $topdir/ok.log || \
62+
die "Control: clean backup unexpectedly failed --check-tables"
63+
vlog "Control passed"
64+
65+
#
66+
# Negative (undo): corrupt a cold page of an undo tablespace.
67+
#
68+
vlog "=== Negative: corrupt undo tablespace ==="
69+
cp -r $topdir/backup $topdir/backup_undo
70+
xtrabackup --prepare --apply-log-only --target-dir=$topdir/backup_undo
71+
UNDO=$(ls $topdir/backup_undo/undo_* 2>/dev/null | head -1)
72+
[ -n "$UNDO" ] || die "could not find an undo tablespace in the backup"
73+
corrupt_last_page "$UNDO"
74+
75+
run_cmd_expect_failure $XB_BIN $XB_ARGS --prepare --check-tables \
76+
--target-dir=$topdir/backup_undo 2>&1 | tee $topdir/undo.log
77+
grep -q "checksum mismatch" $topdir/undo.log || \
78+
die "Negative(undo): checksum mismatch not reported"
79+
grep -q "Table check failed" $topdir/undo.log || \
80+
die "Negative(undo): Table check failed message not found"
81+
grep -qi "posix_fallocate" $topdir/undo.log && \
82+
die "Negative(undo): validation attempted to extend a file"
83+
vlog "Negative(undo) passed"
84+
85+
#
86+
# Negative (ibdata): corrupt a cold page of the system tablespace.
87+
#
88+
vlog "=== Negative: corrupt system tablespace (ibdata1) ==="
89+
cp -r $topdir/backup $topdir/backup_sys
90+
xtrabackup --prepare --apply-log-only --target-dir=$topdir/backup_sys
91+
corrupt_last_page "$topdir/backup_sys/ibdata1"
92+
93+
run_cmd_expect_failure $XB_BIN $XB_ARGS --prepare --check-tables \
94+
--target-dir=$topdir/backup_sys 2>&1 | tee $topdir/sys.log
95+
grep -q "checksum mismatch" $topdir/sys.log || \
96+
die "Negative(ibdata): checksum mismatch not reported"
97+
grep -q "Table check failed" $topdir/sys.log || \
98+
die "Negative(ibdata): Table check failed message not found"
99+
vlog "Negative(ibdata) passed"
100+
101+
#
102+
# Negative (ibdata startup-range / "hot" page): page 5 of the system tablespace
103+
# is TRX_SYS, read by InnoDB during startup recovery -- BEFORE the checksum
104+
# scan. Corrupting it is therefore caught by the normal buffer-pool read
105+
# during recovery (which aborts after retries with "Unable to read page"),
106+
# not by our graceful scan. Either way --prepare must fail (non-zero); this
107+
# documents that startup-range corruption is surfaced, just via a different
108+
# path than the cold-page scan above.
109+
#
110+
vlog "=== Negative: corrupt ibdata1 startup-range page (TRX_SYS, page 5) ==="
111+
cp -r $topdir/backup $topdir/backup_hot
112+
# Do NOT --apply-log-only first: we want recovery to read the corrupt page.
113+
mach_write_8 "$topdir/backup_hot/ibdata1" 5 200 0xDEADBEEFDEADBEEF
114+
run_cmd_expect_failure $XB_BIN $XB_ARGS --prepare --check-tables \
115+
--target-dir=$topdir/backup_hot 2>&1 | tee $topdir/hot.log
116+
grep -Eqi "Unable to read page|page corruption|checksum mismatch|is corrupted|corrupt" \
117+
$topdir/hot.log || \
118+
die "Negative(ibdata hot): startup-range corruption not surfaced"
119+
vlog "Negative(ibdata hot) passed"
120+
121+
vlog "All PXB-3807 sub-tests passed"
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
############################################################################
2+
# PXB-3807 : --check-tables verifies the per-page checksums of the system
3+
# tablespace (ibdata*) and all undo tablespaces, which the B-tree validation
4+
# skips entirely.
5+
#
6+
# This debug test uses a DBUG injection (check_system_inject_corruption) for a
7+
# deterministic positive: with the injection the checksum pass flags pages and
8+
# --prepare --check-tables must fail. It also includes a clean-run control to
9+
# prove the pass does not false-positive on a healthy backup (the buffer-pool
10+
# flush before the scan makes the on-disk image authoritative).
11+
############################################################################
12+
13+
. inc/common.sh
14+
15+
require_debug_pxb_version
16+
17+
start_server
18+
19+
vlog "Create a table and generate some undo (rollback) so undo pages are used"
20+
mysql test <<EOF
21+
CREATE TABLE t1 (a INT PRIMARY KEY AUTO_INCREMENT, b VARCHAR(200));
22+
EOF
23+
for i in $(seq 1 100); do
24+
run_cmd $MYSQL $MYSQL_ARGS test -e \
25+
"INSERT INTO t1 (b) VALUES (REPEAT('x', 200));"
26+
done
27+
run_cmd $MYSQL $MYSQL_ARGS test -e \
28+
"BEGIN; UPDATE t1 SET b = REPEAT('y', 200); ROLLBACK;"
29+
30+
vlog "Take backup"
31+
xtrabackup --backup --target-dir=$topdir/backup
32+
33+
#
34+
# Control: a healthy backup must pass, and the new checksum pass must run.
35+
#
36+
vlog "=== Control: clean system/undo checksum pass ==="
37+
cp -r $topdir/backup $topdir/backup_ok
38+
xtrabackup --prepare --check-tables --target-dir=$topdir/backup_ok 2>&1 \
39+
| tee $topdir/ok.log
40+
grep -q "verifying checksums of tablespace" $topdir/ok.log || \
41+
die "Control: system/undo checksum pass did not run"
42+
grep -q "All table checks passed" $topdir/ok.log || \
43+
die "Control: clean backup unexpectedly failed --check-tables"
44+
vlog "Control passed"
45+
46+
#
47+
# Injected corruption: the checksum pass must report and fail the prepare.
48+
#
49+
vlog "=== Injected corruption: check_system_inject_corruption ==="
50+
cp -r $topdir/backup $topdir/backup_inj
51+
run_cmd_expect_failure $XB_BIN $XB_ARGS --prepare --check-tables \
52+
--debug=d,check_system_inject_corruption \
53+
--target-dir=$topdir/backup_inj 2>&1 | tee $topdir/inj.log
54+
55+
grep -q "checksum mismatch" $topdir/inj.log || \
56+
die "Injected: checksum mismatch not reported"
57+
grep -q "Table check failed" $topdir/inj.log || \
58+
die "Injected: Table check failed message not found"
59+
vlog "Injected corruption sub-test passed"
60+
61+
vlog "All PXB-3807 debug sub-tests passed"

0 commit comments

Comments
 (0)