From 26e0203596a61262d7f2d2d4106b7449d1a46eab Mon Sep 17 00:00:00 2001 From: Wang Xiaofeng Date: Sat, 12 Sep 2026 00:58:20 +0800 Subject: [PATCH] Reject empty MySQL response packets Validate packet payload lengths before reading the response type byte. This prevents an empty packet from borrowing the first byte of a following packet during response parsing. --- src/brpc/policy/mysql/mysql_reply.cpp | 10 ++++++ test/brpc_mysql_reply_parse_unittest.cpp | 41 ++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/brpc/policy/mysql/mysql_reply.cpp b/src/brpc/policy/mysql/mysql_reply.cpp index 2f3a97078c..1a277339f9 100644 --- a/src/brpc/policy/mysql/mysql_reply.cpp +++ b/src/brpc/policy/mysql/mysql_reply.cpp @@ -200,6 +200,11 @@ ParseError MysqlReply::ConsumePartialIOBuf(butil::IOBuf& buf, } uint8_t header[4 + 1]; // use the extra byte to judge message type const uint8_t* p = (const uint8_t*)buf.fetch(header, sizeof(header)); + if (_type == MYSQL_RSP_UNKNOWN && + (p == nullptr || mysql_uint3korr(p) == 0)) { + LOG(ERROR) << "Invalid mysql packet with empty payload"; + return PARSE_ERROR_ABSOLUTELY_WRONG; + } uint8_t type = (_type == MYSQL_RSP_UNKNOWN) ? p[4] : (uint8_t)_type; // During the connection (auth) phase the server may send an AuthMoreData // packet (first byte 0x01) as part of the caching_sha2_password exchange @@ -243,6 +248,11 @@ ParseError MysqlReply::ConsumePartialIOBuf(butil::IOBuf& buf, butil::IOBuf discard; buf.cutn(&discard, amd_total); const uint8_t* p2 = (const uint8_t*)buf.fetch(header, sizeof(header)); + if (p2 == nullptr || mysql_uint3korr(p2) == 0) { + LOG(ERROR) << "Invalid mysql packet with empty payload after " + "fast-auth marker"; + return PARSE_ERROR_ABSOLUTELY_WRONG; + } type = p2[4]; } else { _type = MYSQL_RSP_AUTH_MORE_DATA; diff --git a/test/brpc_mysql_reply_parse_unittest.cpp b/test/brpc_mysql_reply_parse_unittest.cpp index 33ab95a519..f10773dbfd 100644 --- a/test/brpc_mysql_reply_parse_unittest.cpp +++ b/test/brpc_mysql_reply_parse_unittest.cpp @@ -101,6 +101,47 @@ TEST(MysqlReplyParseTest, RejectOversizedTextFieldLength) { ASSERT_EQ(brpc::PARSE_ERROR_ABSOLUTELY_WRONG, rc); } +TEST(MysqlReplyParseTest, RejectZeroPayloadPacket) { + butil::IOBuf buf; + buf.append(std::string("\x00\x00\x00\x01", 4)); + + 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); +} + +TEST(MysqlReplyParseTest, RejectZeroPayloadPacketWithTrailingBytes) { + std::string wire("\x00\x00\x00\x01", 4); + AppendPacket(&wire, 2, std::string("\x00\x00\x00\x00\x00\x00\x00", 7)); + 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); +} + +TEST(MysqlReplyParseTest, RejectZeroPayloadPacketAfterFastAuthMarker) { + std::string wire; + AppendPacket(&wire, 2, std::string("\x01\x03", 2)); + wire.append(std::string("\x00\x00\x00\x03", 4)); + butil::IOBuf buf; + buf.append(wire); + + brpc::MysqlReply reply; + butil::Arena arena; + bool more_results = false; + brpc::ParseError rc = reply.ConsumePartialIOBuf( + buf, &arena, true, brpc::MYSQL_NORMAL_STATEMENT, &more_results); + ASSERT_EQ(brpc::PARSE_ERROR_ABSOLUTELY_WRONG, rc); +} + // A well-formed field whose length matches the bytes present still parses, so // the guard does not reject legitimate result sets. TEST(MysqlReplyParseTest, AcceptWellFormedTextField) {