MDEV-40643: Corrupt Heartbeat Log Event can Crash Slave - #5503
Open
bnestere wants to merge 2 commits into
Open
Conversation
A slave can crash when its master sends an event shorter than the event's own header. A truncated heartbeat makes debug builds fail an assertion in Binary_string::realloc_raw(). On 64-bit release builds it stops the slave IO thread with an error message that omits the log file name. If the master also declares an oversized common header in its format description event, the slave allocates and fills nearly 4GB instead. On 32-bit release builds it writes that error message past the end of a stack buffer. An event under four bytes crashes every build, because the slave checksums roughly 16 EiB and reads far past the end of the packet. Neither queue_event() nor the Heartbeat_log_event constructor bounded the length the master sent. queue_event() handed that length to event_checksum_test(), which subtracted the checksum length from it, and the result wrapped on an event under four bytes. The constructor computed the log file name length as event_len minus the header lengths, and on a truncated heartbeat that subtraction wrapped to a value near 4GB. queue_event() rejected the heartbeat as invalid, so its error path appended the log file name to the error message using the wrapped length. Add the missing bound in both places. queue_event() now rejects an event shorter than the common header before anything reads that header, and the slave IO thread stops with an error. The Heartbeat_log_event constructor now compares event_len against the combined header lengths before the subtraction. A heartbeat that clears the first check but still stops inside its headers keeps ident_len at 0 and log_ident at NULL, so the error path appends nothing. That constructor check also precedes the read of the extended log position, which previously ran on a short event before any validation. A short event from the master now stops the slave IO thread with an error instead of crashing the server. Reviewed-by: TODO Signed-off-by: Brandon Nesterenko <brandon.nesterenko@mariadb.com>
Member
|
Brandon Nesterenko ***@***.***> writes:
A slave can crash when its master sends an event shorter than the
event's own header. A truncated heartbeat makes debug builds fail an
assertion in Binary_string::realloc_raw(). On 64-bit release builds it
stops the slave IO thread with an error message that omits the log
file name. If the master also declares an oversized common header in
its format description event, the slave allocates and fills nearly 4GB
instead. On 32-bit release builds it writes that error message past
the end of a stack buffer. An event under four bytes crashes every
build, because the slave checksums roughly 16 EiB and reads far past
the end of the packet.
Add the missing bound in both places. queue_event() now rejects an event
shorter than the common header before anything reads that header, and
the slave IO thread stops with an error. The Heartbeat_log_event
constructor now compares event_len against the combined header lengths
before the subtraction. A heartbeat that clears the first check but
still stops inside its headers keeps ident_len at 0 and log_ident at
NULL, so the error path appends nothing. That constructor check also
precedes the read of the extended log position, which previously ran on
a short event before any validation. A short event from the master now
stops the slave IO thread with an error instead of crashing the server.
This looks fine, thanks Brandon.
I'd just add an unlikely() to this error check (I noticed you put the
unlikely() on the similar error check in another place):
+ if (event_len <= all_headers_len)
+ return;
This PR is organized in two commits. The first is the regression test and
debug injections; the second is the fix and .result file.
I've seen this pattern used, and I don't mind it as such (ie. it's very good
to actually check that the test case fails before the patch). Just want to
remark that it breaks bisecting for test case failures. Probably bisecting
the full test suite will be impossible anyway with our persistent problems
with sporadic test failures.
- Kristian.
|
Member
|
Thanks for the quick review!
This looks fine, thanks Brandon.
I'd just add an unlikely() to this error check (I noticed you put the
unlikely() on the similar error check in another place):
Right, thanks for the reminder
I've seen this pattern used, and I don't mind it as such (ie. it's very
good
to actually check that the test case fails before the patch). Just
want to
remark that it breaks bisecting for test case failures. Probably
bisecting
the full test suite will be impossible anyway with our persistent
problems
with sporadic test failures.
I only use this pattern during the review, so the reviewer can easily
debug through and understand the failure. I squash them together before
pushing to any main branch.
Brandon
…On Thu, Aug 6, 2026 at 2:29 PM Kristian Nielsen via developers < ***@***.***> wrote:
Brandon Nesterenko ***@***.***> writes:
> A slave can crash when its master sends an event shorter than the
> event's own header. A truncated heartbeat makes debug builds fail an
> assertion in Binary_string::realloc_raw(). On 64-bit release builds it
> stops the slave IO thread with an error message that omits the log
> file name. If the master also declares an oversized common header in
> its format description event, the slave allocates and fills nearly 4GB
> instead. On 32-bit release builds it writes that error message past
> the end of a stack buffer. An event under four bytes crashes every
> build, because the slave checksums roughly 16 EiB and reads far past
> the end of the packet.
> Add the missing bound in both places. queue_event() now rejects an event
> shorter than the common header before anything reads that header, and
> the slave IO thread stops with an error. The Heartbeat_log_event
> constructor now compares event_len against the combined header lengths
> before the subtraction. A heartbeat that clears the first check but
> still stops inside its headers keeps ident_len at 0 and log_ident at
> NULL, so the error path appends nothing. That constructor check also
> precedes the read of the extended log position, which previously ran on
> a short event before any validation. A short event from the master now
> stops the slave IO thread with an error instead of crashing the server.
This looks fine, thanks Brandon.
I'd just add an unlikely() to this error check (I noticed you put the
unlikely() on the similar error check in another place):
> + if (event_len <= all_headers_len)
> + return;
> This PR is organized in two commits. The first is the regression test and
> debug injections; the second is the fix and .result file.
I've seen this pattern used, and I don't mind it as such (ie. it's very
good
to actually check that the test case fails before the patch). Just want to
remark that it breaks bisecting for test case failures. Probably bisecting
the full test suite will be impossible anyway with our persistent problems
with sporadic test failures.
- Kristian.
_______________________________________________
developers mailing list -- ***@***.***
To unsubscribe send an email to ***@***.***
|
Contributor
Author
|
I wonder why my above email was posted under @knielsen's name. That's worrying.. |
Member
|
Brandon Nesterenko ***@***.***> writes:
I only use this pattern during the review, so the reviewer can easily
debug through and understand the failure. I squash them together before
pushing to any main branch.
Ah! That's a nice way to do it actually, I hadn't thought of that, thanks
for the explanation!
- Kristian.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A slave can crash when its master sends an event shorter than the
event's own header. A truncated heartbeat makes debug builds fail an
assertion in Binary_string::realloc_raw(). On 64-bit release builds it
stops the slave IO thread with an error message that omits the log
file name. If the master also declares an oversized common header in
its format description event, the slave allocates and fills nearly 4GB
instead. On 32-bit release builds it writes that error message past
the end of a stack buffer. An event under four bytes crashes every
build, because the slave checksums roughly 16 EiB and reads far past
the end of the packet.
Neither queue_event() nor the Heartbeat_log_event constructor bounded
the length the master sent. queue_event() handed that length to
event_checksum_test(), which subtracted the checksum length from it,
and the result wrapped on an event under four bytes. The constructor
computed the log file name length as event_len minus the header
lengths, and on a truncated heartbeat that subtraction wrapped to a
value near 4GB. queue_event() rejected the heartbeat as invalid, so
its error path appended the log file name to the error message using
the wrapped length.
Add the missing bound in both places. queue_event() now rejects an event
shorter than the common header before anything reads that header, and
the slave IO thread stops with an error. The Heartbeat_log_event
constructor now compares event_len against the combined header lengths
before the subtraction. A heartbeat that clears the first check but
still stops inside its headers keeps ident_len at 0 and log_ident at
NULL, so the error path appends nothing. That constructor check also
precedes the read of the extended log position, which previously ran on
a short event before any validation. A short event from the master now
stops the slave IO thread with an error instead of crashing the server.
This PR is organized in two commits. The first is the regression test and
debug injections; the second is the fix and .result file.
Note I used Claude pretty heavily in creating this patch, yet, I'm sure it took
longer for me to use Claude to go back-and-forth with everything it did
wrong, than if I were to have made this from scratch myself..