# REPRO-2026-00159: libheif: heap-buffer-overflow write decoding 1x4 grid of odd-height tiles ## Summary Status: published Severity: high Type: security Confidence: Unknown ## Identifiers REPRO ID: REPRO-2026-00159 GHSA: GHSA-frfr-f3vg-2g6j CVE: CVE-2026-32740 ## Package Name: libheif Ecosystem: c Affected: <= 1.21.2 Fixed: 1.22.0 ## Root Cause # 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. ## Reproduction Details Reproduced: 2026-05-23T06:58:41.798Z Duration: 2513 seconds Tool calls: 386 Turns: 357 Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00159 pruva-verify GHSA-frfr-f3vg-2g6j pruva-verify CVE-2026-32740 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00159&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00159/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-frfr-f3vg-2g6j - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-32740 - Source: https://github.com/strukturag/libheif ## Artifacts - repro/rca_report.md (analysis, 5817 bytes) - repro/reproduction_steps.sh (reproduction_script, 3343 bytes) - vuln_variant/rca_report.md (analysis, 8336 bytes) - vuln_variant/reproduction_steps.sh (reproduction_script, 4872 bytes) - bundle/context.json (other, 3104 bytes) - bundle/metadata.json (other, 577 bytes) - bundle/ticket.md (ticket, 3078 bytes) - repro/poc.c (other, 4174 bytes) - repro/poc.heif (other, 3340 bytes) - repro/runtime_manifest.json (other, 515 bytes) - repro/validation_verdict.json (other, 2246 bytes) - repro/poc (other, 51824 bytes) - vuln_variant/width_variant_poc.c (other, 4190 bytes) - vuln_variant/extract_area_variant_vuln (other, 35256 bytes) - vuln_variant/stride_variant_poc.c (other, 4189 bytes) - vuln_variant/extract_area_variant_poc.c (other, 1421 bytes) - vuln_variant/width_variant.heif (other, 3342 bytes) - vuln_variant/grid2x2_variant_poc.c (other, 4245 bytes) - vuln_variant/stride_variant.heif (other, 1766 bytes) - vuln_variant/422_grid.heif (other, 3340 bytes) - vuln_variant/check_grid2x2.c (other, 842 bytes) - vuln_variant/extract_area_large_variant_poc.c (other, 1306 bytes) - vuln_variant/check_grid2x2 (other, 37336 bytes) - vuln_variant/root_cause_equivalence.json (other, 1289 bytes) - vuln_variant/422_grid_variant_poc.c (other, 4210 bytes) - vuln_variant/extract_area_large_variant_vuln (other, 34944 bytes) - vuln_variant/patch_analysis.md (documentation, 4740 bytes) - vuln_variant/check_poc.c (other, 956 bytes) - vuln_variant/grid1x4_33_variant_fix (other, 51728 bytes) - vuln_variant/check_stride (other, 37336 bytes) - vuln_variant/variant_manifest.json (other, 2908 bytes) - vuln_variant/runtime_manifest.json (other, 1074 bytes) - vuln_variant/check_stride.c (other, 842 bytes) - vuln_variant/validation_verdict.json (other, 3316 bytes) - vuln_variant/grid1x4_33_variant_poc.c (other, 4190 bytes) - vuln_variant/check_dims.c (other, 1483 bytes) - vuln_variant/grid2x2_variant_vuln (other, 51768 bytes) - vuln_variant/source_identity.json (other, 634 bytes) - vuln_variant/422_grid_variant_vuln (other, 51704 bytes) - vuln_variant/grid2x2.heif (other, 3422 bytes) - vuln_variant/check_poc (other, 37544 bytes) - vuln_variant/grid1x4_33.heif (other, 3212 bytes) - vuln_variant/extract_area_large_variant_fix (other, 34944 bytes) - vuln_variant/width_variant_vuln (other, 51712 bytes) - vuln_variant/check_dims (other, 40648 bytes) - vuln_variant/grid1x4_33_variant_vuln (other, 51728 bytes) - vuln_variant/stride_variant_vuln (other, 51712 bytes) - logs/fix_stdout.txt (other, 85 bytes) - logs/vuln_variant/422_grid_poc (other, 51856 bytes) - logs/vuln_variant/attempt2_vuln.txt (other, 4167 bytes) - logs/vuln_variant/attempt3_fix.txt (other, 97 bytes) - logs/vuln_variant/grid1x4_33_variant_vuln.txt (other, 118057 bytes) - logs/vuln_variant/extract_area_large_vuln (other, 35096 bytes) - logs/vuln_variant/width_variant_vuln.txt (other, 53 bytes) - logs/vuln_variant/422_grid_variant_vuln.txt (other, 48 bytes) - logs/vuln_variant/extract_area_variant_vuln.txt (other, 26 bytes) - logs/vuln_variant/422_grid_vuln (other, 51856 bytes) - logs/vuln_variant/attempt3_vuln.txt (other, 97 bytes) - logs/vuln_variant/422_grid_fix (other, 51856 bytes) - logs/vuln_variant/attempt1_fix.txt (other, 99 bytes) - logs/vuln_variant/grid2x2_variant_vuln.txt (other, 47 bytes) - logs/vuln_variant/repro_run.txt (other, 124438 bytes) - logs/vuln_variant/grid1x4_33_fix (other, 51872 bytes) - logs/vuln_variant/extract_area_large_variant_fix.txt (other, 13 bytes) - logs/vuln_variant/width_variant_check.txt (other, 312793 bytes) - logs/vuln_variant/attempt1_vuln.txt (other, 118443 bytes) - logs/vuln_variant/extract_area_large_variant_vuln.txt (other, 4075 bytes) - logs/vuln_variant/extract_area_large_fix (other, 35096 bytes) - logs/vuln_variant/grid1x4_33_vuln (other, 51872 bytes) - logs/vuln_variant/grid1x4_33_poc (other, 51872 bytes) - logs/vuln_variant/attempt2_fix.txt (other, 13 bytes) - logs/vuln_variant/stride_variant_vuln.txt (other, 54 bytes) - logs/vuln_variant/grid1x4_33_variant_fix.txt (other, 50 bytes) - logs/vuln_stdout.txt (other, 0 bytes) - logs/vuln_stderr.txt (other, 91767 bytes) - logs/fix_stderr.txt (other, 0 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00159 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00159/artifacts/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00159 ## 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