Proof/CVEs & advisories/GHSA-hphq-wq62-4mj3
A 448-byte image, billions of iterations.
OpenEXR’s HTJ2K planar decode computed its row-loop endpoint by adding an unsigned image height to a signed negative origin. The intended endpoint was -32. What the comparison received was 4294967264.
ySampling=2OpenEXR 3.4 introduced HTJ2K support through OpenJPH. The affected logic lives in the planar reconstruction path in src/lib/OpenEXRCore/internal_ht.cpp.
Image-coordinate arithmetic must preserve signed semantics until bounds are validated; a negative image origin must never wrap into an enormous positive loop endpoint.
The triggering geometry requires three things together: HTJ2K-compressed scanline data, a negative Y data-window origin, and vertical channel subsampling such as ySampling=2, which selects the planar path.
In OpenEXR 3.4.13 the decoder obtains image height as an unsigned 32-bit value and then combines it with the signed chunk origin:
ojph::ui32 image_height =
siz.get_image_extent ().y - siz.get_image_offset ().y;
for (int64_t y = decode->chunk.start_y;
y < image_height + decode->chunk.start_y;
y++)For the public candidate the relevant values are effectively image_height = 32 and start_y = -64. The intended end row is -64 + 32 = -32. Because image_height is unsigned, the addition is evaluated after unsigned conversion, so the comparison receives a value near UINT32_MAX:
intended end row : -32
actual end row : 4294967264The loop begins at -64 and therefore has billions of iterations available before reaching the wrapped endpoint.
The published reproducer is a 448-byte scanline EXR:
compression : HTJ2K32_COMPRESSION
data/display window: (0,-64)-(255,-33)
channel : HALF Y, sampling 1x2
sha256 : 9140ba9fc3c1d708f006eb060074aa3d
4b40463dd9a337d799d4c237f5a940feNeighbouring geometry controls matter here: they separate an integer-boundary condition from a generic HTJ2K decoder hang. A small file producing a disproportionately large loop is also why no decompression bomb or memory-pressure payload is needed.
Crafted EXR
HTJ2K scanline data with a negative Y data-window origin.
Planar path
Vertical subsampling selects the affected reconstruction path.
Unsigned addition
Signed negative start plus unsigned height crosses the signedness domain.
Pathological rows
The decoder requests rows long after the small image is exhausted.
3.4.0 – 3.4.13
Every final tag inspected contains the mixed signed/unsigned endpoint expression; decode times out or loops on CPU.
Neighbouring windows
Positive and zero origins, and sampling values of one, do not select the affected path — isolating the integer boundary.
3.4.14
Decode terminates normally. 3.4.15 was also inspected and retains the signed-endpoint fix.
The loop mixed a signed image-space coordinate with an unsigned dimension. The implicit conversion changed the meaning of a legitimate negative endpoint before the bound comparison ever ran.
“The values are individually in range” is not enough. Coordinate systems with negative origins require signed intermediate arithmetic even when width and height are naturally non-negative.
Bounded proof beats a bigger payload.
An availability finding invites escalation — bigger files, more memory, a crash. The stronger result was the opposite: the smallest file that makes the arithmetic visible, plus the geometry controls that rule out a generic decoder hang.
Source mapping
Trace the HTJ2K planar path and identify every expression mixing signed coordinates with unsigned dimensions.
Boundary hypothesis
Predict the exact wrapped endpoint from the candidate geometry before running anything.
Minimal construction
Build a 448-byte candidate plus neighbouring geometries that must stay valid.
Skeptic gate
Refuse to claim memory corruption or disclosure without evidence; keep the claim at availability.
Promotion and disclosure
Keep reproductions bounded and confirm the affected tag range release by release.
The fixed code converts both bounds to signed 64-bit arithmetic before entering the loop, preserving -32 as the intended endpoint:
const int64_t start_y = static_cast<int64_t> (decode->chunk.start_y);
const int64_t end_y = start_y + static_cast<int64_t> (image_height);
for (int64_t y = start_y; y < end_y; y++)The 3.4 release-branch fix is f09250fa2345c8e81b51ac199d7777e107ec6912; the equivalent main-branch commit is 3b8761145ec27dd13cedfd8dbae8c58ff0d5f1bf.
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:HIn applications that synchronously process untrusted EXR files, one crafted image can occupy a worker or thread until a timeout, watchdog, user termination or process kill intervenes.
The CVSS vector reflects a local/user-interaction delivery model. OpenEXR is a library rather than a service, so server-side reachability depends on the integrating application — image upload systems, asset pipelines, render infrastructure and desktop viewers all expose different delivery paths.
| Scope | Value | Basis |
|---|---|---|
| Affected | 3.4.0 through 3.4.13 | every final tag inspected; all contain the vulnerable expression |
| First fixed | 3.4.14 | inspected and runtime-controlled |
| Also inspected | 3.4.15 | retains the signed-endpoint fix |
| Implementation ancestry | 50ba96b1dbe353a98a626c7fd0ff1e50cc8c188f | merge that introduced internal_ht.cpp |
Upgrade to OpenEXR 3.4.14 or later.
For image and codec code, coordinate arithmetic should use a signed type large enough to contain the full window range until bounds have been validated. Avoid relying on implicit integer promotions in expressions that combine dimensions with signed origins.
Regression coverage should include:
- negative Y origins;
- positive and zero origins;
- vertical sampling values greater than one;
- neighbouring geometries that must remain valid;
- explicit assertions on the computed signed start and end rows;
- a bounded decode-time assertion for the original candidate.
- GHSA-hphq-wq62-4mj3 — OpenEXR advisory and public PoC
- Fixing commit — signed 64-bit loop bounds
- OpenEXR 3.4.14 release
Credit: Discovered and reported by Charles Vosburgh. Research was AI-assisted through source mapping, hypothesis generation, release comparison and evidence organization; final validation, disclosure coordination, severity calibration and publication review remained human.