GHSA-c32g-9w8w-3px6 on CTRL-OS 26.05
Aliases: GHSA-c32g-9w8w-3px6
Packages: openexr
Status: Plausible
Advisory Information
Summary
A crafted valid scanline EXR with a subsampled channel can make PyOpenEXR return stale heap data through the normal Python API. The issue is reachable with
OpenEXR.File(path)on an attacker-controlled EXR file.For a channel with
xSampling = 2, PyOpenEXR allocates a full data-window-shaped NumPy array but uses a framebuffer row stride based on the sampled row width. The OpenEXR reader writes the second EXR row into the tail of the first NumPy row, and the final NumPy row remains stale/uninitialized from the Python process heap.Affected code
In
src/wrappers/python/PyOpenEXR.cpp,PyPart::readPixels()uses the full data-window shape for each channel buffer:std::vector<size_t> c_shape = shape; ... C.pixels = py::array_t<float, style>(c_shape);It then inserts a slice whose row stride is shortened by
C.xSampling:size_t yStride = xStride * shape[1] / C.xSampling; frameBuffer.insert(c.name(), Slice::Make(c.channel().type, (void*) basePtr, dw, xStride, yStride, C.xSampling, C.ySampling));For
dataWindow = 8 x 2and channelZwithxSampling = 2, the Python buffer has shape(2, 8), but the native row stride advances only fourfloatelements. This leaves the second Python row stale while returning it as valid channel data.Reproduction
From the OpenEXR v3.4.13 test root used for this report:
cd "report/38. OpenEXR v3.4.13 PyOpenEXR subsampled scanline stale buffer disclosure" ./reproduce.shThe PoC file is accepted by
exrcheck:file .../subsample_z_float_x2_8x2.exr OKExpected key output on v3.4.13 original, v3.4.13 ASAN, v3.4.13 UBSAN, and current upstream main:
dtype float32 shape (2, 8) xs 2 ys 1 row0 [1000.0, 1001.0, 1002.0, 1003.0, 1010.0, 1011.0, 1012.0, 1013.0] row1 [31337.125, 31337.125, 31337.125, 31337.125, 31337.125, 31337.125, 31337.125, 31337.125] marker_count 8Why this proves stale data exposure:
- The EXR stores only four legitimate sampled
Zvalues per row.- The Python reproducer first grooms freed NumPy allocations with marker
31337.125.OpenEXR.File(path)returns a(2, 8)array where row 0 contains both EXR rows and row 1 still contains the groomed marker.This is not an ASAN crash: the stale bytes are inside the allocated NumPy array. ASAN/UBSAN therefore reproduce the logical disclosure without reporting a memory-safety diagnostic.
Validation notes
Verified locally on:
- OpenEXR v3.4.13 original Python build: reproduced,
marker_count 8.- OpenEXR v3.4.13 ASAN Python build: reproduced,
marker_count 8.- OpenEXR v3.4.13 UBSAN Python build: reproduced,
marker_count 8.- Upstream main commit
6820342930d35c7c81a6b220c4a0aca9c2532542: reproduced,marker_count 8.Impact
A Python application that reads untrusted EXR files with PyOpenEXR and logs, serializes, displays, transforms, or returns the resulting NumPy array may expose stale same-process heap contents. The demonstrated impact is information disclosure and incorrect channel layout.
PoC files
The minimized
poc.zipcontains only:
reproduce.sh— runs the focused validation against local OpenEXR builds.poc_materials/subsample_z_float_x2_8x2.exr— crafted valid scanline EXR.poc_materials/repro_read_subsample_default_groom.py— Python reproducer usingOpenEXR.File(path).poc_materials/src/gen_subsample.cpp— generator for the PoC EXR.
Updates
2026-08-11 03:51 CEST
Metadata changes:
- Status for package
openexr: “Plausible”
2026-08-11 03:31 CEST
Metadata changes:
- Status for package
openexr: “New”