Coordinated disclosureGHSA-hphq-wq62-4mj3OpenEXR · C/C++5.5 Moderate

Proof/CVEs & advisories/GHSA-hphq-wq62-4mj3

GHSA.02 — Advisory credit

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.

AdvisoryGHSA-hphq-wq62-4mj3
ProjectOpenEXR
EcosystemC / C++
Affected3.4.0 – 3.4.13
Fixed3.4.14
WeaknessCWE-190 / CWE-835
Severity5.5 Moderate
ReporterCharles Vosburgh
Image-coordinate arithmetic must preserve signed semantics until bounds are validated. A negative origin must never wrap into an enormous positive endpoint.
Weakness
CWE-190 / CWE-835
Trigger
negative Y origin + ySampling=2
Control
neighbouring geometries
Outcome
CPU-exhaustion DoS
Availability only.The published proof does not demonstrate memory corruption, information disclosure, arbitrary code execution or persistence. No evidence supports escalating this finding, and none is claimed here.

OpenEXR 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:

Vulnerable loop endpointinternal_ht.cpp · 3.4.13
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:

What the comparison seesafter unsigned conversion
intended end row : -32
actual end row   : 4294967264

The 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:

Candidate geometry448 bytes
compression        : HTJ2K32_COMPRESSION
data/display window: (0,-64)-(255,-33)
channel            : HALF Y, sampling 1x2
sha256             : 9140ba9fc3c1d708f006eb060074aa3d
                     4b40463dd9a337d799d4c237f5a940fe

Neighbouring 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.

01 · Input

Crafted EXR

HTJ2K scanline data with a negative Y data-window origin.

02 · Route

Planar path

Vertical subsampling selects the affected reconstruction path.

03 · Wrap

Unsigned addition

Signed negative start plus unsigned height crosses the signedness domain.

04 · Loop

Pathological rows

The decoder requests rows long after the small image is exhausted.

Vulnerable path

3.4.0 – 3.4.13

Every final tag inspected contains the mixed signed/unsigned endpoint expression; decode times out or loops on CPU.

Geometry controls

Neighbouring windows

Positive and zero origins, and sampling values of one, do not select the affected path — isolating the integer boundary.

Fixed control

3.4.14

Decode terminates normally. 3.4.15 was also inspected and retains the signed-endpoint fix.

CANDIDATE 448-byte HTJ2K EXR, dataWindow y: -64..-33 EXPECTED end row -32 (start_y + image_height) OBSERVED end row 4294967264 after unsigned conversion 3.4.13 bounded decode run: TIMEOUT (CPU loop) 3.4.14 bounded decode run: NORMAL TERMINATION MARKER=OPENEXR_HTJ2K_ROWLOOP

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.
The SecHive research loop

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.

SK.01

Source mapping

Trace the HTJ2K planar path and identify every expression mixing signed coordinates with unsigned dimensions.

SK.02

Boundary hypothesis

Predict the exact wrapped endpoint from the candidate geometry before running anything.

SK.03

Minimal construction

Build a 448-byte candidate plus neighbouring geometries that must stay valid.

SK.04

Skeptic gate

Refuse to claim memory corruption or disclosure without evidence; keep the claim at availability.

HUMAN

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:

Fixed loop endpointOpenEXR 3.4.14
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.

Moderate
Published advisory severity
5.5
CVSS 3.1
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H

In 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.

ScopeValueBasis
Affected3.4.0 through 3.4.13every final tag inspected; all contain the vulnerable expression
First fixed3.4.14inspected and runtime-controlled
Also inspected3.4.15retains the signed-endpoint fix
Implementation ancestry50ba96b1dbe353a98a626c7fd0ff1e50cc8c188fmerge 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:

  1. negative Y origins;
  2. positive and zero origins;
  3. vertical sampling values greater than one;
  4. neighbouring geometries that must remain valid;
  5. explicit assertions on the computed signed start and end rows;
  6. a bounded decode-time assertion for the original candidate.
2026-07-16
Advisory created upstream.
2026-08-07
OpenEXR 3.4.14 published.
2026-08-07
GHSA-hphq-wq62-4mj3 published with the public PoC.
2026-08-26
Final affected tag range and fixed releases reviewed.
  1. GHSA-hphq-wq62-4mj3 — OpenEXR advisory and public PoC
  2. Fixing commit — signed 64-bit loop bounds
  3. 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.