Details
-
Bug
-
Status: Open (View Workflow)
-
Major
-
Resolution: Unresolved
-
None
-
None
-
Can result in hang or crash
Description
A slave accepts a row event with an invalid column count instead of
rejecting the event as corrupt. When the SQL thread applies that event,
it loops forever without making progress, and replication on that slave
stops for good.
The slave gives no sign that replication has stopped.
SHOW SLAVE STATUS keeps reporting Slave_SQL_Running as Yes
and Last_SQL_Errno as 0, and nothing is written to the error log.
The IO thread keeps receiving the master's binlog, so
Read_Master_Log_Pos advances while Exec_Master_Log_Pos stays at
the bad event. Monitoring that watches the status fields sees a healthy
slave.
Context
A row event (WRITE_ROWS, UPDATE_ROWS or DELETE_ROWS) stores
the number of columns it covers, its width, as a packed integer. A
bitmap with one bit per column follows, marking which columns the row
images carry. A packed integer whose first byte is 0xfb is the
protocol's NULL marker, and net_field_length() returns it as
NULL_LENGTH (~0) rather than as a number. A width is never NULL, so
a 0xfb width only comes from a corrupt event.
The Rows_log_event read constructor in sql/log_event.cc reads
the width into m_width, a uint, and checks that the event has
enough bytes left for the bitmap before building it.
m_width= net_field_length(&ptr_after_width);
|
if (ptr_after_width + (m_width + 7) / 8 > (uchar*)buf + event_len) |
{ m_cols.bitmap= NULL; DBUG_VOID_RETURN; }
|
The constructor serves all three row event types and their compressed
forms. An UPDATE_ROWS event reads a second bitmap of the same width
for its after image, behind the same check.
On apply, Rows_log_event::do_apply_event() walks the row images
from m_curr_row to m_rows_end. Each call to unpack_row()
reads one field for each bit set in the bitmap, then advances
m_curr_row past the bytes it read. The loop ends when
m_curr_row reaches m_rows_end.
A 0xfb width sets m_width to 0xFFFFFFFF. The bounds check
computes (m_width + 7) / 8 in 32-bit arithmetic, which wraps to 0,
so the check finds enough bytes for the bitmap and accepts the event.
my_bitmap_init() then builds a bitmap of 0xFFFFFFFF bits.
bitmap_buffer_size() sizes it in 64-bit arithmetic, so the
allocation is about 512MB, and no bit in the bitmap is set.
On apply, unpack_row() finds no bits set and reads no bytes of the
row image. m_curr_row does not advance, so the loop in
Rows_log_event::do_apply_event() never reaches m_rows_end and
applies the same row image on every pass. With no unique key, each pass
writes a row of default values. With a unique key, the second pass
fails with ER_DUP_ENTRY and the SQL thread stops.
The Table_map_log_event column count had the same vulnerability and
was hardened by MDEV-39689. That hardening did not extend to the row
event width.
Reproduction (only Insert_rows_log_event is shown)
Debug Injection
diff --git a/sql/log_event_server.cc b/sql/log_event_server.cc
|
index 962a4f2be2b..7d678f396da 100644
|
--- a/sql/log_event_server.cc
|
+++ b/sql/log_event_server.cc
|
@@ -5804,6 +5804,8 @@ bool Rows_log_event::write_data_body_metadata(Log_event_writer *writer) |
uchar *bitmap;
|
DBUG_ASSERT(static_cast<size_t>(sbuf_end - sbuf) <= sizeof(sbuf)); |
|
|
+ DBUG_EXECUTE_IF("corrupt_rows_event_null_width", sbuf[0]= 0xfb;); |
+
|
DBUG_DUMP("m_width", sbuf, (size_t) (sbuf_end - sbuf)); |
res= res || write_data(writer, sbuf, (size_t) (sbuf_end - sbuf)); |
From commit
commit c28d46ab431e1cc00e4ebd284f7032315ae062c0 (HEAD -> knielsen_log_event_read_validation, origin/knielsen_log_event_read_validation)
|
Author: Kristian Nielsen <knielsen@knielsen-hq.org>
|
Date: Thu Sep 10 16:08:04 2026 +0200
|
|
|
MDEV-40745: Validate field parsing of optional_metadata
|
.test file
--source include/have_debug.inc
|
--source include/have_innodb.inc
|
--source include/have_binlog_format_row.inc
|
--source include/master-slave.inc
|
|
|
--connection master
|
# No unique key, so the applier re-runs the first row each pass instead |
# of stopping on a duplicate-key error on the second pass. |
CREATE TABLE t1 (a INT, b INT, c INT) ENGINE=InnoDB; |
sync_slave_with_master;
|
|
|
--echo #
|
--echo # Master logs one WRITE_ROWS event whose column-count width byte is
|
--echo # forged to 0xfb, the NULL_LENGTH marker.
|
--echo #
|
--connection master
|
SET @@GLOBAL.debug_dbug= '+d,corrupt_rows_event_null_width'; |
INSERT INTO t1 VALUES (1, 2, 3); |
SET @@GLOBAL.debug_dbug= '-d,corrupt_rows_event_null_width'; |
|
|
--echo #
|
--echo # The slave reads the forged width as 2^32-1, builds a 512MB
|
--echo # all-clear bitmap, and unpack_row() consumes no row bytes, so the
|
--echo # apply loop never advances and re-runs write_row() forever.
|
--echo #
|
--connection slave
|
--let $wait_condition= SELECT COUNT(*)=1 FROM information_schema.PROCESSLIST WHERE COMMAND='Slave_SQL' AND STATE='Write_rows_log_event::write_row()'
|
--source include/wait_condition.inc
|
if (!$success) |
{
|
die The SQL thread did not wedge in write_row(); the forged event was not applied as the bug requires.; |
}
|
|
|
--echo #
|
--echo # Slave positions before next query
|
--echo #
|
--let $read_pos= query_get_value(SHOW SLAVE STATUS, Read_Master_Log_Pos, 1)
|
--let $exec_pos= query_get_value(SHOW SLAVE STATUS, Exec_Master_Log_Pos, 1)
|
--echo # Read_Master_Log_Pos = $read_pos
|
--echo # Exec_Master_Log_Pos = $exec_pos (stays here)
|
|
|
--connection master
|
INSERT INTO t1 VALUES (4, 5, 6); |
sleep 1;
|
|
|
--echo #
|
--echo # The event is read, but never executed.
|
--echo # Exec_Master_Log_Pos is frozen at the forged event.
|
--echo #
|
--connection slave
|
--let $read_pos2= query_get_value(SHOW SLAVE STATUS, Read_Master_Log_Pos, 1)
|
--let $exec_pos2= query_get_value(SHOW SLAVE STATUS, Exec_Master_Log_Pos, 1)
|
--echo # Read_Master_Log_Pos = $read_pos2
|
--echo # Exec_Master_Log_Pos = $exec_pos2 (stays here)
|
|
|
--echo #
|
--echo # But the slave SQL thread is still running without sign of error
|
--echo #
|
--let $running= query_get_value(SHOW SLAVE STATUS, Slave_SQL_Running, 1)
|
--let $errno= query_get_value(SHOW SLAVE STATUS, Last_SQL_Errno, 1)
|
--echo # Slave_SQL_Running = $running
|
--echo # Last_SQL_Errno = $errno |
Reported by fg0x0@wearehackerone.com![]()
Attachments
Issue Links
- relates to
-
MDEV-39689 Slave Overflow on Malformed Table_map_log_event
-
- Closed
-
-
MDEV-41732 Add Missing Binlog Log Event Validation
-
- Closed
-