Skip to content

GHSA-7828-c4f6-7v6p

CVE Information

Summary

A valid scanline EXR with a subsampled channel can make OpenEXR's scanline reader write decoded ZIP chunk rows into the wrong compact FrameBuffer rows. For a channel with ySampling = 3, ZIP uses 16-line chunks; the second chunk starts at file row 16, which is not aligned to the sampling interval. OpenEXR tests sampling with the absolute file row, but computes the destination compact-row offset from the chunk-local row. This shifts later sampled rows upward and leaves the final sampled row unchanged, exposing stale caller-buffer contents if the application later displays, serializes, or transmits the decoded buffer.

This is reachable through normal public EXR input. The PoC file is generated with OpenEXR OutputFile and read with a matching C++ InputFile/FrameBuffer slice.

Affected versions tested

  • OpenEXR v3.4.13: reproduced with original, ASAN, and UBSAN builds.
  • Upstream main: reproduced on latest fetched origin/main commit 6820342930d35c7c81a6b220c4a0aca9c2532542.

Technical details

The relevant invariant is in the generic scanline unpack path. The decoder checks whether a row belongs to a subsampled channel using the absolute row:

int cury = y + decode->chunk.start_y;
if ((cury % decc->y_samples) != 0) continue;

After that, the destination offset is derived from the chunk-local row y:

cdata += ((y - uls) / decc->y_samples) * decc->user_line_stride;

For ySampling = 3 and ZIP's 16-line chunks, file row 18 is accepted because 18 % 3 == 0, but its local row inside the second chunk is 2, so it is written to compact row 2 / 3 == 0 relative to that chunk. The expected compact row for file row 18 is the next sampled row after file row 15. This mismatch shifts sampled rows and leaves the last sampled row stale.

The same file encoded as NO_COMPRESSION is a clean negative control because one-line chunks do not cross a misaligned chunk boundary.

PoC

From the OpenEXR v3.4.13 test workspace:

cd "report/51. OpenEXR v3.4.13 Core ySampling ZIP compact FrameBuffer stale row disclosure"
./reproduce.sh

Expected key output:

v3413_original    zip rc=1    BAD k=5 y=15 x=0 got=1180 exp=1150
v3413_original    none    rc=0    bad=0
v3413_asan    zip rc=1    BAD k=5 y=15 x=0 got=1180 exp=1150
v3413_ubsan   zip rc=1    BAD k=5 y=15 x=0 got=1180 exp=1150
main_original zip rc=1    BAD k=5 y=15 x=0 got=1180 exp=1150
main_original none    rc=0    bad=0
PASS: ySampling ZIP stale row reproduced on v3.4.13 and main original/ASAN/UBSAN; NONE control clean.

A separate latest-main check was also run against origin/main commit 6820342930d35c7c81a6b220c4a0aca9c2532542:

zip_rc=1
BAD k=5 y=15 x=0 got=1180 exp=1150
BAD k=10 y=30 x=0 got=-123 exp=1300
none_rc=0
bad=0

Impact

A victim application that decodes attacker-controlled ZIP scanline EXR files into a compact subsampled FrameBuffer and then trusts or exposes every sampled output row can disclose stale bytes already present in its caller-owned output buffer and can process pixels from the wrong file rows.

The impact should be treated as limited confidentiality exposure plus image data integrity corruption, not memory corruption with control-flow impact.

Files in poc.zip

poc.zip

  • reproduce.sh — builds/runs the focused probe against local OpenEXR builds.
  • poc_materials/y_sampling3_zip.exr — positive ZIP PoC.
  • poc_materials/y_sampling3_none.exr — clean NO_COMPRESSION control.
  • poc_materials/src/make_y_sampling3.cpp — generator for the PoC/control files.
  • poc_materials/src/probe_y_sampling3.cpp — compact FrameBuffer reader that detects the row shift and stale final row.