Details
-
Bug
-
Status: Open (View Workflow)
-
Major
-
Resolution: Unresolved
-
None
-
None
-
Can result in hang or crash
Description
A TABLE_MAP event in a binary log carries an optional block of
column metadata (names, character sets, signedness, primary keys) that
mariadb-binlog --print-table-metadata decodes and prints. When that
block is malformed or corrupt, the tool reads a few bytes past the end
of the buffer it allocated for the block. Under an AddressSanitizer
build the tool aborts. Under a normal build the read returns whatever
bytes follow the allocation, and the tool then decodes further metadata
from a bogus length and prints garbage or crashes.
The same decode runs on a replication slave when it applies a
TABLE_MAP event, so a malformed event in a relay log reaches it
there as well.
The optional-metadata block is a sequence of type-length-value entries.
Table_map_log_event's constructor copies the block into a buffer
that my_malloc() sizes to exactly the block's length
(m_optional_metadata, m_optional_metadata_len).
Optional_metadata_fields::Optional_metadata_fields() in
sql/log_event.cc then walks the block:
for (uchar *field= optional_metadata ; field < metadata_end ; field+= len) |
{
|
Optional_metadata_field_type type=
|
static_cast<Optional_metadata_field_type>(field[0]); |
field++;
|
len= net_field_length(&field); // length prefix decoded here |
...
|
The loop condition bounds the read of the type byte field[ 0] , but
nothing bounds the length prefix that follows. net_field_length()
reads the first byte of the prefix and, for a 0xFE lead byte, reads
four more bytes as the encoded length and advances the cursor by nine,
with no knowledge of metadata_end. A 0xFE placed where the last
prefix begins makes net_field_length() read those four bytes past
the end of the my_malloc() allocation. The reporter states eight
bytes; the actual read is the four bytes of uint4korr(), while the
cursor advances by nine.
The over-read is the entry point. Its bogus return value then drives the
rest of the loop: the cursor is left past the buffer, len is
arbitrary, and the switch either dispatches a parse helper on
out-of-range arguments or the loop skips to field += len and exits.
On branches from 10.6 through 11.8 the per-type parse helpers have no
bounds checks of their own, so a surviving bogus len produces
further reads past the buffer inside them.
MDEV-39689 added the if (unlikely(p > end)) return; guards inside
the per-type parse helpers on the development branch, but it left the
length prefix in the dispatch loop above decoded before any check, so
this over-read remains.
Reproduction
A debug injection places a 0xFE marker on the last byte
the allocation can hold and aims the decoder at it, so the loop's own
net_field_length() reads past the buffer. Note that an ASAN build is
required to reproduce the problem.
The injection:
diff --git a/sql/log_event_server.cc b/sql/log_event_server.cc
|
index 90d832e3626..29f25b3947b 100644
|
--- a/sql/log_event_server.cc
|
+++ b/sql/log_event_server.cc
|
@@ -7070,6 +7070,11 @@ void Table_map_log_event::init_metadata_fields()
|
init_primary_key_field())
|
m_metadata_buf.length(0);
|
}
|
+ DBUG_EXECUTE_IF("write_corrupt_optional_metadata_marker",
|
+ {
|
+ m_metadata_buf.length(0);
|
+ m_metadata_buf.append(STRING_WITH_LEN("\x01\x04\x00\x00\x00\x00\x01\xFE"));
|
+ });
|
DBUG_VOID_RETURN;
|
} |
The test:
--source include/have_debug.inc
|
--source include/have_binlog_format_row.inc
|
|
|
if (`SELECT COUNT(*)= 0 FROM information_schema.system_variables
|
WHERE variable_name= 'have_sanitizer' AND global_value LIKE "ASAN%"`)
|
{
|
--skip Test requires an ASan build to observe the out-of-bounds read
|
}
|
|
|
SET @old_metadata= @@GLOBAL.binlog_row_metadata;
|
SET GLOBAL binlog_row_metadata = FULL;
|
|
|
CREATE TABLE t1 (c_signed INT, c_char CHAR(10), c_unsigned INT UNSIGNED);
|
|
|
SET @old_dbug= @@GLOBAL.debug_dbug;
|
SET @@GLOBAL.debug_dbug= '+d,write_corrupt_optional_metadata_marker';
|
INSERT INTO t1 VALUES (-1, 'x', 1);
|
SET @@GLOBAL.debug_dbug= @old_dbug;
|
|
|
--let $binlog_file= query_get_value(SHOW BINLOG STATUS, File, 1)
|
FLUSH BINARY LOGS;
|
--let $binlog_path= `SELECT CONCAT(@@datadir, '/$binlog_file')`
|
--let $output_file= $MYSQLTEST_VARDIR/tmp/oob.output
|
|
|
--echo # $MYSQL_BINLOG -F --print-table-metadata --base64-output=never $binlog_path > $output_file 2>&1
|
--exec $MYSQL_BINLOG -F --print-table-metadata --base64-output=never $binlog_path > $output_file 2>&1
|
|
|
# Match the lines specific to --print-table-metadata and ensure they are displayed
|
--let SEARCH_PATTERN= (?m-s:# (?:Columns\(| {8}).*)
|
--let SEARCH_FILE= $output_file
|
--let SEARCH_OUTPUT= matches
|
--source include/search_pattern_in_file.inc
|
--remove_file $output_file
|
|
|
DROP TABLE t1;
|
SET GLOBAL binlog_row_metadata = @old_metadata;
|
Reported by: Muntadher H
Attachments
Issue Links
- relates to
-
MDEV-39689 Slave Overflow on Malformed Table_map_log_event
-
- Closed
-