GHSA-24wx-9w62-c96w on CTRL-OS 26.05
Aliases: GHSA-24wx-9w62-c96w
Packages: libheif
Status: Plausible
Advisory Information
Summary
libheif decompresses
mimemetadata items andunci(ISO 23001-17) compressed image data using brotli and zlib/deflate. The brotli pathdecompress_brotli()appends decoder output to astd::vectorin a loop with no output-size check at all, in stark contrast to the siblingdo_inflate()which attempts a 256 MB cap. That zlib cap is itself defective: it checks the small intermediatedstbuffer (always 8192 bytes, reset each loop) rather than the accumulatedoutputvector, and it only executes in theZ_BUF_ERRORbranch that valid compressed data never reaches, so the check is effectively dead code. Neither path routes its output vector throughMemoryHandle, somax_memory_block_sizeandmax_total_memoryare entirely bypassed. An attacker simply supplies a high-ratio brotli or zlib bomb as amimeitem'scontent_encodingpayload; on file openHeifContext::interpret_heif_file_images()iterates every item and decompresses each, so up tomax_items(1000) bombs accumulate.Seven variants share this root cause. Five are confirmed with POC:
Variant Path / trigger CVSS V1: brotli mimeitem, no output limitdecompress_brotli(),content_encoding="br"7.5 V2: brotli unci/mime, three trigger pathsdecompress_brotli(), unci cmpC "brot" + icef7.5 V3: icef non-tile unit overlap, N x decompression unc_decoder.cc, icef overlapping units7.5 V4: zlib 256 MB check checks wrong object do_inflate(), dead-code limit5.5 V5: multi-item compressed metadata accumulation interpret_heif_file_images(), 1000 items7.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); theuncipaths additionally requireWITH_UNCOMPRESSED_CODEC=ON(default OFF). Affected versions >= 1.19.0 (when brotli support was introduced).Root cause
Two compounding root causes:
No output-size limit in
decompress_brotli()(V1, V2, V6, V7).compression_brotli.cc:36-82loops onBrotliDecoderDecompressStream()and appends each chunk tooutputviaoutput.insert()with no size check. The function never usesMemoryHandle, so neithermax_memory_block_sizenormax_total_memoryconstrains the decompressed output. Brotli's high compression ratio (>1000:1) lets a few KB of payload expand to GB.Defective output-size limit in
do_inflate()(V4), and no cumulative tracking across items (V5). The 256 MB check incompression_zlib.cc:131checksdst.size()(the 8192-byte intermediate buffer, reset each iteration) instead of the accumulatedoutputvector, and it lives only in theZ_BUF_ERRORbranch that valid data never hits, making it dead code. Theoutputvector grows unbounded viainsert(), bypassesMemoryHandle/TotalMemoryTracker, and is not subject tomax_memory_block_size/max_total_memory. On top of this,interpret_heif_file_images()decompresses everymimeitem independently with no global decompressed-memory accounting; withmax_items=1000, even a hypothetical per-item 256 MB cap would allow 256 GB cumulative.icef unit overlap (V3). In
unc_decoder.cc, the icef non-tile path decompresses each icef unit independently and appends to adatavector, but never checks whether units overlap. Whenunit_offset_code > 0,unit_offsetis 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.cc—decompress_brotli-libheif/compression_zlib.cc—do_inflate-libheif/codecs/uncompressed/unc_decoder.cc—get_compressed_image_data_uncompressed,do_decompress_data-libheif/codecs/uncompressed/unc_boxes.cc—Box_icef::parse-libheif/file.cc—HeifFile::get_uncompressed_item_data,get_compressed_data_for_item,get_item_data-libheif/context.cc—HeifContext::interpret_heif_file_images-libheif/security_limits.cc—global_security_limitsSecurity 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()andheif_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=ONfor theuncitrigger path.PoC (V1: brotli MIME item, most dangerous variant)
Constructing the malicious file
Create a HEIF file with the following structure:
ftypbox: major brandheicmetabox containing:hdlr(pict)pitm: item 1 (primary image)iinf: two items:
- Item 1:
unci1×1 monochrome image- Item 2:
mimeitem,content_type="application/octet-stream",content_encoding="br"iprp:ispe(1×1) +cmpd(Y) +uncC(v0) +ipmailoc: item 1 = 1 byte image data, item 2 = brotli bomb datairef: item 2 →cdsc→ item 1 (metadata references image)mdat: 1 byte image data + brotli bombThe 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, includingmimeitems withcontent_encoding="br". This callsdecompress_brotli(), which decompresses the brotli bomb into an unboundedoutputvector.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 growthResult: security limit bypass
Even with
max_total_memory=1byte andmax_memory_block_size=1byte, the 784-byte file causes 1507MB of memory allocation. The decompression output vector is not tracked byMemoryHandle, so the security limits are completely bypassed.Result: OOM crash
With memory constrained (e.g.,
ulimit -v 524288for 512MB):terminate called after throwing an instance of 'std::bad_alloc' what(): std::bad_alloc Aborted (core dumped) Exit code: 134All 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 forstd::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:60Amplification
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 throughMemoryHandle.- Fix
do_inflate()to check the accumulatedoutputsize on every iteration (not the intermediatedstbuffer, and not only in theZ_BUF_ERRORbranch), and routeoutputthroughMemoryHandle/TotalMemoryTracker.- Add a global decompressed-memory counter in
interpret_heif_file_images()so cumulative decompressed output across all items is bounded bymax_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”