Skip to content

GHSA-24wx-9w62-c96w on CTRL-OS 26.05

Aliases: GHSA-24wx-9w62-c96w

Packages: libheif

Status: Plausible

Advisory Information

Summary

libheif decompresses mime metadata items and unci (ISO 23001-17) compressed image data using brotli and zlib/deflate. The brotli path decompress_brotli() appends decoder output to a std::vector in a loop with no output-size check at all, in stark contrast to the sibling do_inflate() which attempts a 256 MB cap. That zlib cap is itself defective: it checks the small intermediate dst buffer (always 8192 bytes, reset each loop) rather than the accumulated output vector, and it only executes in the Z_BUF_ERROR branch that valid compressed data never reaches, so the check is effectively dead code. Neither path routes its output vector through MemoryHandle, so max_memory_block_size and max_total_memory are entirely bypassed. An attacker simply supplies a high-ratio brotli or zlib bomb as a mime item's content_encoding payload; on file open HeifContext::interpret_heif_file_images() iterates every item and decompresses each, so up to max_items (1000) bombs accumulate.

Seven variants share this root cause. Five are confirmed with POC:

Variant Path / trigger CVSS
V1: brotli mime item, no output limit decompress_brotli(), content_encoding="br" 7.5
V2: brotli unci/mime, three trigger paths decompress_brotli(), unci cmpC "brot" + icef 7.5
V3: icef non-tile unit overlap, N x decompression unc_decoder.cc, icef overlapping units 7.5
V4: zlib 256 MB check checks wrong object do_inflate(), dead-code limit 5.5
V5: multi-item compressed metadata accumulation interpret_heif_file_images(), 1000 items 7.5
V6: brotli single-call unbounded (alternate framing) decompress_brotli() 7.5
V7: brotli alternate trigger path decompress_brotli() 7.5

We assess the cluster at roughly CVSS 3.1 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H). This score is an automated estimate from our analysis and is provided for reference; the final score is for the maintainer to confirm. V4 is scored AV:L/UI:R because its primary trigger is a locally processed metadata item.

Tested versions

Version / revision Build Result
v1.23.1 (v1.23.1-7-g1a3583bc) ASan affected. Unbounded allocation / OOM confirmed via POC.
v1.23.1 non-ASan affected. OOM confirmed.

Source was not modified. The brotli path requires only HAVE_BROTLI (CMake auto-detects libbrotli-dev); the unci paths additionally require WITH_UNCOMPRESSED_CODEC=ON (default OFF). Affected versions >= 1.19.0 (when brotli support was introduced).

Root cause

Two compounding root causes:

  1. No output-size limit in decompress_brotli() (V1, V2, V6, V7). compression_brotli.cc:36-82 loops on BrotliDecoderDecompressStream() and appends each chunk to output via output.insert() with no size check. The function never uses MemoryHandle, so neither max_memory_block_size nor max_total_memory constrains the decompressed output. Brotli's high compression ratio (>1000:1) lets a few KB of payload expand to GB.

  2. Defective output-size limit in do_inflate() (V4), and no cumulative tracking across items (V5). The 256 MB check in compression_zlib.cc:131 checks dst.size() (the 8192-byte intermediate buffer, reset each iteration) instead of the accumulated output vector, and it lives only in the Z_BUF_ERROR branch that valid data never hits, making it dead code. The output vector grows unbounded via insert(), bypasses MemoryHandle/TotalMemoryTracker, and is not subject to max_memory_block_size/max_total_memory. On top of this, interpret_heif_file_images() decompresses every mime item independently with no global decompressed-memory accounting; with max_items=1000, even a hypothetical per-item 256 MB cap would allow 256 GB cumulative.

  3. icef unit overlap (V3). In unc_decoder.cc, the icef non-tile path decompresses each icef unit independently and appends to a data vector, but never checks whether units overlap. When unit_offset_code > 0, unit_offset is read directly from the file (attacker-controlled), so N units can point at the same compressed data and be decompressed N times. zlib's per-call 256 MB limit only bounds each call, so N overlapping units yield N x 256 MB cumulative output (e.g. ~340 units in 1 KB of icef produce ~85 GB).

Affected files and functions: - libheif/compression_brotli.ccdecompress_brotli - libheif/compression_zlib.ccdo_inflate - libheif/codecs/uncompressed/unc_decoder.ccget_compressed_image_data_uncompressed, do_decompress_data - libheif/codecs/uncompressed/unc_boxes.ccBox_icef::parse - libheif/file.ccHeifFile::get_uncompressed_item_data, get_compressed_data_for_item, get_item_data - libheif/context.ccHeifContext::interpret_heif_file_images - libheif/security_limits.ccglobal_security_limits

Security boundary and prerequisites

  • Attacker: remote, unauthenticated. Sends a crafted HEIF/AVIF file.
  • No authentication required (file parsing requires none).
  • Attack complexity: Low. A single malicious file triggers the issue; the mime/brotli path requires no non-default build option beyond brotli being available.
  • Reachable via heif_context_read_from_file() and heif_context_read_from_memory() (web upload). Header compression is enabled by default in the release/develop CMake presets.
  • V3/V6/V7 require WITH_UNCOMPRESSED_CODEC=ON for the unci trigger path.

PoC (V1: brotli MIME item, most dangerous variant)

Constructing the malicious file

Create a HEIF file with the following structure:

  • ftyp box: major brand heic
  • meta box containing:
  • hdlr (pict)
  • pitm: item 1 (primary image)
  • iinf: two items:
    • Item 1: unci 1×1 monochrome image
    • Item 2: mime item, content_type="application/octet-stream", content_encoding="br"
  • iprp: ispe(1×1) + cmpd(Y) + uncC(v0) + ipma
  • iloc: item 1 = 1 byte image data, item 2 = brotli bomb data
  • iref: item 2 → cdsc → item 1 (metadata references image)
  • mdat: 1 byte image data + brotli bomb

The brotli bomb payload is a large block of repeated zero bytes compressed with brotli at maximum quality. A 784-byte brotli-compressed payload decompresses to 500MB.

Triggering the vulnerability

The vulnerability is triggered by opening the file — no image decode is needed. When libheif opens the file, heif_context_read_from_file()interpret_heif_file_images() automatically processes all metadata items, including mime items with content_encoding="br". This calls decompress_brotli(), which decompresses the brotli bomb into an unbounded output vector.

heif_context_read_from_file()          // file open
  → HeifContext::read_from_file()
    → HeifContext::interpret_heif_file()
      → HeifContext::interpret_heif_file_images()
        → HeifFile::get_uncompressed_item_data()   // processes mime item
          → decompress_brotli()                     // unbounded output growth

Result: security limit bypass

Even with max_total_memory=1 byte and max_memory_block_size=1 byte, the 784-byte file causes 1507MB of memory allocation. The decompression output vector is not tracked by MemoryHandle, so the security limits are completely bypassed.

Result: OOM crash

With memory constrained (e.g., ulimit -v 524288 for 512MB):

terminate called after throwing an instance of 'std::bad_alloc'
  what():  std::bad_alloc
Aborted (core dumped)
Exit code: 134

All official tools crash at the file open stage:

Tool File size Memory limit Exit code
heif-info 784 bytes 512MB 134 (SIGABRT)
heif-dec 784 bytes 512MB 134 (SIGABRT)
heif-thumbnailer 784 bytes 512MB 134 (SIGABRT)

The crash is unrecoverable: heif_context_read_from_file() has no try/catch for std::bad_alloc.

ASan call stack

ERROR: AddressSanitizer: requested allocation size 0x100021e exceeds maximum supported size
    #3 decompress_brotli() at compression_brotli.cc:51
    #4 HeifFile::get_uncompressed_item_data() at file.cc:804
    #5 HeifContext::interpret_heif_file_images() at context.cc:1101
    #6 HeifContext::interpret_heif_file() at context.cc:567
    #7 HeifContext::read_from_file() at context.cc:264
    #8 heif_context_read_from_file() at heif_context.cc:60

Amplification

File size Decompressed size Actual RSS Amplification Security limit Result
377 B 1 MB 10.9 MB 2,781:1 none allocated
416 B 50 MB 169.3 MB 126,031:1 none allocated
784 B 500 MB 1507 MB 668,735:1 1MB bypassed, allocated
784 B 500 MB 668,735:1 512MB OOM (exit 134)
2005 B 2 GB 6007 MB 1,045,961:1 1MB bypassed, allocated

Minimum file size to OOM common container limits:

Container memory Minimum OOM file
128 MB 403 bytes
256 MB 429 bytes
512 MB 482 bytes
1 GB 587 bytes

Impact

  • Availability: unbounded memory allocation leads to OOM and DoS (A:H). A 377-byte file can trigger 10 MB+ allocation; a 403-byte file can OOM a 128 MB container; a 797-byte file can OOM a 2 GB container; amplification reaches up to ~670,000:1.
  • Remote: exploitable by sending a crafted file. All real tools (heif-info, heif-dec, heif-thumbnailer) trigger at file-open.

Suggested fix

  • Add a hard output-size limit to decompress_brotli() consistent with the intended zlib cap (e.g. 256 MB), returning an error when exceeded, and route the output allocation through MemoryHandle.
  • Fix do_inflate() to check the accumulated output size on every iteration (not the intermediate dst buffer, and not only in the Z_BUF_ERROR branch), and route output through MemoryHandle/TotalMemoryTracker.
  • Add a global decompressed-memory counter in interpret_heif_file_images() so cumulative decompressed output across all items is bounded by max_total_memory.
  • In the icef non-tile path, detect overlapping units (or bound total decompressed output across units) before appending.

Reporter and coordination

  • Reporter(s): Yuqi Qiu & Xiang Li
  • Affiliation: Nankai University, AOSP Lab

Updates

2026-08-25 14:41 CEST

Metadata changes:

  • Status for package libheif: “Plausible

2026-08-25 14:40 CEST

Metadata changes:

  • Status for package libheif: “New