Skip to content

GHSA-pf59-r2mc-x746

CVE Information

Summary

A crafted deep scanline EXR with layer-prefixed RGB channels can make PyOpenEXR return stale heap data in coalesced deep RGB sample arrays. The issue is reachable through the public default API, OpenEXR.File(path), on an attacker-controlled EXR file.

PyOpenEXR groups left.R, left.G, and left.B into one Python channel named left when separate_channels=False. For deep images, it allocates each pixel sample array with RGB lanes, but the lane offset calculation only recognizes exact unprefixed names ("G", "B", "A"). As a result, left.G and left.B are decoded into lane 0 instead of lanes 1/2, while lanes 1/2 remain stale and are returned to Python.

Affected code

In src/wrappers/python/PyOpenEXR.cpp, PyPart::setDeepSliceData() creates RGB/RGBA-shaped deep sample arrays and then selects the target lane with exact full-name comparisons:

size_t channel_offset = 0;
if (C._nrgba > 0)
{
    if (!strcmp(c.name(), "G"))
        channel_offset = 1;
    else if (!strcmp(c.name(), "B"))
        channel_offset = 2;
    else if (!strcmp(c.name(), "A"))
        channel_offset = 3;
}

This works for unprefixed G/B, but not for prefixed channel names such as left.G and left.B. The wrapper already decided to coalesce those channels into the left RGB key, so the deep slice setup must also use the prefixed channel suffix or the channelNameToRGBA() result when choosing the lane.

Reproduction

From the OpenEXR v3.4.13 test root used for this report:

cd "report/46. OpenEXR v3.4.13 PyOpenEXR deep prefixed RGB stale lane disclosure"
./reproduce.sh

Expected key output on vulnerable builds:

keys ['left']
outer object (1, 1)
inner float16 (64, 3)
values [[30.0, 203.25, 3798.0], [31.0, 203.25, 3798.0], ...]
nonzero_unwritten_lanes ... count 64 of 128
wrong_gb_lanes 64 of 64

The correct separate_channels=True control returns separate channels with the expected samples:

left.B: [10.0, 11.0, 12.0, ...]
left.G: [20.0, 21.0, 22.0, ...]
left.R: [30.0, 31.0, 32.0, ...]

Why this proves stale lane disclosure:

  1. The EXR contains same-type deep HALF channels left.B, left.G, and left.R with known values.
  2. The reproducer grooms NumPy allocations with recognizable half-float bit patterns before opening the file.
  3. Default OpenEXR.File(path) returns channels()["left"].pixels[0,0] as a (64, 3) array where lane 0 contains R samples, but lanes 1/2 contain groomed stale values instead of G/B samples.

Validation notes

Verified locally on:

  • OpenEXR v3.4.13 original Python build: reproduced, nonzero_unwritten_lanes count 64 of 128, wrong_gb_lanes 64 of 64.
  • OpenEXR v3.4.13 ASAN Python build: reproduced with the same observable stale lanes.
  • OpenEXR v3.4.13 UBSAN Python build: reproduced with the same observable stale lanes.
  • Upstream main commit 6820342930d35c7c81a6b220c4a0aca9c2532542: reproduced with the same observable stale lanes.

Impact

A Python application that reads untrusted deep EXR files with PyOpenEXR's default channel coalescing and then logs, serializes, previews, transforms, or returns the resulting NumPy sample arrays may expose stale same-process heap contents. The demonstrated impact is information disclosure plus incorrect G/B lane data.

PoC files

The minimized poc.zip contains only:

poc.zip

  • reproduce.sh — focused reproduction script for local OpenEXR builds.
  • poc_materials/poc/deep_prefixed_left_rgb_half_1x1_64samp.exr — crafted valid deep EXR.
  • poc_materials/repro_read_prefixed.py — default OpenEXR.File(path) proof script.
  • poc_materials/repro_read_prefixed_control.pyseparate_channels=True control script.
  • poc_materials/gen_deep_prefixed_rgb.cpp — generator for the PoC EXR.