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 93343891 Limit recursion depth of mcpack2pb parser (#3499)
93343891 is described below
commit 933438917249b3f9143b0874de0cd9edcfa5cc38
Author: Weibing Wang <[email protected]>
AuthorDate: Sun Aug 30 23:03:39 2026 +0800
Limit recursion depth of mcpack2pb parser (#3499)
* Limit recursion depth of mcpack2pb parser
The mcpack2pb parser recurses once per nesting level of the incoming
object/array. A message with an excessively deep recursive structure
makes the recursion grow unbounded and the stack overflow, crashing
the process. The serializer already enforces MAX_DEPTH=128, but the
parse path never checked it.
Thread the nesting depth through UnparsedValue and the iterators, and
fail the parse once the depth exceeds MAX_DEPTH, mirroring the
serializer. Deeply nested but legitimate messages keep working.
* Refine depth-limit tests and comments after review
Make the depth semantics in parser.h consistent with the implementation
(UnparsedValue::depth counts the containers the value is nested in,
iterators add one), document that the 3-arg set() intentionally keeps
the depth, and trim the unit test: parse a payload a few levels beyond
MAX_DEPTH instead of 16384 levels, which is equally effective, much
faster and avoids O(depth^2) payload construction. Also destroy the
pthread attribute on every path.
* Align iterator depth default and fix test attribute cleanup
Default the stream-based ObjectIterator/ArrayIterator depth to 1 so the
top-level container always has depth 1 whether built from a value
(depth+1) or directly from a stream, matching the documented semantics
and keeping the MAX_DEPTH guard effective for all constructor paths.
In the unit test, destroy the pthread attribute on a single shared
path right after create (when it is still valid and no longer needed),
instead of unconditionally calling pthread_attr_destroy which would be
undefined behavior if pthread_attr_init failed.
---
src/mcpack2pb/parser-inl.h | 22 ++++--
src/mcpack2pb/parser.cpp | 12 ++--
src/mcpack2pb/parser.h | 45 ++++++++++---
test/brpc_mcpack2pb_unittest.cpp | 141 +++++++++++++++++++++++++++++++++++++++
4 files changed, 200 insertions(+), 20 deletions(-)
diff --git a/src/mcpack2pb/parser-inl.h b/src/mcpack2pb/parser-inl.h
index bdfb95e5..611d2266 100644
--- a/src/mcpack2pb/parser-inl.h
+++ b/src/mcpack2pb/parser-inl.h
@@ -128,20 +128,27 @@ struct IsoItemsHead {
} __attribute__((__packed__));
inline ObjectIterator UnparsedValue::as_object() {
- return ObjectIterator(_stream, _size);
+ return ObjectIterator(_stream, _size, _depth + 1);
}
inline ArrayIterator UnparsedValue::as_array() {
- return ArrayIterator(_stream, _size);
+ return ArrayIterator(_stream, _size, _depth + 1);
}
inline ISOArrayIterator UnparsedValue::as_iso_array() {
return ISOArrayIterator(_stream, _size);
}
-inline void ObjectIterator::init(InputStream* stream, size_t size) {
+inline void ObjectIterator::init(InputStream* stream, size_t size, size_t
depth) {
+ _depth = depth;
_field_count = 0;
_stream = stream;
+ if (_depth > (size_t)MAX_DEPTH) {
+ // The input is nested too deep. Parsing it would recurse until the
+ // stack overflows (CWE-674), so fail like the serializer does when
+ // the nesting level exceeds MAX_DEPTH.
+ return set_bad();
+ }
_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
@@ -169,9 +176,16 @@ inline void ObjectIterator::init(InputStream* stream,
size_t size) {
operator++();
}
-inline void ArrayIterator::init(InputStream* stream, size_t size) {
+inline void ArrayIterator::init(InputStream* stream, size_t size, size_t
depth) {
+ _depth = depth;
_item_count = 0;
_stream = stream;
+ if (_depth > (size_t)MAX_DEPTH) {
+ // The input is nested too deep. Parsing it would recurse until the
+ // stack overflows (CWE-674), so fail like the serializer does when
+ // the nesting level exceeds MAX_DEPTH.
+ return set_bad();
+ }
_expected_popped_bytes = _stream->popped_bytes() + sizeof(ItemsHead);
_expected_popped_end = _stream->popped_bytes() + size;
if (size < sizeof(ItemsHead)) {
diff --git a/src/mcpack2pb/parser.cpp b/src/mcpack2pb/parser.cpp
index 92d7a220..9b8c964b 100644
--- a/src/mcpack2pb/parser.cpp
+++ b/src/mcpack2pb/parser.cpp
@@ -122,7 +122,7 @@ void ObjectIterator::operator++() {
if (!_current_field.name.empty()) {
_current_field.name.remove_suffix(1);
}
- _current_field.value.set((FieldType)head.type(), _stream,
head.value_size());
+ _current_field.value.set((FieldType)head.type(), _stream,
head.value_size(), _depth);
} else if (first_byte & FIELD_SHORT_MASK) {
FieldShortHead head;
if (_stream->cut_packed_pod(&head) != sizeof(FieldShortHead) ||
@@ -143,7 +143,7 @@ void ObjectIterator::operator++() {
if (!_current_field.name.empty()) {
_current_field.name.remove_suffix(1);
}
- _current_field.value.set(type, _stream, head.value_size());
+ _current_field.value.set(type, _stream, head.value_size(), _depth);
} else {
FieldLongHead head;
if (_stream->cut_packed_pod(&head) != sizeof(FieldLongHead) ||
@@ -162,7 +162,7 @@ void ObjectIterator::operator++() {
if (!_current_field.name.empty()) {
_current_field.name.remove_suffix(1);
}
- _current_field.value.set((FieldType)head.type(), _stream,
head.value_size());
+ _current_field.value.set((FieldType)head.type(), _stream,
head.value_size(), _depth);
}
}
@@ -206,7 +206,7 @@ void ArrayIterator::operator++() {
if (name_size) {
_stream->popn(name_size);
}
- _current_field.set((FieldType)head.type(), _stream, head.value_size());
+ _current_field.set((FieldType)head.type(), _stream, head.value_size(),
_depth);
} else if (first_byte & FIELD_SHORT_MASK) {
FieldShortHead head;
if (_stream->cut_packed_pod(&head) != sizeof(FieldShortHead) ||
@@ -227,7 +227,7 @@ void ArrayIterator::operator++() {
if (name_size) {
_stream->popn(name_size);
}
- _current_field.set(type, _stream, head.value_size());
+ _current_field.set(type, _stream, head.value_size(), _depth);
} else {
FieldLongHead head;
if (_stream->cut_packed_pod(&head) != sizeof(FieldLongHead) ||
@@ -246,7 +246,7 @@ void ArrayIterator::operator++() {
if (name_size) {
_stream->popn(name_size);
}
- _current_field.set((FieldType)head.type(), _stream, head.value_size());
+ _current_field.set((FieldType)head.type(), _stream, head.value_size(),
_depth);
}
}
diff --git a/src/mcpack2pb/parser.h b/src/mcpack2pb/parser.h
index d9cc6726..da18eca1 100644
--- a/src/mcpack2pb/parser.h
+++ b/src/mcpack2pb/parser.h
@@ -102,19 +102,34 @@ inline int capped_reserve_count(uint32_t item_count) {
// Represent a piece of unparsed(and unread) data of InputStream.
struct UnparsedValue {
UnparsedValue()
- : _type(FIELD_UNKNOWN), _stream(NULL), _size(0) {}
+ : _type(FIELD_UNKNOWN), _stream(NULL), _size(0), _depth(0) {}
UnparsedValue(FieldType type, InputStream* stream, size_t size)
- : _type(type), _stream(stream), _size(size) {}
+ : _type(type), _stream(stream), _size(size), _depth(0) {}
+ // `depth' is the number of containers this value is nested in
+ // (0 for the top-level object). An iterator unfolded from a value gets
+ // depth+1, i.e. the nesting level of the container itself. Input nested
+ // deeper than MAX_DEPTH is rejected, so that parsing a deeply nested
+ // object fails instead of recursing until the stack overflows.
+ UnparsedValue(FieldType type, InputStream* stream, size_t size, size_t
depth)
+ : _type(type), _stream(stream), _size(size), _depth(depth) {}
+ // Sets the value, keeping the depth of the previous value (a reused
+ // value stays at the same nesting level). Internal code populates
+ // nested values with the 4-arg overload below.
void set(FieldType type, InputStream* stream, size_t size) {
+ set(type, stream, size, _depth);
+ }
+ void set(FieldType type, InputStream* stream, size_t size, size_t depth) {
_type = type;
_stream = stream;
_size = size;
+ _depth = depth;
}
FieldType type() const { return _type; }
InputStream* stream() { return _stream; }
const InputStream* stream() const { return _stream; }
size_t size() const { return _size; }
+ size_t depth() const { return _depth; }
// Convert to concrete value. These functions can only be called once!
ObjectIterator as_object();
@@ -138,10 +153,11 @@ private:
friend class ObjectIterator;
friend class ArrayIterator;
void set_end() { _type = FIELD_UNKNOWN; }
-
+
FieldType _type;
InputStream* _stream;
size_t _size;
+ size_t _depth;
};
std::ostream& operator<<(std::ostream& os, const UnparsedValue& value);
@@ -163,9 +179,15 @@ public:
};
// Parse `n' bytes from `stream' as fields of an object.
- ObjectIterator(InputStream* stream, size_t n) { init(stream, n); }
+ // `depth' is the nesting level of the container being iterated
+ // (1 for the top-level object, since the provided value is already
+ // nested in one container). Input nested deeper than MAX_DEPTH is
+ // rejected to avoid stack overflow on unbounded recursion (CWE-674),
+ // mirroring the serializer's limit.
+ ObjectIterator(InputStream* stream, size_t n, size_t depth = 1)
+ { init(stream, n, depth); }
explicit ObjectIterator(UnparsedValue& value)
- { init(value.stream(), value.size()); }
+ { init(value.stream(), value.size(), value.depth() + 1); }
~ObjectIterator() {}
Field* operator->() { return &_current_field; }
@@ -177,7 +199,7 @@ public:
uint32_t field_count() const { return _field_count; }
private:
- void init(InputStream* stream, size_t n);
+ void init(InputStream* stream, size_t n, size_t depth);
void set_bad() {
set_end();
_stream->set_bad();
@@ -185,13 +207,14 @@ private:
void set_end() { _current_field.value._type = FIELD_UNKNOWN; }
size_t left_size() const
{ return _expected_popped_end - _expected_popped_bytes; }
-
+
Field _current_field;
uint32_t _field_count;
std::string _name_backup_string;
InputStream* _stream;
size_t _expected_popped_bytes;
size_t _expected_popped_end;
+ size_t _depth;
};
// Iterator all items in a (mcpack) array which should be created like this:
@@ -203,9 +226,10 @@ class ArrayIterator {
public:
typedef UnparsedValue Field;
- ArrayIterator(InputStream* stream, size_t size) { init(stream, size); }
+ ArrayIterator(InputStream* stream, size_t size, size_t depth = 1)
+ { init(stream, size, depth); }
explicit ArrayIterator(UnparsedValue& value)
- { init(value.stream(), value.size()); }
+ { init(value.stream(), value.size(), value.depth() + 1); }
~ArrayIterator() {}
Field* operator->() { return &_current_field; }
@@ -217,7 +241,7 @@ public:
uint32_t item_count() const { return _item_count; }
private:
- void init(InputStream* stream, size_t n);
+ void init(InputStream* stream, size_t n, size_t depth);
void set_bad() {
set_end();
_stream->set_bad();
@@ -231,6 +255,7 @@ private:
InputStream* _stream;
size_t _expected_popped_bytes;
size_t _expected_popped_end;
+ size_t _depth;
};
// Iterator all items in an isomorphic array which should be created like this:
diff --git a/test/brpc_mcpack2pb_unittest.cpp b/test/brpc_mcpack2pb_unittest.cpp
index ad040a5b..3ca56d56 100644
--- a/test/brpc_mcpack2pb_unittest.cpp
+++ b/test/brpc_mcpack2pb_unittest.cpp
@@ -18,6 +18,7 @@
// Unit tests for the mcpack2pb parser.
#include <gtest/gtest.h>
+#include <pthread.h>
#include "butil/iobuf.h"
#include "mcpack2pb/parser.h"
@@ -156,6 +157,146 @@ TEST(Mcpack2pbParserTest,
ArrayItemCountIsZeroWhenPayloadSmallerThanHeader) {
EXPECT_EQ(0u, it.item_count());
}
+// Builds the wire bytes of a recursive message `Node { repeated Node
+// children = 1; }' nesting `depth' levels of { children: [ ... ] }, the way
+// protoc-gen-mcpack serializes such a message.
+static void AppendU32(std::string* out, uint32_t value) {
+ char buf[4];
+ buf[0] = (char)(value & 0xff);
+ buf[1] = (char)((value >> 8) & 0xff);
+ buf[2] = (char)((value >> 16) & 0xff);
+ buf[3] = (char)((value >> 24) & 0xff);
+ out->append(buf, 4);
+}
+
+static std::string BuildRecursivePayload(int depth) {
+ // The innermost level is an empty object: an ItemsHead with no items.
+ std::string body;
+ AppendU32(&body, 0);
+ const std::string name = std::string("children\0", 9); // trailing '\0'
+ // Reserve enough space for the final payload so that appending at each
+ // nesting level does not reallocate (the size of each wrapping level is
+ // 1 + 1 + 4 + body + 4 + item + 1 + 1 + 4 + name + arr).
+ body.reserve((size_t)depth * 30);
+ for (int i = 1; i < depth; ++i) {
+ std::string item; // a FIELD_OBJECT item wrapping the inner payload
+ item.push_back(0x10); // FIELD_OBJECT
+ item.push_back(0x00); // name_size = 0
+ AppendU32(&item, (uint32_t)body.size()); // value_size
+ item.append(body);
+
+ std::string arr; // an array holding a single item
+ AppendU32(&arr, 1);
+ arr.append(item);
+
+ std::string child; // FIELD_ARRAY "children"
+ child.push_back(0x20); // FIELD_ARRAY
+ child.push_back((char)name.size()); // name_size
+ AppendU32(&child, (uint32_t)arr.size()); // value_size
+ child.append(name);
+ child.append(arr);
+
+ std::string new_body; // an object holding a single field
+ AppendU32(&new_body, 1);
+ new_body.append(child);
+ body.swap(new_body);
+ }
+ return body;
+}
+
+// Simulates the recursion pattern of the functions generated by
+// protoc-gen-mcpack for a message with a repeated message field
+// (e.g. Node.children): parse_<msg>_body_internal creates an ObjectIterator,
+// set_<msg>_<field> creates an ArrayIterator and calls
+// parse_<msg>_body_internal for each item.
+static bool ParseNodeInternal(mcpack2pb::UnparsedValue& value) {
+ mcpack2pb::ObjectIterator it(value);
+ for (; it != nullptr; ++it) {
+ if (it->name == "children") {
+ if (it->value.type() != mcpack2pb::FIELD_ARRAY) {
+ return false;
+ }
+ mcpack2pb::ArrayIterator it2(it->value);
+ for (; it2 != nullptr; ++it2) {
+ if (it2->type() != mcpack2pb::FIELD_OBJECT ||
+ !ParseNodeInternal(*it2)) {
+ return false;
+ }
+ }
+ }
+ }
+ return value.stream()->good();
+}
+
+struct ParseArgs {
+ const std::string* payload;
+ bool parse_ok;
+};
+
+static void* ParseOnSmallStack(void* arg) {
+ ParseArgs* args = static_cast<ParseArgs*>(arg);
+ butil::IOBuf buf;
+ buf.append(args->payload->data(), args->payload->size());
+ butil::IOBufAsZeroCopyInputStream zc_stream(buf);
+ mcpack2pb::InputStream stream(&zc_stream);
+ mcpack2pb::UnparsedValue value(mcpack2pb::FIELD_OBJECT, &stream,
+ buf.size());
+ args->parse_ok = ParseNodeInternal(value);
+ return nullptr;
+}
+
+// Parses `payload' on a 1 MB stack thread, the size of a NORMAL bthread
+// stack in brpc, so that the test behaves like a request served by brpc.
+static int ParseRecursivePayloadWith1MBStack(const std::string& payload,
+ bool* ok) {
+ ParseArgs args = { &payload, false };
+ pthread_attr_t attr;
+ int rc = pthread_attr_init(&attr);
+ if (rc != 0) {
+ return -1;
+ }
+ rc = pthread_attr_setstacksize(&attr, 1024 * 1024);
+ if (rc != 0) {
+ pthread_attr_destroy(&attr);
+ return -1;
+ }
+ pthread_t tid;
+ rc = pthread_create(&tid, &attr, ParseOnSmallStack, &args);
+ // The attribute is not needed right after create, destroy it here so
+ // that every path below shares one cleanup point.
+ pthread_attr_destroy(&attr);
+ if (rc != 0) {
+ return -1;
+ }
+ rc = pthread_join(tid, nullptr);
+ if (rc != 0) {
+ return -2;
+ }
+ *ok = args.parse_ok;
+ return 0;
+}
+
+TEST(Mcpack2pbParserTest, DeeplyNestedPayloadIsRejectedWithoutStackOverflow) {
+ // A few levels beyond MAX_DEPTH (128) suffice: such input must be
+ // rejected by the depth limit. Before the fix the parse accepted it
+ // (and far deeper input would overflow the 1 MB stack and crash the
+ // process), so this fails without the guard. Keeping the depth small
+ // keeps the test fast even under sanitizers.
+ const std::string payload = BuildRecursivePayload(mcpack2pb::MAX_DEPTH +
2);
+ bool ok = true;
+ ASSERT_EQ(0, ParseRecursivePayloadWith1MBStack(payload, &ok));
+ // The parse must fail cleanly instead of crashing the process.
+ EXPECT_FALSE(ok);
+}
+
+TEST(Mcpack2pbParserTest, ModeratelyNestedPayloadParses) {
+ // 32 levels are well within the depth limit and must parse cleanly.
+ const std::string payload = BuildRecursivePayload(32);
+ bool ok = false;
+ ASSERT_EQ(0, ParseRecursivePayloadWith1MBStack(payload, &ok));
+ EXPECT_TRUE(ok);
+}
+
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
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]