Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions mysql-test/suite/maria/maria-recovery-corrupt-change.result
Original file line number Diff line number Diff line change
@@ -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;
129 changes: 129 additions & 0 deletions mysql-test/suite/maria/maria-recovery-corrupt-change.test
Original file line number Diff line number Diff line change
@@ -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;
18 changes: 15 additions & 3 deletions storage/maria/ma_key_recover.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 */
Expand Down