Skip to content

Reject truncated length-encoded integers in MySQL reply parser - #3575

Open
wwbmmm wants to merge 12 commits into
apache:masterfrom
wwbmmm:oncall/req-20261001-014422
Open

wwbmmm wants to merge 12 commits into
apache:masterfrom
wwbmmm:oncall/req-20261001-014422

Conversation

@wwbmmm

@wwbmmm wwbmmm commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: resolve #N/A

Problem Summary:

parse_encode_length in src/brpc/policy/mysql/mysql_reply.cpp declared an uninitialized uint8_t tmp[N] (N=2/3/8) and called IOBuf::cutn without checking the return value. When a MySQL server sends a packet in which a length-encoded integer's 0xFC/0xFD/0xFE prefix is followed by fewer value bytes than promised, cutn only partially fills tmp and mysql_uint*korr then reads the uninitialized stack bytes as the value. The garbage value propagates into column counts, field lengths and loop bounds, producing unpredictable parse behavior (bogus lengths, stream desync, oversized allocations). By contrast, parse_header in the same file already validates its cutn result — this closes the protection gap.

What is changed and the side effects?

Changed:

  • parse_encode_length now returns int64_t and returns -1 when the prefix byte or its 2/3/8 value bytes are not fully present, or when the prefix is the invalid 0xFF marker. cut1/cutn return values are checked, so no uninitialized memory is ever interpreted.
  • All call sites fail fast with PARSE_ERROR_ABSOLUTELY_WRONG on a truncated value: ResultSetHeader::Parse (column count / extra message), Column::Parse (all six length-encoded strings), Ok::Parse (affected rows / last insert id), text and binary Field::Parse, and the binary TIME/DATETIME parsers.
  • The six duplicated length-encoded-string blocks in Column::Parse are folded into one parse_column_string helper (same checks and log messages as before).

Side effects:

  • Performance effects: none — the fix adds one branch per length-encoded integer on a non-hot parse path.
  • Breaking backward compatibility: no API changes. Packets that were previously parsed with garbage values (truncated length prefixes) are now rejected as malformed, which is the intended behavior; well-formed packets are unaffected.

Check List:

  • Tests: added 4 cases to test/brpc_mysql_reply_parse_unittest.cpp — truncated 0xFC/0xFD/0xFE prefixes in column definitions (the reported scenario), truncated prefixes in row fields, truncated OK packets, plus a positive multi-byte (0xFC) length test. All 9 cases in brpc_mysql_reply_parse_unittest pass, and the auth/handshake/scramble mysql unit tests still pass.
  • Compilable: verified with cmake (BUILD_UNIT_TESTS=ON) full build.

🤖 This PR was automatically created by brpc-oncall

parse_encode_length did not check the return value of IOBuf::cutn, so a
truncated 0xFC/0xFD/0xFE prefix left part of the stack buffer
uninitialized and mysql_uint*korr interpreted that garbage as the value.
A malicious server could turn it into bogus column counts, field lengths
or loop counts. Check cut1/cutn and return -1 on truncated or invalid
input; all call sites now fail with PARSE_ERROR_ABSOLUTELY_WRONG and the
six duplicated column-string blocks are folded into one helper.

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

Decoding can still cross packet boundaries and rejects valid unsigned 64-bit values.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity

Open (2)
What changed in this PR

Hardens MySQL length-encoded integer parsing against truncated input.

Changes:

  • Validates length prefixes and payload bytes.
  • Propagates malformed-packet errors across reply parsers.
  • Adds regression tests for truncated and valid multi-byte lengths.
File Description
src/​brpc/​policy/​mysql/​mysql_reply.cpp Adds validated length decoding and shared column-string parsing.
test/​brpc_mysql_reply_parse_unittest.cpp Adds malformed-packet and positive decoding tests.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/brpc/policy/mysql/mysql_reply.cpp Outdated
Comment thread src/brpc/policy/mysql/mysql_reply.cpp Outdated
Address review feedback on the length-encoded integer hardening:

1. parse_header now optionally cuts the packet payload into a separate
   IOBuf and every sub-parser decodes from that bounded buffer, so a
   truncated field can no longer borrow bytes from a coalesced next
   packet and silently desync the stream.

2. parse_encode_length returns bool with a uint64_t out-param instead
   of an int64_t sentinel, which rejected legitimate values above
   INT64_MAX (e.g. a 2^64-1 affected-rows count in OK packets).

Add regression tests for a truncated prefix followed by a coalesced
packet and for the maximum encodable affected-rows value.

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

Multi-byte result-set column counts are misclassified before reaching the new validation.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Resolved since last review (2)

Comment thread src/brpc/policy/mysql/mysql_reply.cpp
Address review feedback: the dispatcher matched the first payload byte
against synthetic MysqlRspType values, so a result set whose column
count used the multi-byte 0xFC form (252..65535 columns) was
misclassified as a prepare-ok and fed to unchecked fixed-width header
reads; 0xFB/0xFD counts hit the unknown-type fallback; and every
0xFE-leading packet was treated as EOF although MySQL only treats a
SHORT (< 9 payload bytes) 0xFE packet as EOF.

- Dispatch fresh wire bytes 0x01..0xFE to the result-set parser;
  prepare-ok resume is keyed on |_type| instead of the wire byte.
- is_an_eof and the dispatcher now require payload < 9 for EOF, so a
  row starting with an 8-byte length-encoded value is a row.
- All remaining unchecked fixed-width cuts (prepare-ok header, column
  fixed fields, OK status/warnings, EOF, ERR sql-state, auth fixed
  fields and NUL-terminated strings, binary row/field values, binary
  TIME/DATETIME) go through a parse_fixed helper that rejects short
  reads instead of using uninitialized stack memory.

Tests: 0xFC/251-column result sets parse; truncated 0xFC count, huge
0xFE count, 0xFE-leading row and truncated prepare-ok are rejected;
standalone EOF and a full prepare-ok still parse.

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

Several fixed-width parser paths still accept truncated payloads or decode partially initialized data.

Review effort: Balanced
Findings: 2 High severity

Open (2)

Comment thread src/brpc/policy/mysql/mysql_reply.cpp
Cover the review scenario directly: a greeting ending before the 4-byte
thread id (previously left tmp partially uninitialized and continued)
and a greeting with a non-NUL-terminated server version are both
rejected.

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

Bare 0xFB headers and truncated column filler bytes can still be accepted as valid.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
Resolved since last review (2)

Comment thread src/brpc/policy/mysql/mysql_reply.cpp Outdated
A bare 0xFB is the length-encoded NULL marker, never a legal column
count (251 is encoded as FC FB 00). The widened fresh-dispatch range
accepted it as a zero-column result set when followed by two EOF
packets. Exclude 0xFB from result-set dispatch (it falls to the
unknown-type rejection) and reject an explicitly encoded zero column
count in ResultSetHeader::Parse; a result set always carries at least
one column.

Copilot AI 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.

Copilot review overview

🔵 Needs a closer look

Result-set dispatch incorrectly accepts the 0xFB NULL/LOCAL INFILE marker as a column count.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

A truncated column definition remains accepted, and one regression test does not exercise its intended path.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Resolved since last review (1)
Previously missed (2)

In code that hasn't changed since last review

Low severity Distinguish malformed length prefixes from oversized lengths

src/​brpc/​policy/​mysql/​mysql_reply.cpp:225

The combined condition emits an inaccurate message when the length prefix itself is truncated or is 0xFF: parse_encode_length leaves len as zero, so the log claims that “length 0 exceeds” the remaining buffer. Split prefix-decoding failure from the oversized-length check so malformed-prefix diagnostics identify the actual failure.

Low severity Rename test to reflect three-byte length encoding

test/​brpc_mysql_reply_parse_unittest.cpp:378

This name says “SingleByte,” but the test deliberately verifies the three-byte 0xFC 0xFB 0x00 encoding. Rename it to reflect the multi-byte encoding so the test name does not contradict its setup and comment.

Comment thread test/brpc_mysql_reply_parse_unittest.cpp Outdated
- RejectTruncatedGreeting copied only 11 of the 12 bytes of
  "5.7.99-fake\x00", so it failed at the cut_until delimiter check
  instead of exercising the truncated thread-id parse_fixed path it was
  written for. Copy the terminating NUL.
- parse_column_string now distinguishes a truncated/invalid length
  prefix from an oversized length in its log message instead of
  reporting a misleading "length 0 exceeds ...".
- Rename AcceptSingleByte251ColumnCount to AcceptMultiByte251ColumnCount
  to match the three-byte encoding it actually verifies.

Copilot AI 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.

Copilot review overview

🔵 Needs a closer look

A truncated column definition missing its final filler bytes is still accepted.

Review effort: Balanced
Findings: None

Resolved since last review (1)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Reject truncated column-definition filler instead of unchecked removal

src/​brpc/​policy/​mysql/​mysql_reply.cpp:710

The final two-byte column-definition filler is still removed with an unchecked pop_front. If the packet ends immediately after decimal, pop_front(2) removes zero bytes and the column is marked parsed, so this truncated fixed-width field is accepted. Consume it through parse_fixed (as with the preceding fixed fields) so the packet is rejected.

Remaining unchecked pop_front calls on fixed filler fields silently
removed zero bytes from a truncated packet and accepted it as parsed:
the column definition's filler-length byte and final 2 reserved bytes,
the auth greeting's 10 reserved bytes, the ERR packet's '#' sql_state
marker (now also validated) and the prepare-ok header's filler byte.
Consume them through parse_fixed so a truncated tail is rejected.
Add tests for a column definition missing 1..3 tail bytes and for an
ERR packet without the '#' marker.

Copilot AI 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.

Copilot review overview

🔵 Needs a closer look

The expanded protocol changes include an unresolved handshake-error compatibility regression and require human validation.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)

Comment thread src/brpc/policy/mysql/mysql_reply.cpp
An ERR packet sent before capabilities are negotiated (e.g. the
'Too many connections' error) carries no '#' and sql_state; the
unconditional marker check rejected these legitimate packets during
authentication. Peek for the '#' marker instead (as MySQL clients do):
when present, parse the protocol-4.1 layout and keep rejecting a
truncated 5-byte sql_state; otherwise treat the whole tail as the
message with an empty sql_state. This also drops the pre-existing
payload_size >= 9 guard, which rejected short pre-4.1 errors, and
skips the message allocation for an empty tail.

Fixes a fetch misuse found on the way: IOBuf::fetch returns a pointer
into its own storage when the range fits one block, so the marker must
be read through the returned pointer, not the aux buffer.

Tests: initial-handshake ERR parses with the full message, 4.1 ERR
keeps its sql_state, and a truncated sql_state is still rejected.

Copilot AI 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.

Copilot review overview

🔵 Needs a closer look

Broad framing and protocol-dispatch changes need human review to resolve compatibility and regression-coverage concerns.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
Resolved since last review (1)

Comment thread src/brpc/policy/mysql/mysql_reply.cpp Outdated
The '#' sniff could not distinguish a pre-4.1 initial-handshake error
from a 4.1 error whose message starts with '#': '#quota exceeded'
was misparsed as sql_state 'quota' + message ' exceeded', and a short
'#bad' message was rejected as a truncated sql_state. Thread a
protocol41 flag from ParseMysqlMessage through ConsumePartialIOBuf
into Error::Parse: it is false only while the server greeting has not
been processed yet (per-connection AuthContext group still empty),
which is exactly when ERR packets use the pre-4.1 layout ('Too many
connections'); after the HandshakeResponse41 and in the command phase
the 4.1 layout ('#' + sql_state) is required.

Tests: legacy messages starting with '#' (long and short) keep their
message intact with an empty sql_state, a 4.1 error whose message
starts with '#' still parses marker + sql_state, plus the existing
initial-handshake / truncated-sql-state / plain 4.1 controls.

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

Replacing the public parser signatures removes symbols needed by prebuilt clients; compatibility overloads are needed.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Resolved since last review (1)

Comment thread src/brpc/policy/mysql/mysql.h Outdated
A defaulted protocol41 parameter preserves source compatibility but
changes the mangled symbol, breaking prebuilt clients that link against
the old signatures. Restore both legacy signatures as out-of-line
wrappers that forward protocol41=true, and drop the default argument
from the new overloads so overload resolution stays unambiguous.

Copilot AI 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.

Copilot review overview

🔵 Needs a closer look

Packet framing, response dispatch, and handshake changes need protocol-compatibility validation and malformed binary-row regression coverage.

Review effort: Balanced
Findings: None

Resolved since last review (1)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Add binary-row short-read tests for truncated packet data

src/​brpc/​policy/​mysql/​mysql_reply.cpp:884

The new binary-row short-read checks are not exercised by the added row tests: those use MYSQL_NORMAL_STATEMENT, and the prepared-statement integration tests consume well-formed server replies. Add synthetic MYSQL_PREPARED_STATEMENT result sets in test/brpc_mysql_reply_parse_unittest.cpp covering a truncated NULL bitmap, fixed-width numeric value, and string/TIME/DATETIME length or value. Include a coalesced following packet and a valid binary-row control. Assert PARSE_ERROR_ABSOLUTELY_WRONG for malformed rows so the tests verify that decoding cannot borrow bytes from the next packet.

The short-read hardening of the binary protocol path (NULL bitmap,
fixed-width values, string/TIME/DATETIME lengths) had no coverage:
all unit tests used the text protocol and the prepared-statement
integration tests need a live server. Add synthetic
MYSQL_PREPARED_STATEMENT result sets with a valid control row plus
truncated NULL bitmap, truncated LONGLONG value, truncated string
length prefix/value and truncated TIME/DATETIME values, each followed
by a coalesced EOF packet to verify that decoding never borrows bytes
from the next packet.

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

A new regression-test fixture reads past its string literal and needs the supplied inline correction.

Review effort: Balanced
Findings: 1 High severity

Open (1)

Comment thread test/brpc_mysql_reply_parse_unittest.cpp Outdated
The \x00ab escape greedily consumes the following hex digits, so the
literal held 4 bytes while std::string(str, 5) copied five -- a
global-buffer-overflow caught by the ASan CI build. Split the literal
after \x00 so the escape terminates at the quote; the adjacent-literal
concatenation yields the intended 5 bytes fc 05 00 'a' 'b'.

Copilot AI 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.

Copilot review overview

🔵 Needs a closer look

Changes to packet framing, reply dispatch, and authentication parsing warrant final human validation.

Review effort: Balanced
Findings: None

Resolved since last review (1)

@wwbmmm
wwbmmm requested a review from chenBright October 3, 2026 02:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants