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*)&microsecs, 4);
+    MY_PARSE_CHECK(parse_fixed(buf, &microsecs, 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]

Reply via email to