Skip to content
Verified reproduction

CVE-2026-32738: libheif: integer underflow out-of-bounds read crash via crafted HEIF stsc box

CVE-2026-32738 is verified against libheif · c affected versions: <= 1.21.2 fixed version: 1.22.0 vulnerability class: DoS This medium reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00142.

REPRO-2026-00142 libheif · c DoS May 22, 2026 CVE entry .txt
Severity MEDIUM
Confidence HIGH
Reproduced in 50m 30s
Tool calls 313
Spend $2.85
Affected <= 1.21.2
Fixed in 1.22.0
$ pruva-verify REPRO-2026-00142
or curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00142/artifacts/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
02 · The vulnerability

libheif parses HEIF/HEIC sequence files. The Chunk constructor computes the last sample index as m_last_sample = first_sample + samples_per_chunk - 1. A crafted file whose stsc (sample-to-chunk) box sets samples_per_chunk = 0 makes this evaluate to 0 + 0 - 1, which underflows the unsigned 32-bit counter to UINT32_MAX.

Subsequently accessing any sample indexes into an empty std::vector at index 0, dereferencing the null page — an out-of-bounds read that crashes the process with SIGSEGV. Any application that decodes attacker-supplied HEIF files is exposed to this denial of service.

03 · Root cause
# Root Cause Analysis: CVE-2026-32738

## Summary

`libheif` versions 1.21.2 and below contain an integer underflow vulnerability in the `Chunk` constructor during HEIF/HEIC sequence file parsing. When a crafted file's `stsc` (sample-to-chunk) box specifies `samples_per_chunk = 0`, the unsigned 32-bit expression `first_sample + num_samples - 1` underflows to `UINT32_MAX`. This causes all sequence samples to be mapped to an empty chunk. Subsequent sample access attempts to read index 0 of an empty `std::vector`, resulting in a guaranteed null-page read / SIGSEGV crash — a denial of service against any application that decodes attacker-supplied HEIF sequence files.

## Impact

- **Package**: `libheif` (C++ library for HEIF/HEIC/AVIF)
- **Affected versions**: `<= 1.21.2`
- **Fixed version**: `1.22.0`
- **Risk level**: Medium (CVSS 3.1 base 6.5)
- **Consequences**: Denial of service via process crash (SIGSEGV). Any application using libheif to decode HEIF sequences is exposed.

## Root Cause

The vulnerability occurs in two locations in `libheif/sequences/chunk.cc`:

1. **Missing validation in `Box_stsc::parse()`**: The `samples_per_chunk` field is read as a raw `uint32_t` from the file with no rejection of the value `0`, even though ISO 14496-12 requires this field to be ≥ 1.

2. **Unsigned integer underflow in `Chunk::Chunk()` (line ~82)**:
   ```cpp
   m_last_sample = first_sample + num_samples - 1;
   ```
   When `num_samples = 0` and `first_sample = 0`, this evaluates to `0 + 0 - 1 = UINT32_MAX` due to unsigned underflow. The `m_sample_ranges` vector remains empty because the `for (i < 0)` loop never executes.

3. **Out-of-bounds read in `Chunk::get_data_extent_for_sample()` (line ~112)**:
   ```cpp
   extent.set_file_range(m_ctx->get_heif_file(),
                         m_sample_ranges[n - m_first_sample].offset,
                         m_sample_ranges[n - m_first_sample].size);
   ```
   Since `m_sample_ranges` is empty, accessing index `0` dereferences a null pointer (empty vector `begin()`), causing a SEGV.

4. **All samples mapped to the empty chunk**: In `Track::init_sample_timing_table()`, the loop condition `i > m_chunks[current_chunk]->last_sample_number()` is always false because `last_sample_number()` returns `UINT32_MAX`. Consequently, every sample is assigned to chunk 0, ensuring the crash is triggered on the first frame access.

The fix commit is `edc12502` ("Validate stsc sample coverage against stsz/stts") in the `v1.21.2..v1.22.0` history, which adds a validation check in `Track::load()` ensuring the total samples covered by `stsc` entries match the `stsz`/`stts` sample count. Additionally, `Box_stsc::parse()` or related logic in 1.22.0 explicitly rejects `samples_per_chunk = 0` with the controlled error message: `'stsc' box with zero samples per chunk entry`.

## Reproduction Steps

The reproduction script is at `repro/reproduction_steps.sh`.

What the script does:
1. Installs build dependencies (`cmake`, `build-essential`, `git`, `libjpeg-dev`).
2. Clones `libheif` from GitHub.
3. Builds the **vulnerable** version `v1.21.2` and the **fixed** version `v1.22.0` with minimal codecs (only JPEG decoder and uncompressed codec enabled).
4. Creates two tiny JPEG images and uses `heif-enc` to generate a valid 2-frame HEIF sequence.
5. Binary-patches the `stsc` box in the sequence file, changing `samples_per_chunk` from `2` to `0`.
6. Compiles a small C harness that opens the file and calls `heif_track_decode_next_image()`.
7. Runs the harness against both library versions and captures logs.

Expected evidence:
- **Vulnerable (1.21.2)**: The harness crashes with `SIGSEGV` (exit code 139 / signal 11). AddressSanitizer builds show a clear null-page read in `Chunk::get_data_extent_for_sample()`.
- **Fixed (1.22.0)**: The harness returns a controlled decode error: `Invalid input: Unspecified: 'stsc' box with zero samples per chunk entry.` No crash occurs.

## Evidence

Log files generated by the reproduction script:

- `logs/vulnerable.log`:
  ```
  === Testing vulnerable libheif v1.21.2 ===
  Track handler: pict
  Vulnerable exit code: 139
  ```
  Exit code 139 = 128 + 11, confirming a `SIGSEGV` (signal 11) crash.

- `logs/fixed.log`:
  ```
  === Testing fixed libheif v1.22.0 ===
  Error reading file: Invalid input: Unspecified: 'stsc' box with zero samples per chunk entry.
  Fixed exit code: 1
  ```
  The fixed version rejects the malformed file with a clean, controlled error instead of crashing.

AddressSanitizer output from the ASAN-instrumented vulnerable build (captured during verification):
```
DEBUG: get_data_extent_for_sample called with n=0, m_first_sample=0, m_last_sample=4294967295, vector_size=0
AddressSanitizer:DEADLYSIGNAL
==12262==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000008
    #0 Chunk::get_data_extent_for_sample(unsigned int) const
    #1 Track_Visual::decode_next_image_sample(heif_decoding_options const&)
    #2 heif_track_decode_next_image
```

## Recommendations / Next Steps

1. **Upgrade to libheif 1.22.0 or later** — the fix adds explicit validation of `stsc` entries against `stsz`/`stts` sample counts and rejects zero `samples_per_chunk` values.
2. **Add regression tests** — the reproduction file and harness can be incorporated into CI to prevent reintroduction of the bug.
3. **Harden parsing** — consider adding more defensive checks in `Box_stsc::parse()` and `Chunk::Chunk()` to reject invalid zero values immediately, rather than relying on downstream validation.

## Additional Notes

- **Idempotency**: `repro/reproduction_steps.sh` was run twice consecutively; both runs produced identical results (vulnerable crash, fixed clean rejection) and exited with code 0.
- **Edge cases**: The crash is guaranteed as long as any accessed sample maps to a chunk with `samples_per_chunk = 0`. The reproduction uses a minimal 2-frame sequence, but the vulnerability scales to any sequence file where at least one `stsc` entry has a zero sample count.
04 · Reproduction transcript

The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired. Phases: support triages the advisory · repro reproduces it · vuln_variant confirms the fix blocks it · judge verifies.

Loading session...

05 · Artifacts

Scripts, logs, diffs, and output captured during the reproduction.

No artifacts available