GHSA-r864-gwqv-q2hp on CTRL-OS 26.05
Aliases: GHSA-r864-gwqv-q2hp
Packages: openexr
Status: Plausible
Advisory 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=134PoC abort message in each failing harness run:
ZIP short inflate leaked allocator pattern into channel 0: 2028/2048 bytesThe 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 outputThe PoC is a 64x16 one-channel HALF ZIP image. The expected decoded pixel block is
2048bytes, but the zlib stream inflates to20bytes. The remaining2028bytes of the decode buffer are stale allocation contents and are copied to the output channel.PoC package
poc.zipcontains only required reproduction material: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.zipintentionally does not includereport.md, README files, checksum-only files, AFL exploration logs, or source-snippet notes.Reproduction
From the extracted
poc.zipdirectory:OPENEXR_ROOT=/home/br0nzu/openexr/v3.4.13 ./reproduce.shExpected result: safe controls exit
0; the crafted PoC exits134after 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 PASSv3.4.13 ASAN PoC stderr
ZIP short inflate leaked allocator pattern into channel 0: 2028/2048 bytesv3.4.13 UBSAN PoC stderr
ZIP short inflate leaked allocator pattern into channel 0: 2028/2048 bytesv3.4.13 MSAN PoC stderr
ZIP short inflate leaked allocator pattern into channel 0: 2028/2048 byteslocal main ASAN PoC stderr
ZIP short inflate leaked allocator pattern into channel 0: 2028/2048 byteslocal main UBSAN PoC stderr
ZIP short inflate leaked allocator pattern into channel 0: 2028/2048 byteslocal main MSAN PoC stderr
ZIP short inflate leaked allocator pattern into channel 0: 2028/2048 bytesfresh origin/main 68203429 ASAN PoC stderr
ZIP short inflate leaked allocator pattern into channel 0: 2028/2048 bytesImpact
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_sizebefore reconstruction/unpack, or otherwise guarantee the entire unpack buffer is initialized before any caller-visible pixel copy.
Updates
2026-08-11 03:50 CEST
Metadata changes:
- Status for package
openexr: “Plausible”
2026-08-11 03:32 CEST
Metadata changes:
- Status for package
openexr: “New”