Human
Machine
REPRO-2026-00142 MEDIUM DoS
Verified
libheif: integer underflow out-of-bounds read crash via crafted HEIF stsc box
libheif (c) May 22, 2026
What's 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.
Root Cause Analysis
# 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.
One Command
Verify with pruva-verify
Run the Pruva CLI to automatically fetch and execute the reproduction script.
pruva-verify REPRO-2026-00142 or
pruva-verify GHSA-7f2h-cmpf-v9ww or
pruva-verify CVE-2026-32738 Install:
curl -fsSL https://pruva.dev/install.sh | sh Or Run Manually
1
Download the script
curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00142/artifacts/reproduction_steps.sh 2
Make executable
chmod +x reproduction_steps.sh 3
Run the script
./reproduction_steps.sh Run in a VM, container, or disposable environment. This exploits a real vulnerability.
How Pruva Reproduced This
Watch the AI agent's step-by-step process.
Loading session...
Artifacts
No artifacts available