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.

Reply via email to