Skip to content

GHSA-x8xm-cm2c-cfc8 on CTRL-OS 26.05

Aliases: GHSA-x8xm-cm2c-cfc8

Packages: libheif

Status: Plausible

Advisory Information

Summary

libheif supports HEIF derived image types (grid, iovl overlay, and the experimental tili tiled image). The decode path for grid and iovl does not cache the result of decoding a referenced base image, so when many derived items reference the same underlying encoded image through iden (identity) indirection, the base image is fully decoded once per reference. Because the cycle-detection processed_ids set is passed by value through the decode call chain and Box_iref::check_for_double_references() only deduplicates references within a single iref entry, the cross-entry indirect reference structure is legal and the base image can be decoded up to ~996x (grid) or ~998x (overlay). Separately, the experimental tili tiled image type eagerly pre-allocates a std::vector of tile offsets in TiledHeader::set_parameters() without routing the allocation through MemoryHandle, so max_total_memory is never enforced and a single small file can allocate hundreds of MB at file-open time.

Three variants share this cluster. All three are confirmed with POC:

Variant Type CVSS
V1: grid + iden indirect reference (no decode cache, CPU) grid/iden 6.5
V2: overlay + iden indirect reference (no decode cache, CPU) iovl/iden 7.5
V3: tiled m_offsets pre-allocation bypasses MemoryHandle (memory) tili 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. V1 carries UI:R because triggering it requires the consumer to request the grid image, whereas V2 triggers during overlay decode and V3 triggers at file-open.

Tested versions

Version / revision Build Result
v1.23.1 (v1.23.1-7-g1a3583bc) ASan affected. Amplification confirmed via POC.
v1.23.1 non-ASan affected. CPU exhaustion / large allocation confirmed.

Source was not modified. V3 additionally requires ENABLE_EXPERIMENTAL_FEATURES=ON (non-default) for the tili path.

Root cause

Two related root causes:

  1. No decode-result caching across derived items (V1, V2). ImageItem::decode_image() and the derived-image decode paths (ImageItem_Grid::decode_and_paste_tile_image, ImageItem_Overlay::decode_overlay_image, ImageItem_iden::decode_compressed_image) receive the processed_ids cycle-detection set by value. The base image ID therefore never enters the shared set, so repeated references to the same base image are each treated as a fresh decode. Box_iref::check_for_double_references() only deduplicates references within a single iref entry, not across entries, so a grid/overlay item referencing up to ~996/998 distinct iden items that each point at the same base image is a legal structure. The overlay path additionally performs a YUV 4:2:0 to RGB 4:4:4 colorspace conversion per decoded iden, stacking extra CPU cost on top of the decode amplification.

  2. Tiled offsets allocation not routed through MemoryHandle (V3). TiledHeader::set_parameters() calls m_offsets.resize(nTiles) where nTiles can reach 16,777,216. The allocation uses a standard std::vector allocator that bypasses MemoryHandle tracking entirely, so max_total_memory (default 4 GB) is never enforced. The allocation is eager, performed at file-open via ImageItem_Tiled::initialize_decoder() (called from HeifContext::interpret_heif_file_images), rather than at decode time, and uses global security limits instead of context limits.

Affected files and functions: - libheif/image-items/grid.ccImageItem_Grid::decode_and_paste_tile_image, decode_full_grid_image - libheif/image-items/overlay.ccImageItem_Overlay::decode_overlay_image, read_overlay_spec - libheif/image-items/tiled.ccTiledHeader::set_parameters, ImageItem_Tiled::initialize_decoder - libheif/image-items/iden.ccImageItem_iden::decode_compressed_image - libheif/image-items/image_item.ccImageItem::decode_image - libheif/box.ccBox_iref::check_for_double_references, Box_iref::parse - 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.
  • V3 requires the tili feature compiled in (ENABLE_EXPERIMENTAL_FEATURES=ON), which is non-default; this lowers its real-world exploitability.

PoC

V2: iovl + iden CPU amplification (representative, highest impact)

Construct a HEIF file with: - Item 1: unci base image (e.g., 256×256 RGB, 196KB) - Items 2..999: 998 iden items, each referencing Item 1 via dimg - Item 1000: iovl (overlay) item, referencing all 998 iden items via dimg - Total: 1000 items (within max_items=1000 limit)

When heif_decode_image() is called on the iovl item, decode_overlay_image() iterates all 998 iden items. Each iden triggers decode_image(base), which fully decodes the 256×256 base image from scratch. No decode result cache exists. The base image is decoded 998 times.

CPU amplification results:

Base image File size Decode time Amplification
64×64 80 KB 376 ms 668×
256×256 265 KB 5,694 ms 917×
512×512 835 KB 22,786 ms 941×
1024×1024 3.1 MB 91,300 ms 940×
2048×2048 12.1 MB 366,600 ms (6.1 min) 940×

Official tools affected: heif-thumbnailer (265KB file → 5.8s), heif-dec (265KB file → 13.2s). heif-info is not affected (only reads metadata, does not decode).

Security limits bypassed: max_items=1000 (exactly 1000 items, within limit), check_for_double_references() (no duplicates within single iref entry), processed_ids (only prevents circular refs, not repeated base decoding).

V1: grid + iden (same mechanism, grid instead of iovl)

Same file structure but using grid item instead of iovl. check_for_double_references() correctly rejects a grid that directly references the same base image multiple times, but does NOT reject the indirect chain grid → iden → base. GDB verification confirms base image is decoded N times.

V3: tiled m_offsets memory amplification

Construct a HEIF file with a tili item containing tilC box with tile_width=1, tile_height=1 and ispe box with width=4096, height=4096. This results in nTiles = 4096 × 4096 / (1 × 1) = 16,777,216.

When libheif opens this file, TiledHeader::set_parameters() calls m_offsets.resize(16,777,216), allocating ~256MB without MemoryHandle tracking.

Memory amplification results:

Items File size RSS Amplification
1 245 bytes 262 MB 1,121,485:1
5 445 bytes 1,285 MB 3,030,083:1
10 696 bytes 2,566 MB 3,866,053:1

Security limit bypass: max_total_memory=1 byte does not prevent allocation — m_offsets.resize() is not tracked by MemoryHandle.

V3 requires ENABLE_EXPERIMENTAL_FEATURES=ON at build time. The tili item type is not registered in default builds.

Impact

  • Availability (V1, V2): CPU exhaustion. V1 achieves up to ~996x decode amplification; V2 achieves ~940x amplification, with a 265 KB file exceeding a common 5 s API timeout and a 12.1 MB file consuming 366.6 s CPU. A:H.
  • Availability (V3): memory exhaustion. Up to 256 MB per item is allocated untracked; with max_items=1000, up to ~250 GB of untracked memory can be requested from a 245-byte file (1.12Mx amplification). A:H.
  • Remote: exploitable by sending a crafted file. V2 and V3 require no user interaction beyond opening the file.

Suggested fix

  • For V1 and V2: introduce a decode-result cache keyed by item ID (shared across a single top-level decode operation), or pass processed_ids by reference and treat a second visit as a cache hit rather than a cycle. Also enforce an upper bound on the number of derived-image references (dimg) a single item may declare.
  • For V3: route the m_offsets allocation through MemoryHandle so it is counted against max_total_memory, defer the allocation to decode time, and use the context security limits rather than the global limits. Add a max_tiles_per_image security-limit field.

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