Skip to content

GHSA-r864-gwqv-q2hp

CVE Information

OpenEXR v3.4.13 ZIP short inflate uninitialized pixel disclosure

Summary

A crafted ZIP/ZIPS-compressed EXR chunk can contain a valid zlib stream that inflates to fewer bytes than the chunk's expected uncompressed pixel block. OpenEXR accepts the short inflate, reconstructs only that short prefix, and then unpacks the full expected pixel region. The unwritten tail of the internal decode buffer can be copied into caller-visible pixel output.

This is file-triggered through normal OpenEXRCore decode APIs.

Reproduction status

v3.4.13 exrcheck original: PoC accepted as OK
v3.4.13 original/ASAN/UBSAN/MSAN: safe controls rc=0, PoC rc=134
local main original/ASAN/UBSAN/MSAN: safe controls rc=0, PoC rc=134
fresh origin/main 68203429 Release/ASAN: safe controls rc=0, PoC rc=134

PoC abort message in each failing harness run:

ZIP short inflate leaked allocator pattern into channel 0: 2028/2048 bytes

The abort is intentional in the harness after detecting allocator-pattern bytes in decoded caller-visible pixel output.

Root cause

The ZIP decode path records the actual inflated byte count but does not require it to match the expected uncompressed chunk size.

Relevant flow:

src/lib/OpenEXRCore/internal_zip.c
  undo_zip_impl()
  exr_uncompress_buffer(..., &actual_out_bytes)
  rejects only actual_out_bytes > uncompressed_size, not actual_out_bytes < uncompressed_size
  internal_zip_reconstruct_bytes(..., actual_out_bytes) reconstructs only the short prefix

src/lib/OpenEXRCore/compression.c
  decompress_data(..., decode->chunk.unpacked_size) supplies the expected full pixel-block size

src/lib/OpenEXRCore/unpack.c
  unpack_16bit() copies the full channel region from decode->unpacked_buffer to caller output

The PoC is a 64x16 one-channel HALF ZIP image. The expected decoded pixel block is 2048 bytes, but the zlib stream inflates to 20 bytes. The remaining 2028 bytes of the decode buffer are stale allocation contents and are copied to the output channel.

PoC package

poc.zip contains only required reproduction material:

poc.zip

reproduce.sh
poc_materials/base_zip_deflated_64x16_half.exr
poc_materials/base_zip_raw_fallback_64x16_half.exr
poc_materials/poc_zip_short_inflate_20_of_2048.exr
poc_materials/fuzz_zip_short_inflate_api.cpp
poc_materials/make_zip_short_exr.py
poc_materials/logs/final/reproduce_run.log
poc_materials/logs/final/exrcheck_original_poc.out
poc_materials/logs/final/*_poc.err

poc.zip intentionally does not include report.md, README files, checksum-only files, AFL exploration logs, or source-snippet notes.

Reproduction

From the extracted poc.zip directory:

OPENEXR_ROOT=/home/br0nzu/openexr/v3.4.13 ./reproduce.sh

Expected result: safe controls exit 0; the crafted PoC exits 134 after the harness detects stale bytes in caller-visible output.

Complete validation output

The sanitizer builds do not emit an ASAN/UBSAN/MSAN stack trace here. The harness detects stale allocator-pattern bytes and intentionally aborts, so the complete stderr for each sanitizer PoC run is the single line shown below.

reproduce.sh stdout

===== exrcheck_original_poc =====
rc=0
 file /home/br0nzu/openexr/v3.4.13/report/35. OpenEXR v3.4.13 ZIP short inflate uninitialized pixel disclosure/poc_materials/poc_zip_short_inflate_20_of_2048.exr OK
===== v3413_original_base_deflated =====
rc=0
===== v3413_original_base_raw =====
rc=0
===== v3413_original_poc =====
rc=134
ZIP short inflate leaked allocator pattern into channel 0: 2028/2048 bytes
===== v3413_asan_base_deflated =====
rc=0
===== v3413_asan_base_raw =====
rc=0
===== v3413_asan_poc =====
rc=134
ZIP short inflate leaked allocator pattern into channel 0: 2028/2048 bytes
===== v3413_ubsan_base_deflated =====
rc=0
===== v3413_ubsan_base_raw =====
rc=0
===== v3413_ubsan_poc =====
rc=134
ZIP short inflate leaked allocator pattern into channel 0: 2028/2048 bytes
===== v3413_msan_base_deflated =====
rc=0
===== v3413_msan_base_raw =====
rc=0
===== v3413_msan_poc =====
rc=134
ZIP short inflate leaked allocator pattern into channel 0: 2028/2048 bytes
===== main_original_base_deflated =====
rc=0
===== main_original_base_raw =====
rc=0
===== main_original_poc =====
rc=134
ZIP short inflate leaked allocator pattern into channel 0: 2028/2048 bytes
===== main_asan_base_deflated =====
rc=0
===== main_asan_base_raw =====
rc=0
===== main_asan_poc =====
rc=134
ZIP short inflate leaked allocator pattern into channel 0: 2028/2048 bytes
===== main_ubsan_base_deflated =====
rc=0
===== main_ubsan_base_raw =====
rc=0
===== main_ubsan_poc =====
rc=134
ZIP short inflate leaked allocator pattern into channel 0: 2028/2048 bytes
===== main_msan_base_deflated =====
rc=0
===== main_msan_base_raw =====
rc=0
===== main_msan_poc =====
rc=134
ZIP short inflate leaked allocator pattern into channel 0: 2028/2048 bytes
===== origin_main_682_release_base_deflated =====
rc=0
===== origin_main_682_release_base_raw =====
rc=0
===== origin_main_682_release_poc =====
rc=134
ZIP short inflate leaked allocator pattern into channel 0: 2028/2048 bytes
===== origin_main_682_asan_base_deflated =====
rc=0
===== origin_main_682_asan_base_raw =====
rc=0
===== origin_main_682_asan_poc =====
rc=134
ZIP short inflate leaked allocator pattern into channel 0: 2028/2048 bytes
PASS

v3.4.13 ASAN PoC stderr

ZIP short inflate leaked allocator pattern into channel 0: 2028/2048 bytes

v3.4.13 UBSAN PoC stderr

ZIP short inflate leaked allocator pattern into channel 0: 2028/2048 bytes

v3.4.13 MSAN PoC stderr

ZIP short inflate leaked allocator pattern into channel 0: 2028/2048 bytes

local main ASAN PoC stderr

ZIP short inflate leaked allocator pattern into channel 0: 2028/2048 bytes

local main UBSAN PoC stderr

ZIP short inflate leaked allocator pattern into channel 0: 2028/2048 bytes

local main MSAN PoC stderr

ZIP short inflate leaked allocator pattern into channel 0: 2028/2048 bytes

fresh origin/main 68203429 ASAN PoC stderr

ZIP short inflate leaked allocator pattern into channel 0: 2028/2048 bytes

Impact

This demonstrates information disclosure / uninitialized data exposure from decoded pixel output. An attacker-controlled EXR image can cause decoded pixels to contain stale bytes from OpenEXR's decode allocation instead of file-provided samples. Applications that display, save, transform, or transmit decoded pixels may expose those bytes.

Suggested fix

For ZIP/ZIPS chunks, reject the chunk unless actual_out_bytes == uncompressed_size before reconstruction/unpack, or otherwise guarantee the entire unpack buffer is initialized before any caller-visible pixel copy.