From 2958e81867e6c3cd2d9d3ac2f9a5d2f6825dd790 Mon Sep 17 00:00:00 2001 From: KhaledR57 Date: Thu, 20 Aug 2026 23:28:09 +0300 Subject: [PATCH] MDEV-40497 KEY_OP_ADD_PREFIX copies changed_length without checking header_end The KEY_OP_ADD_PREFIX branch of _ma_apply_redo_index() passed changed_length from the redo record to memcpy() without checking it against the end of that record, and bounded the page side with a DBUG_ASSERT that release builds compile out. That assert never bounded changed_length at all: it only related it to insert_length, so a record could satisfy it and still copy past the page. Turn the assert into a runtime check with the four bounds the branch needs. The prefix data must be inside the record, the memcpy must stay inside the page, the bmove_upp destination must stay inside the page, and insert_length must not exceed changed_length. The last one is the relation the assert already tested, without it the gap between the two lengths keeps whatever was on the page, and recovery reports success on an index that is corrupt. The test forges one operand per record with four debug keywords, then crashes the server so recovery has to replay them, and each record trips exactly one of the four bounds. The workload splits and then merges index pages, this record comes from the underflow path, so plain inserts never produce one. --- .../maria-recovery-corrupt-add-prefix.result | 87 ++++++++ .../maria-recovery-corrupt-add-prefix.test | 210 ++++++++++++++++++ storage/maria/ma_key_recover.c | 25 ++- 3 files changed, 318 insertions(+), 4 deletions(-) create mode 100644 mysql-test/suite/maria/maria-recovery-corrupt-add-prefix.result create mode 100644 mysql-test/suite/maria/maria-recovery-corrupt-add-prefix.test diff --git a/mysql-test/suite/maria/maria-recovery-corrupt-add-prefix.result b/mysql-test/suite/maria/maria-recovery-corrupt-add-prefix.result new file mode 100644 index 0000000000000..9983933d4f03c --- /dev/null +++ b/mysql-test/suite/maria/maria-recovery-corrupt-add-prefix.result @@ -0,0 +1,87 @@ +call mtr.add_suppression("File '.*aria_log.000.*' not found"); +drop database if exists mysqltest; +create database mysqltest; +connect admin, localhost, root,,mysqltest,,; +connection default; +use mysqltest; +connection default; +connection admin; +* shut down mysqld, removed logs, restarted it +connection default; +create table t1 (a int, b varchar(200), key(b)) +transactional=1 row_format=page engine=aria; +insert into t1 (a,b) +select seq, concat('k',lpad(seq*7919%100003,6,'0'),repeat('q',150)) +from seq_1_to_2000; +set session debug_dbug="+d,corrupt_add_prefix_changed"; +delete from t1 where a % 4 <> 0; +select count(*) from t1; +count(*) +500 +set session debug_dbug="+d,maria_flush_whole_log,maria_crash"; +set global aria_checkpoint_interval=1; +ERROR HY000: Lost connection to server during query +# Recovery must refuse the record, not apply it and notice later. +FOUND 1 /Aria engine: Redo phase failed/ in mdev40497.err +NOT FOUND /header == header_end|insert_length <= changed_length/ in mdev40497.err +use mysqltest; +# +# Case 2: the logged insert_length pushes the move past the page. +# +create table t2 (a int, b varchar(200), key(b)) +transactional=1 row_format=page engine=aria; +insert into t2 (a,b) +select seq, concat('k',lpad(seq*7919%100003,6,'0'),repeat('q',150)) +from seq_1_to_2000; +set session debug_dbug="+d,corrupt_add_prefix_insert"; +delete from t2 where a % 4 <> 0; +select count(*) from t2; +count(*) +500 +set session debug_dbug="+d,maria_flush_whole_log,maria_crash"; +set global aria_checkpoint_interval=1; +ERROR HY000: Lost connection to server during query +FOUND 1 /Aria engine: Redo phase failed/ in mdev40497b.err +NOT FOUND /header == header_end|insert_length <= changed_length/ in mdev40497b.err +use mysqltest; +# +# Case 3: a record that really carries more prefix data than the page +# can hold. Only the bound on the memcpy destination refuses this one. +# +create table t3 (a int, b varchar(200), key(b)) +transactional=1 row_format=page engine=aria; +insert into t3 (a,b) +select seq, concat('k',lpad(seq*7919%100003,6,'0'),repeat('q',150)) +from seq_1_to_2000; +set session debug_dbug="+d,corrupt_add_prefix_big"; +delete from t3 where a % 4 <> 0; +select count(*) from t3; +count(*) +500 +set session debug_dbug="+d,maria_flush_whole_log,maria_crash"; +set global aria_checkpoint_interval=1; +ERROR HY000: Lost connection to server during query +FOUND 1 /Aria engine: Redo phase failed/ in mdev40497c.err +NOT FOUND /header == header_end|insert_length <= changed_length/ in mdev40497c.err +use mysqltest; +# +# Case 4: insert_length larger than changed_length. Every bound above is +# satisfied, but the record cannot be applied: it would leave the gap +# between the two lengths holding whatever was on the page. +# +create table t4 (a int, b varchar(200), key(b)) +transactional=1 row_format=page engine=aria; +insert into t4 (a,b) +select seq, concat('k',lpad(seq*7919%100003,6,'0'),repeat('q',150)) +from seq_1_to_2000; +set session debug_dbug="+d,corrupt_add_prefix_gap"; +delete from t4 where a % 4 <> 0; +select count(*) from t4; +count(*) +500 +set session debug_dbug="+d,maria_flush_whole_log,maria_crash"; +set global aria_checkpoint_interval=1; +ERROR HY000: Lost connection to server during query +FOUND 1 /Aria engine: Redo phase failed/ in mdev40497d.err +NOT FOUND /header == header_end|insert_length <= changed_length/ in mdev40497d.err +drop database mysqltest; diff --git a/mysql-test/suite/maria/maria-recovery-corrupt-add-prefix.test b/mysql-test/suite/maria/maria-recovery-corrupt-add-prefix.test new file mode 100644 index 0000000000000..913df6d20977c --- /dev/null +++ b/mysql-test/suite/maria/maria-recovery-corrupt-add-prefix.test @@ -0,0 +1,210 @@ +# MDEV-40497: _ma_apply_redo_index() copied KEY_OP_ADD_PREFIX's logged +# changed_length without checking it against the end of the record, and bounded +# the page side with a DBUG_ASSERT that release builds compile out and that +# never bounded changed_length at all. + +--source include/not_embedded.inc +--source include/have_debug.inc +--source include/have_maria.inc +--source include/have_sequence.inc +# the server is killed on purpose below +--source include/not_valgrind.inc + +call mtr.add_suppression("File '.*aria_log.000.*' not found"); + +let $MYSQLD_DATADIR= `select @@datadir`; +let $MARIA_LOG=.; + +--disable_warnings +drop database if exists mysqltest; +--enable_warnings +create database mysqltest; + +# the log is shared with earlier tests, and recovery below replays all of it +connect (admin, localhost, root,,mysqltest,,); +--enable_reconnect +connection default; +use mysqltest; +--enable_reconnect +--source include/maria_empty_logs.inc + +# Long keys so the index splits, then a wide delete so pages merge. Only that +# path logs KEY_OP_ADD_PREFIX; plain inserts never do. +create table t1 (a int, b varchar(200), key(b)) +transactional=1 row_format=page engine=aria; +insert into t1 (a,b) +select seq, concat('k',lpad(seq*7919%100003,6,'0'),repeat('q',150)) +from seq_1_to_2000; + +# Case 1: the record claims 100 bytes more prefix data than it carries, so the +# memcpy reads past the end of the record. The page bound is untouched: the +# copy still lands inside the page, and both old asserts stay true. +set session debug_dbug="+d,corrupt_add_prefix_changed"; +delete from t1 where a % 4 <> 0; +select count(*) from t1; + +# Kill without flushing pages, so recovery has to replay the forged records +--write_line wait $MYSQLTEST_VARDIR/tmp/mysqld.1.expect +set session debug_dbug="+d,maria_flush_whole_log,maria_crash"; +--error 2013 +set global aria_checkpoint_interval=1; + +--echo # Recovery must refuse the record, not apply it and notice later. +# Failed recovery aborts the server, so --bootstrap drives it and exits by +# itself. A failed redo phase leaves pages pinned, so a debug build then trips +# the pagecache cleanup assert on the way out; the log below is the assertion, +# not the exit code. +--write_file $MYSQLTEST_VARDIR/tmp/mdev40497_boot.sql +select 1; +EOF + +--error 1,134 +--exec $MYSQLD_CMD --bootstrap --log-error=$MYSQLTEST_VARDIR/tmp/mdev40497.err < $MYSQLTEST_VARDIR/tmp/mdev40497_boot.sql > $MYSQLTEST_VARDIR/tmp/mdev40497.out 2>&1 + +--let SEARCH_FILE= $MYSQLTEST_VARDIR/tmp/mdev40497.err +--let SEARCH_PATTERN= Aria engine: Redo phase failed +--source include/search_pattern_in_file.inc + +# Unfixed, the copy runs and the record is only noticed by the debug-only +# assert at the end of the loop, or by the branch's own assert in case 2. +--let SEARCH_PATTERN= header == header_end|insert_length <= changed_length +--source include/search_pattern_in_file.inc + +# every prefix insert in this log is forged, it can never be replayed +--error 0,1 +remove_files_wildcard $MYSQLD_DATADIR aria_log.0*; + +--write_line restart $MYSQLTEST_VARDIR/tmp/mysqld.1.expect +--enable_reconnect +--source include/wait_until_connected_again.inc + +use mysqltest; + +--echo # +--echo # Case 2: the logged insert_length pushes the move past the page. +--echo # + +create table t2 (a int, b varchar(200), key(b)) +transactional=1 row_format=page engine=aria; +insert into t2 (a,b) +select seq, concat('k',lpad(seq*7919%100003,6,'0'),repeat('q',150)) +from seq_1_to_2000; + +# The record stays length-consistent here, so the record bound passes and the +# bound on the bmove_upp destination is what refuses it. +set session debug_dbug="+d,corrupt_add_prefix_insert"; +delete from t2 where a % 4 <> 0; +select count(*) from t2; + +--write_line wait $MYSQLTEST_VARDIR/tmp/mysqld.1.expect +set session debug_dbug="+d,maria_flush_whole_log,maria_crash"; +--error 2013 +set global aria_checkpoint_interval=1; + +--error 1,134 +--exec $MYSQLD_CMD --bootstrap --log-error=$MYSQLTEST_VARDIR/tmp/mdev40497b.err < $MYSQLTEST_VARDIR/tmp/mdev40497_boot.sql > $MYSQLTEST_VARDIR/tmp/mdev40497b.out 2>&1 + +--let SEARCH_FILE= $MYSQLTEST_VARDIR/tmp/mdev40497b.err +--let SEARCH_PATTERN= Aria engine: Redo phase failed +--source include/search_pattern_in_file.inc + +--let SEARCH_PATTERN= header == header_end|insert_length <= changed_length +--source include/search_pattern_in_file.inc + +--error 0,1 +remove_files_wildcard $MYSQLD_DATADIR aria_log.0*; + +--write_line restart $MYSQLTEST_VARDIR/tmp/mysqld.1.expect +--enable_reconnect +--source include/wait_until_connected_again.inc + +use mysqltest; + +--echo # +--echo # Case 3: a record that really carries more prefix data than the page +--echo # can hold. Only the bound on the memcpy destination refuses this one. +--echo # + +create table t3 (a int, b varchar(200), key(b)) +transactional=1 row_format=page engine=aria; +insert into t3 (a,b) +select seq, concat('k',lpad(seq*7919%100003,6,'0'),repeat('q',150)) +from seq_1_to_2000; + +set session debug_dbug="+d,corrupt_add_prefix_big"; +delete from t3 where a % 4 <> 0; +select count(*) from t3; + +--write_line wait $MYSQLTEST_VARDIR/tmp/mysqld.1.expect +set session debug_dbug="+d,maria_flush_whole_log,maria_crash"; +--error 2013 +set global aria_checkpoint_interval=1; + +--error 1,134 +--exec $MYSQLD_CMD --bootstrap --log-error=$MYSQLTEST_VARDIR/tmp/mdev40497c.err < $MYSQLTEST_VARDIR/tmp/mdev40497_boot.sql > $MYSQLTEST_VARDIR/tmp/mdev40497c.out 2>&1 + +--let SEARCH_FILE= $MYSQLTEST_VARDIR/tmp/mdev40497c.err +--let SEARCH_PATTERN= Aria engine: Redo phase failed +--source include/search_pattern_in_file.inc + +--let SEARCH_PATTERN= header == header_end|insert_length <= changed_length +--source include/search_pattern_in_file.inc + +--error 0,1 +remove_files_wildcard $MYSQLD_DATADIR aria_log.0*; + +--write_line restart $MYSQLTEST_VARDIR/tmp/mysqld.1.expect +--enable_reconnect +--source include/wait_until_connected_again.inc + +use mysqltest; + +--echo # +--echo # Case 4: insert_length larger than changed_length. Every bound above is +--echo # satisfied, but the record cannot be applied: it would leave the gap +--echo # between the two lengths holding whatever was on the page. +--echo # + +create table t4 (a int, b varchar(200), key(b)) +transactional=1 row_format=page engine=aria; +insert into t4 (a,b) +select seq, concat('k',lpad(seq*7919%100003,6,'0'),repeat('q',150)) +from seq_1_to_2000; + +set session debug_dbug="+d,corrupt_add_prefix_gap"; +delete from t4 where a % 4 <> 0; +select count(*) from t4; + +--write_line wait $MYSQLTEST_VARDIR/tmp/mysqld.1.expect +set session debug_dbug="+d,maria_flush_whole_log,maria_crash"; +--error 2013 +set global aria_checkpoint_interval=1; + +--error 1,134 +--exec $MYSQLD_CMD --bootstrap --log-error=$MYSQLTEST_VARDIR/tmp/mdev40497d.err < $MYSQLTEST_VARDIR/tmp/mdev40497_boot.sql > $MYSQLTEST_VARDIR/tmp/mdev40497d.out 2>&1 + +--let SEARCH_FILE= $MYSQLTEST_VARDIR/tmp/mdev40497d.err +--let SEARCH_PATTERN= Aria engine: Redo phase failed +--source include/search_pattern_in_file.inc + +--let SEARCH_PATTERN= header == header_end|insert_length <= changed_length +--source include/search_pattern_in_file.inc + +--error 0,1 +remove_files_wildcard $MYSQLD_DATADIR aria_log.0*; + +--write_line restart $MYSQLTEST_VARDIR/tmp/mysqld.1.expect +--enable_reconnect +--source include/wait_until_connected_again.inc + +drop database mysqltest; + +remove_file $MYSQLTEST_VARDIR/tmp/mdev40497d.err; +remove_file $MYSQLTEST_VARDIR/tmp/mdev40497d.out; +remove_file $MYSQLTEST_VARDIR/tmp/mdev40497.err; +remove_file $MYSQLTEST_VARDIR/tmp/mdev40497.out; +remove_file $MYSQLTEST_VARDIR/tmp/mdev40497b.err; +remove_file $MYSQLTEST_VARDIR/tmp/mdev40497b.out; +remove_file $MYSQLTEST_VARDIR/tmp/mdev40497c.err; +remove_file $MYSQLTEST_VARDIR/tmp/mdev40497c.out; +remove_file $MYSQLTEST_VARDIR/tmp/mdev40497_boot.sql; diff --git a/storage/maria/ma_key_recover.c b/storage/maria/ma_key_recover.c index acec592b9227a..5b0f66ab5e169 100644 --- a/storage/maria/ma_key_recover.c +++ b/storage/maria/ma_key_recover.c @@ -363,9 +363,19 @@ my_bool _ma_log_prefix(MARIA_PAGE *ma_page, uint changed_length, { /* Add prefix */ DBUG_ASSERT(changed_length >0 && (int) changed_length >= move_length); + /* Each keyword forges one operand, so each record trips one check */ + if (DBUG_IF("corrupt_add_prefix_big")) + changed_length= info->s->block_size - info->s->keypage_header; + if (DBUG_IF("corrupt_add_prefix_insert")) + changed_length= info->s->max_index_block_size - info->s->keypage_header; log_pos[0]= KEY_OP_ADD_PREFIX; - int2store(log_pos+1, move_length); - int2store(log_pos+3, changed_length); + int2store(log_pos+1, + DBUG_IF("corrupt_add_prefix_insert") ? changed_length : + DBUG_IF("corrupt_add_prefix_gap") ? changed_length + 100 : + (uint) move_length); + int2store(log_pos+3, + DBUG_IF("corrupt_add_prefix_changed") ? changed_length + 100 + : changed_length); log_pos+= 5; } @@ -1031,8 +1041,15 @@ uint _ma_apply_redo_index(MARIA_HA *info, DBUG_PRINT("redo", ("key_op_add_prefix: %u %u", insert_length, changed_length)); - DBUG_ASSERT(insert_length <= changed_length && - page_length + insert_length <= max_page_size); + if (unlikely((size_t) (header_end - header) < (size_t) changed_length + 4 || + keypage_header + changed_length > max_page_size || + page_length + insert_length > max_page_size || + insert_length > changed_length)) + { + DBUG_ASSERT(!maria_assert_if_crashed_table); + result= mark_crashed= 1; + goto err; + } bmove_upp(buff + page_length + insert_length, buff + page_length, page_length - keypage_header);