Skip to content

MDEV-39762: add bounds check in query_event_uncompress and row_log_ev…#5413

Open
ARaveala wants to merge 5 commits into
MariaDB:10.11from
ARaveala:MDEV-39762-validate-query-compressed-un-len
Open

MDEV-39762: add bounds check in query_event_uncompress and row_log_ev…#5413
ARaveala wants to merge 5 commits into
MariaDB:10.11from
ARaveala:MDEV-39762-validate-query-compressed-un-len

Conversation

@ARaveala

Copy link
Copy Markdown

Fixes MDEV-39762.

See commit message for full details.

Related:

  • MDEV-39674: similar validation gap in Table_map_log_event parsing (same fix pattern)
  • MDEV-39672: related overflow in mariadb-binlog Table_map parsing

Open questions for reviewer:

  • return 2 is non-idiomatic — enum return type may be preferable
  • constructor paths are fixed but untested — acceptable or separate ticket?
  • assert_grep targets ER_TOO_BIG_FOR_UNCOMPRESS; easy to revert to generic
    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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread sql/log_event.cc
Comment on lines +404 to +405
if (unlikely(un_len > MAX_MAX_ALLOWED_PACKET))
return 2;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Comment thread mysql-test/lib/mtr_socket_relay.pm
Comment thread mysql-test/lib/mtr_socket_relay.pm Outdated

local $SIG{CHLD} = 'IGNORE';
close STDOUT;
fork and exit;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix:copied instruction.

@ParadoxV5

Copy link
Copy Markdown
Contributor

Lecture me: Why do we need to test with full-blown Perl-simulated MitM? I’d just craft the malformed event like the JIRA report and #5270 did.

And then, some general things @gkodinov would say: Please rebase to and target 10.6, and fix your CI failures.

@ParadoxV5
ParadoxV5 requested review from bnestere and gkodinov July 20, 2026 20:58
@ParadoxV5 ParadoxV5 added External Contribution All PRs from entities outside of MariaDB Foundation, Corporation, Codership agreements. Replication Patches involved in replication labels Jul 20, 2026
@ParadoxV5

Copy link
Copy Markdown
Contributor
  • return 2 is non-idiomatic — enum return type may be preferable

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 0/ER_BINLOG_UNCOMPRESS_ERROR/ER_TOO_BIG_FOR_UNCOMPRESS might be even more idiomatic.

  • assert_grep targets ER_TOO_BIG_FOR_UNCOMPRESS; easy to revert to generic
    if preferred (see commit message for details)

That said, whether a new error is preferable over reüsing the generic ER_BINLOG_UNCOMPRESS_ERROR must first depend on whether ER_BINLOG_UNCOMPRESS_ERROR actually makes sense here.

  • constructor paths are fixed but untested — acceptable or separate ticket?

I have not looked carefully, but perhaps they are untested because there are now extraneous guards that prevent the constructor fix from taking effect.
These un_len > MAX_MAX_ALLOWED_PACKETs – is this truly a requirement, or have you added a new limitation that the uncompressed data must not exceed 1GB even if it’s not from a 1GB-capped server?

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).

This is why every (other) fault injection in the repository has --source include/have_debug.inc near the beginning of the test file.

License: I agree to license this contribution under the BSD New 3 Clause license.

At least good job on coming up with the MAX_MAX_ALLOWED_PACKET cap and the MitM test on your own, first-time contributor.
Because my guess is that an AI would just instruct a vibe coder to stick with DBUG_EXECUTE_IFs.

@gkodinov gkodinov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your contribution. This is a preliminary review.

Comment thread mysql-test/lib/mtr_socket_relay.pm Outdated
my ($listen_port, $target_host, $target_port) = @_;

my $listen_sock = IO::Socket::INET->new(
LocalAddr => '127.0.0.1',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not target_host? why localhost?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

# ---------------------------------------------------------------------------
# Phase 1a -- authenticate to the real master as root (no password)
# ---------------------------------------------------------------------------
sub do_master_auth {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. DBUG_EXECUTE_IF — minimal complexity, as you suggest, but debug-build-only and adds production code
  2. 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?

@ParadoxV5 ParadoxV5 Jul 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will leave this decision to the final reviewer.

Comment thread sql/log_event.cc
Comment thread sql/log_event.cc

int32 comp_len= (int32)(len - (tmp - src) -
(contain_checksum ? BINLOG_CHECKSUM_LEN : 0));
uint32 un_len= binlog_get_uncompress_len(tmp);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread sql/log_event.cc
if (end <= tmp)
return 1; //bad event

uint32 un_len= binlog_get_uncompress_len(tmp);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here: this can also go past end

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread sql/log_event.cc
MYF(MY_WME));
if (new_buf)
{
if (!binlog_buf_uncompress(m_rows_buf, new_buf,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
  }

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread storage/columnstore/columnstore
Comment thread libmariadb
@ARaveala
ARaveala force-pushed the MDEV-39762-validate-query-compressed-un-len branch 2 times, most recently from ece2288 to d85f53e Compare July 21, 2026 15:44
@ARaveala

Copy link
Copy Markdown
Author

@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.

@ARaveala

Copy link
Copy Markdown
Author

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 gkodinov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes because of the 10.11 rebase.

@ARaveala
ARaveala changed the base branch from main to 10.11 July 24, 2026 06:58
@ARaveala
ARaveala changed the base branch from 10.11 to main July 24, 2026 06:59
ARaveala added 2 commits July 24, 2026 10:41
…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
@ARaveala
ARaveala force-pushed the MDEV-39762-validate-query-compressed-un-len branch from d85f53e to 3ae07a4 Compare July 24, 2026 07:48
@ARaveala
ARaveala changed the base branch from main to 10.11 July 24, 2026 07:48

@gkodinov gkodinov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Please stand by for the final review.

@gkodinov gkodinov removed their assignment Jul 24, 2026
@ARaveala
ARaveala force-pushed the MDEV-39762-validate-query-compressed-un-len branch 2 times, most recently from d85f53e to 5771553 Compare July 24, 2026 08:11
ARaveala added 2 commits July 24, 2026 12:21
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

External Contribution All PRs from entities outside of MariaDB Foundation, Corporation, Codership agreements. Replication Patches involved in replication

Development

Successfully merging this pull request may close these issues.

4 participants