Skip to content

GHSA-hphq-wq62-4mj3 on CTRL-OS 26.05

Aliases: GHSA-hphq-wq62-4mj3

Packages: openexr

Status: Plausible

Advisory Information

Summary

OpenEXR’s planar HTJ2K decoder uses mixed signed and unsigned arithmetic when calculating the final row of a decoded chunk. A small EXR file with a negative data-window Y origin and vertical channel subsampling causes the loop endpoint to wrap to a very large positive value.

As a result, OpenEXR continues requesting component rows after the declared image data has been exhausted. In a release build, decoding remains CPU-bound until externally terminated.

An attacker who can supply an EXR file to an application using OpenEXR can occupy a decoder thread or worker with a 448-byte file, causing a denial of service.

The issue is dynamically confirmed in OpenEXR 3.4.13. The vulnerable expression is also present in OpenEXR 3.4.0 and was present on the default branch when inspected on July 16, 2026.

Suggested severity: Moderate / Medium

Suggested weaknesses:

  • CWE-190: Integer Overflow or Wraparound
  • CWE-835: Loop with Unreachable Exit Condition

No memory corruption, code execution, information disclosure, or persistence impact is claimed.

Details

The issue is in the planar HTJ2K decoding path in:

src/lib/OpenEXRCore/internal_ht.cpp

image_height is stored as an unsigned OpenJPH integer:

ojph::ui32 image_height =
    siz.get_image_extent().y - siz.get_image_offset().y;

The planar decoding loop then adds this unsigned value to the signed chunk origin:

for (int64_t y = decode->chunk.start_y;
     y < image_height + decode->chunk.start_y;
     y++)

For the supplied proof-of-concept file:

image_height = 32
decode->chunk.start_y = -64

The intended endpoint is:

-64 + 32 = -32

However, because image_height is unsigned, the usual arithmetic conversions cause the negative start_y value to be converted to an unsigned integer. The endpoint therefore becomes:

4294967264

The loop begins at -64 but attempts to continue until it reaches this wrapped positive endpoint.

The proof-of-concept file uses a channel with ySampling=2, which selects the planar decoder path. OpenEXR then continues calling cs.pull() beyond the component’s declared reconstruction height.

In a release build, the supplied direct C++ reproduction consumed approximately one CPU core until terminated after five seconds. In a debug build, an OpenJPH component-bound assertion was observed after the declared rows were exhausted.

The affected logic can be corrected by calculating the endpoint entirely in a signed 64-bit type:

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)
{
    // Existing decoding logic
}

A regression test should decode an HTJ2K file with all of the following properties:

  • A negative Y data-window origin
  • Vertical channel subsampling
  • Planar HTJ2K decoding
  • A valid, bounded component height

The test should verify that decoding completes and returns the expected number of pixels.

PoC

The attached submission package contains:

poc/neg64-y2.exr
poc/zero-y2.exr
poc/neg64-y1.exr
poc/poc.py
poc/reproduce-cpp.sh
poc/reproduce-python.sh

The trigger file has the following properties:

Filename: poc/neg64-y2.exr
Size: 448 bytes
SHA-256: 9140ba9fc3c1d708f006eb060074aa3d4b40463dd9a337d799d4c237f5a940fe
Storage: Scanline
Compression: HTJ2K32_COMPRESSION
Data window: (0,-64)-(255,-33)
Display window: (0,-64)-(255,-33)
Channel: One HALF Y channel
Channel sampling: 1x2

Direct C++ reproduction

Build OpenEXR 3.4.13 and make its pkg-config files available through PKG_CONFIG_PATH.

From the extracted submission package, run:

cd poc
chmod +x reproduce-cpp.sh
./reproduce-cpp.sh

Expected results:

zero-y2.exr:
Decode completes successfully.

neg64-y1.exr:
Decode completes successfully.

neg64-y2.exr:
The program prints READ_BEGIN but does not print READ_OK before the
five-second timeout.

The supplied direct C++ execution log records approximately:

Elapsed time: 5.00 seconds
User CPU time: 4.98 seconds
CPU utilization: 99%
Exit status: 124

Exit status 124 is produced by GNU timeout after terminating the non-returning decoder process.

Python binding reproduction

On Linux with Python 3, virtual-environment support, and GNU timeout, run:

cd poc
chmod +x reproduce-python.sh
./reproduce-python.sh

The script:

  1. Creates a local Python virtual environment.
  2. Installs exactly OpenEXR==3.4.13.
  3. Verifies the SHA-256 of the trigger file.
  4. Decodes the two control files.
  5. Attempts to decode the candidate file with a five-second timeout.

Expected result:

zero-y2.exr:
Decode completes.

neg64-y1.exr:
Decode completes.

neg64-y2.exr:
Decode does not return and is terminated after five seconds.

The Python reproduction confirms the non-returning behavior through OpenEXR’s public bindings. The direct C++ reproduction provides the stronger evidence that the process is actively consuming CPU rather than waiting on an external resource.

Impact

This is a denial-of-service vulnerability in OpenEXR’s HTJ2K decoder.

Any application that decodes attacker-controlled or otherwise untrusted EXR files may be affected, including:

  • Image viewers and editors
  • Media-processing pipelines
  • Thumbnail or preview generators
  • Asset-ingestion services
  • Render-management systems
  • Conversion services
  • Automated content-analysis systems

A 448-byte EXR file is sufficient to occupy a decoding thread or worker until the operation is externally terminated.

Applications that decode files synchronously may become unresponsive. Services with limited worker pools may experience worker exhaustion if multiple malicious files are processed concurrently. Batch-processing systems may stall indefinitely on the affected file unless they enforce per-file execution limits.

The practical severity depends on the surrounding application:

  • Applications without decode timeouts or worker isolation are most exposed.
  • Applications that process files automatically may require no additional user interaction after upload or ingestion.
  • Applications using isolated workers with strict execution deadlines may limit the effect to termination and restart of an individual worker.

OpenEXR itself does not establish whether the file arrives through a local or network interface, so network reachability depends on the integrating application.

This report claims availability impact only. No evidence of memory corruption, arbitrary code execution, information disclosure, or persistent modification was observed. openexr-htj2k-negative-origin-dos.zip

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