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);