What's the vulnerability?

A crafted HEIF/AVIF image with a 1×4 grid of odd-height tiles causes the chroma-plane decoder to write 64 bytes of attacker-controlled chroma data past the end of the chroma-plane allocation during a normal decode with the default build options.

The out-of-bounds write is reachable via the standard decode path on a malicious input file — any application that decodes untrusted HEIF/AVIF with libheif's default build is exposed to a heap-buffer-overflow write.

Root Cause Analysis

# RCA Report: CVE-2026-32740 — libheif Grid Tile Chroma Heap-Buffer-Overflow Write

## Summary
CVE-2026-32740 is a heap-buffer-overflow write vulnerability in libheif's grid tile compositing code. When decoding a HEIF/AVIF image containing a grid of YCbCr 4:2:0 tiles with odd heights (e.g., 1×4 grid of 64×65 tiles), the chroma-plane copy logic in `HeifPixelImage::copy_image_to()` miscalculates the copy bounds. Due to independent ceiling-divisions for the destination offset (`ys`) and copy height (`copy_height`), their sum can exceed the allocated chroma plane height by one row. This results in a 32-byte (or 64-byte across Cb+Cr) out-of-bounds heap write of attacker-controlled chroma data.

## Impact
- **Package**: `libheif` (C++ HEIF/AVIF decoder/encoder library)
- **Affected versions**: `<= 1.21.2`
- **Fixed version**: `1.22.0`
- **Risk level**: High (CVSS 3.1 base 8.8)
- **Consequences**: Any application that decodes untrusted HEIF/AVIF grid images with default build options is exposed. An attacker can craft a malicious file to write 64 bytes of fully controlled chroma data past the end of a heap allocation, enabling heap corruption, potential code execution via heap grooming, or denial of service.

## Root Cause
The vulnerable function is `HeifPixelImage::copy_image_to()` in `libheif/pixelimage.cc` (v1.21.2, around line 964). When compositing a decoded tile into the grid canvas, the code computes:

```cpp
uint32_t copy_height = std::min(src_height, channel_height(h - y0, chroma, channel));
uint32_t ys = channel_height(y0, chroma, channel);
```

For YCbCr 4:2:0, `channel_height(n)` uses ceiling division `(n+1)/2`. When the tile origin `y0` is odd and the tile height is odd, `ys + copy_height` can exceed `channel_height(total_height)` by 1 row:

- Example: total height = 260 (4 × 65), last tile `y0 = 195`
- `ys = ceil(195/2) = 98`
- `copy_height = ceil(65/2) = 33`
- `ys + copy_height = 131`
- `channel_height(260) = ceil(260/2) = 130`
- **Overflow: 1 row (32 bytes for a 64-pixel-wide chroma row)**

The **fix** (in libheif v1.22.0) rewrites `copy_image_to` to compute the copy height from the remaining plane size instead of recomputing it from the tile height:

```cpp
uint32_t copy_width  = std::min(src_width,  channel_width(w, chroma, channel) - xs);
uint32_t copy_height = std::min(src_height, channel_height(h, chroma, channel) - ys);
```

This ensures `ys + copy_height <= channel_height(h, chroma, channel)`, eliminating the rounding mismatch.

## Reproduction Steps
The reproduction is fully automated by `repro/reproduction_steps.sh`.

What the script does:
1. Builds libheif `v1.21.2` (vulnerable) with AddressSanitizer (`-fsanitize=address`) and JPEG encoder/decoder support.
2. Builds libheif `v1.22.0` (fixed) with the same ASan flags.
3. Compiles `repro/poc.c` — a small C harness that uses the libheif C API to:
   - Create four 64×65 YCbCr 4:2:0 images.
   - Add them as JPEG-encoded tiles into a 1×4 grid (overall 64×260).
   - Write the resulting HEIF to `repro/poc.heif`.
   - Decode the grid back via `heif_decode_image()`.
4. Runs the decode step against the **vulnerable** build and captures ASan stderr to `logs/vuln_stderr.txt`.
5. Runs the decode step against the **fixed** build and captures output to `logs/fix_stderr.txt`.
6. Compares the two: confirms ASan heap-buffer-overflow in v1.21.2 and a clean run in v1.22.0.
7. Writes a JSON runtime manifest with SHA256 of the PoC file and verdict.

Expected evidence:
- **Vulnerable**: `ERROR: AddressSanitizer: heap-buffer-overflow` with a `WRITE of size 32` in `memcpy` → `HeifPixelImage::copy_image_to` → `ImageItem_Grid::decode_and_paste_tile_image`.
- **Fixed**: No ASan diagnostic; decode completes with `Decoded OK`.

## Evidence
- **Vulnerable ASan log**: `logs/vuln_stderr.txt`
  - Excerpt:
    ```
    ==2297==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x52500001118f
    WRITE of size 32 at 0x52500001118f thread T4
        #0 in memcpy
        #1 in HeifPixelImage::copy_image_to(...) pixelimage.cc:964
        #2 in ImageItem_Grid::decode_and_paste_tile_image(...) grid.cc:564
    ```
- **Fixed run log**: `logs/fix_stderr.txt`
  - Contains only `Decoded OK` with no ASan error.
- **PoC file**: `repro/poc.heif` (SHA256 recorded in `repro/runtime_manifest.json`).
- **Build environments**:
  - Vulnerable: `external/libheif/build_vuln/libheif/libheif.so` (v1.21.2 + ASan)
  - Fixed: `external/libheif/build_fix/libheif/libheif.so` (v1.22.0 + ASan)

## Recommendations / Next Steps
1. **Upgrade** to libheif `>= 1.22.0` to obtain the corrected `copy_image_to` logic.
2. **Validate** that any downstream packages (e.g., `heif-convert`, image viewers, thumbnailers) are rebuilt against the patched library.
3. **Regression testing**: Add a unit test that creates a 1×4 grid of odd-height 4:2:0 tiles and decodes it under ASan, asserting no heap-buffer-overflow.
4. **Fuzzing**: The grid tile compositing path is a good target for guided fuzzing with odd-dimension chroma-subsampled tiles.

## Additional Notes
- **Idempotency**: `repro/reproduction_steps.sh` has been executed twice consecutively with identical results (ASan overflow on v1.21.2, clean on v1.22.0).
- **Edge cases**: The overflow magnitude depends on the chroma row size. For a 64-pixel-wide image, one chroma row is 32 bytes (Cb or Cr), so the total overflow is 64 bytes across both chroma planes. The advisory notes that different tile heights (e.g., 33 or 65) and different canvas sizes can be used to target different heap bucket sizes.
- **Limitations**: The PoC requires a codec that preserves odd-height chroma planes correctly (JPEG in this reproduction). The built-in uncompressed codec truncates chroma height for odd dimensions, which would mask the overflow; therefore JPEG tiles are used.
One Command

Verify with pruva-verify

Run the Pruva CLI to automatically fetch and execute the reproduction script.

pruva-verify REPRO-2026-00159
or pruva-verify GHSA-frfr-f3vg-2g6j
or pruva-verify CVE-2026-32740
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-00159/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