This is an automated email from the ASF dual-hosted git repository.
chenBright pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brpc.git
The following commit(s) were added to refs/heads/master by this push:
new d03cf2d4 Reject truncated length-encoded integers in MySQL reply
parser (#3575)
d03cf2d4 is described below
commit d03cf2d4ce9bac299845c6d43724af96c5e45cd2
Author: Weibing Wang <[email protected]>
AuthorDate: Tue Oct 6 18:25:06 2026 +0800
Reject truncated length-encoded integers in MySQL reply parser (#3575)
* Reject truncated length-encoded integers in MySQL reply parser
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.
* Bound MySQL packet parsing to the packet payload
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.
* Fix MySQL reply dispatch for multi-byte column counts
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.
* Add truncated greeting tests for MySQL auth parser
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.
* Reject bare 0xFB header and zero column count in MySQL result sets
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.
* Fix greeting test NUL terminator and prefix-failure diagnostics
- 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.
* Validate column/auth/ERR/prepare-ok filler bytes in MySQL parser
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.
* Accept pre-4.1 initial-handshake ERR packets in MySQL parser
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.
* Select MySQL ERR layout from connection phase, not message content
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.
* Keep legacy MySQL ConsumePartialIOBuf symbols for ABI compatibility
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.
* Add binary-row truncation regression tests for MySQL parser
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.
* Fix hex-escape over-read in binary string-field test
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'.
---------
Co-authored-by: brpc-oncall <brpc-oncall@localhost>
---
src/brpc/policy/mysql/mysql.cpp | 12 +-
src/brpc/policy/mysql/mysql.h | 6 +
src/brpc/policy/mysql/mysql_protocol.cpp | 12 +-
src/brpc/policy/mysql/mysql_reply.cpp | 515 ++++++++++++++----------
src/brpc/policy/mysql/mysql_reply.h | 10 +-
test/brpc_mysql_reply_parse_unittest.cpp | 661 ++++++++++++++++++++++++++++++-
6 files changed, 993 insertions(+), 223 deletions(-)
diff --git a/src/brpc/policy/mysql/mysql.cpp b/src/brpc/policy/mysql/mysql.cpp
index 8d49d066..ac2638ca 100644
--- a/src/brpc/policy/mysql/mysql.cpp
+++ b/src/brpc/policy/mysql/mysql.cpp
@@ -457,13 +457,21 @@ void MysqlResponse::Swap(MysqlResponse* other) {
ParseError MysqlResponse::ConsumePartialIOBuf(butil::IOBuf& buf,
bool is_auth,
MysqlStmtType stmt_type) {
+ return ConsumePartialIOBuf(buf, is_auth, stmt_type, true);
+}
+
+ParseError MysqlResponse::ConsumePartialIOBuf(butil::IOBuf& buf,
+ bool is_auth,
+ MysqlStmtType stmt_type,
+ bool protocol41) {
bool more_results = true;
size_t oldsize = 0;
while (more_results) {
oldsize = buf.size();
if (reply_size() == 0) {
ParseError err =
- _first_reply.ConsumePartialIOBuf(buf, &_arena, is_auth,
stmt_type, &more_results);
+ _first_reply.ConsumePartialIOBuf(
+ buf, &_arena, is_auth, stmt_type, &more_results,
protocol41);
if (err != PARSE_OK) {
return err;
}
@@ -484,7 +492,7 @@ ParseError MysqlResponse::ConsumePartialIOBuf(butil::IOBuf&
buf,
}
}
ParseError err = _other_replies[_nreply - 1]->ConsumePartialIOBuf(
- buf, &_arena, is_auth, stmt_type, &more_results);
+ buf, &_arena, is_auth, stmt_type, &more_results, protocol41);
if (err != PARSE_OK) {
return err;
}
diff --git a/src/brpc/policy/mysql/mysql.h b/src/brpc/policy/mysql/mysql.h
index 9ff8d72c..7afa5a7d 100644
--- a/src/brpc/policy/mysql/mysql.h
+++ b/src/brpc/policy/mysql/mysql.h
@@ -198,7 +198,13 @@ public:
// Returns PARSE_ERROR_NOT_ENOUGH_DATA if data in `buf' is not enough to
parse.
// Returns PARSE_ERROR_ABSOLUTELY_WRONG if the parsing
// failed.
+ // Legacy signature kept for binary compatibility with prebuilt clients;
+ // assumes the protocol-4.1 ERR layout.
ParseError ConsumePartialIOBuf(butil::IOBuf& buf, bool is_auth,
MysqlStmtType stmt_type);
+ ParseError ConsumePartialIOBuf(butil::IOBuf& buf,
+ bool is_auth,
+ MysqlStmtType stmt_type,
+ bool protocol41);
// Number of replies in this response.
// (May have more than one reply due to pipeline)
diff --git a/src/brpc/policy/mysql/mysql_protocol.cpp
b/src/brpc/policy/mysql/mysql_protocol.cpp
index 71554062..4a6d1c3f 100644
--- a/src/brpc/policy/mysql/mysql_protocol.cpp
+++ b/src/brpc/policy/mysql/mysql_protocol.cpp
@@ -362,7 +362,17 @@ ParseResult ParseMysqlMessage(butil::IOBuf* source,
}
MysqlStmtType stmt_type = static_cast<MysqlStmtType>(pi.count);
- ParseError err = msg->response.ConsumePartialIOBuf(*source, pi.auth_flags
!= 0, stmt_type);
+ // ERR-packet layout: once the client has sent its HandshakeResponse41
+ // (which carries CLIENT_PROTOCOL_41), or in the command phase, errors
+ // include '#' + sql_state. Before that -- an ERR replacing the server
+ // greeting, e.g. "Too many connections" on a saturated server -- the
+ // message follows the error code directly (pre-4.1 layout). The
+ // per-connection AuthContext group is set as soon as the greeting has
+ // been processed, so an empty group means we are still waiting for it.
+ const bool protocol41 = !pi.auth_flags ||
+ (socket->auth_context() != nullptr &&
!socket->auth_context()->group().empty());
+ ParseError err =
+ msg->response.ConsumePartialIOBuf(*source, pi.auth_flags != 0,
stmt_type, protocol41);
if (FLAGS_mysql_verbose) {
LOG(INFO) << "[MYSQL PARSE] " << msg->response;
}
diff --git a/src/brpc/policy/mysql/mysql_reply.cpp
b/src/brpc/policy/mysql/mysql_reply.cpp
index 1a277339..bf39e1e3 100644
--- a/src/brpc/policy/mysql/mysql_reply.cpp
+++ b/src/brpc/policy/mysql/mysql_reply.cpp
@@ -129,22 +129,22 @@ inline bool is_full_package(const butil::IOBuf& buf) {
}
return true;
}
-// if is eof package
+// if is eof package. Per the MySQL protocol, a 0xFE-leading packet is an
+// EOF only when it is short (payload < 9 bytes); a longer 0xFE-leading
+// packet is a row (or header) whose first length-encoded value needs the
+// 8-byte form (e.g. a >=16MB LONGBLOB field).
inline bool is_an_eof(const butil::IOBuf& buf) {
uint8_t tmp[5];
const uint8_t* p = (const uint8_t*)buf.fetch(tmp, sizeof(tmp));
if (p == nullptr) {
return false;
}
- uint8_t type = p[4];
- if (type == MYSQL_RSP_EOF) {
- return true;
- } else {
- return false;
- }
+ return p[4] == MYSQL_RSP_EOF && mysql_uint3korr(p) < 9;
}
-// parse header
-inline bool parse_header(butil::IOBuf& buf, MysqlHeader* value) {
+// parse header. When |payload| is not null, the packet's payload is cut
+// from |buf| into it so that subsequent field decoding cannot run past the
+// packet boundary into a coalesced next packet.
+inline bool parse_header(butil::IOBuf& buf, MysqlHeader* value, butil::IOBuf*
payload) {
if (!is_full_package(buf)) {
return false;
}
@@ -158,35 +158,92 @@ inline bool parse_header(butil::IOBuf& buf, MysqlHeader*
value) {
buf.cut1((char*)&tmp);
value->seq = tmp;
}
+ if (payload != nullptr) {
+ buf.cutn(payload, value->payload_size);
+ }
return true;
}
// use this carefully, we depending on parse_header for checking IOBuf contain
full package
-inline uint64_t parse_encode_length(butil::IOBuf& buf) {
- if (buf.size() == 0) {
- return 0;
- }
-
- uint64_t value = 0;
+// Parse a MySQL length-encoded integer into |value|. Returns false when the
+// prefix byte or its 2/3/8 value bytes are not fully present in |buf| (a
+// truncated packet), or when the prefix is the invalid 0xFF marker; |value|
+// is set to 0 in that case. Never reads uninitialized memory: on success
+// every byte of the returned value was cut from |buf|.
+inline bool parse_encode_length(butil::IOBuf& buf, uint64_t* value) {
+ *value = 0;
uint8_t f = 0;
- buf.cut1((char*)&f);
+ if (!buf.cut1((char*)&f)) {
+ return false;
+ }
if (f <= 250) {
- value = f;
- } else if (f == 251) {
- value = 0;
- } else if (f == 252) {
- uint8_t tmp[2];
- buf.cutn(tmp, sizeof(tmp));
- value = mysql_uint2korr(tmp);
+ *value = f;
+ return true;
+ }
+ if (f == 251) { // NULL
+ return true;
+ }
+ size_t n = 0;
+ if (f == 252) {
+ n = 2;
} else if (f == 253) {
- uint8_t tmp[3];
- buf.cutn(tmp, sizeof(tmp));
- value = mysql_uint3korr(tmp);
+ n = 3;
} else if (f == 254) {
- uint8_t tmp[8];
- buf.cutn(tmp, sizeof(tmp));
- value = mysql_uint8korr(tmp);
+ n = 8;
+ } else {
+ return false; // 0xFF is not a valid length-encoded prefix
}
- return value;
+ uint8_t tmp[8];
+ if (buf.cutn(tmp, n) != n) {
+ return false;
+ }
+ switch (n) {
+ case 2:
+ *value = mysql_uint2korr(tmp);
+ return true;
+ case 3:
+ *value = mysql_uint3korr(tmp);
+ return true;
+ default:
+ *value = mysql_uint8korr(tmp);
+ return true;
+ }
+}
+
+// Parse one length-encoded string of a column definition into |out|. Both
+// the length prefix and the payload must be fully contained in the remaining
+// buffer; otherwise the packet is malformed and parsing fails instead of
+// publishing uninitialized arena memory or desyncing the stream.
+inline ParseError parse_column_string(butil::IOBuf& buf,
+ butil::Arena* arena,
+ butil::StringPiece* out,
+ const char* field) {
+ uint64_t len = 0;
+ if (!parse_encode_length(buf, &len)) {
+ LOG(WARNING) << "MysqlReply::Column::Parse: " << field
+ << " length prefix is truncated or invalid";
+ return PARSE_ERROR_ABSOLUTELY_WRONG;
+ }
+ if (len > buf.size()) {
+ LOG(WARNING) << "MysqlReply::Column::Parse: " << field << " length "
<< len
+ << " exceeds remaining buffer size " << buf.size();
+ return PARSE_ERROR_ABSOLUTELY_WRONG;
+ }
+ char* d = nullptr;
+ MY_ALLOC_CHECK(my_alloc_check(arena, (size_t)len, d));
+ buf.cutn(d, len);
+ out->set(d, len);
+ return PARSE_OK;
+}
+
+// Cut exactly |n| fixed-width bytes from |buf| into |tmp|. parse_header
+// guarantees the whole packet payload is already buffered, so a short read
+// means the packet is malformed and must be rejected instead of leaving
+// |tmp| (or the destination it feeds) partially uninitialized.
+inline ParseError parse_fixed(butil::IOBuf& buf, void* tmp, size_t n) {
+ if (buf.cutn(tmp, n) != n) {
+ return PARSE_ERROR_ABSOLUTELY_WRONG;
+ }
+ return PARSE_OK;
}
ParseError MysqlReply::ConsumePartialIOBuf(butil::IOBuf& buf,
@@ -194,6 +251,15 @@ ParseError MysqlReply::ConsumePartialIOBuf(butil::IOBuf&
buf,
bool is_auth,
MysqlStmtType stmt_type,
bool* more_results) {
+ return ConsumePartialIOBuf(buf, arena, is_auth, stmt_type, more_results,
true);
+}
+
+ParseError MysqlReply::ConsumePartialIOBuf(butil::IOBuf& buf,
+ butil::Arena* arena,
+ bool is_auth,
+ MysqlStmtType stmt_type,
+ bool* more_results,
+ bool protocol41) {
*more_results = false;
if (!is_full_package(buf)) {
return PARSE_ERROR_NOT_ENOUGH_DATA;
@@ -267,25 +333,39 @@ ParseError MysqlReply::ConsumePartialIOBuf(butil::IOBuf&
buf,
MY_PARSE_CHECK(_data.auth->Parse(buf, arena));
return PARSE_OK;
}
+ // A 0xFE-leading packet is an EOF only when it is short (payload < 9
+ // bytes, per the MySQL protocol); a longer 0xFE-leading packet starts a
+ // length-encoded column count and belongs to the result-set branch below.
+ const bool is_eof_packet =
+ (type == 0xFE) && (_type == MYSQL_RSP_EOF || mysql_uint3korr(p) < 9);
if (type == 0x00 && (is_auth || stmt_type != MYSQL_NEED_PREPARE)) {
_type = MYSQL_RSP_OK;
MY_ALLOC_CHECK(my_alloc_check(arena, 1, _data.ok));
MY_PARSE_CHECK(_data.ok->Parse(buf, arena));
*more_results = _data.ok->status() & MYSQL_SERVER_MORE_RESULTS_EXISTS;
- } else if ((type == 0x00 && stmt_type == MYSQL_NEED_PREPARE) || type ==
MYSQL_RSP_PREPARE_OK) {
+ } else if ((type == 0x00 && stmt_type == MYSQL_NEED_PREPARE) ||
+ _type == MYSQL_RSP_PREPARE_OK) {
_type = MYSQL_RSP_PREPARE_OK;
MY_ALLOC_CHECK(my_alloc_check(arena, 1, _data.prepare_ok));
MY_PARSE_CHECK(_data.prepare_ok->Parse(buf, arena));
} else if (type == 0xFF) {
_type = MYSQL_RSP_ERROR;
MY_ALLOC_CHECK(my_alloc_check(arena, 1, _data.error));
- MY_PARSE_CHECK(_data.error->Parse(buf, arena));
- } else if (type == 0xFE) {
+ MY_PARSE_CHECK(_data.error->Parse(buf, arena, protocol41));
+ } else if (is_eof_packet) {
_type = MYSQL_RSP_EOF;
MY_ALLOC_CHECK(my_alloc_check(arena, 1, _data.eof));
MY_PARSE_CHECK(_data.eof->Parse(buf));
*more_results = _data.eof->status() & MYSQL_SERVER_MORE_RESULTS_EXISTS;
- } else if (type >= 0x01 && type <= 0xFA) {
+ } else if (type >= 0x01 && type <= 0xFE && type != 0xFB) {
+ // Any other leading byte is the length-encoded column count of a
+ // result set, including the multi-byte prefixes 0xFC (252..65535),
+ // 0xFD and a long 0xFE-leading count. These bytes must not be matched
+ // against the synthetic MysqlRspType values: a fresh 0xFC is a
+ // result-set header, not a prepare-ok (resume of an already-
+ // classified reply is keyed on |_type| above instead). A bare 0xFB
+ // is the length-encoded NULL marker and never a legal column count,
+ // so it falls through to the unknown-type rejection below.
_type = MYSQL_RSP_RESULTSET;
MY_ALLOC_CHECK(my_alloc_check(arena, 1, _data.result_set));
MY_PARSE_CHECK(_data.result_set->Parse(buf, arena, !(stmt_type ==
MYSQL_NORMAL_STATEMENT)));
@@ -458,13 +538,17 @@ ParseError MysqlReply::Auth::Parse(butil::IOBuf& buf,
butil::Arena* arena) {
}
const std::string delim(1, 0x00);
MysqlHeader header;
- if (!parse_header(buf, &header)) {
+ butil::IOBuf payload;
+ if (!parse_header(buf, &header, &payload)) {
return PARSE_ERROR_NOT_ENOUGH_DATA;
}
- buf.cut1((char*)&_protocol);
+ MY_PARSE_CHECK(parse_fixed(payload, &_protocol, 1));
{
butil::IOBuf version;
- buf.cut_until(&version, delim);
+ if (payload.cut_until(&version, delim) != 0) {
+ LOG(WARNING) << "MysqlReply::Auth::Parse: server version is not
NUL-terminated";
+ return PARSE_ERROR_ABSOLUTELY_WRONG;
+ }
char* d = nullptr;
MY_ALLOC_CHECK(my_alloc_check(arena, version.size(), d));
version.copy_to(d);
@@ -472,12 +556,15 @@ ParseError MysqlReply::Auth::Parse(butil::IOBuf& buf,
butil::Arena* arena) {
}
{
uint8_t tmp[4];
- buf.cutn(tmp, sizeof(tmp));
+ MY_PARSE_CHECK(parse_fixed(payload, tmp, sizeof(tmp)));
_thread_id = mysql_uint4korr(tmp);
}
{
butil::IOBuf salt;
- buf.cut_until(&salt, delim);
+ if (payload.cut_until(&salt, delim) != 0) {
+ LOG(WARNING) << "MysqlReply::Auth::Parse: salt is not
NUL-terminated";
+ return PARSE_ERROR_ABSOLUTELY_WRONG;
+ }
char* d = nullptr;
MY_ALLOC_CHECK(my_alloc_check(arena, salt.size(), d));
salt.copy_to(d);
@@ -485,42 +572,47 @@ ParseError MysqlReply::Auth::Parse(butil::IOBuf& buf,
butil::Arena* arena) {
}
{
uint8_t tmp[2];
- buf.cutn(&tmp, sizeof(tmp));
+ MY_PARSE_CHECK(parse_fixed(payload, &tmp, sizeof(tmp)));
_capability = mysql_uint2korr(tmp);
}
- buf.cut1((char*)&_collation);
+ MY_PARSE_CHECK(parse_fixed(payload, &_collation, 1));
{
uint8_t tmp[2];
- buf.cutn(tmp, sizeof(tmp));
+ MY_PARSE_CHECK(parse_fixed(payload, tmp, sizeof(tmp)));
_status = mysql_uint2korr(tmp);
}
{
uint8_t tmp[2];
- buf.cutn(tmp, sizeof(tmp));
+ MY_PARSE_CHECK(parse_fixed(payload, tmp, sizeof(tmp)));
_extended_capability = mysql_uint2korr(tmp);
}
- buf.cut1((char*)&_auth_plugin_length);
- buf.pop_front(10);
+ MY_PARSE_CHECK(parse_fixed(payload, &_auth_plugin_length, 1));
+ {
+ uint8_t reserved[10]; // reserved bytes
+ MY_PARSE_CHECK(parse_fixed(payload, reserved, sizeof(reserved)));
+ }
{
butil::IOBuf salt2;
- buf.cut_until(&salt2, delim);
+ if (payload.cut_until(&salt2, delim) != 0) {
+ LOG(WARNING) << "MysqlReply::Auth::Parse: salt2 is not
NUL-terminated";
+ return PARSE_ERROR_ABSOLUTELY_WRONG;
+ }
char* d = nullptr;
MY_ALLOC_CHECK(my_alloc_check(arena, salt2.size(), d));
salt2.copy_to(d);
_salt2.set(d, salt2.size());
}
{
- if (_auth_plugin_length > buf.size()) {
+ if (_auth_plugin_length > payload.size()) {
LOG(ERROR) << "MysqlReply::Auth::Parse: auth_plugin length " <<
_auth_plugin_length
- << " exceeds remaining buffer size " << buf.size();
+ << " exceeds remaining buffer size " << payload.size();
return PARSE_ERROR_ABSOLUTELY_WRONG;
}
char* d = nullptr;
MY_ALLOC_CHECK(my_alloc_check(arena, _auth_plugin_length, d));
- buf.cutn(d, _auth_plugin_length);
+ payload.cutn(d, _auth_plugin_length);
_auth_plugin.set(d, _auth_plugin_length);
}
- buf.clear(); // consume all buf
set_parsed();
return PARSE_OK;
}
@@ -530,18 +622,19 @@ ParseError MysqlReply::AuthMoreData::Parse(butil::IOBuf&
buf, butil::Arena* aren
return PARSE_OK;
}
MysqlHeader header;
- if (!parse_header(buf, &header)) {
+ butil::IOBuf payload;
+ if (!parse_header(buf, &header, &payload)) {
return PARSE_ERROR_NOT_ENOUGH_DATA;
}
_seq = (uint8_t)header.seq;
// Drop the 0x01 AuthMoreData tag; expose only the bytes after it (a
// single status byte 0x03/0x04, or the PEM-encoded RSA public key).
- buf.pop_front(1);
- const int64_t len = (int64_t)header.payload_size - 1;
+ payload.pop_front(1);
+ const int64_t len = (int64_t)payload.size();
if (len > 0) {
char* d = nullptr;
MY_ALLOC_CHECK(my_alloc_check(arena, len, d));
- buf.cutn(d, len);
+ payload.cutn(d, len);
_data.set(d, len);
} else {
_data.set(nullptr, 0);
@@ -555,12 +648,20 @@ ParseError
MysqlReply::ResultSetHeader::Parse(butil::IOBuf& buf) {
return PARSE_OK;
}
MysqlHeader header;
- if (!parse_header(buf, &header)) {
+ butil::IOBuf payload;
+ if (!parse_header(buf, &header, &payload)) {
return PARSE_ERROR_NOT_ENOUGH_DATA;
}
- uint64_t old_size, new_size;
- old_size = buf.size();
- _column_count = parse_encode_length(buf);
+ if (!parse_encode_length(payload, &_column_count)) {
+ LOG(ERROR) << "MysqlReply::ResultSetHeader::Parse: truncated column
count";
+ return PARSE_ERROR_ABSOLUTELY_WRONG;
+ }
+ // A result set always carries at least one column; a zero (or NULL-
+ // marker) count means the packet is not a result-set header at all.
+ if (_column_count == 0) {
+ LOG(ERROR) << "illegal column count " << _column_count;
+ return PARSE_ERROR_ABSOLUTELY_WRONG;
+ }
// Guard against an absurd/malicious column count driving unbounded
// allocations downstream (per-column arrays and the row NULL-bitmap).
// MySQL's hard limit is 4096 columns per table; 65535 is a generous cap
@@ -569,9 +670,11 @@ ParseError
MysqlReply::ResultSetHeader::Parse(butil::IOBuf& buf) {
LOG(ERROR) << "illegal column count " << _column_count;
return PARSE_ERROR_ABSOLUTELY_WRONG;
}
- new_size = buf.size();
- if (old_size - new_size < header.payload_size) {
- _extra_msg = parse_encode_length(buf);
+ if (!payload.empty()) {
+ if (!parse_encode_length(payload, &_extra_msg)) {
+ LOG(ERROR) << "MysqlReply::ResultSetHeader::Parse: truncated extra
message";
+ return PARSE_ERROR_ABSOLUTELY_WRONG;
+ }
} else {
_extra_msg = 0;
}
@@ -584,97 +687,45 @@ ParseError MysqlReply::Column::Parse(butil::IOBuf& buf,
butil::Arena* arena) {
return PARSE_OK;
}
MysqlHeader header;
- if (!parse_header(buf, &header)) {
+ butil::IOBuf payload;
+ if (!parse_header(buf, &header, &payload)) {
return PARSE_ERROR_NOT_ENOUGH_DATA;
}
- // Each length-encoded string must fit within the remaining buffer; an
+ // Each length-encoded string must fit within the remaining payload; an
// oversized length would otherwise drive my_alloc_check/cutn/.set past the
// packet (mirrors the hardened auth_plugin path above).
- uint64_t len = parse_encode_length(buf);
- if (len > buf.size()) {
- LOG(WARNING) << "MysqlReply::Column::Parse: catalog length " << len
- << " exceeds remaining buffer size " << buf.size();
- return PARSE_ERROR_ABSOLUTELY_WRONG;
- }
- char* catalog = nullptr;
- MY_ALLOC_CHECK(my_alloc_check(arena, len, catalog));
- buf.cutn(catalog, len);
- _catalog.set(catalog, len);
-
- len = parse_encode_length(buf);
- if (len > buf.size()) {
- LOG(WARNING) << "MysqlReply::Column::Parse: database length " << len
- << " exceeds remaining buffer size " << buf.size();
- return PARSE_ERROR_ABSOLUTELY_WRONG;
- }
- char* database = nullptr;
- MY_ALLOC_CHECK(my_alloc_check(arena, len, database));
- buf.cutn(database, len);
- _database.set(database, len);
-
- len = parse_encode_length(buf);
- if (len > buf.size()) {
- LOG(WARNING) << "MysqlReply::Column::Parse: table length " << len
- << " exceeds remaining buffer size " << buf.size();
- return PARSE_ERROR_ABSOLUTELY_WRONG;
- }
- char* table = nullptr;
- MY_ALLOC_CHECK(my_alloc_check(arena, len, table));
- buf.cutn(table, len);
- _table.set(table, len);
-
- len = parse_encode_length(buf);
- if (len > buf.size()) {
- LOG(WARNING) << "MysqlReply::Column::Parse: origin_table length " <<
len
- << " exceeds remaining buffer size " << buf.size();
- return PARSE_ERROR_ABSOLUTELY_WRONG;
- }
- char* origin_table = nullptr;
- MY_ALLOC_CHECK(my_alloc_check(arena, len, origin_table));
- buf.cutn(origin_table, len);
- _origin_table.set(origin_table, len);
-
- len = parse_encode_length(buf);
- if (len > buf.size()) {
- LOG(WARNING) << "MysqlReply::Column::Parse: name length " << len
- << " exceeds remaining buffer size " << buf.size();
- return PARSE_ERROR_ABSOLUTELY_WRONG;
- }
- char* name = nullptr;
- MY_ALLOC_CHECK(my_alloc_check(arena, len, name));
- buf.cutn(name, len);
- _name.set(name, len);
-
- len = parse_encode_length(buf);
- if (len > buf.size()) {
- LOG(WARNING) << "MysqlReply::Column::Parse: origin_name length " << len
- << " exceeds remaining buffer size " << buf.size();
- return PARSE_ERROR_ABSOLUTELY_WRONG;
+ MY_PARSE_CHECK(parse_column_string(payload, arena, &_catalog, "catalog"));
+ MY_PARSE_CHECK(parse_column_string(payload, arena, &_database,
"database"));
+ MY_PARSE_CHECK(parse_column_string(payload, arena, &_table, "table"));
+ MY_PARSE_CHECK(parse_column_string(payload, arena, &_origin_table,
"origin_table"));
+ MY_PARSE_CHECK(parse_column_string(payload, arena, &_name, "name"));
+ MY_PARSE_CHECK(parse_column_string(payload, arena, &_origin_name,
"origin_name"));
+ {
+ uint8_t filler_length = 0; // length of the fixed fields below
+ MY_PARSE_CHECK(parse_fixed(payload, &filler_length, 1));
}
- char* origin_name = nullptr;
- MY_ALLOC_CHECK(my_alloc_check(arena, len, origin_name));
- buf.cutn(origin_name, len);
- _origin_name.set(origin_name, len);
- buf.pop_front(1);
{
uint8_t tmp[2];
- buf.cutn(tmp, sizeof(tmp));
+ MY_PARSE_CHECK(parse_fixed(payload, tmp, sizeof(tmp)));
_charset = mysql_uint2korr(tmp);
}
{
uint8_t tmp[4];
- buf.cutn(tmp, sizeof(tmp));
+ MY_PARSE_CHECK(parse_fixed(payload, tmp, sizeof(tmp)));
_length = mysql_uint4korr(tmp);
}
- buf.cut1((char*)&_type);
+ MY_PARSE_CHECK(parse_fixed(payload, &_type, 1));
{
uint8_t tmp[2];
- buf.cutn(tmp, sizeof(tmp));
+ MY_PARSE_CHECK(parse_fixed(payload, tmp, sizeof(tmp)));
_flag = (MysqlFieldFlag)mysql_uint2korr(tmp);
}
- buf.cut1((char*)&_decimal);
- buf.pop_front(2);
+ MY_PARSE_CHECK(parse_fixed(payload, &_decimal, 1));
+ {
+ uint8_t filler[2]; // reserved
+ MY_PARSE_CHECK(parse_fixed(payload, filler, sizeof(filler)));
+ }
set_parsed();
return PARSE_OK;
}
@@ -684,33 +735,36 @@ ParseError MysqlReply::Ok::Parse(butil::IOBuf& buf,
butil::Arena* arena) {
return PARSE_OK;
}
MysqlHeader header;
- if (!parse_header(buf, &header)) {
+ butil::IOBuf payload;
+ if (!parse_header(buf, &header, &payload)) {
return PARSE_ERROR_NOT_ENOUGH_DATA;
}
+ payload.pop_front(1);
- uint64_t old_size, new_size;
- old_size = buf.size();
- buf.pop_front(1);
-
- _affect_row = parse_encode_length(buf);
- _index = parse_encode_length(buf);
+ if (!parse_encode_length(payload, &_affect_row)) {
+ LOG(WARNING) << "MysqlReply::Ok::Parse: truncated affected-rows value";
+ return PARSE_ERROR_ABSOLUTELY_WRONG;
+ }
+ if (!parse_encode_length(payload, &_index)) {
+ LOG(WARNING) << "MysqlReply::Ok::Parse: truncated last-insert-id
value";
+ return PARSE_ERROR_ABSOLUTELY_WRONG;
+ }
{
uint8_t tmp[2];
- buf.cutn(tmp, sizeof(tmp));
+ MY_PARSE_CHECK(parse_fixed(payload, tmp, sizeof(tmp)));
_status = mysql_uint2korr(tmp);
}
{
uint8_t tmp[2];
- buf.cutn(tmp, sizeof(tmp));
+ MY_PARSE_CHECK(parse_fixed(payload, tmp, sizeof(tmp)));
_warning = mysql_uint2korr(tmp);
}
- new_size = buf.size();
- if (old_size - new_size < header.payload_size) {
- const int64_t len = header.payload_size - (old_size - new_size);
+ const int64_t len = (int64_t)payload.size();
+ if (len > 0) {
char* msg = nullptr;
MY_ALLOC_CHECK(my_alloc_check(arena, len, msg));
- buf.cutn(msg, len);
+ payload.cutn(msg, len);
_msg.set(msg, len);
}
set_parsed();
@@ -722,58 +776,71 @@ ParseError MysqlReply::Eof::Parse(butil::IOBuf& buf) {
return PARSE_OK;
}
MysqlHeader header;
- if (!parse_header(buf, &header)) {
+ butil::IOBuf payload;
+ if (!parse_header(buf, &header, &payload)) {
return PARSE_ERROR_NOT_ENOUGH_DATA;
}
- buf.pop_front(1);
+ payload.pop_front(1);
{
uint8_t tmp[2];
- buf.cutn(tmp, sizeof(tmp));
+ MY_PARSE_CHECK(parse_fixed(payload, tmp, sizeof(tmp)));
_warning = mysql_uint2korr(tmp);
}
{
uint8_t tmp[2];
- buf.cutn(tmp, sizeof(tmp));
+ MY_PARSE_CHECK(parse_fixed(payload, tmp, sizeof(tmp)));
_status = mysql_uint2korr(tmp);
}
set_parsed();
return PARSE_OK;
}
-ParseError MysqlReply::Error::Parse(butil::IOBuf& buf, butil::Arena* arena) {
+ParseError MysqlReply::Error::Parse(butil::IOBuf& buf, butil::Arena* arena,
bool protocol41) {
if (is_parsed()) {
return PARSE_OK;
}
MysqlHeader header;
- if (!parse_header(buf, &header)) {
+ butil::IOBuf payload;
+ if (!parse_header(buf, &header, &payload)) {
return PARSE_ERROR_NOT_ENOUGH_DATA;
}
- buf.pop_front(1); // 0xFF
+ // ERR payload: 0xFF(1) + error code(2), followed by either
+ // - protocol-4.1 layout ('#'(1) + sql_state(5) + message), used in the
+ // command phase and after the client's HandshakeResponse41, or
+ // - pre-4.1 layout (the message directly), used by initial-handshake
+ // errors sent before capabilities are negotiated, e.g. "Too many
+ // connections". The layout is selected by the caller from the
+ // connection phase -- the first message byte cannot distinguish the
+ // two, since a legacy message may itself start with '#'.
+ payload.pop_front(1); // 0xFF
{
uint8_t tmp[2];
- buf.cutn(tmp, sizeof(tmp));
+ MY_PARSE_CHECK(parse_fixed(payload, tmp, sizeof(tmp)));
_errcode = mysql_uint2korr(tmp);
}
- buf.pop_front(1); // '#'
- // 5 byte server status
- char* status = nullptr;
- MY_ALLOC_CHECK(my_alloc_check(arena, 5, status));
- buf.cutn(status, 5);
- _status.set(status, 5);
+ if (protocol41) {
+ uint8_t sharp = 0;
+ MY_PARSE_CHECK(parse_fixed(payload, &sharp, 1));
+ if (sharp != '#') {
+ LOG(WARNING) << "MysqlReply::Error::Parse: expected '#' before
sql_state, got "
+ << sharp;
+ return PARSE_ERROR_ABSOLUTELY_WRONG;
+ }
+ char* status = nullptr;
+ MY_ALLOC_CHECK(my_alloc_check(arena, 5, status));
+ MY_PARSE_CHECK(parse_fixed(payload, status, 5));
+ _status.set(status, 5);
+ } else {
+ _status.set(nullptr, 0);
+ }
// error message, Null-Terminated string.
- // payload layout consumed so far: 0xFF(1) + errcode(2) + '#'(1) +
- // sql_state(5) = 9 bytes; guard against a malformed short packet to avoid
- // an unsigned underflow producing a huge length.
- if (header.payload_size < 9) {
- LOG(WARNING) << "MysqlReply::Error::Parse: truncated ERR packet,
payload_size "
- << header.payload_size << " < 9
(0xFF+errcode+'#'+sql_state)";
- return PARSE_ERROR_ABSOLUTELY_WRONG;
+ const uint64_t len = payload.size();
+ if (len > 0) {
+ char* msg = nullptr;
+ MY_ALLOC_CHECK(my_alloc_check(arena, len, msg));
+ payload.cutn(msg, len);
+ _msg.set(msg, len);
}
- uint64_t len = header.payload_size - 9;
- char* msg = nullptr;
- MY_ALLOC_CHECK(my_alloc_check(arena, len, msg));
- buf.cutn(msg, len);
- _msg.set(msg, len);
set_parsed();
return PARSE_OK;
}
@@ -788,16 +855,17 @@ ParseError MysqlReply::Row::Parse(butil::IOBuf& buf,
return PARSE_OK;
}
MysqlHeader header;
- if (!parse_header(buf, &header)) {
+ butil::IOBuf payload;
+ if (!parse_header(buf, &header, &payload)) {
return PARSE_ERROR_NOT_ENOUGH_DATA;
}
if (!binary) { // mysql text protocol
for (uint64_t i = 0; i < column_count; ++i) {
- MY_PARSE_CHECK(fields[i].Parse(buf, columns + i, arena));
+ MY_PARSE_CHECK(fields[i].Parse(payload, columns + i, arena));
}
} else { // mysql binary protocol
uint8_t hdr = 0;
- buf.cut1((char*)&hdr);
+ MY_PARSE_CHECK(parse_fixed(payload, &hdr, 1));
if (hdr != 0x00) {
LOG(WARNING) << "MysqlReply::Row::Parse: binary row packet header
byte is "
<< unsigned(hdr) << ", expected 0x00";
@@ -813,9 +881,9 @@ ParseError MysqlReply::Row::Parse(butil::IOBuf& buf,
for (uint64_t i = 0; i < size; ++i) {
null_mask[i] = 0;
}
- buf.cutn(null_mask, size);
+ MY_PARSE_CHECK(parse_fixed(payload, null_mask, (size_t)size));
for (uint64_t i = 0; i < column_count; ++i) {
- MY_PARSE_CHECK(fields[i].Parse(buf, columns + i, i, column_count,
null_mask, arena));
+ MY_PARSE_CHECK(fields[i].Parse(payload, columns + i, i,
column_count, null_mask, arena));
}
}
set_parsed();
@@ -833,7 +901,11 @@ ParseError MysqlReply::Field::Parse(butil::IOBuf& buf,
// is unsigned flag set
_unsigned = column->_flag & MYSQL_UNSIGNED_FLAG;
// parse encode length
- const uint64_t len = parse_encode_length(buf);
+ uint64_t len = 0;
+ if (!parse_encode_length(buf, &len)) {
+ LOG(WARNING) << "MysqlReply::Field::Parse: truncated length-encoded
field length";
+ return PARSE_ERROR_ABSOLUTELY_WRONG;
+ }
// is it null?
if (len == 0 && !(column->_flag & MYSQL_NOT_NULL_FLAG)) {
_is_nil = true;
@@ -952,20 +1024,20 @@ ParseError MysqlReply::Field::Parse(butil::IOBuf& buf,
break;
case MYSQL_FIELD_TYPE_TINY:
if (column->_flag & MYSQL_UNSIGNED_FLAG) {
- buf.cut1((char*)&_data.tiny);
+ MY_PARSE_CHECK(parse_fixed(buf, &_data.tiny, 1));
} else {
- buf.cut1((char*)&_data.stiny);
+ MY_PARSE_CHECK(parse_fixed(buf, &_data.stiny, 1));
}
break;
case MYSQL_FIELD_TYPE_SHORT:
case MYSQL_FIELD_TYPE_YEAR:
if (column->_flag & MYSQL_UNSIGNED_FLAG) {
uint8_t* p = (uint8_t*)&_data.small;
- buf.cutn(p, 2);
+ MY_PARSE_CHECK(parse_fixed(buf, p, 2));
_data.small = mysql_uint2korr(p);
} else {
uint8_t* p = (uint8_t*)&_data.ssmall;
- buf.cutn(p, 2);
+ MY_PARSE_CHECK(parse_fixed(buf, p, 2));
_data.ssmall = (int16_t)mysql_uint2korr(p);
}
break;
@@ -973,32 +1045,32 @@ ParseError MysqlReply::Field::Parse(butil::IOBuf& buf,
case MYSQL_FIELD_TYPE_LONG:
if (column->_flag & MYSQL_UNSIGNED_FLAG) {
uint8_t* p = (uint8_t*)&_data.integer;
- buf.cutn(p, 4);
+ MY_PARSE_CHECK(parse_fixed(buf, p, 4));
_data.integer = mysql_uint4korr(p);
} else {
uint8_t* p = (uint8_t*)&_data.sinteger;
- buf.cutn(p, 4);
+ MY_PARSE_CHECK(parse_fixed(buf, p, 4));
_data.sinteger = (int32_t)mysql_uint4korr(p);
}
break;
case MYSQL_FIELD_TYPE_LONGLONG:
if (column->_flag & MYSQL_UNSIGNED_FLAG) {
uint8_t* p = (uint8_t*)&_data.bigint;
- buf.cutn(p, 8);
+ MY_PARSE_CHECK(parse_fixed(buf, p, 8));
_data.bigint = mysql_uint8korr(p);
} else {
uint8_t* p = (uint8_t*)&_data.sbigint;
- buf.cutn(p, 8);
+ MY_PARSE_CHECK(parse_fixed(buf, p, 8));
_data.sbigint = (int64_t)mysql_uint8korr(p);
}
break;
case MYSQL_FIELD_TYPE_FLOAT: {
uint8_t* p = (uint8_t*)&_data.float32;
- buf.cutn(p, 4);
+ MY_PARSE_CHECK(parse_fixed(buf, p, 4));
} break;
case MYSQL_FIELD_TYPE_DOUBLE: {
uint8_t* p = (uint8_t*)&_data.float64;
- buf.cutn(p, 8);
+ MY_PARSE_CHECK(parse_fixed(buf, p, 8));
} break;
case MYSQL_FIELD_TYPE_DECIMAL:
case MYSQL_FIELD_TYPE_NEWDECIMAL:
@@ -1014,7 +1086,11 @@ ParseError MysqlReply::Field::Parse(butil::IOBuf& buf,
case MYSQL_FIELD_TYPE_STRING:
case MYSQL_FIELD_TYPE_GEOMETRY:
case MYSQL_FIELD_TYPE_JSON: {
- const uint64_t len = parse_encode_length(buf);
+ uint64_t len = 0;
+ if (!parse_encode_length(buf, &len)) {
+ LOG(WARNING) << "MysqlReply::Field::Parse (binary): truncated
string field length";
+ return PARSE_ERROR_ABSOLUTELY_WRONG;
+ }
// is it null?
if (len == 0 && !(column->_flag & MYSQL_NOT_NULL_FLAG)) {
_is_nil = true;
@@ -1060,7 +1136,11 @@ ParseError
MysqlReply::Field::ParseBinaryTime(butil::IOBuf& buf,
butil::StringPiece& str,
butil::Arena* arena) {
- const uint64_t len = parse_encode_length(buf);
+ uint64_t len = 0;
+ if (!parse_encode_length(buf, &len)) {
+ LOG(ERROR) << "invalid TIME packet length";
+ return PARSE_ERROR_ABSOLUTELY_WRONG;
+ }
// A length of 0, 8 or 12 are the only legal binary TIME encodings.
Anything
// else is a malformed packet -- reject it rather than reading past the
value.
// NOTE: len == 0 is NOT a NULL value (NULL is signalled by the row
@@ -1071,7 +1151,7 @@ ParseError
MysqlReply::Field::ParseBinaryTime(butil::IOBuf& buf,
return PARSE_ERROR_ABSOLUTELY_WRONG;
}
// Never read more value bytes than the packet actually carries.
- if (len > buf.size()) {
+ if ((uint64_t)len > buf.size()) {
LOG(ERROR) << "TIME value length " << len << " exceeds buffer size "
<< buf.size();
return PARSE_ERROR_ABSOLUTELY_WRONG;
}
@@ -1111,12 +1191,12 @@ ParseError
MysqlReply::Field::ParseBinaryTime(butil::IOBuf& buf,
uint8_t neg = 0, hour = 0, min = 0, sec = 0;
if (len >= 8) {
- buf.cut1((char*)&neg);
- buf.cutn(&day, 4);
+ MY_PARSE_CHECK(parse_fixed(buf, &neg, 1));
+ MY_PARSE_CHECK(parse_fixed(buf, &day, 4));
day = mysql_uint4korr((uint8_t*)&day);
- buf.cut1((char*)&hour);
- buf.cut1((char*)&min);
- buf.cut1((char*)&sec);
+ MY_PARSE_CHECK(parse_fixed(buf, &hour, 1));
+ MY_PARSE_CHECK(parse_fixed(buf, &min, 1));
+ MY_PARSE_CHECK(parse_fixed(buf, &sec, 1));
}
// Validate field ranges so the formatted output cannot overflow the buffer
@@ -1185,7 +1265,11 @@ ParseError
MysqlReply::Field::ParseBinaryDataTime(butil::IOBuf& buf,
const MysqlReply::Column*
column,
butil::StringPiece& str,
butil::Arena* arena) {
- const uint64_t len = parse_encode_length(buf);
+ uint64_t len = 0;
+ if (!parse_encode_length(buf, &len)) {
+ LOG(ERROR) << "illegal date time length";
+ return PARSE_ERROR_ABSOLUTELY_WRONG;
+ }
// A length of 0, 4, 7 or 11 are the only legal binary DATE/DATETIME/
// TIMESTAMP encodings. Reject anything else rather than over-reading.
// NOTE: len == 0 is NOT a NULL value (NULL is signalled by the row
@@ -1197,7 +1281,7 @@ ParseError
MysqlReply::Field::ParseBinaryDataTime(butil::IOBuf& buf,
return PARSE_ERROR_ABSOLUTELY_WRONG;
}
// Never read more value bytes than the packet actually carries.
- if (len > buf.size()) {
+ if ((uint64_t)len > buf.size()) {
LOG(ERROR) << "DATETIME value length " << len << " exceeds buffer size
" << buf.size();
return PARSE_ERROR_ABSOLUTELY_WRONG;
}
@@ -1244,15 +1328,15 @@ ParseError
MysqlReply::Field::ParseBinaryDataTime(butil::IOBuf& buf,
uint16_t year = 0;
uint8_t month = 0, day = 0, hour = 0, min = 0, sec = 0;
if (len >= 4) {
- buf.cutn(&year, 2);
+ MY_PARSE_CHECK(parse_fixed(buf, &year, 2));
year = mysql_uint2korr((uint8_t*)&year);
- buf.cut1((char*)&month);
- buf.cut1((char*)&day);
+ MY_PARSE_CHECK(parse_fixed(buf, &month, 1));
+ MY_PARSE_CHECK(parse_fixed(buf, &day, 1));
}
if (len >= 7) {
- buf.cut1((char*)&hour);
- buf.cut1((char*)&min);
- buf.cut1((char*)&sec);
+ MY_PARSE_CHECK(parse_fixed(buf, &hour, 1));
+ MY_PARSE_CHECK(parse_fixed(buf, &min, 1));
+ MY_PARSE_CHECK(parse_fixed(buf, &sec, 1));
}
// Validate field ranges: year < 10000 keeps the 4-digit year within bounds
@@ -1323,7 +1407,7 @@ ParseError
MysqlReply::Field::ParseMicrosecs(butil::IOBuf& buf, uint8_t decimal,
// Always consume the 4 microsecond bytes present on the wire (the caller
// only invokes this when the value length includes them); format them only
// when the column declares 1..6 fractional digits (0 / 0x1f == no
fraction).
- buf.cutn((char*)µsecs, 4);
+ MY_PARSE_CHECK(parse_fixed(buf, µsecs, 4));
if (decimal == 0 || decimal > 6) {
return PARSE_OK;
}
@@ -1453,30 +1537,35 @@ ParseError
MysqlReply::PrepareOk::Header::Parse(butil::IOBuf& buf) {
}
MysqlHeader header;
- if (!parse_header(buf, &header)) {
+ butil::IOBuf payload;
+ if (!parse_header(buf, &header, &payload)) {
return PARSE_ERROR_NOT_ENOUGH_DATA;
}
- buf.pop_front(1);
+ payload.pop_front(1);
{
uint8_t tmp[4];
- buf.cutn(tmp, sizeof(tmp));
+ MY_PARSE_CHECK(parse_fixed(payload, tmp, sizeof(tmp)));
_stmt_id = mysql_uint4korr(tmp);
}
{
uint8_t tmp[2];
- buf.cutn(tmp, sizeof(tmp));
+ MY_PARSE_CHECK(parse_fixed(payload, tmp, sizeof(tmp)));
_column_count = mysql_uint2korr(tmp);
}
{
uint8_t tmp[2];
- buf.cutn(tmp, sizeof(tmp));
+ MY_PARSE_CHECK(parse_fixed(payload, tmp, sizeof(tmp)));
_param_count = mysql_uint2korr(tmp);
}
- buf.pop_front(1);
+ {
+ uint8_t filler = 0;
+ MY_PARSE_CHECK(parse_fixed(payload, &filler, 1));
+ (void)filler;
+ }
{
uint8_t tmp[2];
- buf.cutn(tmp, sizeof(tmp));
+ MY_PARSE_CHECK(parse_fixed(payload, tmp, sizeof(tmp)));
_warning = mysql_uint2korr(tmp);
}
diff --git a/src/brpc/policy/mysql/mysql_reply.h
b/src/brpc/policy/mysql/mysql_reply.h
index 14bf0dbd..bad099ed 100644
--- a/src/brpc/policy/mysql/mysql_reply.h
+++ b/src/brpc/policy/mysql/mysql_reply.h
@@ -195,7 +195,7 @@ public:
butil::StringPiece msg() const;
private:
- ParseError Parse(butil::IOBuf& buf, butil::Arena* arena);
+ ParseError Parse(butil::IOBuf& buf, butil::Arena* arena, bool
protocol41);
DISALLOW_COPY_AND_ASSIGN(Error);
friend class MysqlReply;
@@ -328,11 +328,19 @@ public:
public:
MysqlReply();
+ // Legacy signature kept for binary compatibility with prebuilt clients;
+ // assumes the protocol-4.1 ERR layout (the command phase default).
ParseError ConsumePartialIOBuf(butil::IOBuf& buf,
butil::Arena* arena,
bool is_auth,
MysqlStmtType stmt_type,
bool* more_results);
+ ParseError ConsumePartialIOBuf(butil::IOBuf& buf,
+ butil::Arena* arena,
+ bool is_auth,
+ MysqlStmtType stmt_type,
+ bool* more_results,
+ bool protocol41);
void Swap(MysqlReply& other);
void Print(std::ostream& os) const;
// response type
diff --git a/test/brpc_mysql_reply_parse_unittest.cpp
b/test/brpc_mysql_reply_parse_unittest.cpp
index f10773db..629ae12b 100644
--- a/test/brpc_mysql_reply_parse_unittest.cpp
+++ b/test/brpc_mysql_reply_parse_unittest.cpp
@@ -17,6 +17,7 @@
#include <gtest/gtest.h>
+#include <limits>
#include <string>
#include <vector>
@@ -38,8 +39,9 @@ void AppendPacket(std::string* out, uint8_t seq, const
std::string& payload) {
out->append(payload);
}
-// A minimal text-protocol column definition for a single VAR_STRING column.
-std::string MakeColumnDef() {
+// A minimal text-protocol column definition, parametrized by field type,
+// flags and decimals for binary-protocol tests.
+std::string MakeColumnDef(uint8_t type, uint16_t flags = 0, uint8_t decimals =
0) {
std::string p;
for (int i = 0; i < 6; ++i) { //
catalog/database/table/origin_table/name/origin_name
p.push_back(0x00); // length-encoded empty string
@@ -48,14 +50,18 @@ std::string MakeColumnDef() {
p.push_back(0x21); // charset (2 bytes)
p.push_back(0x00);
p.append(4, '\x00'); // column length (4 bytes)
- p.push_back((char)brpc::MYSQL_FIELD_TYPE_VAR_STRING); // field type
- p.push_back(0x00); // flag (2 bytes): not-null and unsigned
both off
- p.push_back(0x00);
- p.push_back(0x00); // decimals
+ p.push_back((char)type); // field type
+ p.push_back((char)(flags & 0xFF)); // flag (2 bytes)
+ p.push_back((char)(flags >> 8));
+ p.push_back((char)decimals); // decimals
p.append(2, '\x00'); // filler
return p;
}
+std::string MakeColumnDef() {
+ return MakeColumnDef(brpc::MYSQL_FIELD_TYPE_VAR_STRING);
+}
+
std::string MakeEof() {
std::string p;
p.push_back((char)0xFE); // EOF header
@@ -164,4 +170,647 @@ TEST(MysqlReplyParseTest, AcceptWellFormedTextField) {
ASSERT_EQ("hi", reply.next().field(0).string());
}
+// A multi-byte (0xFC-prefixed) length-encoded integer whose value bytes are
+// all present must still decode correctly.
+TEST(MysqlReplyParseTest, AcceptMultiByteTextFieldLength) {
+ std::string field;
+ field.push_back((char)0xFC); // 2-byte length-encoded prefix
+ field.push_back((char)0x04); // length 4
+ field.push_back((char)0x00);
+ field.append("abcd");
+
+ butil::IOBuf buf;
+ buf.append(MakeTextResultSet(field, true));
+
+ brpc::MysqlReply reply;
+ butil::Arena arena;
+ bool more_results = false;
+ brpc::ParseError rc = reply.ConsumePartialIOBuf(
+ buf, &arena, false, brpc::MYSQL_NORMAL_STATEMENT, &more_results);
+ ASSERT_EQ(brpc::PARSE_OK, rc);
+ ASSERT_TRUE(reply.is_resultset());
+ ASSERT_EQ("abcd", reply.next().field(0).string());
+}
+
+// A column definition whose table-name length-encoded integer is truncated
+// (0xFE prefix promises 8 value bytes but only 2 follow) must be rejected.
+// Before the fix, the parser read uninitialized stack memory as the length.
+TEST(MysqlReplyParseTest, RejectTruncatedLenEncInColumnDef) {
+ // (prefix, number of value bytes actually appended)
+ const struct {
+ uint8_t prefix;
+ size_t value_bytes;
+ } cases[] = {
+ {0xFC, 0}, // needs 2, has 0
+ {0xFC, 1}, // needs 2, has 1
+ {0xFD, 1}, // needs 3, has 1
+ {0xFD, 2}, // needs 3, has 2
+ {0xFE, 2}, // needs 8, has 2
+ {0xFE, 7}, // needs 8, has 7
+ };
+ for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) {
+ std::string col;
+ col.push_back(3);
+ col.append("def"); // catalog
+ col.push_back(2);
+ col.append("db"); // database
+ col.push_back((char)cases[i].prefix);
+ col.append(cases[i].value_bytes, '\xff'); // truncated table length
+
+ std::string wire;
+ AppendPacket(&wire, 1, std::string(1, '\x01')); // 1-column result set
+ AppendPacket(&wire, 2, col);
+
+ butil::IOBuf buf;
+ buf.append(wire);
+
+ brpc::MysqlReply reply;
+ butil::Arena arena;
+ bool more_results = false;
+ brpc::ParseError rc = reply.ConsumePartialIOBuf(
+ buf, &arena, false, brpc::MYSQL_NORMAL_STATEMENT, &more_results);
+ ASSERT_EQ(brpc::PARSE_ERROR_ABSOLUTELY_WRONG, rc) << "case " << i;
+ }
+}
+
+// A row field whose length-encoded prefix promises value bytes that are not
+// in the packet must be rejected instead of reading uninitialized stack bytes.
+// NOTE: a 0xFE-prefixed row field cannot be tested here because a text row
+// whose first byte is 0xFE is indistinguishable from an EOF packet; that
+// prefix is covered by RejectTruncatedLenEncInColumnDef above.
+TEST(MysqlReplyParseTest, RejectTruncatedLenEncFieldLength) {
+ const struct {
+ uint8_t prefix;
+ size_t value_bytes;
+ } cases[] = {
+ {0xFC, 0}, // needs 2, has 0
+ {0xFD, 1}, // needs 3, has 1
+ {0xFD, 2}, // needs 3, has 2
+ };
+ for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) {
+ std::string field;
+ field.push_back((char)cases[i].prefix);
+ field.append(cases[i].value_bytes, '\xff');
+
+ butil::IOBuf buf;
+ buf.append(MakeTextResultSet(field, false));
+
+ brpc::MysqlReply reply;
+ butil::Arena arena;
+ bool more_results = false;
+ brpc::ParseError rc = reply.ConsumePartialIOBuf(
+ buf, &arena, false, brpc::MYSQL_NORMAL_STATEMENT, &more_results);
+ ASSERT_EQ(brpc::PARSE_ERROR_ABSOLUTELY_WRONG, rc) << "case " << i;
+ }
+}
+
+// An OK packet whose affected-rows length-encoded integer is truncated must
+// be rejected (previously the uninitialized stack value was used directly).
+TEST(MysqlReplyParseTest, RejectTruncatedOkPacket) {
+ const std::string payloads[] = {
+ std::string("\x00\xFC", 2), // 0x00 marker + 0xFC, no value
bytes
+ std::string("\x00\xFE\xff\xff", 4), // 0x00 marker + 0xFE, only 2 of
8 bytes
+ };
+ for (size_t i = 0; i < sizeof(payloads) / sizeof(payloads[0]); ++i) {
+ std::string wire;
+ AppendPacket(&wire, 0, payloads[i]);
+
+ butil::IOBuf buf;
+ buf.append(wire);
+
+ brpc::MysqlReply reply;
+ butil::Arena arena;
+ bool more_results = false;
+ brpc::ParseError rc = reply.ConsumePartialIOBuf(
+ buf, &arena, false, brpc::MYSQL_NORMAL_STATEMENT, &more_results);
+ ASSERT_EQ(brpc::PARSE_ERROR_ABSOLUTELY_WRONG, rc) << "case " << i;
+ }
+}
+
+// A truncated length prefix must not borrow bytes from a coalesced next
+// packet: the parser only sees the current packet's payload, so even though
+// the following packet's bytes would complete the 0xFC value, the OK packet
+// is rejected instead of silently desyncing the stream.
+TEST(MysqlReplyParseTest, RejectLenEncCrossingPacketBoundary) {
+ std::string wire;
+ AppendPacket(&wire, 0, std::string("\x00\xFC", 2)); // OK marker + 0xFC
prefix
+ AppendPacket(&wire, 1, std::string("\x02\x00", 2)); // coalesced next
packet
+
+ butil::IOBuf buf;
+ buf.append(wire);
+
+ brpc::MysqlReply reply;
+ butil::Arena arena;
+ bool more_results = false;
+ brpc::ParseError rc = reply.ConsumePartialIOBuf(
+ buf, &arena, false, brpc::MYSQL_NORMAL_STATEMENT, &more_results);
+ ASSERT_EQ(brpc::PARSE_ERROR_ABSOLUTELY_WRONG, rc);
+}
+
+// An affected-rows value of 2^64-1 (the largest encodable length-encoded
+// integer) must round-trip through the uint64_t storage, not be rejected as
+// a negative sentinel.
+TEST(MysqlReplyParseTest, AcceptUint64MaxAffectedRows) {
+ std::string payload;
+ payload.push_back('\x00'); // OK marker
+ payload.push_back((char)0xFE); // 8-byte length-encoded
prefix
+ payload.append(8, '\xff'); // affected rows = 2^64 - 1
+ payload.push_back('\x00'); // last insert id = 0
+ payload.append("\x02\x00", 2); // status flags
+ payload.append("\x00\x00", 2); // warnings
+
+ std::string wire;
+ AppendPacket(&wire, 0, payload);
+
+ butil::IOBuf buf;
+ buf.append(wire);
+
+ brpc::MysqlReply reply;
+ butil::Arena arena;
+ bool more_results = false;
+ brpc::ParseError rc = reply.ConsumePartialIOBuf(
+ buf, &arena, false, brpc::MYSQL_NORMAL_STATEMENT, &more_results);
+ ASSERT_EQ(brpc::PARSE_OK, rc);
+ ASSERT_TRUE(reply.is_ok());
+ ASSERT_EQ(std::numeric_limits<uint64_t>::max(), reply.ok().affect_row());
+}
+
+// Build a text result set whose header uses |count_wire| as the raw
+// length-encoded column count and carries |n_defs| column definitions,
+// with no rows.
+std::string MakeResultSetWithColumns(const std::string& count_wire, size_t
n_defs) {
+ std::string wire;
+ AppendPacket(&wire, 1, count_wire);
+ for (size_t i = 0; i < n_defs; ++i) {
+ AppendPacket(&wire, 2, MakeColumnDef());
+ }
+ AppendPacket(&wire, 3, MakeEof()); // EOF after column defs
+ AppendPacket(&wire, 4, MakeEof()); // EOF after (empty) rows
+ return wire;
+}
+
+// Build a binary-protocol (MYSQL_PREPARED_STATEMENT) result set with one
+// column of |col_def| and a single raw binary row |row| (0x00 marker + NULL
+// bitmap + field values). A trailing EOF packet is appended right after the
+// row, so any test whose row is truncated also proves that decoding cannot
+// borrow bytes from the coalesced next packet.
+std::string MakeBinaryResultSet(const std::string& col_def,
+ const std::string& row,
+ bool with_trailing_eof = true) {
+ std::string wire;
+ AppendPacket(&wire, 1, std::string(1, '\x01')); // 1-column result set
+ AppendPacket(&wire, 2, col_def);
+ AppendPacket(&wire, 3, MakeEof()); // EOF after column defs
+ AppendPacket(&wire, 4, row);
+ if (with_trailing_eof) {
+ AppendPacket(&wire, 5, MakeEof()); // EOF after rows
+ }
+ return wire;
+}
+
+brpc::ParseError ParseWire(const std::string& wire,
+ brpc::MysqlReply* reply,
+ butil::Arena* arena,
+ brpc::MysqlStmtType stmt_type =
brpc::MYSQL_NORMAL_STATEMENT,
+ bool is_auth = false,
+ bool protocol41 = true) {
+ butil::IOBuf buf;
+ buf.append(wire);
+ bool more_results = false;
+ return reply->ConsumePartialIOBuf(buf, arena, is_auth, stmt_type,
&more_results, protocol41);
+}
+
+// A result set whose column count needs the multi-byte 0xFC form (252..65535
+// columns) is a result set, not a prepare-ok: the dispatcher must not match
+// the wire byte 0xFC against the synthetic MYSQL_RSP_PREPARE_OK value.
+TEST(MysqlReplyParseTest, AcceptMultiByteColumnCount) {
+ std::string count_wire;
+ count_wire.push_back((char)0xFC); // 2-byte length-encoded prefix
+ count_wire.push_back((char)0x00); // 256, little-endian
+ count_wire.push_back((char)0x01);
+
+ brpc::MysqlReply reply;
+ butil::Arena arena;
+ ASSERT_EQ(brpc::PARSE_OK, ParseWire(MakeResultSetWithColumns(count_wire,
256), &reply, &arena));
+ ASSERT_TRUE(reply.is_resultset());
+ ASSERT_EQ(256u, reply.column_count());
+}
+
+// Column count 251 needs the multi-byte 0xFC form on the wire (the single
+// byte 0xFB is the length-encoded NULL marker, so a compliant server never
+// emits it as a count); it must dispatch to the result-set branch even though
+// 0xFB/0xFC collide with synthetic MysqlRspType values.
+TEST(MysqlReplyParseTest, AcceptMultiByte251ColumnCount) {
+ std::string count_wire;
+ count_wire.push_back((char)0xFC); // 2-byte length-encoded prefix
+ count_wire.push_back((char)0xFB); // 251, little-endian
+ count_wire.push_back((char)0x00);
+
+ brpc::MysqlReply reply;
+ butil::Arena arena;
+ ASSERT_EQ(brpc::PARSE_OK, ParseWire(MakeResultSetWithColumns(count_wire,
251), &reply, &arena));
+ ASSERT_TRUE(reply.is_resultset());
+ ASSERT_EQ(251u, reply.column_count());
+}
+
+// A truncated multi-byte column count (0xFC with no value bytes) must be
+// rejected by the result-set header parser instead of being misclassified as
+// a prepare-ok and read with unchecked fixed-width cuts.
+TEST(MysqlReplyParseTest, RejectTruncatedMultiByteColumnCount) {
+ std::string wire;
+ AppendPacket(&wire, 1, std::string(1, '\xFC'));
+
+ brpc::MysqlReply reply;
+ butil::Arena arena;
+ ASSERT_EQ(brpc::PARSE_ERROR_ABSOLUTELY_WRONG, ParseWire(wire, &reply,
&arena));
+}
+
+// A long 0xFE-leading packet is a length-encoded column count (>= 2^24), not
+// an EOF; it must be rejected by the column-count cap instead of being
+// parsed as an EOF packet.
+TEST(MysqlReplyParseTest, RejectHugeColumnCountFePrefix) {
+ std::string count_wire;
+ count_wire.push_back((char)0xFE); // 8-byte length-encoded prefix
+ count_wire.append(8, '\xff'); // claims 2^64 - 1 columns
+
+ std::string wire;
+ AppendPacket(&wire, 1, count_wire);
+
+ brpc::MysqlReply reply;
+ butil::Arena arena;
+ ASSERT_EQ(brpc::PARSE_ERROR_ABSOLUTELY_WRONG, ParseWire(wire, &reply,
&arena));
+}
+
+// A short 0xFE-leading packet (< 9 payload bytes) is still an EOF reply.
+TEST(MysqlReplyParseTest, AcceptStandaloneEofReply) {
+ std::string wire;
+ AppendPacket(&wire, 0, MakeEof());
+
+ brpc::MysqlReply reply;
+ butil::Arena arena;
+ ASSERT_EQ(brpc::PARSE_OK, ParseWire(wire, &reply, &arena));
+ ASSERT_TRUE(reply.is_eof());
+}
+
+// A row packet starting with a long 0xFE length-encoded value is a row, not
+// an EOF: within a result set, EOF is only a SHORT 0xFE-leading packet.
+TEST(MysqlReplyParseTest, RejectRowStartingWithFeLenenc) {
+ std::string field;
+ field.push_back((char)0xFE); // 8-byte length-encoded prefix
+ field.append(8, '\xff'); // claims a huge value, 0 bytes present
+
+ std::string wire;
+ AppendPacket(&wire, 1, std::string(1, '\x01')); // 1-column result set
+ AppendPacket(&wire, 2, MakeColumnDef());
+ AppendPacket(&wire, 3, MakeEof()); // EOF after column defs
+ AppendPacket(&wire, 4, field); // the row
+ AppendPacket(&wire, 5, MakeEof()); // real EOF after rows
+
+ brpc::MysqlReply reply;
+ butil::Arena arena;
+ ASSERT_EQ(brpc::PARSE_ERROR_ABSOLUTELY_WRONG, ParseWire(wire, &reply,
&arena));
+}
+
+// A well-formed prepare-ok reply (COM_STMT_PREPARE response) parses.
+TEST(MysqlReplyParseTest, AcceptPrepareOk) {
+ std::string payload;
+ payload.push_back('\x00'); // OK-like marker
+ payload.append("\x01\x00\x00\x00", 4); // statement id
+ payload.append("\x00\x00", 2); // column count
+ payload.append("\x00\x00", 2); // param count
+ payload.push_back('\x00'); // filler
+ payload.append("\x00\x00", 2); // warnings
+
+ std::string wire;
+ AppendPacket(&wire, 0, payload);
+
+ brpc::MysqlReply reply;
+ butil::Arena arena;
+ ASSERT_EQ(brpc::PARSE_OK,
+ ParseWire(wire, &reply, &arena, brpc::MYSQL_NEED_PREPARE));
+ ASSERT_TRUE(reply.is_prepare_ok());
+}
+
+// A truncated prepare-ok header must be rejected instead of reading
+// uninitialized stack bytes as statement id / column / param counts.
+TEST(MysqlReplyParseTest, RejectTruncatedPrepareOk) {
+ std::string wire;
+ AppendPacket(&wire, 0, std::string("\x00\x01\x00\x00", 4));
+
+ brpc::MysqlReply reply;
+ butil::Arena arena;
+ ASSERT_EQ(brpc::PARSE_ERROR_ABSOLUTELY_WRONG,
+ ParseWire(wire, &reply, &arena, brpc::MYSQL_NEED_PREPARE));
+}
+
+// A handshake greeting that ends before the 4-byte thread id must be
+// rejected; previously the unchecked cutn left tmp partially uninitialized
+// and Auth::Parse continued successfully with garbage thread id.
+TEST(MysqlReplyParseTest, RejectTruncatedGreeting) {
+ std::string payload;
+ payload.push_back('\x0a'); // protocol version 10
+ payload.append("5.7.99-fake\x00", 12); // NUL-terminated server version
+ payload.append("\x01\x02", 2); // only 2 of the 4 thread-id bytes
+
+ std::string wire;
+ AppendPacket(&wire, 0, payload);
+
+ brpc::MysqlReply reply;
+ butil::Arena arena;
+ ASSERT_EQ(brpc::PARSE_ERROR_ABSOLUTELY_WRONG, ParseWire(wire, &reply,
&arena, brpc::MYSQL_NORMAL_STATEMENT, /*is_auth=*/true));
+}
+
+// A handshake greeting whose server version is not NUL-terminated (the
+// delimiter was never reached) must be rejected.
+TEST(MysqlReplyParseTest, RejectUnterminatedGreetingVersion) {
+ std::string payload;
+ payload.push_back('\x0a'); // protocol version 10
+ payload.append("5.7.99-fake", 10); // no NUL terminator
+
+ std::string wire;
+ AppendPacket(&wire, 0, payload);
+
+ brpc::MysqlReply reply;
+ butil::Arena arena;
+ ASSERT_EQ(brpc::PARSE_ERROR_ABSOLUTELY_WRONG, ParseWire(wire, &reply,
&arena, brpc::MYSQL_NORMAL_STATEMENT, /*is_auth=*/true));
+}
+
+// A bare 0xFB (the length-encoded NULL marker, never a legal column count)
+// must not be accepted as a zero-column result set even when followed by
+// well-formed EOF packets.
+TEST(MysqlReplyParseTest, RejectBareFbHeader) {
+ std::string wire;
+ AppendPacket(&wire, 1, std::string(1, '\xFB'));
+ AppendPacket(&wire, 2, MakeEof());
+ AppendPacket(&wire, 3, MakeEof());
+
+ brpc::MysqlReply reply;
+ butil::Arena arena;
+ ASSERT_EQ(brpc::PARSE_ERROR_ABSOLUTELY_WRONG, ParseWire(wire, &reply,
&arena));
+}
+
+// An explicitly encoded zero column count (0xFC 0x00 0x00) is not a legal
+// result-set header either; a result set always carries at least one column.
+TEST(MysqlReplyParseTest, RejectZeroColumnCount) {
+ std::string count_wire;
+ count_wire.push_back((char)0xFC);
+ count_wire.push_back((char)0x00);
+ count_wire.push_back((char)0x00);
+
+ std::string wire;
+ AppendPacket(&wire, 1, count_wire);
+ AppendPacket(&wire, 2, MakeEof());
+ AppendPacket(&wire, 3, MakeEof());
+
+ brpc::MysqlReply reply;
+ butil::Arena arena;
+ ASSERT_EQ(brpc::PARSE_ERROR_ABSOLUTELY_WRONG, ParseWire(wire, &reply,
&arena));
+}
+
+// A column definition truncated in its fixed tail (missing the filler byte
+// after origin_name, or missing the final 2 reserved bytes after decimal)
+// must be rejected instead of being accepted via an unchecked pop_front.
+TEST(MysqlReplyParseTest, RejectTruncatedColumnDefTail) {
+ // |drop_tail| bytes removed from a complete column definition.
+ for (size_t drop_tail = 1; drop_tail <= 3; ++drop_tail) {
+ std::string col = MakeColumnDef();
+ col.resize(col.size() - drop_tail);
+
+ std::string wire;
+ AppendPacket(&wire, 1, std::string(1, '\x01')); // 1-column result set
+ AppendPacket(&wire, 2, col);
+
+ brpc::MysqlReply reply;
+ butil::Arena arena;
+ ASSERT_EQ(brpc::PARSE_ERROR_ABSOLUTELY_WRONG, ParseWire(wire, &reply,
&arena))
+ << "drop_tail " << drop_tail;
+ }
+}
+
+// An initial-handshake ERR packet (sent before CLIENT_PROTOCOL_41 is
+// negotiated, e.g. "Too many connections") carries no '#' and sql_state;
+// it must parse with the whole tail as the message instead of being
+// rejected as malformed.
+TEST(MysqlReplyParseTest, AcceptInitialHandshakeErr) {
+ std::string payload;
+ payload.push_back((char)0xFF); // ERR marker
+ payload.append("\x10\x04", 2); // error code 1040, little-endian
+ payload.append("Too many connections"); // message, no '#' + sql_state
+
+ std::string wire;
+ AppendPacket(&wire, 0, payload);
+
+ brpc::MysqlReply reply;
+ butil::Arena arena;
+ ASSERT_EQ(brpc::PARSE_OK,
+ ParseWire(wire, &reply, &arena, brpc::MYSQL_NORMAL_STATEMENT,
+ /*is_auth=*/true, /*protocol41=*/false));
+ ASSERT_TRUE(reply.is_error());
+ ASSERT_EQ(1040u, reply.error().errcode());
+ ASSERT_EQ(butil::StringPiece("Too many connections"), reply.error().msg());
+}
+
+// A legacy (pre-4.1) ERR message that itself starts with '#' must be kept
+// intact; the first message byte cannot be used to sniff the layout.
+TEST(MysqlReplyParseTest, AcceptLegacyErrMessageStartingWithHash) {
+ std::string payload;
+ payload.push_back((char)0xFF); // ERR marker
+ payload.append("\x60\x04", 2); // error code 1120, little-endian
+ payload.append("#quota exceeded"); // message starting with '#'
+
+ std::string wire;
+ AppendPacket(&wire, 0, payload);
+
+ brpc::MysqlReply reply;
+ butil::Arena arena;
+ ASSERT_EQ(brpc::PARSE_OK,
+ ParseWire(wire, &reply, &arena, brpc::MYSQL_NORMAL_STATEMENT,
+ /*is_auth=*/true, /*protocol41=*/false));
+ ASSERT_TRUE(reply.is_error());
+ ASSERT_EQ(1120u, reply.error().errcode());
+ ASSERT_EQ(butil::StringPiece("#quota exceeded"), reply.error().msg());
+ ASSERT_TRUE(reply.error().status().empty());
+
+ // The same short message must not be rejected as a truncated sql_state.
+ std::string short_payload;
+ short_payload.push_back((char)0xFF);
+ short_payload.append("\x50\x04", 2);
+ short_payload.append("#bad");
+ std::string short_wire;
+ AppendPacket(&short_wire, 0, short_payload);
+
+ brpc::MysqlReply short_reply;
+ butil::Arena short_arena;
+ ASSERT_EQ(brpc::PARSE_OK,
+ ParseWire(short_wire, &short_reply, &short_arena,
brpc::MYSQL_NORMAL_STATEMENT,
+ /*is_auth=*/true, /*protocol41=*/false));
+ ASSERT_EQ(butil::StringPiece("#bad"), short_reply.error().msg());
+}
+
+// A protocol-4.1 ERR whose message itself starts with '#': the marker and
+// sql_state still come from the wire layout, and the message keeps its '#'.
+TEST(MysqlReplyParseTest, AcceptProtocol41ErrMessageStartingWithHash) {
+ std::string payload;
+ payload.push_back((char)0xFF); // ERR marker
+ payload.append("\x1f\x04", 2); // error code 1055, little-endian
+ payload.push_back('#');
+ payload.append("42000", 5); // sql state
+ payload.append("#boom", 5); // message starting with '#'
+
+ std::string wire;
+ AppendPacket(&wire, 0, payload);
+
+ brpc::MysqlReply reply;
+ butil::Arena arena;
+ ASSERT_EQ(brpc::PARSE_OK, ParseWire(wire, &reply, &arena));
+ ASSERT_TRUE(reply.is_error());
+ ASSERT_EQ(1055u, reply.error().errcode());
+ ASSERT_EQ(butil::StringPiece("42000"), reply.error().status());
+ ASSERT_EQ(butil::StringPiece("#boom"), reply.error().msg());
+}
+
+// A protocol-4.1 ERR packet whose '#' marker is present but whose sql_state
+// is truncated must still be rejected.
+TEST(MysqlReplyParseTest, RejectTruncatedSqlState) {
+ std::string payload;
+ payload.push_back((char)0xFF); // ERR marker
+ payload.append("\x1f\x04", 2); // error code 1055, little-endian
+ payload.append("#AB", 3); // '#' but only 2 of 5 state
bytes
+
+ std::string wire;
+ AppendPacket(&wire, 0, payload);
+
+ brpc::MysqlReply reply;
+ butil::Arena arena;
+ ASSERT_EQ(brpc::PARSE_ERROR_ABSOLUTELY_WRONG, ParseWire(wire, &reply,
&arena));
+}
+
+// A well-formed protocol-4.1 ERR packet ('#' + 5-byte sql_state + message)
+// still parses with its sql_state populated.
+TEST(MysqlReplyParseTest, AcceptProtocol41Err) {
+ std::string payload;
+ payload.push_back((char)0xFF); // ERR marker
+ payload.append("\x1f\x04", 2); // error code 1055, little-endian
+ payload.push_back('#');
+ payload.append("42000", 5); // sql state
+ payload.append("boom", 4); // message
+
+ std::string wire;
+ AppendPacket(&wire, 0, payload);
+
+ brpc::MysqlReply reply;
+ butil::Arena arena;
+ ASSERT_EQ(brpc::PARSE_OK, ParseWire(wire, &reply, &arena));
+ ASSERT_TRUE(reply.is_error());
+ ASSERT_EQ(1055u, reply.error().errcode());
+ ASSERT_EQ(butil::StringPiece("42000"), reply.error().status());
+ ASSERT_EQ(butil::StringPiece("boom"), reply.error().msg());
+}
+
+// A well-formed binary-protocol row parses and round-trips its fixed-width
+// value.
+TEST(MysqlReplyParseTest, AcceptBinaryRow) {
+ std::string row;
+ row.push_back('\x00'); // binary row marker
+ row.push_back('\x00'); // NULL bitmap (1 byte for 1 column): no
NULLs
+ row.append(8, '\x2a'); // 8-byte unsigned LONGLONG value
+
+ brpc::MysqlReply reply;
+ butil::Arena arena;
+ ASSERT_EQ(brpc::PARSE_OK,
+ ParseWire(MakeBinaryResultSet(
+ MakeColumnDef(brpc::MYSQL_FIELD_TYPE_LONGLONG,
+ brpc::MYSQL_UNSIGNED_FLAG),
+ row),
+ &reply, &arena, brpc::MYSQL_PREPARED_STATEMENT));
+ ASSERT_TRUE(reply.is_resultset());
+ ASSERT_EQ(1u, reply.row_count());
+ ASSERT_EQ(0x2a2a2a2a2a2a2a2aULL, reply.next().field(0).bigint());
+}
+
+// A binary row truncated inside its NULL bitmap (or missing it entirely)
+// must be rejected, without borrowing the coalesced following EOF packet.
+TEST(MysqlReplyParseTest, RejectTruncatedBinaryNullBitmap) {
+ std::string row;
+ row.push_back('\x00'); // marker only, NULL bitmap missing
+
+ brpc::MysqlReply reply;
+ butil::Arena arena;
+ ASSERT_EQ(brpc::PARSE_ERROR_ABSOLUTELY_WRONG,
+ ParseWire(MakeBinaryResultSet(
+ MakeColumnDef(brpc::MYSQL_FIELD_TYPE_LONGLONG,
+ brpc::MYSQL_UNSIGNED_FLAG),
+ row),
+ &reply, &arena, brpc::MYSQL_PREPARED_STATEMENT));
+}
+
+// A binary row whose fixed-width numeric value ends early must be rejected;
+// the missing bytes cannot be taken from the coalesced EOF packet.
+TEST(MysqlReplyParseTest, RejectTruncatedBinaryFixedValue) {
+ std::string row;
+ row.push_back('\x00'); // binary row marker
+ row.push_back('\x00'); // NULL bitmap
+ row.append(4, '\x2a'); // only 4 of the 8 LONGLONG bytes
+
+ brpc::MysqlReply reply;
+ butil::Arena arena;
+ ASSERT_EQ(brpc::PARSE_ERROR_ABSOLUTELY_WRONG,
+ ParseWire(MakeBinaryResultSet(
+ MakeColumnDef(brpc::MYSQL_FIELD_TYPE_LONGLONG,
+ brpc::MYSQL_UNSIGNED_FLAG),
+ row),
+ &reply, &arena, brpc::MYSQL_PREPARED_STATEMENT));
+}
+
+// A binary string field whose length-encoded prefix or value is truncated
+// must be rejected.
+TEST(MysqlReplyParseTest, RejectTruncatedBinaryStringField) {
+ const std::string tails[] = {
+ std::string("\xFC", 1), // prefix, 0 of 2 length
bytes
+ std::string("\xFC\x05\x00"
+ "ab",
+ 5), // length 5, only 2 bytes
follow
+ };
+ for (size_t i = 0; i < sizeof(tails) / sizeof(tails[0]); ++i) {
+ std::string row;
+ row.push_back('\x00'); // binary row marker
+ row.push_back('\x00'); // NULL bitmap
+ row.append(tails[i]);
+
+ brpc::MysqlReply reply;
+ butil::Arena arena;
+ ASSERT_EQ(brpc::PARSE_ERROR_ABSOLUTELY_WRONG,
+ ParseWire(MakeBinaryResultSet(MakeColumnDef(), row), &reply,
&arena,
+ brpc::MYSQL_PREPARED_STATEMENT))
+ << "case " << i;
+ }
+}
+
+// Binary TIME/DATETIME values whose length-encoded length promises more
+// bytes than the row carries must be rejected.
+TEST(MysqlReplyParseTest, RejectTruncatedBinaryTimeAndDatetime) {
+ const struct {
+ uint8_t type;
+ uint8_t len;
+ size_t value_bytes;
+ } cases[] = {
+ {brpc::MYSQL_FIELD_TYPE_TIME, 8, 3}, // TIME claims 8, has 3
+ {brpc::MYSQL_FIELD_TYPE_DATETIME, 4, 1}, // DATETIME claims 4, has 1
+ };
+ for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) {
+ std::string row;
+ row.push_back('\x00'); // binary row marker
+ row.push_back('\x00'); // NULL bitmap
+ row.push_back((char)cases[i].len);
+ row.append(cases[i].value_bytes, '\x11');
+
+ brpc::MysqlReply reply;
+ butil::Arena arena;
+ ASSERT_EQ(brpc::PARSE_ERROR_ABSOLUTELY_WRONG,
+ ParseWire(MakeBinaryResultSet(MakeColumnDef(cases[i].type),
row),
+ &reply, &arena, brpc::MYSQL_PREPARED_STATEMENT))
+ << "case " << i;
+ }
+}
+
} // namespace
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]