Hi Matej,
On Tue, 2026-09-08 at 21:16 +0200, Matej Smycka wrote:
> __libelf_decompress_zstd () allocates the output buffer from the
> uncompressed size taken out of the section's compression header:
>
> void *buf_out = malloc (size_out ?: 1);
>
> size_out is chdr.ch_size, an attacker-controlled field of the section's
> Elf{32,64}_Chdr. The zlib decompressor rejects implausible expansion
> ratios before allocating:
>
> if (unlikely (size_out / 1032 > size_in))
> {
> __libelf_seterrno (ELF_E_INVALID_DATA);
> return NULL;
> }
>
> but __libelf_decompress_zstd () has no such guard, so a section whose
> header declares a huge ch_size over a tiny compressed payload drives an
> arbitrarily large allocation. The path is reachable from
> __libelf_decompress_elf (), i.e. via elf_compress (scn, 0, 0) and via
> elf_strptr () on a SHF_COMPRESSED string section, so any consumer that
> reads a crafted ELF can be made to request the allocation.
>
> Add the same kind of ratio check to __libelf_decompress_zstd () before
> the malloc. The bound follows from the zstd frame format: the smallest
> possible block is a 3-byte block header followed by a 1-byte RLE
> payload, and a single block expands to at most ZSTD_BLOCKSIZE_MAX
> (128 KiB) bytes, so no valid frame can expand by more than
> ZSTD_BLOCKSIZE_MAX / 4 = 32768:1. Real data gets close to this: a
> 256 MiB all-zero section compresses to 8211 bytes of zstd payload
> (32692:1) and still round-trips through elf_compress () with the check
> in place.
>
> Reproduced with a 305-byte ELF whose .comp section is SHF_COMPRESSED
> with ch_type ELFCOMPRESS_ZSTD, ch_size 0x7fffffff00 and an 8-byte
> payload: on an unfixed build libelf requests malloc (549755813632)
> (512 GiB); on a fixed build the decompress fails with
> ELF_E_INVALID_DATA and no oversized allocation is attempted.
>
> * libelf/elf_compress.c (__libelf_decompress_zstd): Reject a
> size_out larger than size_in times the maximum zstd expansion
> ratio before allocating the output buffer.
>
> Signed-off-by: Matej Smycka <[email protected]>
> ---
> v2: derive the ratio from ZSTD_BLOCKSIZE_MAX and the minimum block size
> instead of a bare 32768, explain it in a comment, and add the
> ChangeLog entry (Mark).
The comment explains it well, thanks for turning this from a magic
number into a "real calculation".
It looks like a good sanity check. Pushed.
https://sourceware.org/cgit/elfutils/commit/?id=0a8e36237d58
Thanks,
Mark
> libelf/elf_compress.c | 15 +++++++++++++++
> 1 file changed, 15 insertions(+)
>
> diff --git a/libelf/elf_compress.c b/libelf/elf_compress.c
> index 4c14a561..685e4a79 100644
> --- a/libelf/elf_compress.c
> +++ b/libelf/elf_compress.c
> @@ -410,6 +410,21 @@ __libelf_decompress_zlib (void *buf_in, size_t size_in,
> size_t size_out)
> static void *
> __libelf_decompress_zstd (void *buf_in, size_t size_in, size_t size_out)
> {
> + /* Catch highly unlikely compression ratios so we don't allocate
> + some giant amount of memory for nothing. The smallest zstd block
> + is a 3-byte block header followed by a 1-byte RLE payload, and a
> + single block expands to at most ZSTD_BLOCKSIZE_MAX bytes, so no
> + valid frame can expand by more than ZSTD_BLOCKSIZE_MAX / 4
> + (32768:1). See doc/zstd_compression_format.md in the zstd
> + sources. */
> + const size_t zstd_min_block = 3 + 1;
> + const size_t zstd_max_ratio = ZSTD_BLOCKSIZE_MAX / zstd_min_block;
> + if (unlikely (size_out / zstd_max_ratio > size_in))
> + {
> + __libelf_seterrno (ELF_E_INVALID_DATA);
> + return NULL;
> + }
> +
> /* Malloc might return NULL when requesting zero size. This is highly
> unlikely, it would only happen when the compression was forced.
> But we do need a non-NULL buffer to return and set as result.