__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).

 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.
-- 
2.47.3

Reply via email to