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

wasphin 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 bac1b3b2 Reject malformed mcpack input instead of CHECK-fatal in 
parser (#3576)
bac1b3b2 is described below

commit bac1b3b2be6f84dbe86fbb7b3c8760e9655b70f3
Author: Weibing Wang <[email protected]>
AuthorDate: Mon Oct 5 23:18:04 2026 +0800

    Reject malformed mcpack input instead of CHECK-fatal in parser (#3576)
    
    * Reject malformed mcpack input instead of CHECK-fatal in parser
    
    The mcpack2pb parser used CHECK(false) to handle malformed input in
    unbox(), the object/array/isoarray iterators and the value conversion
    functions. CHECK(false) logs at FATAL level and aborts the process when
    -crash_on_fatal_log is on, so a single malformed nshead+mcpack request
    could terminate a server and drop all in-flight requests (CWE-617).
    
    Replace the wire-controlled CHECK(false) sites with LOG(ERROR) plus the
    existing error propagation (set_bad()/return 0, which the generated
    parsing code checks), and mark the stream bad in unbox() and the
    truncated as_string()/as_binary() paths so failed parses are reported
    by stream()->good() as before. The serializer and code-generator CHECKs
    are untouched since they are not driven by network input.
    
    * Avoid wire-controlled eager resize in as_string/as_binary
    
    Follow-up to the CHECK(false) hardening: the claimed value size of
    string/binary fields comes from the wire and can be close to UINT32_MAX.
    resize(size) before reading allocates that amount up front, so a tiny
    request could trigger an uncaught std::bad_alloc/std::length_error and
    still terminate the process.
    
    Read in bounded chunks and grow the output only for bytes that are
    actually present; a short read marks the stream bad as before.
    
    * Reject truncated primitive reads in cut_packed_pod
    
    The value-returning InputStream::cut_packed_pod<T>() left its result
    uninitialized when cutn() read fewer than sizeof(T) bytes, which is
    reachable when the enclosing object declares enough bytes but the real
    input ends inside a primitive value. The indeterminate value was then
    consumed or logged (UB), and the parse continued as if the input were
    complete.
    
    Zero-initialize the result, return a zero value and mark the stream bad
    on a short read, so the generated code fails the parse. The pointer
    version is unchanged: its callers already check the returned size.
    
    * Validate the trailing NUL when reading mcpack strings
    
    as_string() skipped the trailing byte with popn(1), which returns 0 on
    an exhausted stream and leaves it good. A string whose content was
    present but whose stream ended exactly before the required trailing
    '\0' (or ended with a non-NUL byte) was therefore accepted as valid.
    
    Read the terminator and reject the input when it is missing or not a
    NUL, marking the stream bad and clearing the output.
    
    * Avoid double copy of valid string/binary payload
    
    cut_bytes_to_string() copied every byte twice on valid input: once from
    the input into a stack buffer and once into the destination string. Grow
    the destination by at most one 8 KiB chunk at a time and cut directly
    into it, restoring the single-copy behavior of the original resize+cutn
    path while keeping the bounded allocation.
    
    ---------
    
    Co-authored-by: brpc-oncall <brpc-oncall@localhost>
---
 src/mcpack2pb/parser-inl.h       |  24 ++-
 src/mcpack2pb/parser.cpp         | 151 ++++++++++------
 test/brpc_mcpack2pb_unittest.cpp | 376 +++++++++++++++++++++++++++++++++++++++
 3 files changed, 485 insertions(+), 66 deletions(-)

diff --git a/src/mcpack2pb/parser-inl.h b/src/mcpack2pb/parser-inl.h
index 611d2266..030597ab 100644
--- a/src/mcpack2pb/parser-inl.h
+++ b/src/mcpack2pb/parser-inl.h
@@ -82,7 +82,10 @@ inline size_t InputStream::cut_packed_pod(T* packed_pod) {
 
 template <typename T>
 inline T InputStream::cut_packed_pod() {
-    T packed_pod;
+    // Zero-initialize so a short read never returns an indeterminate value
+    // (reading one is UB), and mark the stream bad so the truncated input
+    // fails the parse instead of being consumed as a garbage value.
+    T packed_pod = T();
     if (_size >= (int)sizeof(T)) {
         packed_pod = *(T*)_data;
         _data = (const char*)_data + sizeof(T);
@@ -90,7 +93,10 @@ inline T InputStream::cut_packed_pod() {
         _popped_bytes += sizeof(T);
         return packed_pod;
     }
-    cutn(&packed_pod, sizeof(T));
+    if (cutn(&packed_pod, sizeof(T)) != sizeof(T)) {
+        set_bad();
+        return T();
+    }
     return packed_pod;
 }
     
@@ -189,12 +195,12 @@ inline void ArrayIterator::init(InputStream* stream, 
size_t size, size_t depth)
     _expected_popped_bytes = _stream->popped_bytes() + sizeof(ItemsHead);
     _expected_popped_end = _stream->popped_bytes() + size;
     if (size < sizeof(ItemsHead)) {
-        CHECK(false) << "buffer(size=" << size << ") is not enough";
+        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();
     }
     _item_count = items_head.item_count;
@@ -218,25 +224,25 @@ inline void ISOArrayIterator::init(InputStream* stream, 
size_t size) {
     _item_count = 0;
     _left_item_count = 0;
     if (size < sizeof(IsoItemsHead)) {
-        CHECK(false) << "Not enough data";
+        LOG(ERROR) << "Not enough data";
         return set_bad();
     }
     IsoItemsHead items_head;
     if (_stream->cut_packed_pod(&items_head) != sizeof(IsoItemsHead)) {
-        CHECK(false) << "Not enough data";
+        LOG(ERROR) << "Not enough data";
         return set_bad();
     }
     _item_type = (PrimitiveFieldType)items_head.type;
     _item_size = get_primitive_type_size(_item_type);
     if (!_item_size) {
-        CHECK(false) << "type=" << type2str(_item_type)
+        LOG(ERROR) << "type=" << type2str(_item_type)
                    << " in primitive isoarray is not primitive";
         return set_bad();
     }
     const size_t items_full_size = size - sizeof(IsoItemsHead);
     _item_count = items_full_size / _item_size;
     if (_item_count * _item_size != items_full_size) {
-        CHECK(false) << "inconsistent item_count(" << _item_count
+        LOG(ERROR) << "inconsistent item_count(" << _item_count
                    << ") and value_size(" << items_full_size
                    << "), item_size=" << _item_size;
         return set_bad();
@@ -261,7 +267,7 @@ inline void ISOArrayIterator::operator++() {
     _buf_index = 0;
     if (_stream->cutn(_item_buf, _buf_count * _item_size) !=
         _buf_count * _item_size) {
-        CHECK(false) << "Not enough data";
+        LOG(ERROR) << "Not enough data";
         return set_bad();
     }
     _left_item_count -= _buf_count;
diff --git a/src/mcpack2pb/parser.cpp b/src/mcpack2pb/parser.cpp
index 9b8c964b..5cfb2bd9 100644
--- a/src/mcpack2pb/parser.cpp
+++ b/src/mcpack2pb/parser.cpp
@@ -89,13 +89,13 @@ void ObjectIterator::operator++() {
             // skipping untouched values is acceptible.
             _stream->popn(_current_field.value.size());
         } else if (_stream->popped_bytes() < _expected_popped_bytes) {
-            CHECK(false) << "value of name=" << _current_field.name
-                         << " is not fully consumed, expected="
-                         << _expected_popped_bytes << " actually="
-                         << _stream->popped_bytes();
+            LOG(ERROR) << "value of name=" << _current_field.name
+                       << " is not fully consumed, expected="
+                       << _expected_popped_bytes << " actually="
+                       << _stream->popped_bytes();
             return set_bad();
         } else {
-            CHECK(false) << "Over popped in value of name=" << 
_current_field.name
+            LOG(ERROR) << "Over popped in value of name=" << 
_current_field.name
                        << " expected=" << _expected_popped_bytes << " 
actually="
                        << _stream->popped_bytes();
             return set_bad();
@@ -109,7 +109,7 @@ void ObjectIterator::operator++() {
         FieldFixedHead head;
         if (_stream->cut_packed_pod(&head) != sizeof(FieldFixedHead) ||
             left_size() < head.full_size()) {
-            CHECK(false) << "buffer(size=" << left_size() << ") is not enough";
+            LOG(ERROR) << "buffer(size=" << left_size() << ") is not enough";
             return set_bad();
         }
         _expected_popped_bytes = _stream->popped_bytes() + head.full_size()
@@ -127,7 +127,7 @@ void ObjectIterator::operator++() {
         FieldShortHead head;
         if (_stream->cut_packed_pod(&head) != sizeof(FieldShortHead) ||
             left_size() < head.full_size()) {
-            CHECK(false) << "buffer(size=" << left_size() << ") is not enough";
+            LOG(ERROR) << "buffer(size=" << left_size() << ") is not enough";
             return set_bad();
         }
         _expected_popped_bytes = _stream->popped_bytes() + head.full_size()
@@ -148,7 +148,7 @@ void ObjectIterator::operator++() {
         FieldLongHead head;
         if (_stream->cut_packed_pod(&head) != sizeof(FieldLongHead) ||
             left_size() < head.full_size()) {
-            CHECK(false) << "buffer(size=" << left_size() << ") is not enough";
+            LOG(ERROR) << "buffer(size=" << left_size() << ") is not enough";
             return set_bad();
         }
         _expected_popped_bytes = _stream->popped_bytes() + head.full_size()
@@ -173,12 +173,12 @@ void ArrayIterator::operator++() {
             // skipping untouched values is acceptible.
             _stream->popn(_current_field.size());
         } else if (_stream->popped_bytes() < _expected_popped_bytes) {
-            CHECK(false) << "previous value is not fully consumed, expected="
+            LOG(ERROR) << "previous value is not fully consumed, expected="
                          << _expected_popped_bytes << " actually="
                          << _stream->popped_bytes();
             return set_bad();
         } else {
-            CHECK(false) << "Over popped in previous value, expected="
+            LOG(ERROR) << "Over popped in previous value, expected="
                        << _expected_popped_bytes << " actually="
                        << _stream->popped_bytes();
             return set_bad();
@@ -192,7 +192,7 @@ void ArrayIterator::operator++() {
         FieldFixedHead head;
         if (_stream->cut_packed_pod(&head) != sizeof(FieldFixedHead) ||
             left_size() < head.full_size()) {
-            CHECK(false) << "buffer(size=" << left_size() << ") is not enough";
+            LOG(ERROR) << "buffer(size=" << left_size() << ") is not enough";
             return set_bad();
         }
         _expected_popped_bytes = _stream->popped_bytes() + head.full_size()
@@ -211,7 +211,7 @@ void ArrayIterator::operator++() {
         FieldShortHead head;
         if (_stream->cut_packed_pod(&head) != sizeof(FieldShortHead) ||
             left_size() < head.full_size()) {
-            CHECK(false) << "buffer(size=" << left_size() << ") is not enough";
+            LOG(ERROR) << "buffer(size=" << left_size() << ") is not enough";
             return set_bad();
         }
         _expected_popped_bytes = _stream->popped_bytes() + head.full_size()
@@ -232,7 +232,7 @@ void ArrayIterator::operator++() {
         FieldLongHead head;
         if (_stream->cut_packed_pod(&head) != sizeof(FieldLongHead) ||
             left_size() < head.full_size()) {
-            CHECK(false) << "buffer(size=" << left_size() << ") is not enough";
+            LOG(ERROR) << "buffer(size=" << left_size() << ") is not enough";
             return set_bad();
         }
         _expected_popped_bytes = _stream->popped_bytes() + head.full_size()
@@ -252,20 +252,27 @@ void ArrayIterator::operator++() {
 
 size_t unbox(InputStream* stream) {
     FieldLongHead head;
+    // Note that these are wire-controlled inputs, so reject (set_bad +
+    // return 0, which the generated code checks) rather than CHECK-fatal,
+    // which would let a single malformed request terminate the process.
     if (stream->cut_packed_pod(&head) != sizeof(FieldLongHead)) {
-        CHECK(false) << "Input buffer is not enough";
+        LOG(ERROR) << "Input buffer is not enough";
+        stream->set_bad();
         return 0;
     }
     if (head.type() != FIELD_OBJECT) {
-        CHECK(false) << "type=" << type2str(head.type()) << " is not object";
+        LOG(ERROR) << "type=" << type2str(head.type()) << " is not object";
+        stream->set_bad();
         return 0;
     }
     if (!(head.type() & FIELD_NON_DELETED_MASK)) {
-        CHECK(false) << "The item is deleted";
+        LOG(ERROR) << "The item is deleted";
+        stream->set_bad();
         return 0;
     }
     if (head.name_size() != 0) {
-        CHECK(false) << "The object should not have name";
+        LOG(ERROR) << "The object should not have name";
+        stream->set_bad();
         return 0;
     }
     return head.value_size();
@@ -299,24 +306,24 @@ int64_t UnparsedValue::as_int64(const char* var) {
         if (value <= (uint64_t)std::numeric_limits<int64_t>::max()) {
             return (int64_t)value;
         }
-        CHECK(false) << "uint64=" << value << " to " << var << " overflows";
+        LOG(ERROR) << "uint64=" << value << " to " << var << " overflows";
         _stream->set_bad();
         return std::numeric_limits<int64_t>::max();
     }
     case PRIMITIVE_FIELD_BOOL:
         return _stream->cut_packed_pod<bool>();
     case PRIMITIVE_FIELD_FLOAT:
-        CHECK(false) << "Can't set float=" << _stream->cut_packed_pod<float>()
+        LOG(ERROR) << "Can't set float=" << _stream->cut_packed_pod<float>()
                      << " to " << var;
         _stream->set_bad();
         return 0;
     case PRIMITIVE_FIELD_DOUBLE:
-        CHECK(false) << "Can't set double=" << 
_stream->cut_packed_pod<double>()
+        LOG(ERROR) << "Can't set double=" << _stream->cut_packed_pod<double>()
                      << " to " << var;
         _stream->set_bad();
         return 0;
     }
-    CHECK(false) << "Can't set type=" << type2str(_type) << " to " << var;
+    LOG(ERROR) << "Can't set type=" << type2str(_type) << " to " << var;
     _stream->set_bad();
     return 0;
 }
@@ -328,7 +335,7 @@ uint64_t UnparsedValue::as_uint64(const char* var) {
         if (value >= 0) {
             return (uint64_t)value;
         }
-        CHECK(false) << "Can't set int8=" << value << " to " << var;
+        LOG(ERROR) << "Can't set int8=" << value << " to " << var;
         _stream->set_bad();
         return 0;
     }
@@ -337,7 +344,7 @@ uint64_t UnparsedValue::as_uint64(const char* var) {
         if (value >= 0) {
             return (uint64_t)value;
         }
-        CHECK(false) << "Can't set int16=" << value << " to " << var;
+        LOG(ERROR) << "Can't set int16=" << value << " to " << var;
         _stream->set_bad();
         return 0;
     }
@@ -346,7 +353,7 @@ uint64_t UnparsedValue::as_uint64(const char* var) {
         if (value >= 0) {
             return (uint64_t)value;
         }
-        CHECK(false) << "Can't set int32=" << value << " to " << var;
+        LOG(ERROR) << "Can't set int32=" << value << " to " << var;
         _stream->set_bad();
         return 0;
     }
@@ -355,7 +362,7 @@ uint64_t UnparsedValue::as_uint64(const char* var) {
         if (value >= 0) {
             return (uint64_t)value;
         }
-        CHECK(false) << "Can't set int64=" << value << " to " << var;
+        LOG(ERROR) << "Can't set int64=" << value << " to " << var;
         _stream->set_bad();
         return 0;
     }
@@ -370,17 +377,17 @@ uint64_t UnparsedValue::as_uint64(const char* var) {
     case PRIMITIVE_FIELD_BOOL:
         return _stream->cut_packed_pod<bool>();
     case PRIMITIVE_FIELD_FLOAT:
-        CHECK(false) << "Can't set float=" << _stream->cut_packed_pod<float>()
+        LOG(ERROR) << "Can't set float=" << _stream->cut_packed_pod<float>()
                      << " to " << var;
         _stream->set_bad();
         return 0;
     case PRIMITIVE_FIELD_DOUBLE:
-        CHECK(false) << "Can't set double=" << 
_stream->cut_packed_pod<double>()
+        LOG(ERROR) << "Can't set double=" << _stream->cut_packed_pod<double>()
                      << " to " << var;
         _stream->set_bad();
         return 0;
     }
-    CHECK(false) << "Can't set type=" << type2str(_type) << " to " << var;
+    LOG(ERROR) << "Can't set type=" << type2str(_type) << " to " << var;
     _stream->set_bad();
     return 0;
 }
@@ -396,11 +403,11 @@ int32_t UnparsedValue::as_int32(const char* var) {
     case PRIMITIVE_FIELD_INT64: {
         const int64_t value = _stream->cut_packed_pod<int64_t>();
         if (value > std::numeric_limits<int32_t>::max()) {
-            CHECK(false) << "int64=" << value << " to " << var << " overflows";
+            LOG(ERROR) << "int64=" << value << " to " << var << " overflows";
             _stream->set_bad();
             return std::numeric_limits<int32_t>::max();
         } else if (value < std::numeric_limits<int32_t>::min()) {
-            CHECK(false) << "int64=" << value << " to " << var << " 
underflows";
+            LOG(ERROR) << "int64=" << value << " to " << var << " underflows";
             _stream->set_bad();
             return std::numeric_limits<int32_t>::min();
         }
@@ -415,7 +422,7 @@ int32_t UnparsedValue::as_int32(const char* var) {
         if (value <= (uint32_t)std::numeric_limits<int32_t>::max()) {
             return (int32_t)value;
         }
-        CHECK(false) << "uint32=" << value << " to " << var << " overflows";
+        LOG(ERROR) << "uint32=" << value << " to " << var << " overflows";
         _stream->set_bad();
         return std::numeric_limits<int32_t>::max();
     }
@@ -424,24 +431,24 @@ int32_t UnparsedValue::as_int32(const char* var) {
         if (value <= (uint64_t)std::numeric_limits<int32_t>::max()) {
             return (int32_t)value;
         }
-        CHECK(false) << "uint64=" << value << " to " << var << " overflows";
+        LOG(ERROR) << "uint64=" << value << " to " << var << " overflows";
         _stream->set_bad();
         return std::numeric_limits<int32_t>::max();
     }
     case PRIMITIVE_FIELD_BOOL:
         return _stream->cut_packed_pod<bool>();
     case PRIMITIVE_FIELD_FLOAT:
-        CHECK(false) << "Can't set float=" << _stream->cut_packed_pod<float>()
+        LOG(ERROR) << "Can't set float=" << _stream->cut_packed_pod<float>()
                      << " to " << var;
         _stream->set_bad();
         return 0;
     case PRIMITIVE_FIELD_DOUBLE:
-        CHECK(false) << "Can't set double=" << 
_stream->cut_packed_pod<double>()
+        LOG(ERROR) << "Can't set double=" << _stream->cut_packed_pod<double>()
                      << " to " << var;
         _stream->set_bad();
         return 0;
     }
-    CHECK(false) << "Can't set type=" << type2str(_type) << " to " << var;
+    LOG(ERROR) << "Can't set type=" << type2str(_type) << " to " << var;
     _stream->set_bad();
     return 0;
 }
@@ -453,7 +460,7 @@ uint32_t UnparsedValue::as_uint32(const char* var) {
         if (value >= 0) {
             return (uint32_t)value;
         }
-        CHECK(false) << "Can't set int8=" << value << " to " << var;
+        LOG(ERROR) << "Can't set int8=" << value << " to " << var;
         _stream->set_bad();
         return 0;
     }
@@ -462,7 +469,7 @@ uint32_t UnparsedValue::as_uint32(const char* var) {
         if (value >= 0) {
             return (uint32_t)value;
         }
-        CHECK(false) << "Can't set int16=" << value << " to " << var;
+        LOG(ERROR) << "Can't set int16=" << value << " to " << var;
         _stream->set_bad();
         return 0;
     }
@@ -471,7 +478,7 @@ uint32_t UnparsedValue::as_uint32(const char* var) {
         if (value >= 0) {
             return (uint32_t)value;
         }
-        CHECK(false) << "Can't set int32=" << value << " to " << var;
+        LOG(ERROR) << "Can't set int32=" << value << " to " << var;
         _stream->set_bad();
         return 0;
     }
@@ -481,7 +488,7 @@ uint32_t UnparsedValue::as_uint32(const char* var) {
             value <= (int64_t)std::numeric_limits<uint32_t>::max()) {
             return (uint32_t)value;
         }
-        CHECK(false) << "Can't set int64=" << value << " to " << var;
+        LOG(ERROR) << "Can't set int64=" << value << " to " << var;
         _stream->set_bad();
         return 0;
     }
@@ -496,24 +503,24 @@ uint32_t UnparsedValue::as_uint32(const char* var) {
         if (value <= std::numeric_limits<uint32_t>::max()) {
             return (uint32_t)value;
         }
-        CHECK(false) << "uint64=" << value << " to " << var << " overflows";
+        LOG(ERROR) << "uint64=" << value << " to " << var << " overflows";
         _stream->set_bad();
         return std::numeric_limits<uint32_t>::max();
     }
     case PRIMITIVE_FIELD_BOOL:
         return _stream->cut_packed_pod<bool>();
     case PRIMITIVE_FIELD_FLOAT:
-        CHECK(false) << "Can't set float=" << _stream->cut_packed_pod<float>()
+        LOG(ERROR) << "Can't set float=" << _stream->cut_packed_pod<float>()
                      << " to " << var;
         _stream->set_bad();
         return 0;
     case PRIMITIVE_FIELD_DOUBLE:
-        CHECK(false) << "Can't set double=" << 
_stream->cut_packed_pod<double>()
+        LOG(ERROR) << "Can't set double=" << _stream->cut_packed_pod<double>()
                      << " to " << var;
         _stream->set_bad();
         return 0;
     }
-    CHECK(false) << "Can't set type=" << type2str(_type) << " to " << var;
+    LOG(ERROR) << "Can't set type=" << type2str(_type) << " to " << var;
     _stream->set_bad();
     return 0;
 }
@@ -539,17 +546,17 @@ bool UnparsedValue::as_bool(const char* var) {
     case PRIMITIVE_FIELD_BOOL:
         return _stream->cut_packed_pod<bool>();
     case PRIMITIVE_FIELD_FLOAT:
-        CHECK(false) << "Can't set float=" << _stream->cut_packed_pod<float>()
+        LOG(ERROR) << "Can't set float=" << _stream->cut_packed_pod<float>()
                      << " to " << var;
         _stream->set_bad();
         return false;
     case PRIMITIVE_FIELD_DOUBLE:
-        CHECK(false) << "Can't set double=" << 
_stream->cut_packed_pod<double>()
+        LOG(ERROR) << "Can't set double=" << _stream->cut_packed_pod<double>()
                      << " to " << var;
         _stream->set_bad();
         return false;
     }
-    CHECK(false) << "Can't set type=" << type2str(_type) << " to " << var;
+    LOG(ERROR) << "Can't set type=" << type2str(_type) << " to " << var;
     _stream->set_bad();
     return false;
 }
@@ -560,7 +567,7 @@ float UnparsedValue::as_float(const char* var) {
     } else if (_type == FIELD_FLOAT) {
         return _stream->cut_packed_pod<float>();
     }
-    CHECK(false) << "Can't set type=" << type2str(_type) << " to " << var;
+    LOG(ERROR) << "Can't set type=" << type2str(_type) << " to " << var;
     _stream->set_bad();
     return 0;
 }
@@ -571,11 +578,38 @@ double UnparsedValue::as_double(const char* var) {
     } else if (_type == FIELD_FLOAT) {
         return _stream->cut_packed_pod<float>();
     }
-    CHECK(false) << "Can't set type=" << type2str(_type) << " to " << var;
+    LOG(ERROR) << "Can't set type=" << type2str(_type) << " to " << var;
     _stream->set_bad();
     return 0;
 }
 
+// Copy `size' bytes from `stream' into `out'. `size' is wire-controlled and
+// may far exceed the bytes actually present, so grow `out' by at most one
+// chunk at a time and cut directly into it (single copy): an eager
+// resize(size) could throw an uncaught std::bad_alloc/std::length_error on
+// attacker-crafted sizes and terminate the process. Returns false and marks
+// the stream bad on a short read.
+static bool cut_bytes_to_string(InputStream* stream, std::string* out,
+                                size_t size, const char* var) {
+    out->clear();
+    const size_t kChunkSize = 8192;
+    size_t total = 0;
+    while (total < size) {
+        const size_t left = size - total;
+        const size_t chunk = left < kChunkSize ? left : kChunkSize;
+        out->resize(total + chunk);
+        const size_t n = stream->cutn(&(*out)[total], chunk);
+        if (n != chunk) {
+            LOG(ERROR) << "Not enough data for " << var;
+            stream->set_bad();
+            out->clear();
+            return false;
+        }
+        total += n;
+    }
+    return true;
+}
+
 void UnparsedValue::as_string(std::string* out, const char* var) {
     if (_size < 1) {
         // A string field must contain at least the trailing '\0'.
@@ -586,12 +620,19 @@ void UnparsedValue::as_string(std::string* out, const 
char* var) {
         _stream->set_bad();
         return;
     }
-    out->resize(_size - 1);
-    if (_stream->cutn(&(*out)[0], _size - 1) != _size - 1) {
-        CHECK(false) << "Not enough data for " << var;
+    if (!cut_bytes_to_string(_stream, out, _size - 1, var)) {
+        return;
+    }
+    // The string must end with a trailing '\0'. Read and validate it,
+    // otherwise a stream that ends exactly before the terminator accepts a
+    // truncated string with the stream still good.
+    char terminator = 0;
+    if (_stream->cutn(&terminator, 1) != 1 || terminator != '\0') {
+        LOG(ERROR) << "Invalid string terminator for " << var;
+        _stream->set_bad();
+        out->clear();
         return;
     }
-    _stream->popn(1);
 }
 
 std::string UnparsedValue::as_string(const char* var) {
@@ -601,11 +642,7 @@ std::string UnparsedValue::as_string(const char* var) {
 }
 
 void UnparsedValue::as_binary(std::string* out, const char* var) {
-    out->resize(_size);
-    if (_stream->cutn(&(*out)[0], _size) != _size) {
-        CHECK(false) << "Not enough data for " << var;
-        return;
-    }
+    cut_bytes_to_string(_stream, out, _size, var);
 }
 
 std::string UnparsedValue::as_binary(const char* var) {
diff --git a/test/brpc_mcpack2pb_unittest.cpp b/test/brpc_mcpack2pb_unittest.cpp
index 3ca56d56..b40a30c4 100644
--- a/test/brpc_mcpack2pb_unittest.cpp
+++ b/test/brpc_mcpack2pb_unittest.cpp
@@ -334,4 +334,380 @@ TEST(Mcpack2pbParserTest, EmptyObjectStillParses) {
     EXPECT_TRUE(stream.good());
 }
 
+TEST(Mcpack2pbParserTest, UnboxTruncatedBufferIsRejected) {
+    // A body shorter than the 6-byte FieldLongHead must be rejected by
+    // unbox() instead of hitting a fatal CHECK (which aborts the process
+    // when -crash_on_fatal_log is on).
+    const unsigned char data[] = { 0xff, 0x01, 0x02 };
+    butil::IOBuf body;
+    body.append(data, sizeof(data));
+
+    butil::IOBufAsZeroCopyInputStream zc_stream(body);
+    mcpack2pb::InputStream stream(&zc_stream);
+    EXPECT_EQ(0u, mcpack2pb::unbox(&stream));
+    EXPECT_FALSE(stream.good());
+}
+
+TEST(Mcpack2pbParserTest, UnboxNonObjectTypeIsRejected) {
+    // The top-level value must be an object; here it is an int8.
+    const unsigned char data[] = {
+        0x11, 0x00, 0x05, 0x00, 0x00, 0x00,  // FieldLongHead, type=int8
+    };
+    butil::IOBuf body;
+    body.append(data, sizeof(data));
+
+    butil::IOBufAsZeroCopyInputStream zc_stream(body);
+    mcpack2pb::InputStream stream(&zc_stream);
+    EXPECT_EQ(0u, mcpack2pb::unbox(&stream));
+    EXPECT_FALSE(stream.good());
+}
+
+TEST(Mcpack2pbParserTest, UnboxNamedTopLevelObjectIsRejected) {
+    // The wrapping object must be unnamed.
+    const unsigned char data[] = {
+        0x10, 0x03, 0x05, 0x00, 0x00, 0x00,  // FieldLongHead, name_size=3
+        0x61, 0x62, 0x00,                    // name "ab"
+    };
+    butil::IOBuf body;
+    body.append(data, sizeof(data));
+
+    butil::IOBufAsZeroCopyInputStream zc_stream(body);
+    mcpack2pb::InputStream stream(&zc_stream);
+    EXPECT_EQ(0u, mcpack2pb::unbox(&stream));
+    EXPECT_FALSE(stream.good());
+}
+
+TEST(Mcpack2pbParserTest, ObjectFieldBeyondBufferIsRejected) {
+    // An object field whose head claims value_size=100 while the buffer
+    // holds nothing must set the iterator bad instead of hitting a fatal
+    // CHECK.
+    const unsigned char data[] = {
+        0x10, 0x00, 0x0c, 0x00, 0x00, 0x00,  // top object, value_size=12
+        0x01, 0x00, 0x00, 0x00,              // item_count=1
+        0x10, 0x02, 0x64, 0x00, 0x00, 0x00,  // field "a", value_size=100
+        0x61, 0x00,
+    };
+    butil::IOBuf body;
+    body.append(data, sizeof(data));
+
+    butil::IOBufAsZeroCopyInputStream zc_stream(body);
+    mcpack2pb::InputStream stream(&zc_stream);
+    ASSERT_EQ(12u, mcpack2pb::unbox(&stream));
+    mcpack2pb::ObjectIterator it(&stream, body.size() - stream.popped_bytes());
+    EXPECT_TRUE(it == NULL);
+    EXPECT_FALSE(stream.good());
+}
+
+TEST(Mcpack2pbParserTest, StringFieldTruncatedDataIsRejected) {
+    // The top-level head claims value_size=1000 but the stream holds far
+    // less, and the string field claims value_size=80 with a single byte of
+    // data. as_string() must fail cleanly (mark the stream bad) instead of
+    // hitting a fatal CHECK, and advancing the iterator afterwards must not
+    // abort either.
+    const unsigned char data[] = {
+        0x10, 0x00, 0xe8, 0x03, 0x00, 0x00,  // top object, value_size=1000
+        0x01, 0x00, 0x00, 0x00,              // item_count=1
+        0x50, 0x04, 0x50, 0x00, 0x00, 0x00,  // string field "msg", size=80
+        0x6d, 0x73, 0x67, 0x00,
+        0x61,                                // only 1 byte of the string
+    };
+    butil::IOBuf body;
+    body.append(data, sizeof(data));
+
+    butil::IOBufAsZeroCopyInputStream zc_stream(body);
+    mcpack2pb::InputStream stream(&zc_stream);
+    ASSERT_EQ(1000u, mcpack2pb::unbox(&stream));
+    // The generated code trusts the value size claimed by unbox().
+    mcpack2pb::ObjectIterator it(&stream, 1000);
+    ASSERT_TRUE(it != NULL);
+    ASSERT_EQ(mcpack2pb::FIELD_STRING, it->value.type());
+    std::string value;
+    it->value.as_string(&value, "msg");
+    EXPECT_FALSE(stream.good());
+    // Advancing past the unconsumed truncated value must not abort.
+    ++it;
+    EXPECT_TRUE(it == NULL);
+}
+
+TEST(Mcpack2pbParserTest, ArrayPayloadSmallerThanHeaderIsRejected) {
+    // An array field whose declared value_size=2 is smaller than the 4-byte
+    // ItemsHead must set the iterator bad instead of hitting a fatal CHECK.
+    const unsigned char data[] = {
+        0x10, 0x00, 0x0e, 0x00, 0x00, 0x00,  // top object, value_size=14
+        0x01, 0x00, 0x00, 0x00,              // item_count=1
+        0x20, 0x02, 0x02, 0x00, 0x00, 0x00,  // array field "a", value_size=2
+        0x61, 0x00,
+        0x00, 0x00,
+    };
+    butil::IOBuf body;
+    body.append(data, sizeof(data));
+
+    butil::IOBufAsZeroCopyInputStream zc_stream(body);
+    mcpack2pb::InputStream stream(&zc_stream);
+    ASSERT_EQ(14u, mcpack2pb::unbox(&stream));
+    mcpack2pb::ObjectIterator it(&stream, body.size() - stream.popped_bytes());
+    ASSERT_TRUE(it != NULL);
+    ASSERT_EQ(mcpack2pb::FIELD_ARRAY, it->value.type());
+    mcpack2pb::ArrayIterator it2(it->value);
+    EXPECT_TRUE(it2 == NULL);
+    EXPECT_FALSE(stream.good());
+}
+
+TEST(Mcpack2pbParserTest, ArrayItemBeyondBufferIsRejected) {
+    // An array item whose head claims value_size=100 while nothing follows
+    // must set the iterator bad instead of hitting a fatal CHECK.
+    const unsigned char data[] = {
+        0x10, 0x00, 0x16, 0x00, 0x00, 0x00,  // top object, value_size=22
+        0x01, 0x00, 0x00, 0x00,              // item_count=1
+        0x20, 0x02, 0x0a, 0x00, 0x00, 0x00,  // array field "a", value_size=10
+        0x61, 0x00,
+        0x01, 0x00, 0x00, 0x00,              // item_count=1
+        0x10, 0x00, 0x64, 0x00, 0x00, 0x00,  // item object, value_size=100
+    };
+    butil::IOBuf body;
+    body.append(data, sizeof(data));
+
+    butil::IOBufAsZeroCopyInputStream zc_stream(body);
+    mcpack2pb::InputStream stream(&zc_stream);
+    ASSERT_EQ(22u, mcpack2pb::unbox(&stream));
+    mcpack2pb::ObjectIterator it(&stream, body.size() - stream.popped_bytes());
+    ASSERT_TRUE(it != NULL);
+    ASSERT_EQ(mcpack2pb::FIELD_ARRAY, it->value.type());
+    mcpack2pb::ArrayIterator it2(it->value);
+    EXPECT_TRUE(it2 == NULL);
+    EXPECT_FALSE(stream.good());
+}
+
+TEST(Mcpack2pbParserTest, ISOArrayNonPrimitiveTypeIsRejected) {
+    // An isomorphic array whose element type byte is FIELD_OBJECT (not a
+    // primitive type) must set the iterator bad instead of hitting a fatal
+    // CHECK.
+    const unsigned char data[] = {
+        0x10, 0x00, 0x0d, 0x00, 0x00, 0x00,  // top object, value_size=13
+        0x01, 0x00, 0x00, 0x00,              // item_count=1
+        0x30, 0x02, 0x01, 0x00, 0x00, 0x00,  // isoarray field "a", size=1
+        0x61, 0x00,
+        0x10,                                // element type = FIELD_OBJECT
+    };
+    butil::IOBuf body;
+    body.append(data, sizeof(data));
+
+    butil::IOBufAsZeroCopyInputStream zc_stream(body);
+    mcpack2pb::InputStream stream(&zc_stream);
+    ASSERT_EQ(13u, mcpack2pb::unbox(&stream));
+    mcpack2pb::ObjectIterator it(&stream, body.size() - stream.popped_bytes());
+    ASSERT_TRUE(it != NULL);
+    ASSERT_EQ(mcpack2pb::FIELD_ISOARRAY, it->value.type());
+    mcpack2pb::ISOArrayIterator it2(it->value);
+    EXPECT_TRUE(it2 == NULL);
+    EXPECT_FALSE(stream.good());
+}
+
+TEST(Mcpack2pbParserTest, ISOArrayInconsistentItemSizeIsRejected) {
+    // An int32 isomorphic array whose payload (5 bytes) is not a multiple
+    // of the element size (4) must set the iterator bad instead of hitting
+    // a fatal CHECK.
+    const unsigned char data[] = {
+        0x10, 0x00, 0x12, 0x00, 0x00, 0x00,  // top object, value_size=18
+        0x01, 0x00, 0x00, 0x00,              // item_count=1
+        0x30, 0x02, 0x06, 0x00, 0x00, 0x00,  // isoarray field "a", size=6
+        0x61, 0x00,
+        0x14,                                // element type = int32
+        0x01, 0x02, 0x03, 0x04, 0x05,        // 5 bytes, not a multiple of 4
+    };
+    butil::IOBuf body;
+    body.append(data, sizeof(data));
+
+    butil::IOBufAsZeroCopyInputStream zc_stream(body);
+    mcpack2pb::InputStream stream(&zc_stream);
+    ASSERT_EQ(18u, mcpack2pb::unbox(&stream));
+    mcpack2pb::ObjectIterator it(&stream, body.size() - stream.popped_bytes());
+    ASSERT_TRUE(it != NULL);
+    ASSERT_EQ(mcpack2pb::FIELD_ISOARRAY, it->value.type());
+    mcpack2pb::ISOArrayIterator it2(it->value);
+    EXPECT_TRUE(it2 == NULL);
+    EXPECT_FALSE(stream.good());
+}
+
+TEST(Mcpack2pbParserTest, FloatFieldReadAsIntegerIsRejected) {
+    // Reading a float field as an integer is a type mismatch caused by
+    // input data: it must mark the stream bad and return 0 instead of
+    // hitting a fatal CHECK.
+    const unsigned char data[] = {
+        0x10, 0x00, 0x0c, 0x00, 0x00, 0x00,  // top object, value_size=12
+        0x01, 0x00, 0x00, 0x00,              // item_count=1
+        0x44, 0x02,                          // fixed head: float field "a"
+        0x61, 0x00,
+        0x00, 0x00, 0x80, 0x3f,              // 1.0f
+    };
+    butil::IOBuf body;
+    body.append(data, sizeof(data));
+
+    butil::IOBufAsZeroCopyInputStream zc_stream(body);
+    mcpack2pb::InputStream stream(&zc_stream);
+    ASSERT_EQ(12u, mcpack2pb::unbox(&stream));
+    mcpack2pb::ObjectIterator it(&stream, body.size() - stream.popped_bytes());
+    ASSERT_TRUE(it != NULL);
+    ASSERT_EQ(mcpack2pb::FIELD_FLOAT, it->value.type());
+    EXPECT_EQ("a", it->name.as_string());
+    EXPECT_EQ(0, it->value.as_int64("a"));
+    EXPECT_FALSE(stream.good());
+}
+
+TEST(Mcpack2pbParserTest, StringFieldHugeClaimedSizeDoesNotThrow) {
+    // The string field claims a ~4GB value_size while the
+    // buffer holds a single byte. as_string() must not eagerly resize to
+    // the claimed size (an uncaught std::bad_alloc/std::length_error would
+    // terminate the process) and must reject the input cleanly.
+    const unsigned char data[] = {
+        0x10, 0x00, 0xff, 0xff, 0xff, 0xff,  // top object, 
value_size=UINT32_MAX
+        0x01, 0x00, 0x00, 0x00,              // item_count=1
+        0x50, 0x04, 0xe0, 0xff, 0xff, 0xff,  // string field "msg", size=~4GB
+        0x6d, 0x73, 0x67, 0x00,
+        0x61,                                // only 1 byte of the string
+    };
+    butil::IOBuf body;
+    body.append(data, sizeof(data));
+
+    butil::IOBufAsZeroCopyInputStream zc_stream(body);
+    mcpack2pb::InputStream stream(&zc_stream);
+    ASSERT_EQ(0xffffffffu, mcpack2pb::unbox(&stream));
+    // The generated code trusts the value size claimed by unbox().
+    mcpack2pb::ObjectIterator it(&stream, 0xffffffffu);
+    ASSERT_TRUE(it != NULL);
+    ASSERT_EQ(mcpack2pb::FIELD_STRING, it->value.type());
+    std::string value;
+    it->value.as_string(&value, "msg");
+    EXPECT_TRUE(value.empty());
+    EXPECT_FALSE(stream.good());
+}
+
+TEST(Mcpack2pbParserTest, BinaryFieldHugeClaimedSizeDoesNotThrow) {
+    // Same as above for a binary field claiming a ~4GB size: it must not be 
fed
+    // into an eager resize() before the data is proven to exist.
+    const unsigned char data[] = {
+        0x10, 0x00, 0xff, 0xff, 0xff, 0xff,  // top object, 
value_size=UINT32_MAX
+        0x01, 0x00, 0x00, 0x00,              // item_count=1
+        0x60, 0x02, 0xe0, 0xff, 0xff, 0xff,  // binary field "a", size=~4GB
+        0x61, 0x00,
+        0x62,                                // only 1 byte of data
+    };
+    butil::IOBuf body;
+    body.append(data, sizeof(data));
+
+    butil::IOBufAsZeroCopyInputStream zc_stream(body);
+    mcpack2pb::InputStream stream(&zc_stream);
+    ASSERT_EQ(0xffffffffu, mcpack2pb::unbox(&stream));
+    mcpack2pb::ObjectIterator it(&stream, 0xffffffffu);
+    ASSERT_TRUE(it != NULL);
+    ASSERT_EQ(mcpack2pb::FIELD_BINARY, it->value.type());
+    std::string value;
+    it->value.as_binary(&value, "a");
+    EXPECT_TRUE(value.empty());
+    EXPECT_FALSE(stream.good());
+}
+
+TEST(Mcpack2pbParserTest, Int32FieldTruncatedPayloadIsRejected) {
+    // The top-level head claims value_size=64 but the stream ends in the
+    // middle of the int32 field value. The truncated read must mark the
+    // stream bad (so the generated code fails the parse) and must not
+    // return an indeterminate value.
+    const unsigned char data[] = {
+        0x10, 0x00, 0x40, 0x00, 0x00, 0x00,  // top object, value_size=64
+        0x01, 0x00, 0x00, 0x00,              // item_count=1
+        0x14, 0x02,                          // fixed head: int32 field "a"
+        0x61, 0x00,
+        0x2a,                                // only 1 byte of the 4-byte value
+    };
+    butil::IOBuf body;
+    body.append(data, sizeof(data));
+
+    butil::IOBufAsZeroCopyInputStream zc_stream(body);
+    mcpack2pb::InputStream stream(&zc_stream);
+    ASSERT_EQ(64u, mcpack2pb::unbox(&stream));
+    // The generated code trusts the value size claimed by unbox().
+    mcpack2pb::ObjectIterator it(&stream, 64);
+    ASSERT_TRUE(it != NULL);
+    ASSERT_EQ(mcpack2pb::FIELD_INT32, it->value.type());
+    EXPECT_EQ(0, it->value.as_int32("a"));
+    EXPECT_FALSE(stream.good());
+}
+
+TEST(Mcpack2pbParserTest, FloatFieldTruncatedPayloadIsRejected) {
+    // Same truncation reached through a type-mismatch log path: as_int64()
+    // on a float field reads the 4-byte float payload, which must be
+    // zero-initialized on the short read (logging an indeterminate float is
+    // UB) and must mark the stream bad.
+    const unsigned char data[] = {
+        0x10, 0x00, 0x40, 0x00, 0x00, 0x00,  // top object, value_size=64
+        0x01, 0x00, 0x00, 0x00,              // item_count=1
+        0x44, 0x02,                          // fixed head: float field "a"
+        0x61, 0x00,
+        0x2a,                                // only 1 byte of the 4-byte value
+    };
+    butil::IOBuf body;
+    body.append(data, sizeof(data));
+
+    butil::IOBufAsZeroCopyInputStream zc_stream(body);
+    mcpack2pb::InputStream stream(&zc_stream);
+    ASSERT_EQ(64u, mcpack2pb::unbox(&stream));
+    mcpack2pb::ObjectIterator it(&stream, 64);
+    ASSERT_TRUE(it != NULL);
+    ASSERT_EQ(mcpack2pb::FIELD_FLOAT, it->value.type());
+    EXPECT_EQ(0, it->value.as_int64("a"));
+    EXPECT_FALSE(stream.good());
+}
+
+TEST(Mcpack2pbParserTest, StringFieldMissingTerminatorIsRejected) {
+    // The string content ("a") is fully present but the stream ends exactly
+    // before the required trailing '\0'. The terminator must be read and
+    // validated instead of being skipped with popn(1) (which returns 0 on
+    // an exhausted stream and leaves it good, accepting a truncated string).
+    const unsigned char data[] = {
+        0x10, 0x00, 0x14, 0x00, 0x00, 0x00,  // top object, value_size=20
+        0x01, 0x00, 0x00, 0x00,              // item_count=1
+        0x50, 0x02, 0x02, 0x00, 0x00, 0x00,  // string field "a", size=2
+        0x61, 0x00,
+        0x61,                                // content "a", no terminator
+    };
+    butil::IOBuf body;
+    body.append(data, sizeof(data));
+
+    butil::IOBufAsZeroCopyInputStream zc_stream(body);
+    mcpack2pb::InputStream stream(&zc_stream);
+    ASSERT_EQ(20u, mcpack2pb::unbox(&stream));
+    mcpack2pb::ObjectIterator it(&stream, 20);
+    ASSERT_TRUE(it != NULL);
+    ASSERT_EQ(mcpack2pb::FIELD_STRING, it->value.type());
+    std::string value;
+    it->value.as_string(&value, "a");
+    EXPECT_TRUE(value.empty());
+    EXPECT_FALSE(stream.good());
+}
+
+TEST(Mcpack2pbParserTest, StringFieldNonNulTerminatorIsRejected) {
+    // A string whose trailing byte is not '\0' is malformed and must be
+    // rejected as well.
+    const unsigned char data[] = {
+        0x10, 0x00, 0x14, 0x00, 0x00, 0x00,  // top object, value_size=20
+        0x01, 0x00, 0x00, 0x00,              // item_count=1
+        0x50, 0x02, 0x02, 0x00, 0x00, 0x00,  // string field "a", size=2
+        0x61, 0x00,
+        0x61, 0x62,                          // "a" with 'b' as terminator
+    };
+    butil::IOBuf body;
+    body.append(data, sizeof(data));
+
+    butil::IOBufAsZeroCopyInputStream zc_stream(body);
+    mcpack2pb::InputStream stream(&zc_stream);
+    ASSERT_EQ(20u, mcpack2pb::unbox(&stream));
+    mcpack2pb::ObjectIterator it(&stream, 20);
+    ASSERT_TRUE(it != NULL);
+    ASSERT_EQ(mcpack2pb::FIELD_STRING, it->value.type());
+    std::string value;
+    it->value.as_string(&value, "a");
+    EXPECT_TRUE(value.empty());
+    EXPECT_FALSE(stream.good());
+}
+
 }  // namespace


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

Reply via email to