GHSA-8ggg-fhxp-95p9 on CTRL-OS 26.05
Aliases: GHSA-8ggg-fhxp-95p9
Packages: openexr
Status: Plausible
Advisory Information
Summary
A malformed HTJ2K-compressed EXR file can provide a duplicate-but-in-range HT channel map. OpenEXR accepts that map without enforcing the required one-to-one permutation invariant, so one or more output channels are copied from an internal decode scratch buffer region that was never written. This can disclose stale/uninitialized heap data to applications that decode attacker-controlled EXR images.
This is driven by a public malformed EXR input, not by an invalid caller FrameBuffer or an API-only precondition. The same PoC reproduces on OpenEXR v3.4.13 and on the local upstream
mainbuild.Details
OpenEXR's HTJ2K chunk header contains a codestream-component-to-file-channel mapping. The decoder currently verifies the map length and that each
file_indexis less thandecode->channel_count, but it does not verify that the map is a permutation.Broken invariant:
For N decoded HTJ2K components, cs_to_file_ch must be a permutation of [0, N). Duplicate file indices and missing file indices must be rejected.Affected locations in v3.4.13:
sources/openexr-3.4.13/src/lib/OpenEXRCore/internal_ht_common.cpp:291-314read_header()readsNCHand eachfile_indexfrom the public chunk bytes.- No duplicate or missing-index validation is performed.
sources/openexr-3.4.13/src/lib/OpenEXRCore/internal_ht.cpp:194-210ht_undo_impl()checks only map size andfile_i >= decode->channel_count.- There is no
seen[file_i]/permutation check.sources/openexr-3.4.13/src/lib/OpenEXRCore/internal_ht.cpp:322-350- The non-planar path writes decoded component
cintoline_pixels + cs_to_file_ch[c].raster_line_offset.- If multiple components map to the same
file_i, they overwrite the same file-channel slot and leave another file-channel slot unwritten.sources/openexr-3.4.13/src/lib/OpenEXRCore/coding.c:245-257internal_decode_alloc_buffer()allocates the scratch buffer but does not clear it.sources/openexr-3.4.13/src/lib/OpenEXRCore/unpack.c:1285-1286generic_unpack()later converts/copies all advertised file-channel samples from that scratch buffer into the caller's FrameBuffer, including the channel region HTJ2K never wrote.The attached PoC starts from a valid 4-channel HTJ2K scanline EXR. Only the 8 bytes of the HT channel map are changed:
Original HT map bytes: 0003 0002 0001 0000 PoC HT map bytes: 0000 0000 0000 0000The map values are all individually in range for a 4-channel image, so the current checks accept them. Because all components map to file channel 0, channels B/G/R remain unwritten in the decode scratch buffer but are still copied to the caller output.
Validation evidence:
builds/original/bin/exrheader poc_htj2k_duplicate_map_all_zero.exrparses the file normally.builds/original/bin/exrcheck poc_htj2k_duplicate_map_all_zero.exrreportsOK.builds/asan/bin/exrcheck poc_htj2k_duplicate_map_all_zero.exrreportsOK.builds/msan/bin/exrcheck poc_htj2k_duplicate_map_all_zero.exrreportsMemorySanitizer: use-of-uninitialized-valueingeneric_unpack()/imath_half_to_float, with allocation origin ininternal_decode_alloc_buffer().- The no-sanitizer full-file harness using the public
InputFile/FrameBufferAPI reproduces a deterministic data leak by installing a publicContextInitializerallocator that fills allocations with0xA5. On the PoC, channel B is copied out as allocator-fill bytes; on the valid base file it exits cleanly.- The same full-file harness reproduces on local upstream
mainoriginal and ASAN builds.- UBSAN builds currently abort first on an existing OpenJPH alignment instrumentation issue in
external/OpenJPH/src/core/others/ojph_mem.cpp:122; the base file is clean in the harness, and this UBSAN noise is separate from the OpenEXR channel-map bug.AFL++ was used only after the invariant and reachability were identified:
- Full-file AFL expanded coverage but did not reach the exact map mutation before stop (
229730execs regular,153549execs deterministic, 0 crashes), which is expected because most full-EXR mutations corrupt unrelated structure.- A narrower map-window harness injected AFL-controlled HT map values into the valid EXR template and then decoded the generated public EXR through OpenEXR. It found 4 proof crashes in 34 seconds / 700 execs. The first AFL crash reconstructed the attached full public EXR PoC.
PoC
Artifacts are in
poc.zip:poc_materials/poc_htj2k_duplicate_map_all_zero.exr # malformed public EXR PoC poc_materials/base_htj2k32_scan_rgba_16x16.exr # valid negative control poc_materials/fuzz_htj2k_duplicate_map_api.cpp # full-public-EXR harness poc_materials/fuzz_htj2k_duplicate_map_window_api.cpp # AFL map-window proof harness poc_materials/reproduce.sh # local reproduction commands poc_materials/verification_matrix_afl_poc.txt # v3.4.13/main evidence poc_materials/source_evidence_snippets.txt # source line snippets security_advisories_25.tsv # referenced security docs/advisoriesFrom the repository root
/home/br0nzu/openexr/v3.4.13, after unzipping or using the report path directly:cd /home/br0nzu/openexr/v3.4.13 REPORT='report/34. OpenEXR v3.4.13 HTJ2K duplicate channel map uninitialized data disclosure' POC="$REPORT/poc_materials/poc_htj2k_duplicate_map_all_zero.exr" BASE="$REPORT/poc_materials/base_htj2k32_scan_rgba_16x16.exr" # Public tool behavior on v3.4.13. builds/original/bin/exrheader "$POC" builds/original/bin/exrcheck "$POC" # observed: OK builds/asan/bin/exrcheck "$POC" # observed: OK # Sanitizer evidence on the public EXR file. MSAN_OPTIONS=abort_on_error=1:symbolize=1:allocator_may_return_null=1:exit_code=86 \ builds/msan/bin/exrcheck "$POC" # observed: use-of-uninitialized-value in generic_unpack(), origin internal_decode_alloc_buffer() # Full-public-EXR harness, no sanitizer: negative control and PoC. harness/bin/fuzz_htj2k_duplicate_map_api_original "$BASE" # observed: rc=0 harness/bin/fuzz_htj2k_duplicate_map_api_original "$POC" # observed: abort / rc=134 # Full-public-EXR harness under ASAN/MSAN. ASAN_OPTIONS=abort_on_error=1:symbolize=1:detect_leaks=0:allocator_may_return_null=1 \ harness/bin/fuzz_htj2k_duplicate_map_api_asan "$POC" MSAN_OPTIONS=abort_on_error=1:symbolize=1:allocator_may_return_null=1:exit_code=86 \ harness/bin/fuzz_htj2k_duplicate_map_api_msan "$POC" # Upstream main verification (if the local main builds are present). harness/bin/main_fuzz_htj2k_duplicate_map_api_original "$BASE" # observed: rc=0 harness/bin/main_fuzz_htj2k_duplicate_map_api_original "$POC" # observed: abort / rc=134 ASAN_OPTIONS=abort_on_error=1:symbolize=1:detect_leaks=0:allocator_may_return_null=1 \ harness/bin/main_fuzz_htj2k_duplicate_map_api_asan "$POC" # observed: abort / rc=134Expected no-sanitizer harness stderr for the PoC:
HTJ2K duplicate map leaked allocator pattern into channel B (512 bytes)The provided
poc_materials/reproduce.shruns the same checks.Impact
This is a confidentiality issue / uninitialized data disclosure (CWE-908 / CWE-200). An attacker who can supply an EXR file to an application using OpenEXR HTJ2K decode can cause one or more decoded output channels to be populated from stale heap contents instead of actual image samples. Applications that save, display, transmit, analyze, or otherwise expose decoded pixel data may unintentionally disclose bytes from the decoding process heap.
The root cause is in OpenEXR's HTJ2K channel-map validation, and the trigger is a normal product input: a crafted EXR image file. The local upstream
mainbuild is still affected, so the likely fix is to reject any HTJ2K map that is not a strict permutation of[0, channel_count)before any component is copied intodecode->unpacked_buffer. Zero-filling the scratch buffer can reduce disclosure risk, but it would not fix the channel-confusion invariant and should be treated only as defense in depth.
Updates
2026-08-11 03:51 CEST
Metadata changes:
- Status for package
openexr: “Plausible”
2026-08-11 03:31 CEST
Metadata changes:
- Status for package
openexr: “New”