Skip to content

GHSA-prr3-4q3r-hmf3

CVE Information

poc-openexr-deep-zero-table.zip

Summary

A crafted compressed deep-scanline EXR with a zero-length sample-count table causes OpenEXRCore to read uninitialized heap memory in unpack_sample_table().

The malformed file reaches the issue through the normal OpenEXR C and C++ decode/check paths. The issue reproduces in OpenEXR v3.4.13 and current main.

This is a distinct current-version trigger related to CVE-2025-64181. The public deep-sample-table testcase for CVE-2025-64181 is rejected by current OpenEXR, whereas this zero-length trigger skips the sample-table decompression validation and still reaches the uninitialized read.

I request a new CVE for this distinct current-version validation bypass. I am disclosing the relationship to CVE-2025-64181 so that the OpenEXR maintainers and CNA can make the final assignment decision.

The demonstrated result is a deterministically reproducible sanitizer-detected heap use of uninitialized values. I have not demonstrated code execution, an out-of-bounds access, or proven information disclosure.

Details

Tested versions on x86-64 Linux:

  • OpenEXR v3.4.13: c1194b2cb23a1bdf76fe5e756b22e8436b9a98c9
  • OpenEXR current main: 55888dbd69f9e9edfc5495934fbfa4db37f474b1

Both source trees were unmodified. The tests were performed with source builds of OpenEXRCore; a prebuilt PyPI wheel was not separately tested.

The PoC is 58,517 bytes and differs from the upstream 11.deep.exr image at exactly one byte:

one-based offset 2821 (0x0b04): 0x11 -> 0x00

This changes the compressed deep sample-count table size from 17 to 0.

PoC SHA-256:

bb634485ba04e655cc71fee372dafcac1f6adfd5d2efdb6f0fcb2c14bde488da

The vulnerable state transition is:

  1. exr_read_scanline_chunk_info() rejects a negative sample-table size but accepts sample_count_table_size == 0 for a compressed deep scanline:

https://github.com/AcademySoftwareFoundation/openexr/blob/55888dbd69f9e9edfc5495934fbfa4db37f474b1/src/lib/OpenEXRCore/chunk.c#L997-L1059

  1. default_read_chunk() allocates and reads packed_sample_count_table only when the declared size is greater than zero:

https://github.com/AcademySoftwareFoundation/openexr/blob/55888dbd69f9e9edfc5495934fbfa4db37f474b1/src/lib/OpenEXRCore/decoding.c#L151-L215

  1. update_pack_unpack_ptrs() allocates the full decompressed width * height * sizeof(int32_t) table. The heap allocation is not initialized:

https://github.com/AcademySoftwareFoundation/openexr/blob/55888dbd69f9e9edfc5495934fbfa4db37f474b1/src/lib/OpenEXRCore/decoding.c#L20-L57

  1. exr_uncompress_chunk() decompresses the sample table only when packed_sample_count_table is non-null. The zero-length state therefore skips decompression and its validation:

https://github.com/AcademySoftwareFoundation/openexr/blob/55888dbd69f9e9edfc5495934fbfa4db37f474b1/src/lib/OpenEXRCore/compression.c#L513-L539

  1. The existing zero-fill path is restricted to EXR_COMPRESSION_NONE and does not initialize this compressed state: https://github.com/AcademySoftwareFoundation/openexr/blob/55888dbd69f9e9edfc5495934fbfa4db37f474b1/src/lib/OpenEXRCore/decoding.c#L589-L648

  2. unpack_sample_table() then reads the uninitialized entries to check sample-count monotonicity and calculate the total sample count: https://github.com/AcademySoftwareFoundation/openexr/blob/55888dbd69f9e9edfc5495934fbfa4db37f474b1/src/lib/OpenEXRCore/decoding.c#L219-L280

Representative MemorySanitizer result:

use-of-uninitialized-value
#0 unpack_sample_table
#1 exr_decoding_run

Uninitialized value was created by a heap allocation
#0 malloc
#1 internal_exr_alloc
#2 internal_decode_alloc_buffer
#3 update_pack_unpack_ptrs

Validation matrix:

Configuration Result
NexusSan, two upstream OpenEXR harnesses 6/6 UMR reports
MemorySanitizer C runner, current main heap-origin UMR
MemorySanitizer C runner, v3.4.13 same heap-origin UMR

NexusSan is a standalone object-state sanitizer. Independent MemorySanitizer builds confirm the same heap-origin initializedness violation on both tested OpenEXR revisions.

Relationship to CVE-2025-64181 / GHSA-3h9h-qfvw-98hq:

  • The existing advisory lists an unpack_sample_table (deep scanline) manifestation.
  • Its public archive2 testcase is 412 bytes with SHA-256: a20763b1613557f2fcc3848633a190e0afa446f49302a6c66446e9b2c18aebca.
  • That public testcase exits normally on all four current NexusSan harness configurations.
  • The current C++ path rejects it with: Unable to decompress sample table 4 -> 4100.
  • The new trigger declares a zero-length table, so no packed-table pointer is created and the decompression validation that rejects the public testcase is not reached.
  • CVE-2025-64181 lists OpenEXR 3.4.3 as patched, but this distinct trigger reproduces in v3.4.13 and current main.

Suggested remediation is either to reject an invalid absent sample table before allocation or, if an all-zero deep sample table is valid, initialize the complete decompressed table to the all-zero representation regardless of compression type before unpack_sample_table() runs.

Please add an initializedness regression test for a compressed deep-scanline chunk with a zero-length sample-count table.

PoC

Attachments:

  • poc-openexr-deep-zero-table.zip
  • contains poc.exr
  • contains POC_MANIFEST.txt
  • deep_core_decode_runner.c
  • msan-main-summary.log
  • msan-v3.4.13-summary.log
  • nexus-summary.log

ZIP SHA-256:

f00458a178b119ead5422616d37a4cdeee3cb6a28761da63dd4e42766fcd6b1d

Extracted poc.exr SHA-256:

bb634485ba04e655cc71fee372dafcac1f6adfd5d2efdb6f0fcb2c14bde488da

Reproduction procedure:

  1. Check out either tested OpenEXR revision.
  2. Build OpenEXRCore with Clang MemorySanitizer and origin tracking. Use -fsanitize=memory, -fsanitize-memory-track-origins=2, debug information, and frame pointers for both compilation and linking.
  3. Compile the attached deep_core_decode_runner.c with the same MemorySanitizer instrumentation and link it against the instrumented OpenEXRCore build.
  4. Extract poc.exr and run:
MSAN_OPTIONS=halt_on_error=1:exit_code=86:symbolize=1:track_origins=2 \
  ./deep_core_decode_runner_msan poc.exr

Expected result:

  • process exit code 86;
  • MemorySanitizer reports use-of-uninitialized-value in unpack_sample_table();
  • the allocation origin passes through internal_decode_alloc_buffer() and update_pack_unpack_ptrs().

The attached MemorySanitizer summaries were generated with this C runner. The NexusSan results were generated separately with the two upstream OpenEXR OSS-Fuzz harnesses.

Without an initializedness sanitizer, the same input may exit normally or produce nondeterministic behavior; a normal exit does not mean that the uninitialized read did not occur.

Impact

Applications that validate or decode attacker-controlled deep-scanline EXR files with an affected OpenEXRCore build can reach this flaw through normal product input.

Uninitialized heap values influence:

  • sample-count monotonicity validation;
  • accumulated deep sample totals;
  • subsequent deep-output allocation decisions.

The directly demonstrated impact is undefined and nondeterministic behavior caused by a heap use of uninitialized values. A crash or denial of service is plausible, but a stable native crash without an initializedness sanitizer has not been demonstrated.

This vector assumes that a user or local application opens the crafted file. An unattended service that automatically ingests untrusted EXR files may justify different AV/UI metrics.

Credit requested: He Huang as finder and reporter, with acknowledgement that the issue was found using NexusSan, if acceptable.