# OpenEXR has a signed 32-bit Overflow in PIZ Decoder Leads to OOB Read/Write

Signed 32-bit overflow in OpenEXR PIZ decoder's wavbuf advance wraps the pointer, causing out-of-bounds read/write during wavelet decode of a crafted EXR file.

- Payout: Research
- Severity: high
- Program: OpenEXR
- Found by: quangIO
- Disclosed: 2026-04-08
- CVE: CVE-2026-34588
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-588r-cr5c-w6hf
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-588r-cr5c-w6hf
- Tags: area:oss domain:oss impact:dos impact:integrity kind:research platform:ghsa sev:high tech:python technique:source-review vuln:dos vuln:memory-corruption

**Target:** PIZ decompression path in libOpenEXRCore (internal_piz.c)
**Root cause:** `wavbuf += nx * ny * wcount` is computed in signed 32-bit int; a crafted channel layout makes the product wrap, so the next channel's in-place wavelet decode runs at a wrong address.
**Steps:**
1. Craft EXR with width `67108862`, height `32`, channel A FLOAT 1x1, channel B HALF sampling `33554431 x 16` so `width * 32 * 2 = 4294967168` wraps to `-128`.
2. Build exrcheck with ASAN and run `./build-asan/bin/exrcheck /tmp/piz_scanline_redzone.exr`.
3. Observe signed integer overflow at internal_piz.c:373 and heap-buffer-overflow READ of size 2 in `wdec14_4` at internal_piz.c:148.
4. Confirm write primitive via valgrind memcheck on release build: invalid read at internal_piz.c:150 and invalid write at internal_piz.c:171.
**How found:** Code review of the PIZ undo path spotted the signed int multiplication advancing wavbuf; reporter then engineered channel dimensions whose product wraps to a small negative offset, placing the next access just before the allocation for a clean ASAN redzone hit.
**Exploitation:** Attacker supplies a malicious EXR file; when a victim application decodes it with PIZ compression, the wrapped wavbuf pointer causes in-place wavelet decode to read and write outside the scratch buffer, yielding crash or memory corruption.
**Insight:** In image codecs, per-channel buffer offsets computed as `nx*ny*count` in int are a classic wrap source; craft dimensions whose product lands just past 2^31 so the wrapped pointer lands in a redzone, and use valgrind to prove the write half of an in-place decode.

## Proof of concept (copied exactly from the original report)

**Crafted EXR dimensions causing signed overflow**

```
- width: `67108862`
- height: `32`
- channel A: `FLOAT`, sampling `1 x 1`
- channel B: `HALF`, sampling `33554431 x 16`

This makes:

width * 32 * 2 = 4294967168

which wraps signed 32-bit arithmetic to `-128`.
```

**Valgrind command proving OOB read/write**

```
valgrind --tool=memcheck --leak-check=no --track-origins=no \
  --error-limit=no --num-callers=20 \
  ./build-relwithdebinfo/bin/exrcheck /tmp/piz_scanline_redzone.exr

Observed result:

- `Invalid read of size 2` at [internal_piz.c:150](/home/pop/sec/openexr/src/lib/OpenEXRCore/internal_piz.c#L150)
- `Invalid write of size 2` at [internal_piz.c:171]
```

