Details
-
Bug
-
Status: In Progress (View Workflow)
-
Blocker
-
Resolution: Unresolved
-
None
-
Can result in hang or crash
-
Q3/2026 Replic. Development
Description
A Table_map event that declares more columns than its field metadata covers
makes the applier read past the metadata buffer. Three consequences follow.
- The walk runs up to 1,908,874,270 bytes past the end of the block, so the
replica takes a SIGSEGV. - The bytes read become the source's declared column types, and
show_sql_type() renders them into ER_SLAVE_CONVERSION_FAILED
(sql/rpl_utility_server.cc:1000), so replica heap bytes surface as column
lengths in Last_Error and in the error log. - When the decoded types happen to look compatible with the replica's own,
table_def::calc_field_size() uses them to decide how many row-image bytes
to consume. The replica then reads further out of bounds while unpacking the
Rows_log_event and applies shifted values to its own tables, rather than
failing.
table_def::table_def() walks m_size columns and indexes
field_metadata[index] unconditionally (sql/rpl_utility.cc:203 to
:269). The loop is guarded only by if (m_size && metadata_size);
metadata_size is never compared against index, and there is not even
the assert(index <= metadata_size) that MySQL's pre-fix code carried.
Seven column types advance index by two bytes (SET, ENUM, STRING, BIT,
VARCHAR, VARCHAR_COMPRESSED, NEWDECIMAL) and eleven by one, so the walk can
demand up to 2 * m_colcnt bytes.
The event reader enforces only an upper bound
(sql/log_event.cc:3769):
if (m_field_metadata_size <= (m_colcnt * 2)) /* no lower bound */ |
then allocates exactly m_field_metadata_size bytes and copies exactly
that many. A Table_map declaring many MYSQL_TYPE_STRING columns while
encoding m_field_metadata_size of 1 satisfies that check, satisfies the
event-length checks MDEV-39689 added, and satisfies
if (m_size && metadata_size). is_valid() is only
m_memory != NULL, so the event is accepted and
Table_map_log_event::do_apply_event() (sql/log_event_server.cc:6624)
builds the table_def.
With slave_max_allowed_packet at its 1 GB default, the byte budget allows
954,437,139 columns, so the loop reads 1,908,874,278 bytes. int index
peaks just below INT_MAX, so no signed overflow intervenes.
Two further paths reach the same loop. mysqlbinlog v or -flashback
on a crafted file, through Table_map_log_event::create_table_def(), which
needs no server at all and is the most accessible. And BINLOG, gated by
BINLOG_REPLAY_ACL or SUPER_ACL.
Note that MDEV-39689 does not cover this. That commit touches
sql/log_event.cc and sql/log_event_server.cc only, and every bound it
added asks whether a declared length fits inside the event. None asks whether
m_field_metadata_size is large enough for the metadata the
m_coltype bytes demand, and the attack makes it smaller, not larger.
sql/rpl_utility.cc has never carried a bound. All maintained branches are
affected.
To reproduce:
Patch file:
--- a/sql/log_event_server.cc
|
+++ b/sql/log_event_server.cc
|
@@ Table_map_log_event::Table_map_log_event(THD *thd, TABLE *tbl, ...)
|
m_field_metadata_size= save_field_metadata();
|
DBUG_ASSERT(m_field_metadata_size <= (m_colcnt * 2));
|
|
|
+ DBUG_EXECUTE_IF("corrupt_table_map_shrink_field_metadata", |
+ m_field_metadata_size= 1;);
|
Test file:
--source include/have_debug.inc
|
--source include/have_binlog_format_row.inc
|
--source include/master-slave.inc
|
|
|
--connection slave
|
call mtr.add_suppression("Slave SQL: Column 0 of table .test.t1. cannot be converted");
|
call mtr.add_suppression("Error running query, slave SQL thread aborted");
|
|
|
# Every column is MYSQL_TYPE_STRING, so the decode loop wants 2 bytes each
|
# while the event carries 1 byte in total.
|
--connection master
|
create table t1 (c1 char(10), c2 char(10), c3 char(10), c4 char(10),
|
c5 char(10), c6 char(10), c7 char(10), c8 char(10),
|
c9 char(10), c10 char(10), c11 char(10), c12 char(10),
|
c13 char(10), c14 char(10), c15 char(10), c16 char(10),
|
c17 char(10), c18 char(10), c19 char(10), c20 char(10),
|
c21 char(10), c22 char(10), c23 char(10), c24 char(10));
|
set @@session.debug_dbug= "+d,corrupt_table_map_shrink_field_metadata";
|
insert into t1 values ('a','b','c','d','e','f','g','h',
|
'i','j','k','l','m','n','o','p',
|
'q','r','s','t','u','v','w','x');
|
set @@session.debug_dbug= "";
|
--source include/save_master_gtid.inc
|
|
|
# table_def::table_def() reads 48 bytes from a 1-byte block.
|
--connection slave
|
--let $slave_sql_errno= 1677
|
--source include/wait_for_slave_sql_error.inc
|
|
|
# Both definitions are char(10), so a conversion failure is only reachable if
|
# the second metadata byte of column 0 came from outside the event.
|
--let $assert_text= The SQL thread reported a conversion failure for column 0
|
--let $assert_file= $MYSQLTEST_VARDIR/log/mysqld.2.err
|
--let $assert_select= Column 0 of table .test.t1. cannot be converted from type
|
--let $assert_count= 1
|
--source include/assert_grep.inc
|
|
|
# The target type in that message is the replica's own char(10 octets), so
|
# this anchors on the source type the replica decoded.
|
--let $assert_text= The decoded source type is not the char(10) the source sent
|
--let $assert_file= $MYSQLTEST_VARDIR/log/mysqld.2.err
|
--let $assert_select= cannot be converted from type .char\(10 octets\)
|
--let $assert_count= 0
|
--source include/assert_grep.inc
|
24 columns is the smallest count that leaves the allocation. The metadata block
is the second of two in the reader's my_multi_malloc, ALIGN_SIZE rounds
both to 8 bytes, and a debug build follows each with an 8-byte
ALLOC_BARRIER, so the 32-byte allocation ends at field_metadata[16] and
a walk of 16 bytes or fewer stays inside it and faults on nothing.
A replica applying a Table_map that declares 24 char(10) columns and
one metadata byte reports:
[ERROR] Slave SQL: Column 0 of table 'test.t1' cannot be converted from type
|
'char(165 octets)' to type 'char(10 octets) character set latin1', Gtid 0-1-2,
|
Internal MariaDB error code: 1677
|
Both definitions are char(10), so the error is itself the proof.
x = field_metadata[0] << 8 | field_metadata[1]
(sql/rpl_utility.cc:229) took 254, the real type byte the event carried,
and 165 from past it. 165 is 0xA5, MariaDB's own TRASH_ALLOC fill, so that
byte is uninitialized heap from the 8-byte ALIGN_SIZE rounding of the
1-byte block, and it reached the error log.
Attachments
Issue Links
- relates to
-
MDEV-39689 Slave Overflow on Malformed Table_map_log_event
-
- Closed
-
-
MDEV-40644 Slave SQL Thread Overflow on Malformed Table_map_log_event
-
- In Progress
-