Skip to content

GHSA-54cp-3rq6-7mq8

CVE Information

Summary

OpenEXRUtil has a heap out-of-bounds write in Imf_4_0::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.

This is reachable through the public OpenEXRUtil DeepImage API and can lead to heap corruption and process crashes.

Details

Affected code:

src/lib/OpenEXRUtil/ImfSampleCountChannel.cpp

void
SampleCountChannel::set (int r, unsigned int newNumSamples[])
{
    int x = level ().dataWindow ().min.x;
    int y = r + level ().dataWindow ().min.x;

    for (int i = 0; i < pixelsPerRow (); ++i, ++x)
        set (x, y, newNumSamples[i]);
}

The y calculation should use level().dataWindow().min.y.

For example, with a valid data window (0, 1) - (1, 1) and r = 0, the expected target Y coordinate is 1, but the code computes 0.

The lower-level SampleCountChannel::set(int x, int y, unsigned int) then computes an index using _base + y * pixelsPerRow() + x without first calling boundsCheck(), despite the public header documenting the setters as bounds-checked.

Observed in GDB on commit e9133442dda6139aa395d0e87f3b00e7f31199a6:

dataWindow.min = (0, 1)
dataWindow.max = (1, 1)
pixelsPerRow = 2
pixelsPerColumn = 1
x = 0
y = 0
_numSamples = 0x555555775590
_base       = 0x555555775588
computed index = -2

So the row setter writes to _numSamples[-2], corrupting memory before the allocated sample-count array.

The same vulnerable code is present in at least v3.2.4, v3.3.5, v3.4.12, and current main.

PoC

Minimal C++ reproducer:

#include <ImfDeepImage.h>
#include <ImfSampleCountChannel.h>
#include <Imath/ImathBox.h>

namespace IMF = OPENEXR_IMF_NAMESPACE;
namespace IM = IMATH_NAMESPACE;

int main()
{
    IMF::DeepImage img;
    img.insertChannel("Z", IMF::FLOAT, 1, 1, false);

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

    IMF::SampleCountChannel& sc = img.level(0).sampleCounts();

    unsigned int counts[2] = {1, 1};

    // row 0 is valid because pixelsPerColumn() == 1.
    // Expected y: dataWindow.min.y + 0 == 1.
    // Actual y: dataWindow.min.x + 0 == 0.
    sc.set(0, counts);

    return 0;
}

Expected result under a memory-debug build is an out-of-bounds write before _numSamples. In non-ASAN builds this can later abort with:

free(): invalid pointer

Representative stack from fuzzing/GDB:

Imf_4_0::SampleCountChannel::set(int, int, unsigned int)
src/lib/OpenEXRUtil/ImfSampleCountChannel.cpp

Imf_4_0::SampleCountChannel::set(int, unsigned int*)
src/lib/OpenEXRUtil/ImfSampleCountChannel.cpp:229

Impact

This is a heap out-of-bounds write in OpenEXRUtil's public DeepImage API.

Applications that use SampleCountChannel::set(row, array) with image data windows whose X and Y origins differ can corrupt heap memory and crash. The confirmed impact is denial of service through heap corruption. I have not confirmed that a crafted EXR file directly reaches this row setter through the standard file loading path.

Suggested fix:

int x = level ().dataWindow ().min.x;
int y = r + level ().dataWindow ().min.y;

Also consider validating r in the row setter and calling boundsCheck(x, y) in SampleCountChannel::set(int x, int y, unsigned int) to match the documented bounds-checked behavior.