Skip to content

Commit 7bc1c38

Browse files
committed
librepgp: cap decompressed data and packet count during dump
A crafted nested compressed data packet made the dump consume unbounded memory and time: each zlib layer expands up to ~1032x, and the JSON dump accumulated the whole decompressed packet stream in the DOM (fuzz_dump OOM at 2.1GB from a 4KB input, also reachable via rnp --list-packets on a crafted file). The existing MAXIMUM_NESTING_LEVEL and MAXIMUM_STREAM_PKTS checks did not help since they bound neither the decompressed size nor the number of non-streaming packets. Each dump call now shares a 256MB decompressed-data budget, enforced by a limiting source on top of every decompressed stream, and stops after 32768 packets. Reaching a limit truncates the dump gracefully.
1 parent 4ec426f commit 7bc1c38

5 files changed

Lines changed: 146 additions & 12 deletions

File tree

‎src/librepgp/stream-def.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@
6161
#define MAXIMUM_STREAM_PKTS 16
6262
#define MAXIMUM_ERROR_PKTS 64
6363

64+
/* Limits for the packet dump, making nested decompression bombs and packet
65+
* floods non-lethal for memory/time (both text and JSON dump paths) */
66+
#define MAXIMUM_DUMP_PKTS 32768
67+
#define MAXIMUM_DUMP_DECOMPRESSED_SIZE (256 * 1024 * 1024)
68+
6469
/* Maximum text line length supported by GnuPG */
6570
#define MAXIMUM_GNUPG_LINELEN 19995
6671

‎src/librepgp/stream-dump.cpp‎

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,55 @@ dst_hexdump(pgp_dest_t &dst, const std::vector<uint8_t> &data)
578578
namespace rnp {
579579
using namespace pgp;
580580

581+
/* Source wrapper which limits the number of bytes which may be read through it,
582+
* used on top of the decompressed packet contents during the dump. */
583+
typedef struct dump_limited_src_param_t {
584+
pgp_source_t * readsrc;
585+
std::shared_ptr<dump_budget_t> budget;
586+
} dump_limited_src_param_t;
587+
588+
static bool
589+
dump_limited_src_read(pgp_source_t *src, void *buf, size_t len, size_t *readres)
590+
{
591+
auto param = static_cast<dump_limited_src_param_t *>(src->param);
592+
if (!param->budget->left) {
593+
if (!param->budget->hit) {
594+
RNP_LOG("too much decompressed data during the dump, stopping.");
595+
param->budget->hit = true;
596+
}
597+
return false;
598+
}
599+
len = std::min(len, param->budget->left);
600+
if (!param->readsrc->read(buf, len, readres)) {
601+
return false;
602+
}
603+
param->budget->left -= *readres;
604+
return true;
605+
}
606+
607+
static void
608+
dump_limited_src_close(pgp_source_t *src)
609+
{
610+
free(src->param);
611+
}
612+
613+
static rnp_result_t
614+
init_dump_limited_src(pgp_source_t * src,
615+
pgp_source_t * readsrc,
616+
const std::shared_ptr<dump_budget_t> &budget)
617+
{
618+
if (!init_src_common(src, sizeof(dump_limited_src_param_t))) {
619+
return RNP_ERROR_OUT_OF_MEMORY; // LCOV_EXCL_LINE
620+
}
621+
auto param = static_cast<dump_limited_src_param_t *>(src->param);
622+
param->readsrc = readsrc;
623+
param->budget = budget;
624+
src->raw_read = dump_limited_src_read;
625+
src->raw_close = dump_limited_src_close;
626+
src->type = PGP_STREAM_PARLEN_PACKET;
627+
return RNP_SUCCESS;
628+
}
629+
581630
void
582631
DumpContext::copy_params(const DumpContext &ctx)
583632
{
@@ -588,6 +637,8 @@ DumpContext::copy_params(const DumpContext &ctx)
588637
layers = ctx.layers;
589638
stream_pkts = ctx.stream_pkts;
590639
failures = ctx.failures;
640+
dumped_pkts = ctx.dumped_pkts;
641+
zbudget = ctx.zbudget;
591642
}
592643

593644
bool
@@ -1442,6 +1493,10 @@ DumpContextDst::dump_compressed()
14421493
if (ret) {
14431494
return ret;
14441495
}
1496+
Source lsrc;
1497+
if ((ret = init_dump_limited_src(&lsrc.src(), &zsrc->src(), zbudget))) {
1498+
return ret;
1499+
}
14451500

14461501
dst_printf(dst, "Compressed data packet\n");
14471502
indent_dest_increase(dst);
@@ -1451,10 +1506,15 @@ DumpContextDst::dump_compressed()
14511506
dst_print_zalg(dst, NULL, (pgp_compression_type_t) zalg);
14521507
dst_printf(dst, "Decompressed contents:\n");
14531508

1454-
std::unique_ptr<DumpContextDst> ctx(new DumpContextDst(zsrc->src(), dst));
1509+
std::unique_ptr<DumpContextDst> ctx(new DumpContextDst(lsrc.src(), dst));
14551510
ctx->copy_params(*this);
14561511
ret = ctx->dump(true);
14571512
copy_params(*ctx);
1513+
if (ret && zbudget->hit) {
1514+
/* limit on the decompressed data was reached - dump what we have so far */
1515+
dst_printf(dst, ":too much decompressed data, stopping.\n");
1516+
ret = RNP_SUCCESS;
1517+
}
14581518
indent_dest_decrease(dst);
14591519
return ret;
14601520
}
@@ -1521,6 +1581,16 @@ DumpContextDst::dump_raw_packets()
15211581
}
15221582

15231583
while (!src.eof()) {
1584+
if (zbudget->hit) {
1585+
dst_printf(dst, ":too much decompressed data, stopping.\n");
1586+
return RNP_SUCCESS;
1587+
}
1588+
if (++dumped_pkts > MAXIMUM_DUMP_PKTS) {
1589+
RNP_LOG("Too many packets during the dump.");
1590+
dst_printf(dst, ":too many packets, stopping.\n");
1591+
return RNP_SUCCESS;
1592+
}
1593+
15241594
pgp_packet_hdr_t hdr{};
15251595
size_t off = src.readb;
15261596
rnp_result_t hdrret = stream_peek_packet_hdr(&src, &hdr);
@@ -1672,7 +1742,17 @@ DumpContextDst::dump(bool raw_only)
16721742
DumpContextDst ctx(armor.src(), dst);
16731743
ctx.copy_params(*this);
16741744
ret = ctx.dump(true);
1675-
if (ret) {
1745+
if (ret && !zbudget->hit) {
1746+
break;
1747+
}
1748+
/* accumulate counters, but not layers: each armored message is
1749+
* dumped at the same nesting level */
1750+
stream_pkts = ctx.stream_pkts;
1751+
failures = ctx.failures;
1752+
dumped_pkts = ctx.dumped_pkts;
1753+
if (zbudget->hit) {
1754+
dst_printf(dst, ":too much decompressed data, stopping.\n");
1755+
ret = RNP_SUCCESS;
16761756
break;
16771757
}
16781758
}
@@ -2520,6 +2600,10 @@ DumpContextJson::dump_compressed(nlohmann::ordered_json &pkt)
25202600
if (ret) {
25212601
return ret;
25222602
}
2603+
Source lsrc;
2604+
if ((ret = init_dump_limited_src(&lsrc.src(), &zsrc->src(), zbudget))) {
2605+
return ret;
2606+
}
25232607

25242608
uint8_t zalg;
25252609
get_compressed_src_alg(&zsrc->src(), &zalg);
@@ -2528,10 +2612,15 @@ DumpContextJson::dump_compressed(nlohmann::ordered_json &pkt)
25282612
}
25292613

25302614
nlohmann::ordered_json contents;
2531-
DumpContextJson ctx(zsrc->src(), &contents);
2615+
DumpContextJson ctx(lsrc.src(), &contents);
25322616
ctx.copy_params(*this);
25332617
ret = ctx.dump(true);
25342618
copy_params(ctx);
2619+
if (zbudget->hit) {
2620+
/* limit on the decompressed data was reached - dump what we have so far */
2621+
pkt["contents"] = std::move(contents);
2622+
return RNP_SUCCESS;
2623+
}
25352624
if (!ret) {
25362625
pkt["contents"] = std::move(contents);
25372626
}
@@ -2615,6 +2704,14 @@ DumpContextJson::dump_raw_packets()
26152704
}
26162705

26172706
while (!src.eof()) {
2707+
if (zbudget->hit) {
2708+
break;
2709+
}
2710+
if (++dumped_pkts > MAXIMUM_DUMP_PKTS) {
2711+
RNP_LOG("Too many packets during the dump.");
2712+
break;
2713+
}
2714+
26182715
auto & pkt = pkts.emplace_back(nlohmann::ordered_json::object());
26192716
pgp_packet_hdr_t hdr{};
26202717
if (!dump_pkt_hdr(hdr, pkt)) {
@@ -2737,12 +2834,21 @@ DumpContextJson::dump(bool raw_only)
27372834
DumpContextJson ctx(armor.src(), &block);
27382835
ctx.copy_params(*this);
27392836
ret = ctx.dump(true);
2740-
if (ret) {
2837+
if (ret && !zbudget->hit) {
27412838
break;
27422839
}
2840+
/* accumulate counters, but not layers: each armored message is
2841+
* dumped at the same nesting level */
2842+
stream_pkts = ctx.stream_pkts;
2843+
failures = ctx.failures;
2844+
dumped_pkts = ctx.dumped_pkts;
27432845
if (block.is_array()) {
27442846
res.insert(res.end(), block.begin(), block.end());
27452847
}
2848+
if (zbudget->hit) {
2849+
ret = RNP_SUCCESS;
2850+
break;
2851+
}
27462852
}
27472853
*json = std::move(res);
27482854
return ret;

‎src/librepgp/stream-dump.h‎

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,31 +30,48 @@
3030
#include <stdint.h>
3131
#include <stdbool.h>
3232
#include <sys/types.h>
33+
#include <memory>
3334
#include <nlohmann/json.hpp>
3435
#include "rnp.h"
36+
#include "stream-def.h"
3537
#include "stream-common.h"
3638
#include "stream-packet.h"
3739
#include "sig_subpacket.hpp"
3840
#include "key_material.hpp"
3941

4042
namespace rnp {
43+
44+
/* Budget of the decompressed data which may be processed during the single dump
45+
* call. Shared between all dump layers, so nested decompression bombs cannot
46+
* exceed it (see issue with the fuzz_dump OOM). */
47+
struct dump_budget_t {
48+
size_t left;
49+
bool hit;
50+
};
51+
4152
class DumpContext {
4253
protected:
43-
bool dump_mpi{};
44-
bool dump_packets{};
45-
bool dump_grips{};
46-
size_t layers{};
47-
size_t stream_pkts{};
48-
size_t failures{};
49-
pgp_source_t &src;
54+
bool dump_mpi{};
55+
bool dump_packets{};
56+
bool dump_grips{};
57+
size_t layers{};
58+
size_t stream_pkts{};
59+
size_t failures{};
60+
size_t dumped_pkts{};
61+
/* shared between all (nested) dump contexts */
62+
std::shared_ptr<dump_budget_t> zbudget{};
63+
pgp_source_t & src;
5064

5165
virtual rnp_result_t dump_raw_packets() = 0;
5266
void copy_params(const DumpContext &ctx);
5367
bool get_aead_hdr(pgp_aead_hdr_t &hdr);
5468
bool skip_cleartext();
5569

5670
public:
57-
DumpContext(pgp_source_t &asrc) : src(asrc){};
71+
DumpContext(pgp_source_t &asrc)
72+
: zbudget(std::make_shared<dump_budget_t>(
73+
dump_budget_t{MAXIMUM_DUMP_DECOMPRESSED_SIZE, false})),
74+
src(asrc){};
5875
virtual ~DumpContext(){};
5976

6077
void
4 KB
Binary file not shown.

‎src/tests/fuzz_dump.cpp‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,10 @@ TEST_F(rnp_tests, test_fuzz_dump)
6161

6262
data = file_to_vec(DATA_PATH "abrt-5093675862917120");
6363
assert_int_equal(dump_LLVMFuzzerTestOneInput(data.data(), data.size()), 0);
64+
65+
/* nested decompression bomb: must be stopped by the dump limits */
66+
data = file_to_vec(DATA_PATH "outofmemory-12fd9b02d8465f2f");
67+
start = time(NULL);
68+
assert_int_equal(dump_LLVMFuzzerTestOneInput(data.data(), data.size()), 0);
69+
assert_true(time(NULL) - start <= 30);
6470
}

0 commit comments

Comments
 (0)