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