MDEV-39762: add bounds check in query_event_uncompress and row_log_ev…#5413
MDEV-39762: add bounds check in query_event_uncompress and row_log_ev…#5413ARaveala wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request addresses MDEV-39762 by adding validation to prevent stack overflows and crashes caused by oversized un_len fields in compressed binlog events. It introduces checks in sql/log_event.cc and sql/slave.cc to reject events exceeding MAX_MAX_ALLOWED_PACKET and gracefully stop the replica with an appropriate error. Additionally, a Perl-based MITM proxy test suite is added to verify this behavior. The review feedback highlights several improvement opportunities: replacing magic numbers with named constants/enums for return statuses, explicitly binding the test relay socket to 127.0.0.1 instead of 0.0.0.0 for security, and properly handling potential fork failures in the relay script.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if (unlikely(un_len > MAX_MAX_ALLOWED_PACKET)) | ||
| return 2; |
There was a problem hiding this comment.
Using magic numbers like 2 (and 1) as return values for query_event_uncompress and row_log_event_uncompress makes the code harder to maintain and understand. It would be much more idiomatic and self-documenting to define an enum or named constants for these return statuses (e.g., UNCOMPRESS_OK = 0, UNCOMPRESS_ERR = 1, UNCOMPRESS_TOO_BIG = 2) in sql/log_event.h and use them consistently here and in sql/slave.cc.
|
|
||
| local $SIG{CHLD} = 'IGNORE'; | ||
| close STDOUT; | ||
| fork and exit; |
There was a problem hiding this comment.
If fork fails, it returns undef. In Perl, fork and exit; will evaluate to false on failure, meaning the parent process will not exit and will continue executing the child's path, leading to unexpected behavior or hangs. It is safer to explicitly check if fork is defined and handle the error.
my $pid = fork;
die "setup_relay: fork failed: $!" unless defined $pid;
exit if $pid;
It’s not that non-idiomatic since some old helper functions do return 0/1/2/-1/etc. and only explain what those IDs mean in code comments. In this specific case, though, returning
That said, whether a new error is preferable over reüsing the generic
I have not looked carefully, but perhaps they are untested because there are now extraneous guards that prevent the constructor fix from taking effect.
This is why every (other) fault injection in the repository has
At least good job on coming up with the |
| my ($listen_port, $target_host, $target_port) = @_; | ||
|
|
||
| my $listen_sock = IO::Socket::INET->new( | ||
| LocalAddr => '127.0.0.1', |
There was a problem hiding this comment.
why not target_host? why localhost?
| # --------------------------------------------------------------------------- | ||
| # Phase 1a -- authenticate to the real master as root (no password) | ||
| # --------------------------------------------------------------------------- | ||
| sub do_master_auth { |
There was a problem hiding this comment.
Not really a fan of this hard-coding of the protocol. It's something that will need updating as we go forward. I'd rather do DBUG_EXECUTE_IF() style of test and inject some faulty data, as the MDEV suggests (and even provides). But this is something for the final reviewer I guess.
There was a problem hiding this comment.
Thank you for the review. I misunderstood Brandon's Jira suggestion of "pre-creating such an invalid event", I built a MITM proxy thinking it avoided runtime injection, but, it is still effectively runtime injection. I acknowledge the hardcoded protocol is a valid maintenance concern.
Re-reading the Jira discussion, I believe Brandon's suggestion maps to approach 2 below, which may also address your maintenance concern:
- DBUG_EXECUTE_IF — minimal complexity, as you suggest, but debug-build-only and adds production code
- Pre-crafted binary file in std_data/ — capture a real compressed event from a running master, patch the un_len bytes offline, commit the result alongside a generator script that documents every byte modified. No production code changes, works on release builds, no protocol hardcoding.
Could you and Brandon confirm the preferred direction? I will hold all changes until clarified.
On the boundary validation gaps in binlog_get_uncompress_len() and binlog_buf_uncompress(), should these be filed as new tickets similar to MDEV-39672/39674, or addressed here?
There was a problem hiding this comment.
DBUG_EXECUTE_IF — minimal complexity, as you suggest, but debug-build-only and adds production code
Debug-build-only means not production code, does it?
Unless, riddle me this, there’re people running debug builds on prod rather than release builds.
On the test approach, the DBUG_EXECUTE_IF injection was noted in the Jira discussion as unnecessarily bloating the code base, so I avoided it.
I indeed objected that the provided injection is a specialized bloat, but I left the discussion open on whether a more generalized and lean injection is possible.
I misunderstood Brandon's Jira suggestion of "pre-creating such an invalid event", I built a MITM proxy thinking it avoided runtime injection, but, it is still effectively runtime injection.
Or, if DBUG_EXECUTE_IF displeases us, handcrafting a binlog file is technically also an option.
On ER_TOO_BIG_FOR_UNCOMPRESS — suggested in the Jira discussion for both query and row events.
👍
The IO thread guard catches network-based attacks; the constructor guard covers direct relay log injection. Currently untested, happy to revisit if it warrants a separate ticket or tests wanted.
🤔 Does the constructor guard also cover network-based attacks if there is no IO thread guard?
There was a problem hiding this comment.
Thank you for the clarification on DBUG_EXECUTE_IF, I misunderstood the distinction between source bloat and binary bloat. Investigating the constructor guard code path and the handcrafted binlog file approach before replying further. Will follow up shortly.
There was a problem hiding this comment.
DBUG_EXECUTE_IF is compiled out in release builds so no binary bloat, the concern was source bloat, which remains regardless of build type.
Network path: hostile master → IO thread → queue_event → uncompress guard → relay log. If the guard fires, the event never reaches the relay log and the constructor is never called.
SQL thread path: relay log file → next_event → read_log_event_no_checksum → constructor → uncompress_buf guard. This path seems to bypass queue_event entirely, no equivalent guard was found between next_event and the constructor, though this warrants further review.
Whether the constructor guard provides meaningful protection on the SQL thread path warrants further review.
Side note — DBUG_ASSERT(event_len <= UINT32_MAX) appears in some cases in read_log_event_no_checksum but not the compressed row event cases. Not sure if intentional.
Confirmation needed:
- Binary file approach or DBUG_EXECUTE_IF? i would like to try the binary file approach .
- Both tests (IO thread + SQL thread paths) in this PR, or separate tickets? If binary file approach succeeds, both could be containable under one ticket
- Boundary fixes for binlog_get_uncompress_len/binlog_buf_uncompress, in scope or separate tickets? Function signature changes I would suggest separate tickets. If binary file approach succeeds, a test case for these should follow a similar pattern, could be containable under one ticket.
- ER_TOO_BIG_FOR_UNCOMPRESS, keep or revert to generic error? Can be added or removed easily, open to suggestions on whether the approach can be improved.
There was a problem hiding this comment.
I will leave this decision to the final reviewer.
|
|
||
| int32 comp_len= (int32)(len - (tmp - src) - | ||
| (contain_checksum ? BINLOG_CHECKSUM_LEN : 0)); | ||
| uint32 un_len= binlog_get_uncompress_len(tmp); |
There was a problem hiding this comment.
binlog_get_uncompress_len() itself can read past the buffer's end. it reads the first byte, then decides how much more it must read: 1-3 more. What if that 2nd read goes past end?
There was a problem hiding this comment.
Valid concern. Initial investigation suggests a check at the call site using the already available comp_len may address this without changing the function signature. A fix inside the function itself would require a signature change, possibly a separate ticket.
While Waiting direction on the test approach, I will explore the binary file approach in the meantime, if it works, testing this scenario with a truncated event should reuse the same infrastructure. Will follow up with findings.
| if (end <= tmp) | ||
| return 1; //bad event | ||
|
|
||
| uint32 un_len= binlog_get_uncompress_len(tmp); |
There was a problem hiding this comment.
same here: this can also go past end
There was a problem hiding this comment.
end is already available at this point. The existing if (end <= tmp) check can be extended to if (end <= tmp || end - tmp < 1 + (tmp[0] & 0x07)) to also verify there are enough bytes for the length field. Awaiting direction on test approach, but exploring the binary file approach in the meantime, if it works, testing this with a truncated event should reuse the same infrastructure. Will follow up with findings.
| MYF(MY_WME)); | ||
| if (new_buf) | ||
| { | ||
| if (!binlog_buf_uncompress(m_rows_buf, new_buf, |
There was a problem hiding this comment.
fwiw, binlog_buf_uncompress() doesn't exactly check lenlen's boundaries too. It could be bigger than len:
int binlog_buf_uncompress(const uchar *src, uchar *dst, uint32 len,
uint32 *newlen)
{
if ((src[0] & 0x80) == 0)
return 1;
uint32 lenlen= src[0] & 0x07;
uLongf buflen= *newlen; // zlib type
uint32 alg= (src[0] & 0x70) >> 4;
switch(alg) {
case 0:
// zlib
if (uncompress((Bytef *)dst, &buflen,
(const Bytef*)src + 1 + lenlen, len - 1 - lenlen) != Z_OK)
return 1;
break;
default:
//TODO
//bad algorithm
return 1;
}
There was a problem hiding this comment.
Similar boundary validation concern noted. binlog_buf_uncompress receives ```len`` as a parameter which may allow a check inside the function itself. Awaiting test approach direction but will investigate in the meantime.
ece2288 to
d85f53e
Compare
|
@ParadoxV5 Thank you for the feedback. On the test approach, the DBUG_EXECUTE_IF injection was noted in the Jira discussion as unnecessarily bloating the code base, so I avoided it. The MITM proxy was my interpretation of "pre-creating the invalid event", I acknowledge this is still effectively runtime injection. Awaiting final reviewer direction before changing approach. On ER_TOO_BIG_FOR_UNCOMPRESS — suggested in the Jira discussion for both query and row events. On MAX_MAX_ALLOWED_PACKET — this is the compile-time hard ceiling for max_allowed_packet on MariaDB servers. The same concern about non-MariaDB masters applies equally to the guards in query_event_uncompress and row_log_event_uncompress, not just the constructor. If there is a known setup where a non-MariaDB master could produce un_len > MAX_MAX_ALLOWED_PACKET, I am happy to try test against it. On the constructor guards, called by the SQL thread via the relay log, separate from the IO thread path. The IO thread guard catches network-based attacks; the constructor guard covers direct relay log injection. Currently untested, happy to revisit if it warrants a separate ticket or tests wanted. |
|
CI failures (main.except on aarch64, main.innodb_mrr_cpk on embedded) appear unrelated to this fix. rpl_binlog_compress_overflow tests should be skipped on embedded builds via not_embedded.inc |
gkodinov
left a comment
There was a problem hiding this comment.
Requesting changes because of the 10.11 rebase.
…ent_uncompress Integer overflow in query_event_uncompress() and row_log_event_uncompress() when un_len is near UINT32_MAX. ALIGN_SIZE() truncation produces a wrong buffer size causing a stack overflow (SIGABRT) in binlog_buf_uncompress(). Fix: reject un_len > MAX_MAX_ALLOWED_PACKET before any buffer allocation in both uncompress functions and their constructors. Return value 2 surfaces ER_TOO_BIG_FOR_UNCOMPRESS (error 1256) in slave.cc rather than the generic ER_BINLOG_UNCOMPRESS_ERROR. This is non-idiomatic (everything else returns 0/1) — an enum return type may be preferable, left as reviewer decision. Test: Two Perl MITM proxy intercepts replication stream and patches un_len to 0xFFFFFFFC in QUERY_COMPRESSED_EVENT and WRITE_ROWS_COMPRESSED_EVENT respectively. Without fix: server crashes (SIGABRT). With fix: IO thread stops cleanly with ER_SLAVE_RELAY_LOG_WRITE_FAILURE (1595), logging ER_TOO_BIG_FOR_UNCOMPRESS (1256). Shared proxy infrastructure added: - mysql-test/lib/mtr_socket_relay.pm — generic socket relay setup - mysql-test/lib/rpl_binlog_mitm.pm — MySQL protocol helpers, replication handshake, pre-dump relay Coverage gap: constructor fix has no test - requires a different trigger condition, not reachable via the current replication proxy approach. Coverage gaps noted for reviewer: - Constructor paths (Query_compressed_log_event, Write/Update/Delete_rows_compressed_log_event) are fixed but not tested — not reachable via the replication proxy approach - assert_grep checks for ER_TOO_BIG_FOR_UNCOMPRESS message; if reviewer prefers generic error, change return 2 to return 1 and update assert_grep to "Uncompress the compressed binlog failed", test will also need updating. WIP: MDEV-39762 row event path and proxy refactor WIP: MDEV-39762 cleanup and final test fixes
d85f53e to
3ae07a4
Compare
gkodinov
left a comment
There was a problem hiding this comment.
LGTM. Please stand by for the final review.
d85f53e to
5771553
Compare
The GTID value set during cleanup is environment specific and differs between machines. Use --replace_result to mask it in the result file so the test passes consistently on any server.
Add RESET MASTER on slave before setting gtid_slave_pos to clear the slave's own binary log, preventing GTID conflict warning during cleanup. Mask the GTID position value in result files with --replace_result as it is environment-specific.
Fixes MDEV-39762.
See commit message for full details.
Related:
Open questions for reviewer:
if preferred (see commit message for details)
Test notes: MITM proxy simulates a hostile peer patching un_len in transit,
the only approach that works on release builds (DBUG_EXECUTE_IF is
debug-only). Proxy infrastructure is reusable; if location or naming
of the shared .pm files needs adjusting, that is a move only.
Maintenance: if QUERY_HEADER_LEN or ROWS_HEADER_LEN_V1 change in
log_event.h, the proxy offset calculations need updating — noted in
each proxy file.
License: I agree to license this contribution under the BSD New 3 Clause license.