GHSA-9pfw-x4vc-xqx7
CVE Information
Summary
OpenEXRCore has a deterministic self-deadlock in
exr_get_chunk_table_offset()on write contexts before the header has been written.The function uses
EXR_LOCK_WRITE_AND_DEFINE_PART(part_index), which locks the context whenctxt->mode == EXR_CONTEXT_WRITE. It then returns without using the matching unlock macro. A later API that attempts to take the same lock, such asexr_get_count(), blocks forever.This is reachable through the public OpenEXRCore C API and causes denial of service through a process hang. I have not confirmed memory corruption, information disclosure, or RCE.
Details
Affected code:
src/lib/OpenEXRCore/chunk.c src/lib/OpenEXRCore/internal_structs.hThe vulnerable function:
exr_result_t exr_get_chunk_table_offset ( exr_const_context_t ctxt, int part_index, uint64_t* chunk_offset_out) { EXR_LOCK_WRITE_AND_DEFINE_PART (part_index); if (!chunk_offset_out) return ctxt->standard_error (ctxt, EXR_ERR_INVALID_ARGUMENT); *chunk_offset_out = part->chunk_table_offset; return EXR_ERR_SUCCESS; }Location on tested
main:src/lib/OpenEXRCore/chunk.c:512-523The lock macro takes the context mutex for write-mode contexts:
#define EXR_LOCK_WRITE_AND_DEFINE_PART(pi) \ exr_const_priv_part_t part; \ if (!ctxt) return EXR_ERR_MISSING_CONTEXT_ARG; \ if (ctxt->mode == EXR_CONTEXT_WRITE) internal_exr_lock (ctxt); \ if (pi < 0 || pi >= ctxt->num_parts) \ { \ if (ctxt->mode == EXR_CONTEXT_WRITE) internal_exr_unlock (ctxt); \ return ctxt->print_error (...); \ } \ part = ctxt->parts[pi]Location on tested
main:src/lib/OpenEXRCore/internal_structs.h:299-312
exr_get_chunk_table_offset()has two lock-leaking return paths after the macro has acquired the lock:chunk_offset_out == NULL -> return EXR_ERR_INVALID_ARGUMENT without unlock success path -> return EXR_ERR_SUCCESS without unlockTriggering call sequence from the PoC:
exr_start_write() exr_add_part() exr_get_chunk_table_offset(ctxt, part, &chunk_table_offset) acquires write lock returns without unlocking exr_get_count(ctxt, &count) tries to acquire the same lock blocks foreverObserved timeout behavior:
timeout 3s rc=124Observed GDB stack on latest
maincommitb25f9dc9af78d662eb45969e9e1cd395082d1f13:#0 futex_wait(...) #1 __GI___lll_lock_wait(...) #2 lll_mutex_lock_optimized(...) #3 ___pthread_mutex_lock(...) #4 internal_exr_lock(...) at src/lib/OpenEXRCore/internal_structs.h:260 #5 exr_get_count(...) at src/lib/OpenEXRCore/part.c:26 #6 main() at afl-findings/poc/poc_core_chunk_table_offset_lock_hang.c:40ASAN/UBSAN build information:
clang: Ubuntu clang version 18.1.3 cmake: 3.28.3 CMAKE_BUILD_TYPE=Debug CMAKE_C_FLAGS=-fsanitize=address,undefined -fno-omit-frame-pointer -g -O1 CMAKE_CXX_FLAGS=-fsanitize=address,undefined -fno-omit-frame-pointer -g -O1No ASAN/UBSAN diagnostic is expected for the primary failure mode because this bug is a lock leak / self-deadlock, not a memory-safety violation. The instrumented build is still useful for reproducible symbols and for confirming the PoC runs against the local OpenEXRCore build.
Version scope from source inspection:
v3.2.9: exr_get_chunk_table_offset() absent v3.3.11: vulnerable implementation present v3.4.12: vulnerable implementation present main: vulnerable implementation presentPoC
Minimal C reproducer:
#include <openexr.h> #include <stdint.h> #include <unistd.h> static void ignore_error (exr_const_context_t ctxt, exr_result_t code, const char* msg) { (void) ctxt; (void) code; (void) msg; } int main (void) { exr_context_t ctxt = NULL; exr_context_initializer_t init = EXR_DEFAULT_CONTEXT_INITIALIZER; int part = -1; int count = 0; uint64_t chunk_table_offset = 0; const char* path = "/tmp/openexr_core_chunk_table_lock.exr"; init.error_handler_fn = ignore_error; if (exr_start_write (&ctxt, path, EXR_WRITE_FILE_DIRECTLY, &init) != EXR_ERR_SUCCESS) return 1; if (exr_add_part (ctxt, "p", EXR_STORAGE_SCANLINE, &part) != EXR_ERR_SUCCESS) return 1; (void) exr_get_chunk_table_offset (ctxt, part, &chunk_table_offset); // In EXR_CONTEXT_WRITE mode, exr_get_count() tries to take the same lock. (void) exr_get_count (ctxt, &count); (void) exr_finish (&ctxt); unlink (path); return count; }Reproduction command on the latest-main validation build:
timeout 3s \ /root/openexr_latest_check/openexr-main/latest-check/bin/poc_core_chunk_table_offset_lock_hang echo $?Expected result:
124
124is GNUtimeout's exit code when the child process does not terminate before the timeout expires.Saved evidence:
evidence/afl-findings/poc/poc_core_chunk_table_offset_lock_hang.c evidence/afl-findings/poc/poc_core_chunk_table_offset_lock_hang.timeout.out evidence/afl-findings/poc/poc_core_chunk_table_offset_lock_hang.gdb.txt evidence/latest-main-check/logs/poc_core_chunk_table_offset_lock_hang.run.out evidence/latest-main-check/logs/poc_core_chunk_table_offset_lock_hang.gdb_latest_main.txtImpact
This is a public C API denial-of-service bug in OpenEXRCore.
Applications that call
exr_get_chunk_table_offset()on a write context before header write completion can leave the context locked. A later operation that tries to lock the same context can hang forever.Confirmed impact:
availability loss self-deadlock process hang public C API denial of serviceNot confirmed:
remote code execution information disclosure memory corruption direct crafted .exr file trigger through the standard reader pathSuggested severity:
ModerateSuggested CWE:
CWE-667: Improper Locking CWE-833: DeadlockSuggested fix:
Use the matching unlock-on-return macro for every return path after
EXR_LOCK_WRITE_AND_DEFINE_PART()succeeds. In particular, both thechunk_offset_out == NULLpath and the success path should unlock before returning.