Skip to content

Commit b16e013

Browse files
committed
Bound wire-declared sizes and decompressed output during parsing
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. - 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 previous message is still being assembled on the same chunk stream. Bound the declared message length by -max_body_size and drop the stale partial body when a new message header arrives. - mcpack2pb trusted the wire-decla- mcpack2pb trusted the wire-decla- mcpack2pb trusted the wire-dected Reserve() c- mcpack2pb trusted the wire-decla- mcpack2pb trusted tand cap the reserved size.
1 parent fb2f6ef commit b16e013

13 files changed

Lines changed: 328 additions & 22 deletions

src/brpc/amf.cpp

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
// under the License.
1717

1818

19+
#include <algorithm>
1920
#include <google/protobuf/descriptor.h>
2021
#include "butil/sys_byteorder.h"
2122
#include "butil/logging.h"
@@ -286,6 +287,31 @@ AMFArray* AMFObject::MutableArray(const std::string& name) {
286287
return _fields[name].MutableArray();
287288
}
288289

290+
// Read `len' bytes of string data in bounded chunks. The declared length
291+
// comes from the (untrusted) stream and may be much larger than the data
292+
// actually present, so growing the output as bytes arrive keeps a tiny
293+
// truncated message from forcing an allocation of up to
294+
// FLAGS_amf_max_string_size bytes before the availability check.
295+
static const size_t AMF_STRING_READ_CHUNK_SIZE = 64 * 1024;
296+
297+
static bool ReadAMFStringData(std::string* str, AMFInputStream* stream,
298+
uint32_t len) {
299+
str->clear();
300+
size_t nread = 0;
301+
while (nread < len) {
302+
const size_t to_read =
303+
std::min((size_t)len - nread, AMF_STRING_READ_CHUNK_SIZE);
304+
str->resize(nread + to_read);
305+
if (stream->cutn(&(*str)[nread], to_read) != to_read) {
306+
str->clear();
307+
LOG(ERROR) << "stream is not long enough";
308+
return false;
309+
}
310+
nread += to_read;
311+
}
312+
return true;
313+
}
314+
289315
static bool ReadAMFShortStringBody(std::string* str, AMFInputStream* stream) {
290316
uint16_t len = 0;
291317
if (stream->cut_u16(&len) != 2u) {
@@ -295,13 +321,7 @@ static bool ReadAMFShortStringBody(std::string* str, AMFInputStream* stream) {
295321
if (!CheckAMFStringSize(len)) {
296322
return false;
297323
}
298-
str->resize(len);
299-
if (len != 0 && stream->cutn(&(*str)[0], len) != len) {
300-
str->clear();
301-
LOG(ERROR) << "stream is not long enough";
302-
return false;
303-
}
304-
return true;
324+
return ReadAMFStringData(str, stream, len);
305325
}
306326

307327
static bool ReadAMFLongStringBody(std::string* str, AMFInputStream* stream) {
@@ -313,13 +333,7 @@ static bool ReadAMFLongStringBody(std::string* str, AMFInputStream* stream) {
313333
if (!CheckAMFStringSize(len)) {
314334
return false;
315335
}
316-
str->resize(len);
317-
if (len != 0 && stream->cutn(&(*str)[0], len) != len) {
318-
str->clear();
319-
LOG(ERROR) << "stream is not long enough";
320-
return false;
321-
}
322-
return true;
336+
return ReadAMFStringData(str, stream, len);
323337
}
324338

325339
bool ReadAMFString(std::string* str, AMFInputStream* stream) {

src/brpc/compress.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
// under the License.
1717

1818

19+
#include <limits>
20+
#include <gflags/gflags.h>
1921
#include "butil/logging.h"
2022
#include "json2pb/json_to_pb.h"
2123
#include "brpc/compress.h"
@@ -24,6 +26,24 @@
2426

2527
namespace brpc {
2628

29+
DEFINE_uint64(max_decompressed_body_size, 0,
30+
"Maximum size (in bytes) that a single compressed message body"
31+
" may decompress to, guarding against decompression bombs."
32+
" 0 (the default) means 32 times -max_body_size. Raise this"
33+
" flag explicitly if larger decompressed messages are expected");
34+
35+
uint64_t MaxDecompressedBodySize() {
36+
const uint64_t limit = FLAGS_max_decompressed_body_size;
37+
if (limit > 0) {
38+
return limit;
39+
}
40+
const uint64_t base = FLAGS_max_body_size;
41+
if (base > std::numeric_limits<uint64_t>::max() / 32) {
42+
return std::numeric_limits<uint64_t>::max();
43+
}
44+
return base * 32;
45+
}
46+
2747
static const int MAX_HANDLER_SIZE = 1024;
2848
static CompressHandler s_handler_map[MAX_HANDLER_SIZE] = { { nullptr, nullptr, nullptr } };
2949

src/brpc/compress.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@
2727

2828
namespace brpc {
2929

30+
DECLARE_uint64(max_decompressed_body_size);
31+
32+
// Effective limit (in bytes) on the decompressed size of a single message
33+
// body: FLAGS_max_decompressed_body_size, or 32 x FLAGS_max_body_size when
34+
// the flag is 0 (the default). Decompressors must fail once their output
35+
// exceeds this limit, otherwise a small compressed body that passes
36+
// -max_body_size may expand to tens of GiB (decompression bomb).
37+
uint64_t MaxDecompressedBodySize();
38+
3039
// Serializer can be used to implement custom serialization
3140
// before compression with user callback.
3241
class Serializer : public NonreflectableMessage<Serializer> {

src/brpc/policy/gzip_compress.cpp

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
// under the License.
1717

1818

19+
#include <limits>
1920
#include <google/protobuf/io/gzip_stream.h> // GzipXXXStream
21+
#include <google/protobuf/io/zero_copy_stream_impl_lite.h> // LimitingInputStream
2022
#include <google/protobuf/text_format.h>
2123
#include "butil/logging.h"
2224
#include "brpc/policy/gzip_compress.h"
@@ -73,11 +75,26 @@ static bool Decompress(const butil::IOBuf& data, google::protobuf::Message* msg,
7375
google::protobuf::io::GzipInputStream::Format format) {
7476
butil::IOBufAsZeroCopyInputStream wrapper(data);
7577
google::protobuf::io::GzipInputStream gzip(&wrapper, format);
78+
// Cap the decompressed size: zlib expands up to ~1032x, so a body that
79+
// passes -max_body_size in compressed form may still decompress to tens
80+
// of GiB (decompression bomb). The limiting stream stops feeding the
81+
// parser once the cap is hit, bounding the memory materialized here.
82+
const uint64_t limit = MaxDecompressedBodySize();
83+
const int64_t hard_limit =
84+
limit < (uint64_t)std::numeric_limits<int64_t>::max()
85+
? (int64_t)limit + 1 : std::numeric_limits<int64_t>::max();
86+
google::protobuf::io::LimitingInputStream limited_in(&gzip, hard_limit);
7687
bool ok;
7788
if (msg->GetDescriptor() == Deserializer::descriptor()) {
78-
ok = ((Deserializer*)msg)->DeserializeFrom(&gzip);
89+
ok = ((Deserializer*)msg)->DeserializeFrom(&limited_in);
7990
} else {
80-
ok = msg->ParseFromZeroCopyStream(&gzip);
91+
ok = msg->ParseFromZeroCopyStream(&limited_in);
92+
}
93+
if (ok && (uint64_t)limited_in.ByteCount() > limit) {
94+
LOG(WARNING) << "Decompressed size exceeds"
95+
" -max_decompressed_body_size=" << limit
96+
<< ", format=" << Format2CStr(format);
97+
return false;
8198
}
8299
if (!ok) {
83100
LOG(WARNING) << "Fail to deserialize input message="
@@ -141,6 +158,11 @@ inline bool GzipDecompressBase(
141158
butil::IOBufAsZeroCopyInputStream wrapper(data);
142159
google::protobuf::io::GzipInputStream in(&wrapper, format);
143160
butil::IOBufAsZeroCopyOutputStream out(msg);
161+
// Cap the decompressed size: zlib expands up to ~1032x, so a body that
162+
// passes -max_body_size in compressed form may still decompress to tens
163+
// of GiB (decompression bomb).
164+
const uint64_t limit = MaxDecompressedBodySize();
165+
uint64_t total_out = 0;
144166
const void* data_in = nullptr;
145167
int size_in = 0;
146168
void* data_out = nullptr;
@@ -154,6 +176,13 @@ inline bool GzipDecompressBase(
154176
}
155177
const int size_cp = std::min(size_in, size_out);
156178
memcpy(data_out, data_in, size_cp);
179+
total_out += size_cp;
180+
if (total_out > limit) {
181+
LOG(WARNING) << "Decompressed size exceeds"
182+
" -max_decompressed_body_size=" << limit
183+
<< ", format=" << Format2CStr(format);
184+
return false;
185+
}
157186
size_in -= size_cp;
158187
data_in = (char*)data_in + size_cp;
159188
size_out -= size_cp;

src/brpc/policy/rtmp_protocol.cpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1460,6 +1460,23 @@ ParseResult RtmpChunkStream::Feed(const RtmpBasicHeader& bh,
14601460
}
14611461
timestamp_delta = mh.timestamp;
14621462
mh.message_length = ReadBigEndian3Bytes(p + 3);
1463+
if (mh.message_length > FLAGS_max_body_size) {
1464+
LOG(ERROR) << socket->remote_side() << ": message_length="
1465+
<< mh.message_length << " in chunk_stream=" << _cs_id
1466+
<< " is too large";
1467+
return MakeParseError(PARSE_ERROR_TOO_BIG_DATA);
1468+
}
1469+
if (!_r.msg_body.empty()) {
1470+
// The new message header arrived before the previous message on
1471+
// this chunk stream completed. Drop the stale partial body,
1472+
// otherwise it would prefix the new message and, with repeated
1473+
// mid-message headers, grow `msg_body' without bound.
1474+
LOG(WARNING) << socket->remote_side() << ": Discard "
1475+
<< _r.msg_body.size() << " bytes of an incomplete"
1476+
" message in chunk_stream=" << _cs_id
1477+
<< " overridden by a ChunkType0 header";
1478+
_r.msg_body.clear();
1479+
}
14631480
_r.left_message_length = mh.message_length;
14641481
cur_chunk_size = std::min(chunk_size_in, _r.left_message_length);
14651482
if (source->size() < header_len + cur_chunk_size) {
@@ -1507,6 +1524,23 @@ ParseResult RtmpChunkStream::Feed(const RtmpBasicHeader& bh,
15071524
}
15081525
mh.timestamp = _r.last_msg_header.timestamp + timestamp_delta;
15091526
mh.message_length = ReadBigEndian3Bytes(p + 3);
1527+
if (mh.message_length > FLAGS_max_body_size) {
1528+
LOG(ERROR) << socket->remote_side() << ": message_length="
1529+
<< mh.message_length << " in chunk_stream=" << _cs_id
1530+
<< " is too large";
1531+
return MakeParseError(PARSE_ERROR_TOO_BIG_DATA);
1532+
}
1533+
if (!_r.msg_body.empty()) {
1534+
// The new message header arrived before the previous message on
1535+
// this chunk stream completed. Drop the stale partial body,
1536+
// otherwise it would prefix the new message and, with repeated
1537+
// mid-message headers, grow `msg_body' without bound.
1538+
LOG(WARNING) << socket->remote_side() << ": Discard "
1539+
<< _r.msg_body.size() << " bytes of an incomplete"
1540+
" message in chunk_stream=" << _cs_id
1541+
<< " overridden by a ChunkType1 header";
1542+
_r.msg_body.clear();
1543+
}
15101544
_r.left_message_length = mh.message_length;
15111545
cur_chunk_size = std::min(chunk_size_in, _r.left_message_length);
15121546
if (source->size() < header_len + cur_chunk_size) {

src/brpc/policy/snappy_compress.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,23 @@ bool SnappyCompress(const butil::IOBuf& in, butil::IOBuf* out) {
7676
}
7777

7878
bool SnappyDecompress(const butil::IOBuf& in, butil::IOBuf* out) {
79+
{
80+
// Reject bodies whose declared uncompressed length exceeds the
81+
// decompression cap (decompression bomb): -max_body_size is checked
82+
// against the compressed bytes only.
83+
butil::IOBufAsSnappySource length_source(in);
84+
uint32_t uncompressed_len = 0;
85+
if (!butil::snappy::GetUncompressedLength(&length_source,
86+
&uncompressed_len)) {
87+
return false;
88+
}
89+
if (uncompressed_len > MaxDecompressedBodySize()) {
90+
LOG(WARNING) << "Uncompressed size=" << uncompressed_len
91+
<< " exceeds -max_decompressed_body_size="
92+
<< MaxDecompressedBodySize();
93+
return false;
94+
}
95+
}
7996
butil::IOBufAsSnappySource source(in);
8097
butil::IOBufAsSnappySink sink(*out);
8198
return butil::snappy::Uncompress(&source, &sink);

src/mcpack2pb/generator.cpp

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -227,14 +227,14 @@ bool generate_declarations(const std::set<std::string>& ref_msgs,
227227
" $msg$* const msg = static_cast<$msg$*>(msg_base);\n" \
228228
" if (value.type() == ::mcpack2pb::FIELD_ISOARRAY) {\n" \
229229
" ::mcpack2pb::ISOArrayIterator it(value);\n" \
230-
" msg->mutable_$lcfield$()->Reserve(it.item_count());\n" \
230+
" msg->mutable_$lcfield$()->Reserve(::mcpack2pb::capped_reserve_count(it.item_count()));\n" \
231231
" for (; it != NULL; ++it) {\n" \
232232
" msg->add_$lcfield$(it.as_"#fntype "());\n" \
233233
" }\n" \
234234
" return value.stream()->good();\n" \
235235
" } else if (value.type() == ::mcpack2pb::FIELD_ARRAY) {\n" \
236236
" ::mcpack2pb::ArrayIterator it(value);\n" \
237-
" msg->mutable_$lcfield$()->Reserve(it.item_count());\n" \
237+
" msg->mutable_$lcfield$()->Reserve(::mcpack2pb::capped_reserve_count(it.item_count()));\n" \
238238
" for (; it != NULL; ++it) {\n" \
239239
" msg->add_$lcfield$(it->as_"#fntype "(\"$field$\"));\n" \
240240
" }\n" \
@@ -323,14 +323,14 @@ static bool generate_parsing(const google::protobuf::Descriptor* d,
323323
" $msg$* const msg = static_cast<$msg$*>(msg_base);\n"
324324
" if (value.type() == ::mcpack2pb::FIELD_ISOARRAY) {\n"
325325
" ::mcpack2pb::ISOArrayIterator it(value);\n"
326-
" msg->mutable_$lcfield$()->Reserve(it.item_count());\n"
326+
" msg->mutable_$lcfield$()->Reserve(::mcpack2pb::capped_reserve_count(it.item_count()));\n"
327327
" for (; it != NULL; ++it) {\n"
328328
" msg->add_$lcfield$(($enum$)it.as_int32());\n"
329329
" }\n"
330330
" return value.stream()->good();\n"
331331
" } else if (value.type() == ::mcpack2pb::FIELD_ARRAY) {\n"
332332
" ::mcpack2pb::ArrayIterator it(value);\n"
333-
" msg->mutable_$lcfield$()->Reserve(it.item_count());\n"
333+
" msg->mutable_$lcfield$()->Reserve(::mcpack2pb::capped_reserve_count(it.item_count()));\n"
334334
" for (; it != NULL; ++it) {\n"
335335
" msg->add_$lcfield$(($enum$)it->as_int32(\"$enum$\"));\n"
336336
" }\n"
@@ -361,7 +361,7 @@ static bool generate_parsing(const google::protobuf::Descriptor* d,
361361
" $msg$* const msg = static_cast<$msg$*>(msg_base);\n"
362362
" if (value.type() == ::mcpack2pb::FIELD_ARRAY) {\n"
363363
" ::mcpack2pb::ArrayIterator it(value);\n"
364-
" msg->mutable_$lcfield$()->Reserve(it.item_count());\n"
364+
" msg->mutable_$lcfield$()->Reserve(::mcpack2pb::capped_reserve_count(it.item_count()));\n"
365365
" for (; it != NULL; ++it) {\n"
366366
" if (it->type() == ::mcpack2pb::FIELD_STRING) {\n"
367367
" it->as_string(msg->add_$lcfield$(), \"$field$\");\n"
@@ -457,7 +457,7 @@ static bool generate_parsing(const google::protobuf::Descriptor* d,
457457
" return value.stream()->good();\n"
458458
" } else if (value.type() == ::mcpack2pb::FIELD_ARRAY) {\n"
459459
" ::mcpack2pb::ArrayIterator it(value);\n"
460-
" msg->mutable_$lcfield$()->Reserve(it.item_count());\n"
460+
" msg->mutable_$lcfield$()->Reserve(::mcpack2pb::capped_reserve_count(it.item_count()));\n"
461461
" for (; it != NULL; ++it) {\n"
462462
" if (it->type() == ::mcpack2pb::FIELD_OBJECT) {\n"
463463
" if (!parse_$vmsg2$_body_internal(msg->add_$lcfield$(), *it)) {\n"

src/mcpack2pb/parser-inl.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,16 @@ inline void ObjectIterator::init(InputStream* stream, size_t size) {
149149
CHECK(false) << "buffer(size=" << size << ") is not enough";
150150
return set_bad();
151151
}
152+
// `size' covers ItemsHead plus all fields and every field head takes at
153+
// least 2 bytes (FieldFixedHead), thus a valid item_count never exceeds
154+
// half of the remaining value size. The count is copied verbatim from
155+
// the wire, reject inconsistent values instead of trusting them.
156+
if (size < sizeof(ItemsHead) ||
157+
items_head.item_count > (size - sizeof(ItemsHead)) / 2) {
158+
CHECK(false) << "inconsistent item_count(" << items_head.item_count
159+
<< ") and value_size(" << size << ")";
160+
return set_bad();
161+
}
152162
_field_count = items_head.item_count;
153163
operator++();
154164
}

src/mcpack2pb/parser.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,17 @@ class ObjectIterator;
8888
class ArrayIterator;
8989
class ISOArrayIterator;
9090

91+
// Bound the argument of RepeatedField::Reserve() calls that generated
92+
// parsing code derives from a wire-declared item count. The declared count
93+
// is under control of the remote side and is not necessarily backed by
94+
// actual bytes, so reserving it verbatim lets a tiny message trigger a huge
95+
// allocation. Repeated fields grow on demand past this bound, thus parsing
96+
// of genuinely large arrays is unaffected.
97+
inline int capped_reserve_count(uint32_t item_count) {
98+
const uint32_t MAX_RESERVE_COUNT = 1024;
99+
return (int)(item_count < MAX_RESERVE_COUNT ? item_count : MAX_RESERVE_COUNT);
100+
}
101+
91102
// Represent a piece of unparsed(and unread) data of InputStream.
92103
struct UnparsedValue {
93104
UnparsedValue()

test/brpc_mcpack2pb_unittest.cpp

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,4 +156,41 @@ TEST(Mcpack2pbParserTest, ArrayItemCountIsZeroWhenPayloadSmallerThanHeader) {
156156
EXPECT_EQ(0u, it.item_count());
157157
}
158158

159+
TEST(Mcpack2pbParserTest, ObjectItemCountIsRejectedWhenInconsistentWithSize) {
160+
// An mcpack object whose ItemsHead declares an absurd field count for
161+
// the given payload must be rejected instead of being trusted: the
162+
// count is copied verbatim from the wire and every field head takes at
163+
// least 2 bytes, so a valid item count never exceeds half of the
164+
// remaining bytes.
165+
const unsigned char data[] = {
166+
0xff, 0xff, 0xff, 0x7f, // item_count = 0x7fffffff
167+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
168+
0x00, 0x00, 0x00, 0x00,
169+
};
170+
butil::IOBuf body;
171+
body.append(data, sizeof(data));
172+
173+
butil::IOBufAsZeroCopyInputStream zc_stream(body);
174+
mcpack2pb::InputStream stream(&zc_stream);
175+
mcpack2pb::ObjectIterator it(&stream, sizeof(data));
176+
EXPECT_TRUE(it == NULL);
177+
EXPECT_FALSE(stream.good());
178+
}
179+
180+
TEST(Mcpack2pbParserTest, EmptyObjectStillParses) {
181+
// A consistent empty object head (item_count 0) must still initialize
182+
// an empty iterator normally.
183+
const unsigned char data[] = {
184+
0x00, 0x00, 0x00, 0x00, // item_count = 0
185+
};
186+
butil::IOBuf body;
187+
body.append(data, sizeof(data));
188+
189+
butil::IOBufAsZeroCopyInputStream zc_stream(body);
190+
mcpack2pb::InputStream stream(&zc_stream);
191+
mcpack2pb::ObjectIterator it(&stream, sizeof(data));
192+
EXPECT_TRUE(it == NULL);
193+
EXPECT_TRUE(stream.good());
194+
}
195+
159196
} // namespace

0 commit comments

Comments
 (0)