From 6cf2bc22a431a275cde8e70dae43168c007fcb2f Mon Sep 17 00:00:00 2001 From: Prathamesh Hukkeri Date: Sat, 15 Aug 2026 12:09:46 +0530 Subject: [PATCH] MDEV-40759: Fix --force-read crash on checksum-failing Format Description event With --force-read, a Format Description event whose checksum is invalid is returned by Log_event::read_log_event() as an Unknown_log_event instead of NULL. check_header()'s prelude blindly down-cast such an event to Format_description_log_event, which is undefined behaviour (a vptr error under UBSan) and, in --base64-output modes other than AUTO, also silently swallowed the event. Report the checksum-failing event through process_event() in both output modes and continue with the default Format Description event. In Log_event::read_log_event(), only assert 'no result on error' when --force-read is not in effect, and free the discarded best-effort Unknown_log_event so it is not leaked. Add a binlog suite test that simulates the checksum failure with --debug=d,simulate_checksum_test_failure. --- client/mysqlbinlog.cc | 34 ++++++++++++++----- .../r/binlog_mysqlbinlog_force_read.result | 25 ++++++++++++++ .../t/binlog_mysqlbinlog_force_read.test | 29 ++++++++++++++++ sql/log_event.cc | 12 ++++++- 4 files changed, 90 insertions(+), 10 deletions(-) create mode 100644 mysql-test/suite/binlog/r/binlog_mysqlbinlog_force_read.result create mode 100644 mysql-test/suite/binlog/t/binlog_mysqlbinlog_force_read.test diff --git a/client/mysqlbinlog.cc b/client/mysqlbinlog.cc index 9ba461c1d9b5e..0729cc2bdc77c 100644 --- a/client/mysqlbinlog.cc +++ b/client/mysqlbinlog.cc @@ -3187,12 +3187,11 @@ static Exit_status check_header(IO_CACHE* file, else if (buf[EVENT_TYPE_OFFSET] == FORMAT_DESCRIPTION_EVENT) { /* This is 5.0 */ - Format_description_log_event *new_description_event; + Log_event *new_description_event; my_b_seek(file, tmp_pos); /* seek back to event's start */ - if (!(new_description_event= (Format_description_log_event*) - Log_event::read_log_event(file, &read_error, - glob_description_event, - opt_verify_binlog_checksum))) + if (!(new_description_event= Log_event::read_log_event(file, &read_error, + glob_description_event, + opt_verify_binlog_checksum))) /* EOF can't be hit here normally, so it's a real error */ { error("Could not read a Format_description_log_event event at " @@ -3200,6 +3199,24 @@ static Exit_status check_header(IO_CACHE* file, (ulonglong)tmp_pos); return ERROR_STOP; } + if (new_description_event->get_type_code() != FORMAT_DESCRIPTION_EVENT) + { + /* + With --force-read, an event whose checksum is invalid is + returned as an Unknown_log_event instead of NULL. Such an event + does not describe the log's format, so report it the way the main + loop would and continue with the default description event below. + */ + DBUG_ASSERT(new_description_event->get_type_code() == UNKNOWN_EVENT); + Exit_status retval= process_event(print_event_info, + new_description_event, tmp_pos, + logname); + if (retval != OK_CONTINUE) + return retval; + break; + } + Format_description_log_event *new_fde= + static_cast(new_description_event); if (opt_base64_output_mode == BASE64_OUTPUT_AUTO) { /* @@ -3207,10 +3224,9 @@ static Exit_status check_header(IO_CACHE* file, the new one, so we should not do it ourselves in this case. */ - DBUG_ASSERT(tmp_pos + new_description_event->data_written == + DBUG_ASSERT(tmp_pos + new_fde->data_written == my_b_tell(file)); - Exit_status retval= process_event(print_event_info, - new_description_event, tmp_pos, + Exit_status retval= process_event(print_event_info, new_fde, tmp_pos, logname); if (retval != OK_CONTINUE) return retval; @@ -3218,7 +3234,7 @@ static Exit_status check_header(IO_CACHE* file, else { delete glob_description_event; - glob_description_event= new_description_event; + glob_description_event= new_fde; } DBUG_PRINT("info",("Setting description_event")); } diff --git a/mysql-test/suite/binlog/r/binlog_mysqlbinlog_force_read.result b/mysql-test/suite/binlog/r/binlog_mysqlbinlog_force_read.result new file mode 100644 index 0000000000000..6a506e99d4f35 --- /dev/null +++ b/mysql-test/suite/binlog/r/binlog_mysqlbinlog_force_read.result @@ -0,0 +1,25 @@ +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (1); +/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=1*/; +/*!40019 SET @@session.max_delayed_threads=0*/; +/*!50003 SET @OLD_COMPLETION_TYPE=@@COMPLETION_TYPE,COMPLETION_TYPE=0*/; +DELIMITER /*!*/; +# at 4 +# Encrypted event +DELIMITER ; +# End of log file +ROLLBACK /* added by mysqlbinlog */; +/*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/; +/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=0*/; +/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=1*/; +/*!40019 SET @@session.max_delayed_threads=0*/; +/*!50003 SET @OLD_COMPLETION_TYPE=@@COMPLETION_TYPE,COMPLETION_TYPE=0*/; +DELIMITER /*!*/; +# at 4 +# Encrypted event +DELIMITER ; +# End of log file +ROLLBACK /* added by mysqlbinlog */; +/*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/; +/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=0*/; +DROP TABLE t1; diff --git a/mysql-test/suite/binlog/t/binlog_mysqlbinlog_force_read.test b/mysql-test/suite/binlog/t/binlog_mysqlbinlog_force_read.test new file mode 100644 index 0000000000000..6fb706cedcf43 --- /dev/null +++ b/mysql-test/suite/binlog/t/binlog_mysqlbinlog_force_read.test @@ -0,0 +1,29 @@ +# MDEV-40759: mariadb-binlog --force-read --start-position crashes on +# Unknown event +# +# With --force-read, a Format Description event whose checksum is invalid is +# returned as an Unknown_log_event instead of NULL (Log_event::read_log_event). +# check_header()'s prelude wrongly treated such an event as a Format +# Description event, which is undefined behaviour (a vptr error under UBSan) +# and, in --base64-output modes other than AUTO, silently swallowed the event. +# +# The checksum failure is simulated with --debug=d,simulate_checksum_test_failure. +--source include/mysqlbinlog_have_debug.inc +--source include/have_binlog_format_mixed.inc + +# Grow the current binlog past the Format Description event so that +# check_header()'s --start-position prelude has to read it. +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (1); + +--let $MYSQL_DATADIR= `SELECT @@datadir` +--let $binlog_file= query_get_value(SHOW BINLOG STATUS, File, 1) +--let $binlog_start= query_get_value(SHOW BINLOG STATUS, Position, 1) + +# Must not crash, and must report the checksum-failing Format Description event. +--exec $MYSQL_BINLOG --verify-binlog-checksum --force-read --debug=d,simulate_checksum_test_failure --start-position=$binlog_start $MYSQL_DATADIR/$binlog_file + +# Must not silently drop the event when not in AUTO mode either. +--exec $MYSQL_BINLOG --verify-binlog-checksum --force-read --base64-output=NEVER --debug=d,simulate_checksum_test_failure --start-position=$binlog_start $MYSQL_DATADIR/$binlog_file + +DROP TABLE t1; diff --git a/sql/log_event.cc b/sql/log_event.cc index 8e6de277a080e..b5ecccb9ee4af 100644 --- a/sql/log_event.cc +++ b/sql/log_event.cc @@ -930,11 +930,21 @@ Log_event* Log_event::read_log_event(IO_CACHE* file, int *out_error, err: if (unlikely(error)) { - DBUG_ASSERT(!res); #ifdef MYSQL_CLIENT + /* + read_log_event(char*,...) may return a best-effort Unknown_log_event + together with an error string when reading with --force-read. Such an + event cannot be used (it carries no format information), so free it and + report a generic unknown event instead. The "no result on error" + assertion below only applies to the non-force-read case. + */ if (force_opt) + { + delete res; DBUG_RETURN(new Unknown_log_event()); + } #endif + DBUG_ASSERT(!res); /* The SQL slave thread will check *out_error to know