-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Fix OOB reads upon malformed Format Description and v2 Rows Events #5419
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 10.6
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| SET @saved_dbug= @@GLOBAL.debug_dbug; | ||
| SET @@GLOBAL.debug_dbug= '+d,truncate_fde_at_post_header_len'; | ||
| FLUSH BINARY LOGS; | ||
| SHOW BINLOG EVENTS IN 'master-bin.000002'; | ||
| ERROR HY000: Error when executing command SHOW BINLOG EVENTS: Wrong offset or I/O error | ||
| SET @@GLOBAL.debug_dbug= '+d,truncate_fde_common_header_len'; | ||
| FLUSH BINARY LOGS; | ||
| SHOW BINLOG EVENTS IN 'master-bin.000003'; | ||
| ERROR HY000: Error when executing command SHOW BINLOG EVENTS: Wrong offset or I/O error | ||
| SET @@GLOBAL.debug_dbug= @saved_dbug; | ||
| RESET MASTER; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,6 +35,18 @@ SELECT COUNT(*) FROM t3; | |
| # Reset. | ||
| DROP TABLE t1, t2, t3; | ||
|
|
||
| # MDEV-39485 Heap-buffer-overflow upon read in `Rows_log_event` constructor | ||
| # | ||
| # This binlog file contains a normal Format Description | ||
| # Event followed by 3 malformed v2 Write Rows Events: | ||
| # 1. A tag followed by no data | ||
| # 2. An undersized tagged data followed by an unrecognized tag | ||
| # 3. A tag followed by a length longer than the entire event | ||
| --let SEARCH_PATTERN= [Uu]nknown [Ee]vent | ||
| --let SEARCH_FILE= $MYSQLTEST_VARDIR/tmp/invalid_row_v2_tag.sql | ||
| --exec $MYSQL_BINLOG --force-read --verbose suite/binlog/std_data/invalid_row_v2_tag.001 > $SEARCH_FILE | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Testing with |
||
| --source include/search_pattern_in_file.inc | ||
|
|
||
|
|
||
| --echo ==== Read modern binlog (version 5.1.23) ==== | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| --source include/have_debug.inc | ||
| --source include/have_binlog_format_mixed.inc # format-agnostic | ||
|
|
||
| # OOB read on malformed `Format_description_log_event` | ||
| # | ||
| # Verify that Format Description Events with truncated contents fails gracefully | ||
| # rather than underflow the post-header count and lead to buffer over-read. | ||
|
|
||
| SET @saved_dbug= @@GLOBAL.debug_dbug; | ||
|
|
||
|
|
||
| # MDEV-40366: `used_checksum_alg` | ||
|
|
||
| SET @@GLOBAL.debug_dbug= '+d,truncate_fde_at_post_header_len'; | ||
| FLUSH BINARY LOGS; | ||
|
|
||
| --let $binlog_file= query_get_value(SHOW BINLOG STATUS, File, 1) | ||
| --error ER_ERROR_WHEN_EXECUTING_COMMAND | ||
| --eval SHOW BINLOG EVENTS IN '$binlog_file' | ||
|
|
||
|
|
||
| # MDEV-40365: `common_header_len` & `post_header_len` | ||
|
|
||
| SET @@GLOBAL.debug_dbug= '+d,truncate_fde_common_header_len'; | ||
| FLUSH BINARY LOGS; | ||
|
|
||
| --let $binlog_file= query_get_value(SHOW BINLOG STATUS, File, 1) | ||
| --error ER_ERROR_WHEN_EXECUTING_COMMAND | ||
| --eval SHOW BINLOG EVENTS IN '$binlog_file' | ||
|
|
||
| # Clean-up | ||
| SET @@GLOBAL.debug_dbug= @saved_dbug; | ||
| RESET MASTER; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1022,7 +1022,7 @@ Log_event* Log_event::read_log_event(const uchar *buf, uint event_len, | |
| my_bool crc_check, | ||
| my_bool print_errors) | ||
| { | ||
| Log_event* ev; | ||
| Log_event* ev= nullptr; | ||
| enum enum_binlog_checksum_alg alg; | ||
| DBUG_ENTER("Log_event::read_log_event(char*,...)"); | ||
| DBUG_ASSERT(fdle != 0); | ||
|
|
@@ -1041,14 +1041,21 @@ Log_event* Log_event::read_log_event(const uchar *buf, uint event_len, | |
| } | ||
|
|
||
| uint event_type= buf[EVENT_TYPE_OFFSET]; | ||
| switch (event_type) { | ||
| case FORMAT_DESCRIPTION_EVENT: | ||
| // If event is FD the descriptor is in it. | ||
| if (unlikely(get_checksum_alg(buf, event_len, &alg))) | ||
| goto exit; | ||
| break; | ||
| case START_EVENT_V3: | ||
| // all following START events in the current file are without checksum | ||
| if (event_type == START_EVENT_V3) | ||
| (const_cast< Format_description_log_event *>(fdle))->checksum_alg= BINLOG_CHECKSUM_ALG_OFF; | ||
| // fall-through | ||
| default: | ||
| /* | ||
| CRC verification by SQL and Show-Binlog-Events master side. | ||
| The caller has to provide @fdle->checksum_alg to | ||
| be the last seen FD's (A) descriptor. | ||
| If event is FD the descriptor is in it. | ||
| Notice, FD of the binlog can be only in one instance and therefore | ||
| Show-Binlog-Events executing master side thread needs just to know | ||
| the only FD's (A) value - whereas RL can contain more. | ||
|
|
@@ -1063,11 +1070,9 @@ Log_event* Log_event::read_log_event(const uchar *buf, uint event_len, | |
|
|
||
| Notice, a pre-checksum FD version forces alg := BINLOG_CHECKSUM_ALG_UNDEF. | ||
| */ | ||
| alg= (event_type != FORMAT_DESCRIPTION_EVENT) ? | ||
| fdle->checksum_alg : get_checksum_alg(buf, event_len); | ||
| alg= fdle->checksum_alg; | ||
| // Emulate the corruption during reading an event | ||
| DBUG_EXECUTE_IF("corrupt_read_log_event_char", | ||
| if (event_type != FORMAT_DESCRIPTION_EVENT) | ||
| { | ||
| uchar *debug_event_buf_c= const_cast<uchar*>(buf); | ||
| int debug_cor_pos= rand() % (event_len - BINLOG_CHECKSUM_LEN); | ||
|
|
@@ -1076,6 +1081,7 @@ Log_event* Log_event::read_log_event(const uchar *buf, uint event_len, | |
| DBUG_SET("-d,corrupt_read_log_event_char"); | ||
| } | ||
| ); | ||
| } | ||
| if (crc_check && event_checksum_test(const_cast<uchar*>(buf), event_len, alg)) | ||
| { | ||
| #ifdef MYSQL_CLIENT | ||
|
|
@@ -2269,33 +2275,39 @@ Format_description_log_event(const uchar *buf, uint event_len, | |
| common_header_len(0), post_header_len(NULL), event_type_permutation(0) | ||
| { | ||
| DBUG_ENTER("Format_description_log_event::Format_description_log_event(char*,...)"); | ||
| if (!Start_log_event_v3::is_valid()) | ||
| if (unlikely( | ||
| event_len < LOG_EVENT_MINIMAL_HEADER_LEN + ST_POST_HEADER_LEN_OFFSET || | ||
| !Start_log_event_v3::is_valid())) | ||
| DBUG_VOID_RETURN; /* sanity check */ | ||
| buf+= LOG_EVENT_MINIMAL_HEADER_LEN; | ||
| if ((common_header_len=buf[ST_COMMON_HEADER_LEN_OFFSET]) < OLD_HEADER_LEN) | ||
| if (unlikely( | ||
| (common_header_len=buf[ST_COMMON_HEADER_LEN_OFFSET]) < OLD_HEADER_LEN)) | ||
| DBUG_VOID_RETURN; /* sanity check */ | ||
| number_of_event_types= | ||
| event_len - (LOG_EVENT_MINIMAL_HEADER_LEN + ST_COMMON_HEADER_LEN_OFFSET + 1); | ||
| event_len - (LOG_EVENT_MINIMAL_HEADER_LEN + ST_POST_HEADER_LEN_OFFSET); | ||
| DBUG_PRINT("info", ("common_header_len=%d number_of_event_types=%d", | ||
| common_header_len, number_of_event_types)); | ||
| /* If alloc fails, we'll detect it in is_valid() */ | ||
|
|
||
| post_header_len= (uint8*) my_memdup(PSI_INSTRUMENT_ME, | ||
| buf+ST_COMMON_HEADER_LEN_OFFSET+1, | ||
| number_of_event_types* | ||
| sizeof(*post_header_len), | ||
| MYF(0)); | ||
| calc_server_version_split(); | ||
| buf+= ST_POST_HEADER_LEN_OFFSET; | ||
| if (!is_version_before_checksum(&server_version_split)) | ||
| { | ||
| /* the last bytes are the checksum alg desc and value (or value's room) */ | ||
| /* the last bytes are the checksum alg desc */ | ||
| if (unlikely(number_of_event_types < BINLOG_CHECKSUM_ALG_DESC_LEN)) | ||
| DBUG_VOID_RETURN; /* sanity check: But there are no last bytes. */ | ||
| number_of_event_types -= BINLOG_CHECKSUM_ALG_DESC_LEN; | ||
| checksum_alg= (enum_binlog_checksum_alg)post_header_len[number_of_event_types]; | ||
| checksum_alg= (enum_binlog_checksum_alg)buf[number_of_event_types]; | ||
| } | ||
| else | ||
| { | ||
| checksum_alg= BINLOG_CHECKSUM_ALG_UNDEF; | ||
| } | ||
| post_header_len= (uint8*) my_memdup(PSI_INSTRUMENT_ME, | ||
| buf, | ||
| number_of_event_types* | ||
| sizeof(*post_header_len), | ||
| MYF(0)); | ||
| deduct_options_written_to_bin_log(); | ||
| reset_crypto(); | ||
|
|
||
|
|
@@ -2408,35 +2420,50 @@ Format_description_log_event::is_version_before_checksum(const master_version_sp | |
| } | ||
|
|
||
| /** | ||
| @param buf buffer holding serialized FD event | ||
| @param len netto (possible checksum is stripped off) length of the event buf | ||
|
|
||
| @return the version-safe checksum alg descriptor where zero | ||
| @param buf buffer holding serialized FD event including the 4-byte checksum | ||
| @param len length of the event buf | ||
| @param alg output the version-safe checksum alg descriptor where zero | ||
| designates no checksum, 255 - the orginator is | ||
| checksum-unaware (effectively no checksum) and the actuall | ||
| [1-254] range alg descriptor. | ||
| @return whether this is an invalid FD event | ||
| */ | ||
| enum enum_binlog_checksum_alg get_checksum_alg(const uchar *buf, ulong len) | ||
| bool get_checksum_alg(const uchar *buf, ulong len, | ||
| enum enum_binlog_checksum_alg *alg) | ||
|
Comment on lines
+2431
to
+2432
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Andrei made a curious suggestion to always checksum FDEs. The current code design is rather silly: |
||
| { | ||
| enum enum_binlog_checksum_alg ret; | ||
| constexpr ptrdiff_t POST_HEADER_LEN_OFFSET= | ||
| LOG_EVENT_MINIMAL_HEADER_LEN + ST_POST_HEADER_LEN_OFFSET; | ||
| char version[ST_SERVER_VER_LEN]; | ||
|
|
||
| DBUG_ENTER("get_checksum_alg"); | ||
| DBUG_ASSERT(buf[EVENT_TYPE_OFFSET] == FORMAT_DESCRIPTION_EVENT); | ||
|
|
||
| if (unlikely(len < POST_HEADER_LEN_OFFSET)) | ||
| DBUG_RETURN(true); | ||
| memcpy(version, | ||
| buf + LOG_EVENT_MINIMAL_HEADER_LEN + ST_SERVER_VER_OFFSET, | ||
| ST_SERVER_VER_LEN); | ||
| version[ST_SERVER_VER_LEN - 1]= 0; | ||
|
|
||
| Format_description_log_event::master_version_split version_split(version); | ||
| ret= Format_description_log_event::is_version_before_checksum(&version_split) | ||
| ? BINLOG_CHECKSUM_ALG_UNDEF | ||
| : (enum_binlog_checksum_alg)buf[len - BINLOG_CHECKSUM_LEN - BINLOG_CHECKSUM_ALG_DESC_LEN]; | ||
| DBUG_ASSERT(ret == BINLOG_CHECKSUM_ALG_OFF || | ||
| ret == BINLOG_CHECKSUM_ALG_UNDEF || | ||
| ret == BINLOG_CHECKSUM_ALG_CRC32); | ||
| DBUG_RETURN(ret); | ||
| if (Format_description_log_event::is_version_before_checksum(&version_split)) | ||
| *alg= BINLOG_CHECKSUM_ALG_UNDEF; | ||
| else | ||
| { | ||
| /* | ||
| len >= POST_HEADER_LEN_OFFSET > | ||
| BINLOG_CHECKSUM_LEN + BINLOG_CHECKSUM_ALG_DESC_LEN | ||
| */ | ||
| ulong checksum_alg_offset= | ||
| len - BINLOG_CHECKSUM_LEN - BINLOG_CHECKSUM_ALG_DESC_LEN; | ||
| if (unlikely(checksum_alg_offset < POST_HEADER_LEN_OFFSET)) | ||
| DBUG_RETURN(true); | ||
| *alg= static_cast<enum_binlog_checksum_alg>(buf[checksum_alg_offset]); | ||
| } | ||
| DBUG_ASSERT(*alg == BINLOG_CHECKSUM_ALG_OFF || | ||
| *alg == BINLOG_CHECKSUM_ALG_UNDEF || | ||
| *alg == BINLOG_CHECKSUM_ALG_CRC32); | ||
| DBUG_RETURN(false); | ||
| } | ||
|
|
||
| Start_encryption_log_event:: | ||
|
|
@@ -3396,9 +3423,17 @@ Rows_log_event::Rows_log_event(const uchar *buf, uint event_len, | |
| case RW_V_EXTRAINFO_TAG: | ||
| { | ||
| /* Have an 'extra info' section, read it in */ | ||
| assert((end - pos) >= EXTRA_ROW_INFO_HDR_BYTES); | ||
| if (unlikely((end - pos) <= EXTRA_ROW_INFO_LEN_OFFSET)) | ||
| { | ||
| m_cols.bitmap= 0; | ||
| DBUG_VOID_RETURN; | ||
| } | ||
| uint8 infoLen= pos[EXTRA_ROW_INFO_LEN_OFFSET]; | ||
| assert((end - pos) >= infoLen); | ||
| if (unlikely(infoLen < EXTRA_ROW_INFO_HDR_BYTES || (end-pos) < infoLen)) | ||
| { | ||
| m_cols.bitmap= 0; | ||
| DBUG_VOID_RETURN; | ||
| } | ||
|
ParadoxV5 marked this conversation as resolved.
|
||
| /* Just store/use the first tag of this type, skip others */ | ||
| if (likely(!m_extra_row_data)) | ||
| { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since MariaDB does not generate v2 Rows Events, here’s a handcrafted binlog file.