Details
-
Bug
-
Status: Closed (View Workflow)
-
Blocker
-
Resolution: Fixed
-
None
-
Can result in hang or crash
-
Fix crash in Rotate event when followed by a Format Description event describing no Rotate event support
-
Q3/2026 Replic. Development
Description
A master that installs a Format_description event whose post_header_len
array is shorter than four entries, and then sends a Rotate, makes the
slave index that array out of bounds. That value becomes the Rotate
event's post-header length, which decides both the position the slave
records and the offset it copies the new binlog name from. Either way
replication stops. If the value exceeds the event length the constructor
abandons the event, is_valid() fails, and the IO thread stops with a
relay log write failure. If it is small the slave adopts a name taken
from the wrong offset, which is the low byte of the event's position
field followed by a NUL, so the next reconnect asks the master for a
binlog that does not exist and fails with error 1236.
Log_event::read_log_event() has the check that would prevent it, and states
why (sql/log_event.cc:1098):
if (event_type > fdle->number_of_event_types && |
event_type != FORMAT_DESCRIPTION_EVENT)
|
/* It is unsafe to use the fdle if its post_header_len |
array does not include the event type. */ |
Grepping number_of_event_types across sql/ shows that is the only
validation of an incoming event type against the installed descriptor. And
queue_event() bypasses it for ROTATE_EVENT by constructing directly
(sql/slave.cc:6608), while Rotate_log_event's constructor indexes
unconditionally (sql/log_event.cc:2588):
uint8 post_header_len= description_event->post_header_len[ROTATE_EVENT-1];
|
header_is_valid() requires no minimum on number_of_event_types, and
my_malloc bumps a zero-length request to one byte and returns non-NULL, so
post_header_len != NULL holds and such a descriptor is installed.
Of the event types queue_event() builds without the general path,
ROTATE_EVENT is the only one that indexes post_header_len.
HEARTBEAT_LOG_EVENT and GTID_EVENT use common_header_len, and
Gtid_log_event::peek() bounds event_len itself.
FORMAT_DESCRIPTION_EVENT and GTID_LIST_EVENT go through
Log_event::read_log_event() and so are already covered.
To reproduce:
Reaching this end to end takes two master-side steps, because the short
descriptor has to be installed before the Rotate arrives.
The descriptor is truncated in the sender, after the sender has parsed its own
copy, reusing the fix_checksum() pattern that
send_format_descriptor_event() already uses for its log_pos and
created patches. 77 bytes is the size to aim for: number_of_event_types
is event_len - 76, and one more byte is then taken off it for the checksum
algorithm descriptor, so the array is memdup'd at one byte and index
ROTATE_EVENT-1 is three bytes past it. That single byte is also what
get_checksum_alg() reads on the way in and what becomes the descriptor's own
checksum_alg, so setting it to CRC32 keeps checksum handling correct on both
sides and leaves the length as the only anomaly.
Patch file:
--- a/sql/sql_repl.cc
|
+++ b/sql/sql_repl.cc
|
@@ static int send_format_descriptor_event(...) |
+ DBUG_EXECUTE_IF("binlog_sender_short_post_header_len", |
+ {
|
+ char *ev= (char*) packet->ptr() + ev_offset; |
+ ulong short_len= LOG_EVENT_MINIMAL_HEADER_LEN +
|
+ ST_COMMON_HEADER_LEN_OFFSET + 2 + BINLOG_CHECKSUM_LEN;
|
+ ev[LOG_EVENT_MINIMAL_HEADER_LEN + ST_COMMON_HEADER_LEN_OFFSET + 1]=
|
+ (char) BINLOG_CHECKSUM_ALG_CRC32; |
+ packet->length(ev_offset + short_len);
|
+ int4store(ev + EVENT_LEN_OFFSET, short_len);
|
+ fix_checksum(info->current_checksum_alg, packet, ev_offset);
|
+ });
|
+
|
/* send it */ |
Test file:
--source include/have_debug.inc
|
--source include/have_binlog_format_row.inc
|
--source include/master-slave.inc
|
|
|
--connection master
|
create table t1 (a int);
|
insert into t1 values (1);
|
--source include/save_master_gtid.inc
|
--connection slave
|
--source include/sync_with_master_gtid.inc
|
|
|
--connection master
|
set @@global.debug_dbug= "+d,binlog_sender_short_post_header_len";
|
|
|
# Reconnect so the source resends its descriptor. The slave is caught up, so
|
# the Rotate below is the first event to arrive after it. queue_event() builds
|
# that Rotate directly and reads post_header_len[3] from a one byte array.
|
--connection slave
|
--source include/stop_slave.inc
|
--source include/start_slave.inc
|
|
|
--connection master
|
FLUSH LOGS;
|
|
|
--connection slave
|
--source include/sync_with_master_gtid.inc
|
The slave has to be caught up before the injection, so that the Rotate is the
first event after the descriptor. With number_of_event_types at 0 every
other event type is above it and read_log_event() rejects it, which is also
what happens to the events following the Rotate.
Attachments
Issue Links
- relates to
-
MDEV-40365 OOB read for common_header_len & post_header_len on malformed Format_description_log_event
-
- Closed
-
-
MDEV-40648 Replication Undefined Behavior on Malformed Rotate Log Event
-
- Closed
-