This is an automated email from the ASF dual-hosted git repository.

wwbmmm 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 0eaed790 Bound wire-declared sizes and decompressed output during 
parsing (#3476)
0eaed790 is described below

commit 0eaed79081cbad6d453f93627d32adc5c6c242e2
Author: Weibing Wang <[email protected]>
AuthorDate: Sun Aug 30 21:27:37 2026 +0800

    Bound wire-declared sizes and decompressed output during parsing (#3476)
    
    Several parsers that handle client-controlled input trusted sizes
    declared on the wire without bounding them, which could make a
    connection consume far more memory than the message itself:
    
    - gzip/zlib/snappy decompression had no output cap: -max_body_size
      is only checked against the compressed bytes, so a small body
      could decompress to tens of GiB. Add -max_decompressed_body_size
      (default 32x -max_body_size, 0 means use the default) and enforce
      it in all three decompressors. The output cap is applied via a
      local ZeroCopyInputStream wrapper so it builds against every
      protobuf version CI uses (>= 3.5.1); the wrapper clips oversized
      blocks without double-backing-up the wrapped stream.
    - AMF string readers resized the output buffer to the declared
      length before checking how many bytes were actually available;
      read the string in bounded chunks instead.
    - RTMP chunk headers may re-declare a message while a pr- RTMP chunk 
headers may re-declare a message while a pr- RTMP chunk headers may re-deed - 
RTMP chunk headers may re-declare a message while a pr- RTMP ody- RTMP chunk 
headers may re-declare a message while a pr- RTwir- RTMP chunk headers may 
re-declare a message while a pr- cou- RTMP chunk headers may re-declare a 
message while a pr- RTMP ch r- RTMP chunk headers may re-declare a message 
while a pr- RTMPvalues instead of CHECK-fatal.
---
 src/brpc/amf.cpp                           |  42 +++++++----
 src/brpc/compress.cpp                      |  20 ++++++
 src/brpc/compress.h                        |  10 +++
 src/brpc/policy/gzip_compress.cpp          | 112 ++++++++++++++++++++++++++++-
 src/brpc/policy/rtmp_protocol.cpp          |  34 +++++++++
 src/brpc/policy/snappy_compress.cpp        |  17 +++++
 src/mcpack2pb/generator.cpp                |  12 ++--
 src/mcpack2pb/parser-inl.h                 |  18 ++++-
 src/mcpack2pb/parser.h                     |  11 +++
 test/brpc_mcpack2pb_unittest.cpp           |  37 ++++++++++
 test/brpc_rtmp_unittest.cpp                |  76 ++++++++++++++++++++
 test/brpc_snappy_compress_unittest.cpp     |  32 +++++++++
 test/brpc_sofa_pbrpc_protocol_unittest.cpp |  14 ++++
 13 files changed, 412 insertions(+), 23 deletions(-)

diff --git a/src/brpc/amf.cpp b/src/brpc/amf.cpp
index 3cd44d37..290ce642 100644
--- a/src/brpc/amf.cpp
+++ b/src/brpc/amf.cpp
@@ -16,6 +16,7 @@
 // under the License.
 
 
+#include <algorithm>
 #include <google/protobuf/descriptor.h>
 #include "butil/sys_byteorder.h"
 #include "butil/logging.h"
@@ -286,6 +287,31 @@ AMFArray* AMFObject::MutableArray(const std::string& name) 
{
     return _fields[name].MutableArray();
 }
 
+// Read `len' bytes of string data in bounded chunks. The declared length
+// comes from the (untrusted) stream and may be much larger than the data
+// actually present, so growing the output as bytes arrive keeps a tiny
+// truncated message from forcing an allocation of up to
+// FLAGS_amf_max_string_size bytes before the availability check.
+static const size_t AMF_STRING_READ_CHUNK_SIZE = 64 * 1024;
+
+static bool ReadAMFStringData(std::string* str, AMFInputStream* stream,
+                              uint32_t len) {
+    str->clear();
+    size_t nread = 0;
+    while (nread < len) {
+        const size_t to_read =
+            std::min((size_t)len - nread, AMF_STRING_READ_CHUNK_SIZE);
+        str->resize(nread + to_read);
+        if (stream->cutn(&(*str)[nread], to_read) != to_read) {
+            str->clear();
+            LOG(ERROR) << "stream is not long enough";
+            return false;
+        }
+        nread += to_read;
+    }
+    return true;
+}
+
 static bool ReadAMFShortStringBody(std::string* str, AMFInputStream* stream) {
     uint16_t len = 0;
     if (stream->cut_u16(&len) != 2u) {
@@ -295,13 +321,7 @@ static bool ReadAMFShortStringBody(std::string* str, 
AMFInputStream* stream) {
     if (!CheckAMFStringSize(len)) {
         return false;
     }
-    str->resize(len);
-    if (len != 0 && stream->cutn(&(*str)[0], len) != len) {
-        str->clear();
-        LOG(ERROR) << "stream is not long enough";
-        return false;
-    }
-    return true;
+    return ReadAMFStringData(str, stream, len);
 }
 
 static bool ReadAMFLongStringBody(std::string* str, AMFInputStream* stream) {
@@ -313,13 +333,7 @@ static bool ReadAMFLongStringBody(std::string* str, 
AMFInputStream* stream) {
     if (!CheckAMFStringSize(len)) {
         return false;
     }
-    str->resize(len);
-    if (len != 0 && stream->cutn(&(*str)[0], len) != len) {
-        str->clear();
-        LOG(ERROR) << "stream is not long enough";
-        return false;
-    }
-    return true;
+    return ReadAMFStringData(str, stream, len);
 }
 
 bool ReadAMFString(std::string* str, AMFInputStream* stream) {
diff --git a/src/brpc/compress.cpp b/src/brpc/compress.cpp
index 9f9939c9..9b026a20 100644
--- a/src/brpc/compress.cpp
+++ b/src/brpc/compress.cpp
@@ -16,6 +16,8 @@
 // under the License.
 
 
+#include <limits>
+#include <gflags/gflags.h>
 #include "butil/logging.h"
 #include "json2pb/json_to_pb.h"
 #include "brpc/compress.h"
@@ -24,6 +26,24 @@
 
 namespace brpc {
 
+DEFINE_uint64(max_decompressed_body_size, 0,
+              "Maximum size (in bytes) that a single compressed message body"
+              " may decompress to, guarding against decompression bombs."
+              " 0 (the default) means 32 times -max_body_size. Raise this"
+              " flag explicitly if larger decompressed messages are expected");
+
+uint64_t MaxDecompressedBodySize() {
+    const uint64_t limit = FLAGS_max_decompressed_body_size;
+    if (limit > 0) {
+        return limit;
+    }
+    const uint64_t base = FLAGS_max_body_size;
+    if (base > std::numeric_limits<uint64_t>::max() / 32) {
+        return std::numeric_limits<uint64_t>::max();
+    }
+    return base * 32;
+}
+
 static const int MAX_HANDLER_SIZE = 1024;
 static CompressHandler s_handler_map[MAX_HANDLER_SIZE] = { { nullptr, nullptr, 
nullptr } };
 
diff --git a/src/brpc/compress.h b/src/brpc/compress.h
index a6c61648..c9e39f87 100644
--- a/src/brpc/compress.h
+++ b/src/brpc/compress.h
@@ -20,6 +20,7 @@
 #define BRPC_COMPRESS_H
 
 #include <google/protobuf/message.h>              // Message
+#include <gflags/gflags_declare.h>               // DECLARE_uint64
 #include "butil/iobuf.h"                           // butil::IOBuf
 #include "butil/logging.h"
 #include "brpc/options.pb.h"                     // CompressType
@@ -27,6 +28,15 @@
 
 namespace brpc {
 
+DECLARE_uint64(max_decompressed_body_size);
+
+// Effective limit (in bytes) on the decompressed size of a single message
+// body: FLAGS_max_decompressed_body_size, or 32 x FLAGS_max_body_size when
+// the flag is 0 (the default). Decompressors must fail once their output
+// exceeds this limit, otherwise a small compressed body that passes
+// -max_body_size may expand to tens of GiB (decompression bomb).
+uint64_t MaxDecompressedBodySize();
+
 // Serializer can be used to implement custom serialization
 // before compression with user callback.
 class Serializer : public NonreflectableMessage<Serializer> {
diff --git a/src/brpc/policy/gzip_compress.cpp 
b/src/brpc/policy/gzip_compress.cpp
index 73a6f02f..5f1ad72b 100644
--- a/src/brpc/policy/gzip_compress.cpp
+++ b/src/brpc/policy/gzip_compress.cpp
@@ -16,7 +16,9 @@
 // under the License.
 
 
+#include <limits>
 #include <google/protobuf/io/gzip_stream.h>    // GzipXXXStream
+#include <google/protobuf/io/zero_copy_stream.h>   // ZeroCopyInputStream
 #include <google/protobuf/text_format.h>
 #include "butil/logging.h"
 #include "brpc/policy/gzip_compress.h"
@@ -26,6 +28,81 @@
 namespace brpc {
 namespace policy {
 
+namespace {
+
+// A ZeroCopyInputStream wrapper that stops reading from the underlying stream
+// once a limit of bytes has been handed out. Different protobuf releases
+// disagree on the availability/location of the stock LimitingInputStream (it
+// does not exist before ~3.19), so implement the same behaviour locally to
+// stay portable across the protobuf versions CI builds against.
+class DelegatingLimitingInputStream : public 
google::protobuf::io::ZeroCopyInputStream {
+public:
+    DelegatingLimitingInputStream(google::protobuf::io::ZeroCopyInputStream* 
input,
+                                  int64_t limit)
+        : _input(input), _limit(limit), _bytes_read(0), _excess(0) {}
+
+    bool Next(const void** data, int* size) override {
+        if (_bytes_read >= _limit) {
+            return false;
+        }
+        if (!_input->Next(data, size)) {
+            return false;
+        }
+        const int64_t total = _bytes_read + *size;
+        if (total > _limit) {
+            // Clip the tail that does not fit below the limit and record how
+            // much of the wrapped stream's block is being withheld instead of
+            // calling BackUp() right now: a consumer backing up the exposed
+            // prefix would otherwise trigger a second BackUp() on the
+            // wrapped stream without an intervening Next(), which violates
+            // the ZeroCopyInputStream contract.
+            _excess = (int)(total - _limit);
+            *size -= _excess;
+            _bytes_read = _limit;
+        } else {
+            _excess = 0;
+            _bytes_read = total;
+        }
+        return true;
+    }
+
+    void BackUp(int count) override {
+        _bytes_read -= count;
+        if (_excess > 0) {
+            // Give back the requested prefix together with the clipped tail
+            // in the single BackUp() the wrapped stream allows after the last
+            // Next(), landing both at the right offset.
+            _input->BackUp(count + _excess);
+            _excess = 0;
+        } else {
+            _input->BackUp(count);
+        }
+    }
+
+    bool Skip(int count) override {
+        // Bound the skip by what still fits below the limit.
+        const int64_t remaining = _limit - _bytes_read;
+        if (count > remaining) {
+            return false;
+        }
+        if (_input->Skip(count)) {
+            _bytes_read += count;
+            return true;
+        }
+        return false;
+    }
+
+    int64_t ByteCount() const override { return _bytes_read; }
+
+private:
+    google::protobuf::io::ZeroCopyInputStream* _input;
+    int64_t _limit;
+    int64_t _bytes_read;
+    int _excess;
+};
+
+}  // namespace
+
 const char* Format2CStr(google::protobuf::io::GzipOutputStream::Format format) 
{
     switch (format) {
     case google::protobuf::io::GzipOutputStream::GZIP:
@@ -73,11 +150,26 @@ static bool Decompress(const butil::IOBuf& data, 
google::protobuf::Message* msg,
                        google::protobuf::io::GzipInputStream::Format format) {
     butil::IOBufAsZeroCopyInputStream wrapper(data);
     google::protobuf::io::GzipInputStream gzip(&wrapper, format);
+    // Cap the decompressed size: zlib expands up to ~1032x, so a body that
+    // passes -max_body_size in compressed form may still decompress to tens
+    // of GiB (decompression bomb). The limiting stream stops feeding the
+    // parser once the cap is hit, bounding the memory materialized here.
+    const uint64_t limit = MaxDecompressedBodySize();
+    const int64_t hard_limit =
+        limit < (uint64_t)std::numeric_limits<int64_t>::max()
+        ? (int64_t)limit + 1 : std::numeric_limits<int64_t>::max();
+    DelegatingLimitingInputStream limited_in(&gzip, hard_limit);
     bool ok;
     if (msg->GetDescriptor() == Deserializer::descriptor()) {
-        ok = ((Deserializer*)msg)->DeserializeFrom(&gzip);
+        ok = ((Deserializer*)msg)->DeserializeFrom(&limited_in);
     } else {
-        ok = msg->ParseFromZeroCopyStream(&gzip);
+        ok = msg->ParseFromZeroCopyStream(&limited_in);
+    }
+    if (ok && (uint64_t)limited_in.ByteCount() > limit) {
+        LOG(WARNING) << "Decompressed size exceeds"
+                        " -max_decompressed_body_size=" << limit
+                     << ", format=" << Format2CStr(format);
+        return false;
     }
     if (!ok) {
         LOG(WARNING) << "Fail to deserialize input message="
@@ -141,6 +233,11 @@ inline bool GzipDecompressBase(
     butil::IOBufAsZeroCopyInputStream wrapper(data);
     google::protobuf::io::GzipInputStream in(&wrapper, format);
     butil::IOBufAsZeroCopyOutputStream out(msg);
+    // Cap the decompressed size: zlib expands up to ~1032x, so a body that
+    // passes -max_body_size in compressed form may still decompress to tens
+    // of GiB (decompression bomb).
+    const uint64_t limit = MaxDecompressedBodySize();
+    uint64_t total_out = 0;
     const void* data_in = nullptr;
     int size_in = 0;
     void* data_out = nullptr;
@@ -154,6 +251,17 @@ inline bool GzipDecompressBase(
         }
         const int size_cp = std::min(size_in, size_out);
         memcpy(data_out, data_in, size_cp);
+        total_out += size_cp;
+        if (total_out > limit) {
+            LOG(WARNING) << "Decompressed size exceeds"
+                            " -max_decompressed_body_size=" << limit
+                         << ", format=" << Format2CStr(format);
+            // out.Next() already moved the whole output block into `msg';
+            // give back the unwritten tail (still uninitialized) before
+            // leaving, otherwise it stays in the caller's IOBuf.
+            out.BackUp(size_out);
+            return false;
+        }
         size_in -= size_cp;
         data_in = (char*)data_in + size_cp;
         size_out -= size_cp;
diff --git a/src/brpc/policy/rtmp_protocol.cpp 
b/src/brpc/policy/rtmp_protocol.cpp
index 2b50c6a3..8ad63fb9 100644
--- a/src/brpc/policy/rtmp_protocol.cpp
+++ b/src/brpc/policy/rtmp_protocol.cpp
@@ -1460,6 +1460,23 @@ ParseResult RtmpChunkStream::Feed(const RtmpBasicHeader& 
bh,
         }
         timestamp_delta = mh.timestamp;
         mh.message_length = ReadBigEndian3Bytes(p + 3);
+        if (mh.message_length > FLAGS_max_body_size) {
+            LOG(ERROR) << socket->remote_side() << ": message_length="
+                       << mh.message_length << " in chunk_stream=" << _cs_id
+                       << " is too large";
+            return MakeParseError(PARSE_ERROR_TOO_BIG_DATA);
+        }
+        if (!_r.msg_body.empty()) {
+            // The new message header arrived before the previous message on
+            // this chunk stream completed. Drop the stale partial body,
+            // otherwise it would prefix the new message and, with repeated
+            // mid-message headers, grow `msg_body' without bound.
+            LOG(WARNING) << socket->remote_side() << ": Discard "
+                         << _r.msg_body.size() << " bytes of an incomplete"
+                         " message in chunk_stream=" << _cs_id
+                         << " overridden by a ChunkType0 header";
+            _r.msg_body.clear();
+        }
         _r.left_message_length = mh.message_length;
         cur_chunk_size = std::min(chunk_size_in, _r.left_message_length);
         if (source->size() < header_len + cur_chunk_size) {
@@ -1507,6 +1524,23 @@ ParseResult RtmpChunkStream::Feed(const RtmpBasicHeader& 
bh,
         }
         mh.timestamp = _r.last_msg_header.timestamp + timestamp_delta;
         mh.message_length = ReadBigEndian3Bytes(p + 3);
+        if (mh.message_length > FLAGS_max_body_size) {
+            LOG(ERROR) << socket->remote_side() << ": message_length="
+                       << mh.message_length << " in chunk_stream=" << _cs_id
+                       << " is too large";
+            return MakeParseError(PARSE_ERROR_TOO_BIG_DATA);
+        }
+        if (!_r.msg_body.empty()) {
+            // The new message header arrived before the previous message on
+            // this chunk stream completed. Drop the stale partial body,
+            // otherwise it would prefix the new message and, with repeated
+            // mid-message headers, grow `msg_body' without bound.
+            LOG(WARNING) << socket->remote_side() << ": Discard "
+                         << _r.msg_body.size() << " bytes of an incomplete"
+                         " message in chunk_stream=" << _cs_id
+                         << " overridden by a ChunkType1 header";
+            _r.msg_body.clear();
+        }
         _r.left_message_length = mh.message_length;
         cur_chunk_size = std::min(chunk_size_in, _r.left_message_length);
         if (source->size() < header_len + cur_chunk_size) {
diff --git a/src/brpc/policy/snappy_compress.cpp 
b/src/brpc/policy/snappy_compress.cpp
index 8019b97b..e78d3c8b 100644
--- a/src/brpc/policy/snappy_compress.cpp
+++ b/src/brpc/policy/snappy_compress.cpp
@@ -76,6 +76,23 @@ bool SnappyCompress(const butil::IOBuf& in, butil::IOBuf* 
out) {
 }
 
 bool SnappyDecompress(const butil::IOBuf& in, butil::IOBuf* out) {
+    {
+        // Reject bodies whose declared uncompressed length exceeds the
+        // decompression cap (decompression bomb): -max_body_size is checked
+        // against the compressed bytes only.
+        butil::IOBufAsSnappySource length_source(in);
+        uint32_t uncompressed_len = 0;
+        if (!butil::snappy::GetUncompressedLength(&length_source,
+                                                  &uncompressed_len)) {
+            return false;
+        }
+        if (uncompressed_len > MaxDecompressedBodySize()) {
+            LOG(WARNING) << "Uncompressed size=" << uncompressed_len
+                         << " exceeds -max_decompressed_body_size="
+                         << MaxDecompressedBodySize();
+            return false;
+        }
+    }
     butil::IOBufAsSnappySource source(in);
     butil::IOBufAsSnappySink sink(*out);
     return butil::snappy::Uncompress(&source, &sink);
diff --git a/src/mcpack2pb/generator.cpp b/src/mcpack2pb/generator.cpp
index 26aa4b5b..f5a46a5c 100644
--- a/src/mcpack2pb/generator.cpp
+++ b/src/mcpack2pb/generator.cpp
@@ -227,14 +227,14 @@ bool generate_declarations(const std::set<std::string>& 
ref_msgs,
     "  $msg$* const msg = static_cast<$msg$*>(msg_base);\n"             \
     "  if (value.type() == ::mcpack2pb::FIELD_ISOARRAY) {\n"               \
     "    ::mcpack2pb::ISOArrayIterator it(value);\n"                       \
-    "    msg->mutable_$lcfield$()->Reserve(it.item_count());\n"         \
+    "    
msg->mutable_$lcfield$()->Reserve(::mcpack2pb::capped_reserve_count(it.item_count()));\n"
         \
     "    for (; it != NULL; ++it) {\n"                                  \
     "      msg->add_$lcfield$(it.as_"#fntype "());\n"                   \
     "    }\n"                                                           \
     "    return value.stream()->good();\n"                              \
     "  } else if (value.type() == ::mcpack2pb::FIELD_ARRAY) {\n"           \
     "    ::mcpack2pb::ArrayIterator it(value);\n"                          \
-    "    msg->mutable_$lcfield$()->Reserve(it.item_count());\n"         \
+    "    
msg->mutable_$lcfield$()->Reserve(::mcpack2pb::capped_reserve_count(it.item_count()));\n"
         \
     "    for (; it != NULL; ++it) {\n"                                  \
     "      msg->add_$lcfield$(it->as_"#fntype "(\"$field$\"));\n" \
     "    }\n"                                                           \
@@ -323,14 +323,14 @@ static bool generate_parsing(const 
google::protobuf::Descriptor* d,
                     "  $msg$* const msg = static_cast<$msg$*>(msg_base);\n"
                     "  if (value.type() == ::mcpack2pb::FIELD_ISOARRAY) {\n"
                     "    ::mcpack2pb::ISOArrayIterator it(value);\n"
-                    "    
msg->mutable_$lcfield$()->Reserve(it.item_count());\n" 
+                    "    
msg->mutable_$lcfield$()->Reserve(::mcpack2pb::capped_reserve_count(it.item_count()));\n"
 
                     "    for (; it != NULL; ++it) {\n"
                     "      msg->add_$lcfield$(($enum$)it.as_int32());\n"
                     "    }\n"
                     "    return value.stream()->good();\n"
                     "  } else if (value.type() == ::mcpack2pb::FIELD_ARRAY) 
{\n"            
                     "    ::mcpack2pb::ArrayIterator it(value);\n"
-                    "    msg->mutable_$lcfield$()->Reserve(it.item_count());\n"
+                    "    
msg->mutable_$lcfield$()->Reserve(::mcpack2pb::capped_reserve_count(it.item_count()));\n"
                     "    for (; it != NULL; ++it) {\n"
                     "      
msg->add_$lcfield$(($enum$)it->as_int32(\"$enum$\"));\n"
                     "    }\n"
@@ -361,7 +361,7 @@ static bool generate_parsing(const 
google::protobuf::Descriptor* d,
                     "  $msg$* const msg = static_cast<$msg$*>(msg_base);\n"
                     "  if (value.type() == ::mcpack2pb::FIELD_ARRAY) {\n"
                     "    ::mcpack2pb::ArrayIterator it(value);\n"
-                    "    msg->mutable_$lcfield$()->Reserve(it.item_count());\n"
+                    "    
msg->mutable_$lcfield$()->Reserve(::mcpack2pb::capped_reserve_count(it.item_count()));\n"
                     "    for (; it != NULL; ++it) {\n"
                     "      if (it->type() == ::mcpack2pb::FIELD_STRING) {\n"
                     "        it->as_string(msg->add_$lcfield$(), 
\"$field$\");\n"
@@ -457,7 +457,7 @@ static bool generate_parsing(const 
google::protobuf::Descriptor* d,
                     "    return value.stream()->good();\n"
                     "  } else if (value.type() == ::mcpack2pb::FIELD_ARRAY) 
{\n"
                     "    ::mcpack2pb::ArrayIterator it(value);\n"
-                    "    msg->mutable_$lcfield$()->Reserve(it.item_count());\n"
+                    "    
msg->mutable_$lcfield$()->Reserve(::mcpack2pb::capped_reserve_count(it.item_count()));\n"
                     "    for (; it != NULL; ++it) {\n"
                     "      if (it->type() == ::mcpack2pb::FIELD_OBJECT) {\n"
                     "        if 
(!parse_$vmsg2$_body_internal(msg->add_$lcfield$(), *it)) {\n"
diff --git a/src/mcpack2pb/parser-inl.h b/src/mcpack2pb/parser-inl.h
index 235bb540..bdfb95e5 100644
--- a/src/mcpack2pb/parser-inl.h
+++ b/src/mcpack2pb/parser-inl.h
@@ -144,9 +144,25 @@ inline void ObjectIterator::init(InputStream* stream, 
size_t size) {
     _stream = stream;
     _expected_popped_bytes = _stream->popped_bytes() + sizeof(ItemsHead);
     _expected_popped_end = _stream->popped_bytes() + size;
+    // Every field head takes at least 2 bytes (FieldFixedHead), so a valid
+    // item_count never exceeds half of the remaining value size. The count
+    // is copied verbatim from the wire, reject inconsistent values instead
+    // of trusting them. Guard the size before reading ItemsHead so payloads
+    // shorter than the header are not read past their declared boundary.
+    // Note that these are wire-controlled inputs, so reject (set_bad) rather
+    // than CHECK-fatal, which would terminate the process.
+    if (size < sizeof(ItemsHead)) {
+        LOG(ERROR) << "buffer(size=" << size << ") is not enough";
+        return set_bad();
+    }
     ItemsHead items_head;
     if (_stream->cut_packed_pod(&items_head) != sizeof(ItemsHead)) {
-        CHECK(false) << "buffer(size=" << size << ") is not enough";
+        LOG(ERROR) << "buffer(size=" << size << ") is not enough";
+        return set_bad();
+    }
+    if (items_head.item_count > (size - sizeof(ItemsHead)) / 2) {
+        LOG(ERROR) << "inconsistent item_count(" << items_head.item_count
+                   << ") and value_size(" << size << ")";
         return set_bad();
     }
     _field_count = items_head.item_count;
diff --git a/src/mcpack2pb/parser.h b/src/mcpack2pb/parser.h
index d897c5e1..d9cc6726 100644
--- a/src/mcpack2pb/parser.h
+++ b/src/mcpack2pb/parser.h
@@ -88,6 +88,17 @@ class ObjectIterator;
 class ArrayIterator;
 class ISOArrayIterator;
 
+// Bound the argument of RepeatedField::Reserve() calls that generated
+// parsing code derives from a wire-declared item count. The declared count
+// is under control of the remote side and is not necessarily backed by
+// actual bytes, so reserving it verbatim lets a tiny message trigger a huge
+// allocation. Repeated fields grow on demand past this bound, thus parsing
+// of genuinely large arrays is unaffected.
+inline int capped_reserve_count(uint32_t item_count) {
+    const uint32_t MAX_RESERVE_COUNT = 1024;
+    return (int)(item_count < MAX_RESERVE_COUNT ? item_count : 
MAX_RESERVE_COUNT);
+}
+
 // Represent a piece of unparsed(and unread) data of InputStream.
 struct UnparsedValue {
     UnparsedValue()
diff --git a/test/brpc_mcpack2pb_unittest.cpp b/test/brpc_mcpack2pb_unittest.cpp
index c0261540..ad040a5b 100644
--- a/test/brpc_mcpack2pb_unittest.cpp
+++ b/test/brpc_mcpack2pb_unittest.cpp
@@ -156,4 +156,41 @@ TEST(Mcpack2pbParserTest, 
ArrayItemCountIsZeroWhenPayloadSmallerThanHeader) {
     EXPECT_EQ(0u, it.item_count());
 }
 
+TEST(Mcpack2pbParserTest, ObjectItemCountIsRejectedWhenInconsistentWithSize) {
+    // An mcpack object whose ItemsHead declares an absurd field count for
+    // the given payload must be rejected instead of being trusted: the
+    // count is copied verbatim from the wire and every field head takes at
+    // least 2 bytes, so a valid item count never exceeds half of the
+    // remaining bytes.
+    const unsigned char data[] = {
+        0xff, 0xff, 0xff, 0x7f,  // item_count = 0x7fffffff
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00,
+    };
+    butil::IOBuf body;
+    body.append(data, sizeof(data));
+
+    butil::IOBufAsZeroCopyInputStream zc_stream(body);
+    mcpack2pb::InputStream stream(&zc_stream);
+    mcpack2pb::ObjectIterator it(&stream, sizeof(data));
+    EXPECT_TRUE(it == NULL);
+    EXPECT_FALSE(stream.good());
+}
+
+TEST(Mcpack2pbParserTest, EmptyObjectStillParses) {
+    // A consistent empty object head (item_count 0) must still initialize
+    // an empty iterator normally.
+    const unsigned char data[] = {
+        0x00, 0x00, 0x00, 0x00,  // item_count = 0
+    };
+    butil::IOBuf body;
+    body.append(data, sizeof(data));
+
+    butil::IOBufAsZeroCopyInputStream zc_stream(body);
+    mcpack2pb::InputStream stream(&zc_stream);
+    mcpack2pb::ObjectIterator it(&stream, sizeof(data));
+    EXPECT_TRUE(it == NULL);
+    EXPECT_TRUE(stream.good());
+}
+
 }  // namespace
diff --git a/test/brpc_rtmp_unittest.cpp b/test/brpc_rtmp_unittest.cpp
index 8c6df7c1..9264a8e0 100644
--- a/test/brpc_rtmp_unittest.cpp
+++ b/test/brpc_rtmp_unittest.cpp
@@ -659,6 +659,82 @@ TEST(RtmpTest, amf_rejects_oversized_ecma_array_count) {
     EXPECT_FALSE(brpc::ReadAMFObject(&obj, &istream));
 }
 
+TEST(RtmpTest, amf_truncated_long_string_does_not_allocate_declared_size) {
+    // Regression: a tiny message declaring a huge (but under-the-cap)
+    // string length must not cause the declared size to be allocated
+    // before the bytes are actually available in the stream.
+    const uint32_t declared_len = 8 * 1024 * 1024;
+    std::string req_buf;
+    AppendAMFLongStringHeader(&req_buf, declared_len);
+    req_buf.append("only-a-few-bytes", 16);
+
+    google::protobuf::io::ArrayInputStream zc_stream(req_buf.data(), 
req_buf.size());
+    brpc::AMFInputStream istream(&zc_stream);
+    std::string result;
+    EXPECT_FALSE(brpc::ReadAMFString(&result, &istream));
+    EXPECT_TRUE(result.empty());
+    // Reading is chunked, so a truncated stream leaves at most one chunk
+    // of capacity behind instead of the full declared length.
+    EXPECT_LT(result.capacity(), (size_t)declared_len);
+}
+
+TEST(RtmpTest, amf_reads_long_string_larger_than_one_chunk) {
+    const std::string big(200 * 1024, 'x');
+    std::string req_buf;
+    {
+        google::protobuf::io::StringOutputStream zc_stream(&req_buf);
+        brpc::AMFOutputStream ostream(&zc_stream);
+        brpc::WriteAMFString(big, &ostream);
+        ASSERT_TRUE(ostream.good());
+    }
+    google::protobuf::io::ArrayInputStream zc_stream(req_buf.data(), 
req_buf.size());
+    brpc::AMFInputStream istream(&zc_stream);
+    std::string result;
+    ASSERT_TRUE(brpc::ReadAMFString(&result, &istream));
+    ASSERT_EQ(big, result);
+}
+
+TEST(RtmpTest, chunk_stream_rejects_message_length_over_max_body_size) {
+    int pipe_fds[2];
+    ASSERT_EQ(0, pipe(pipe_fds));
+    butil::fd_guard guard0(pipe_fds[0]);   // read end, closed by this guard
+    butil::fd_guard guard1(pipe_fds[1]);   // write end, handed over to Socket
+
+    brpc::SocketId id;
+    brpc::SocketOptions options;
+    options.fd = guard1.release();         // Socket takes ownership of the fd
+    ASSERT_EQ(0, brpc::Socket::Create(options, &id));
+    brpc::SocketUniquePtr sock;
+    ASSERT_EQ(0, brpc::Socket::Address(id, &sock));
+
+    brpc::policy::RtmpContext ctx(nullptr, nullptr);
+    ctx.SetState(sock->remote_side(),
+                 brpc::policy::RtmpContext::STATE_RECEIVED_C2);
+
+    // The message length declared by a chunk header is remote-controlled and
+    // was never bounded: with repeated mid-message headers a connection's
+    // reassembly buffer could grow without limit. A type-0 header declaring
+    // a length above -max_body_size must be rejected up front.
+    GFLAGS_NAMESPACE::FlagSaver flag_saver;
+    brpc::FLAGS_max_body_size = 1024;
+
+    std::string chunk;
+    chunk.push_back((char)0x02);   // basic header: fmt=0, cs_id=2
+    chunk.append(3, '\0');         // timestamp = 0
+    chunk.push_back('\0');         // message_length (3 bytes) = 4096
+    chunk.push_back((char)0x10);
+    chunk.push_back('\0');
+    chunk.push_back((char)0x02);   // message_type = Abort
+    chunk.append(4, '\0');         // stream_id = 0 (little endian)
+    chunk.append(128, '\0');       // one full chunk of payload
+
+    butil::IOBuf buf;
+    buf.append(chunk);
+    ASSERT_EQ(brpc::PARSE_ERROR_TOO_BIG_DATA,
+              ctx.Feed(&buf, sock.get()).error());
+}
+
+
 TEST(RtmpTest, amf_rejects_oversized_strict_array_count) {
     ScopedAMFLimit scoped_limit(&brpc::FLAGS_amf_max_array_size, 1);
 
diff --git a/test/brpc_snappy_compress_unittest.cpp 
b/test/brpc_snappy_compress_unittest.cpp
index 94b54dfd..a71b1766 100644
--- a/test/brpc_snappy_compress_unittest.cpp
+++ b/test/brpc_snappy_compress_unittest.cpp
@@ -20,12 +20,14 @@
 // Date: 2015/01/20 19:01:06
 
 #include <gtest/gtest.h>
+#include <gflags/gflags.h>
 #include "gperftools_helper.h"
 #include "butil/third_party/snappy/snappy.h"
 #include "butil/macros.h"
 #include "butil/iobuf.h"
 #include "butil/time.h"
 #include "snappy_message.pb.h"
+#include "brpc/compress.h"
 #include "brpc/policy/snappy_compress.h"
 #include "brpc/policy/gzip_compress.h"
 
@@ -253,3 +255,33 @@ TEST_F(test_compress_method, mass_snappy_iobuf) {
     ASSERT_TRUE(strcmp(check_str.c_str(), text) == 0);
     delete [] text;
 }
+
+TEST_F(test_compress_method, decompressed_size_capped) {
+    // Regression test: decompressors used to enforce no output limit, so a
+    // small compressed body (checked against -max_body_size in compressed
+    // form only) could decompress to tens of GiB (decompression bomb).
+    GFLAGS_NAMESPACE::FlagSaver flag_saver;
+    brpc::FLAGS_max_decompressed_body_size = 1024;
+
+    butil::IOBuf raw;
+    raw.append(std::string(64 * 1024, '\0'));
+
+    butil::IOBuf gzipped;
+    ASSERT_TRUE(brpc::policy::GzipCompress(raw, &gzipped, nullptr));
+    butil::IOBuf out;
+    ASSERT_FALSE(brpc::policy::GzipDecompress(gzipped, &out));
+
+    butil::IOBuf snappied;
+    ASSERT_TRUE(brpc::policy::SnappyCompress(raw, &snappied));
+    out.clear();
+    ASSERT_FALSE(brpc::policy::SnappyDecompress(snappied, &out));
+
+    // Payloads under the cap still decompress fine.
+    brpc::FLAGS_max_decompressed_body_size = 1024 * 1024;
+    out.clear();
+    ASSERT_TRUE(brpc::policy::GzipDecompress(gzipped, &out));
+    ASSERT_EQ(raw.size(), out.size());
+    out.clear();
+    ASSERT_TRUE(brpc::policy::SnappyDecompress(snappied, &out));
+    ASSERT_EQ(raw.size(), out.size());
+}
diff --git a/test/brpc_sofa_pbrpc_protocol_unittest.cpp 
b/test/brpc_sofa_pbrpc_protocol_unittest.cpp
index 5a44e89c..e03575e5 100644
--- a/test/brpc_sofa_pbrpc_protocol_unittest.cpp
+++ b/test/brpc_sofa_pbrpc_protocol_unittest.cpp
@@ -282,6 +282,20 @@ TEST_F(SofaTest, process_response_after_eof) {
     ASSERT_TRUE(_socket->Failed());
 }
 
+TEST_F(SofaTest, reject_huge_meta_size) {
+    // A header declaring a huge meta_size must be rejected up front,
+    // otherwise the parser keeps buffering meta_size bytes although the
+    // body itself passes the -max_body_size check.
+    butil::IOBuf buf;
+    const uint32_t meta_size = 0xFFFFFFFFu;
+    const uint64_t body_size = 0;
+    const uint64_t msg_size = meta_size + body_size;
+    AppendSofaTestHeader(&buf, meta_size, body_size, msg_size);
+    brpc::ParseResult pr =
+            brpc::policy::ParseSofaMessage(&buf, _socket.get(), false, 
nullptr);
+    ASSERT_EQ(brpc::PARSE_ERROR_TOO_BIG_DATA, pr.error());
+}
+
 TEST_F(SofaTest, process_response_error_code) {
     const int ERROR_CODE = 12345;
     brpc::policy::SofaRpcMeta meta;


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to