Skip to content

GHSA-9pfw-x4vc-xqx7 on CTRL-OS 26.05

Aliases: GHSA-9pfw-x4vc-xqx7

Packages: openexr

Status: Plausible

Advisory 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 when ctxt->mode == EXR_CONTEXT_WRITE. It then returns without using the matching unlock macro. A later API that attempts to take the same lock, such as exr_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.h

The 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-523

The 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 unlock

Triggering 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 forever

Observed timeout behavior:

timeout 3s rc=124

Observed GDB stack on latest main commit b25f9dc9af78d662eb45969e9e1cd395082d1f13:

#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:40

ASAN/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 -O1

No 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 present

PoC

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

124 is GNU timeout'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.txt

Impact

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 service

Not confirmed:

remote code execution
information disclosure
memory corruption
direct crafted .exr file trigger through the standard reader path

Suggested severity:

Moderate

Suggested CWE:

CWE-667: Improper Locking
CWE-833: Deadlock

Suggested fix:

Use the matching unlock-on-return macro for every return path after EXR_LOCK_WRITE_AND_DEFINE_PART() succeeds. In particular, both the chunk_offset_out == NULL path and the success path should unlock before returning.

Updates

2026-07-10 18:49 CEST

Metadata changes:

  • Status for package openexr: “Plausible

2026-07-07 22:43 CEST

Metadata changes:

  • Status for package openexr: “New

(Amended on: 2026-07-10 18:46 CEST)