diff --git a/mysql-test/suite/maria/maria-recovery-corrupt-del-prefix.result b/mysql-test/suite/maria/maria-recovery-corrupt-del-prefix.result new file mode 100644 index 0000000000000..cafe724c171c4 --- /dev/null +++ b/mysql-test/suite/maria/maria-recovery-corrupt-del-prefix.result @@ -0,0 +1,27 @@ +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_del_prefix"; +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 crash on the bmove. +FOUND 1 /Aria engine: Redo phase failed/ in mdev40494.err +NOT FOUND /AddressSanitizer|got signal 11/ in mdev40494.err +drop database mysqltest; diff --git a/mysql-test/suite/maria/maria-recovery-corrupt-del-prefix.test b/mysql-test/suite/maria/maria-recovery-corrupt-del-prefix.test new file mode 100644 index 0000000000000..a7bebfe3a9e88 --- /dev/null +++ b/mysql-test/suite/maria/maria-recovery-corrupt-del-prefix.test @@ -0,0 +1,85 @@ +# MDEV-40494: _ma_apply_redo_index() subtracted KEY_OP_DEL_PREFIX's logged +# length from page_length guarded only by a DBUG_ASSERT, which release builds +# compile out. A length larger than the used page underflows the bmove size to +# about 4G and wraps page_length. + +--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 underflow and +# merge. Only that path logs KEY_OP_DEL_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; + +# Forge the logged length of every KEY_OP_DEL_PREFIX below. org_size is the +# whole used page, which is keypage_header bytes more than the branch allows. +set session debug_dbug="+d,corrupt_del_prefix"; +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 crash on the bmove. +# 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/mdev40494_boot.sql +select 1; +EOF + +--error 1,134 +--exec $MYSQLD_CMD --bootstrap --log-error=$MYSQLTEST_VARDIR/tmp/mdev40494.err < $MYSQLTEST_VARDIR/tmp/mdev40494_boot.sql > $MYSQLTEST_VARDIR/tmp/mdev40494.out 2>&1 + +--let SEARCH_FILE= $MYSQLTEST_VARDIR/tmp/mdev40494.err +--let SEARCH_PATTERN= Aria engine: Redo phase failed +--source include/search_pattern_in_file.inc + +# Unfixed, the bmove runs with a ~4G size and the process dies on it: a +# sanitizer report where there is one, SIGSEGV where there is not. The refusal +# above ends in SIGABRT instead, so this stays NOT FOUND on both. +--let SEARCH_PATTERN= AddressSanitizer|got signal 11 +--source include/search_pattern_in_file.inc + +# every prefix delete 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 + +drop database mysqltest; + +remove_file $MYSQLTEST_VARDIR/tmp/mdev40494.err; +remove_file $MYSQLTEST_VARDIR/tmp/mdev40494.out; +remove_file $MYSQLTEST_VARDIR/tmp/mdev40494_boot.sql; diff --git a/storage/maria/ma_key_recover.c b/storage/maria/ma_key_recover.c index acec592b9227a..c96913090067d 100644 --- a/storage/maria/ma_key_recover.c +++ b/storage/maria/ma_key_recover.c @@ -346,7 +346,9 @@ my_bool _ma_log_prefix(MARIA_PAGE *ma_page, uint changed_length, { /* Delete prefix */ log_pos[0]= KEY_OP_DEL_PREFIX; - int2store(log_pos+1, -move_length); + int2store(log_pos+1, + DBUG_IF("corrupt_del_prefix") ? ma_page->org_size + : (uint) -move_length); log_pos+= 3; if (changed_length) { @@ -1046,7 +1048,12 @@ uint _ma_apply_redo_index(MARIA_HA *info, uint length= uint2korr(header); header+= 2; DBUG_PRINT("redo", ("key_op_del_prefix: %u", length)); - DBUG_ASSERT(length <= page_length - keypage_header); + if (unlikely(keypage_header + length > page_length)) + { + DBUG_ASSERT(!maria_assert_if_crashed_table); + result= mark_crashed= 1; + goto err; + } bmove(buff + keypage_header, buff + keypage_header + length, page_length - keypage_header - length);