# REPRO-2026-00142: libheif: integer underflow out-of-bounds read crash via crafted HEIF stsc box ## Summary Status: published Severity: medium Type: security Confidence: Unknown ## Identifiers REPRO ID: REPRO-2026-00142 GHSA: GHSA-7f2h-cmpf-v9ww CVE: CVE-2026-32738 ## Package Name: libheif Ecosystem: c Affected: <= 1.21.2 Fixed: 1.22.0 ## 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. ## Reproduction Details Reproduced: 2026-05-22T10:44:46.710Z Duration: 3030 seconds Tool calls: 313 Turns: 266 Handoffs: 3 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00142 pruva-verify GHSA-7f2h-cmpf-v9ww pruva-verify CVE-2026-32738 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00142&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00142/artifacts/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - GitHub Advisory: https://github.com/advisories/GHSA-7f2h-cmpf-v9ww - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-32738 - Source: https://github.com/strukturag/libheif ## Artifacts - repro/rca_report.md (analysis, 6008 bytes) - repro/reproduction_steps.sh (reproduction_script, 7185 bytes) - vuln_variant/rca_report.md (analysis, 7285 bytes) - vuln_variant/reproduction_steps.sh (reproduction_script, 7906 bytes) - bundle/context.json (other, 2724 bytes) - bundle/metadata.json (other, 717 bytes) - bundle/ticket.md (ticket, 3218 bytes) - repro/validation_verdict.json (other, 1319 bytes) - vuln_variant/root_cause_equivalence.json (other, 1157 bytes) - vuln_variant/patch_analysis.md (documentation, 4379 bytes) - vuln_variant/variant_manifest.json (other, 2780 bytes) - vuln_variant/validation_verdict.json (other, 2705 bytes) - vuln_variant/source_identity.json (other, 695 bytes) - logs/vulnerable.log (log, 89 bytes) - logs/fixed.log (log, 151 bytes) - logs/variant_evidence.log (log, 1192 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00142 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00142/artifacts/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00142 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev