diff --git a/mysql-test/suite/maria/maria-recovery-corrupt-change.result b/mysql-test/suite/maria/maria-recovery-corrupt-change.result new file mode 100644 index 0000000000000..2ce6e8e1baf12 --- /dev/null +++ b/mysql-test/suite/maria/maria-recovery-corrupt-change.result @@ -0,0 +1,38 @@ +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(60), key(a), key(b)) +transactional=1 row_format=page engine=aria; +set session debug_dbug="+d,corrupt_change_length"; +select count(*) from t1; +count(*) +200 +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 mdev40498.err +NOT FOUND /Table file is corrupted/ in mdev40498.err +use mysqltest; +# +# The write side: a forged page offset pushes the copy past the page. +# +create table t2 (a int, b varchar(60), key(a), key(b)) +transactional=1 row_format=page engine=aria; +set session debug_dbug="+d,corrupt_change_offset"; +select count(*) from t2; +count(*) +200 +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 mdev40498b.err +NOT FOUND /Table file is corrupted/ in mdev40498b.err +drop database mysqltest; diff --git a/mysql-test/suite/maria/maria-recovery-corrupt-change.test b/mysql-test/suite/maria/maria-recovery-corrupt-change.test new file mode 100644 index 0000000000000..6e45b4fe117de --- /dev/null +++ b/mysql-test/suite/maria/maria-recovery-corrupt-change.test @@ -0,0 +1,129 @@ +# MDEV-40498: _ma_apply_redo_index() copied KEY_OP_CHANGE's logged length +# without checking it against the end of the record, and bounded the write with +# a DBUG_ASSERT, which release builds compile out. + +--source include/not_embedded.inc +--source include/have_debug.inc +--source include/have_maria.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 + +create table t1 (a int, b varchar(60), key(a), key(b)) +transactional=1 row_format=page engine=aria; + +# Forge the logged length of every KEY_OP_CHANGE below. The forged value is the +# largest the page bound accepts, so the read bound is what refuses the record. +set session debug_dbug="+d,corrupt_change_length"; + +# Scattered keys so inserts land mid-page. +--disable_query_log +let $i= 40; +while ($i) +{ + eval insert into t1 (a,b) values ($i*7919%10007,concat('v',$i*7)),($i*104729%10007,concat('w',$i*3)),($i*7919%9973,concat('x',$i*11)),($i*104729%9973,concat('y',$i*13)),($i*7919%9967,concat('z',$i*17)); + dec $i; +} +--enable_query_log +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 itself +--write_file $MYSQLTEST_VARDIR/tmp/mdev40498_boot.sql +select 1; +EOF + +--error 1 +--exec $MYSQLD_CMD --bootstrap --log-error=$MYSQLTEST_VARDIR/tmp/mdev40498.err < $MYSQLTEST_VARDIR/tmp/mdev40498_boot.sql > $MYSQLTEST_VARDIR/tmp/mdev40498.out 2>&1 + +--let SEARCH_FILE= $MYSQLTEST_VARDIR/tmp/mdev40498.err +--let SEARCH_PATTERN= Aria engine: Redo phase failed +--source include/search_pattern_in_file.inc + +# Unfixed, the record is applied and the damage only shows up later. +# Refusing it up front keeps this NOT FOUND. +--let SEARCH_PATTERN= Table file is corrupted +--source include/search_pattern_in_file.inc + +# every record 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 # The write side: a forged page offset pushes the copy past the page. +--echo # + +create table t2 (a int, b varchar(60), key(a), key(b)) +transactional=1 row_format=page engine=aria; + +# The record stays length-consistent here, so the page bound is what refuses it. +set session debug_dbug="+d,corrupt_change_offset"; + +--disable_query_log +let $i= 40; +while ($i) +{ + eval insert into t2 (a,b) values ($i*7919%10007,concat('v',$i*7)),($i*104729%10007,concat('w',$i*3)),($i*7919%9973,concat('x',$i*11)),($i*104729%9973,concat('y',$i*13)),($i*7919%9967,concat('z',$i*17)); + dec $i; +} +--enable_query_log +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 +--exec $MYSQLD_CMD --bootstrap --log-error=$MYSQLTEST_VARDIR/tmp/mdev40498b.err < $MYSQLTEST_VARDIR/tmp/mdev40498_boot.sql > $MYSQLTEST_VARDIR/tmp/mdev40498b.out 2>&1 + +--let SEARCH_FILE= $MYSQLTEST_VARDIR/tmp/mdev40498b.err +--let SEARCH_PATTERN= Aria engine: Redo phase failed +--source include/search_pattern_in_file.inc + +--let SEARCH_PATTERN= Table file is corrupted +--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/mdev40498.err; +remove_file $MYSQLTEST_VARDIR/tmp/mdev40498.out; +remove_file $MYSQLTEST_VARDIR/tmp/mdev40498b.err; +remove_file $MYSQLTEST_VARDIR/tmp/mdev40498b.out; +remove_file $MYSQLTEST_VARDIR/tmp/mdev40498_boot.sql; diff --git a/storage/maria/ma_key_recover.c b/storage/maria/ma_key_recover.c index acec592b9227a..babb00f1b7bc6 100644 --- a/storage/maria/ma_key_recover.c +++ b/storage/maria/ma_key_recover.c @@ -567,7 +567,9 @@ my_bool _ma_log_add(MARIA_PAGE *ma_page, else { log_pos[0]= KEY_OP_OFFSET; - int2store(log_pos+1, offset); + int2store(log_pos+1, + (DBUG_IF("corrupt_change_offset") && + (int) changed_length > move_length) ? current_size : offset); log_pos+= 3; if (move_length) { @@ -600,7 +602,9 @@ my_bool _ma_log_add(MARIA_PAGE *ma_page, } log_pos[0]= KEY_OP_CHANGE; } - int2store(log_pos+1, changed_length); + int2store(log_pos+1, (log_pos[0] == KEY_OP_CHANGE && + DBUG_IF("corrupt_change_length")) ? + current_size - offset : changed_length); log_pos+= 3; log_array[TRANSLOG_INTERNAL_PARTS + 0].str= log_data; @@ -1017,7 +1021,15 @@ uint _ma_apply_redo_index(MARIA_HA *info, { uint length= uint2korr(header); DBUG_PRINT("redo", ("key_op_change: %u", length)); - DBUG_ASSERT(page_offset != 0 && page_offset + length <= page_length); + + /* The data has to be inside the record and fit the used page */ + if (unlikely((size_t) (header_end - header) < (size_t) length + 2 || + page_offset == 0 || page_offset + length > page_length)) + { + DBUG_ASSERT(!maria_assert_if_crashed_table); + result= mark_crashed= 1; + goto err; + } memcpy(buff + page_offset, header + 2 , length); page_offset+= length; /* Put offset after changed length */