Skip to content

GHSA-p42q-g5c9-mh9w on CTRL-OS 26.05

Aliases: GHSA-p42q-g5c9-mh9w

Packages: openexr

Status: Plausible

Advisory Information

Summary

OpenEXR v3.4.13 has an ILP32-only input-side out-of-bounds read in deep-tiled sample-count decoding. A malformed deeptile file can combine a huge valid tile area with a 4-byte sample-count table so that OpenEXRCore computes the sample-count workspace size in 32-bit size_t and wraps. The public C decode path crashes an unsanitized 32-bit binary with SIGSEGV, ASAN confirms a READ crash in OpenEXRCore/decoding.c, and the public C++ DeepTiledInputFile::readPixelSampleCounts() path produces an ASAN heap-buffer-overflow READ.

Details

The malformed file is a deeptile image with NO_COMPRESSION, one FLOAT channel, data/tile size 268435456 x 4, and a deep chunk whose sample_count_table_size is only 4 bytes.

Relevant source:

  • sources/openexr-3.4.13/src/lib/OpenEXRCore/validation.c:593-595: each tile dimension is rejected only when it exceeds INT_MAX / 4; 268435456 x 4 passes.
  • sources/openexr-3.4.13/src/lib/OpenEXRCore/chunk.c:1368-1371: the deep-tile sample-count table is checked only for non-negative/multiple-of-4 shape under NO_COMPRESSION, not against tile_width * tile_height * sizeof(int32_t).
  • sources/openexr-3.4.13/src/lib/OpenEXRCore/chunk.c:1415-1418: the attacker-controlled table size is stored in cinfo->sample_count_table_size.
  • sources/openexr-3.4.13/src/lib/OpenEXRCore/decoding.c:29-36: sampsize is computed as (size_t)width * (size_t)height [ + 1 ] * 4. On ILP32, 268435456 * 4 * 4 wraps to zero, and (268435456 * 4 + 1) * 4 wraps to four.
  • sources/openexr-3.4.13/src/lib/OpenEXRCore/decoding.c:227-267: unpack_sample_table() iterates over the full logical w*h sample table.
  • sources/openexr-3.4.13/src/lib/OpenEXR/ImfDeepTiledInputFile.cpp:690 and :968-1063: DeepTiledInputFile::readPixelSampleCounts() reaches exr_decoding_run() with EXR_DECODE_SAMPLE_COUNTS_AS_INDIVIDUAL.

Two decode modes show the issue:

  1. Core C API sample-data-only decode without EXR_DECODE_SAMPLE_COUNTS_AS_INDIVIDUAL: sampsize wraps to 0, sample_count_table is null/zero-sized, and the original ILP32 binary crashes with SIGSEGV. ASAN reports a READ crash at decoding.c:256 in unpack_sample_table().
  2. C++ DeepTiledInputFile::readPixelSampleCounts(): the individual-count flag makes wrapped sampsize == 4, aliases the 4-byte packed sample-count table, and ASAN reports a heap-buffer-overflow READ at decoding.c:236 through DeepTiledInputFile::readPixelSampleCounts().

PoC

Package contents

poc.zip contains only the files needed to rebuild, run, and verify the standalone malformed EXR PoC.

poc.zip

reproduce.sh
repro/build_repro.sh
repro/run_repro.sh
poc_artifacts/poc_ilp32_deeptiled_input_sampletable.exr
poc_artifacts/repro_ilp32_deeptiled_input_sampletable_file.cpp
poc_artifacts/repro_ilp32_deeptiled_input_sampletable_cpp.cpp
logs/original_core.stdout
logs/original_core.stderr
logs/original_core.rc
logs/asan_core.stdout
logs/asan_core.stderr
logs/asan_core.rc
logs/ubsan_core.stdout
logs/ubsan_core.stderr
logs/ubsan_core.rc
logs/asan_cpp.stdout
logs/asan_cpp.stderr
logs/asan_cpp.rc

Reproduction

Run from the OpenEXR v3.4.13 repository root after the existing ILP32 original/ASAN/UBSAN builds are present:

mkdir -p artifacts/tmp/report20-poc
unzip -o 'report/20. OpenEXR v3.4.13 ILP32 DeepTiledInputFile sample count table decode OOB read/poc.zip' -d artifacts/tmp/report20-poc

artifacts/tmp/report20-poc/repro/build_repro.sh original
artifacts/tmp/report20-poc/repro/build_repro.sh asan
artifacts/tmp/report20-poc/repro/build_repro.sh ubsan

artifacts/tmp/report20-poc/repro/run_repro.sh original core
artifacts/tmp/report20-poc/repro/run_repro.sh asan core
artifacts/tmp/report20-poc/repro/run_repro.sh ubsan core
artifacts/tmp/report20-poc/repro/run_repro.sh asan cpp

A top-level helper runs the same sequence:

./reproduce.sh

Expected results:

original core -> rc=139, SIGSEGV while decoding the malformed deep-tiled EXR
ASAN core    -> rc=134, READ crash at decoding.c:256 in unpack_sample_table()
UBSAN core   -> rc=134, null pointer passed to memcpy at decoding.c:630
ASAN C++     -> rc=134, heap-buffer-overflow READ via DeepTiledInputFile::readPixelSampleCounts()

Full original core-path output

logs/original_core.rc:

139

logs/original_core.stderr:

timeout: the monitored command dumped core

logs/original_core.stdout is empty:


Full ASAN core-path output

logs/asan_core.rc:

134

logs/asan_core.stderr:

AddressSanitizer:DEADLYSIGNAL
=================================================================
==3705280==ERROR: AddressSanitizer: SEGV on unknown address 0x00000000 (pc 0x567af6c0 bp 0xffae3778 sp 0xffae3720 T0)
==3705280==The signal is caused by a READ memory access.
==3705280==Hint: address points to the zero page.
    #0 0x567af6c0 in unpack_sample_table /home/br0nzu/openexr/v3.4.13/sources/openexr-3.4.13/src/lib/OpenEXRCore/decoding.c:256:59
    #1 0x567aea67 in exr_decoding_run /home/br0nzu/openexr/v3.4.13/sources/openexr-3.4.13/src/lib/OpenEXRCore/decoding.c:644:14
    #2 0x5672babf in main /home/br0nzu/openexr/v3.4.13/report/20. OpenEXR v3.4.13 ILP32 DeepTiledInputFile sample count table decode OOB read/poc_artifacts/repro_ilp32_deeptiled_input_sampletable_file.cpp:20:21
    #3 0xf7914cb8  (/lib/i386-linux-gnu/libc.so.6+0x24cb8) (BuildId: 0f6a23454651f08cb9b65ff6544d658177b59947)
    #4 0xf7914d7b in __libc_start_main (/lib/i386-linux-gnu/libc.so.6+0x24d7b) (BuildId: 0f6a23454651f08cb9b65ff6544d658177b59947)
    #5 0x5664f2c6 in _start (/home/br0nzu/openexr/v3.4.13/report/20. OpenEXR v3.4.13 ILP32 DeepTiledInputFile sample count table decode OOB read/repro/bin/repro_ilp32_deeptiled_input_sampletable_file_asan+0x252c6) (BuildId: 10ea841b5f1ec604d2b13be1a2620b4e0e9a8e4c)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /home/br0nzu/openexr/v3.4.13/sources/openexr-3.4.13/src/lib/OpenEXRCore/decoding.c:256:59 in unpack_sample_table
==3705280==ABORTING
timeout: the monitored command dumped core

logs/asan_core.stdout is empty:


Full UBSAN core-path output

logs/ubsan_core.rc:

134

logs/ubsan_core.stderr:

/home/br0nzu/openexr/v3.4.13/sources/openexr-3.4.13/src/lib/OpenEXRCore/decoding.c:630:25: runtime error: null pointer passed as argument 1, which is declared to never be null
/usr/include/string.h:44:28: note: nonnull attribute specified here
    #0 0x566bc006 in exr_decoding_run /home/br0nzu/openexr/v3.4.13/sources/openexr-3.4.13/src/lib/OpenEXRCore/decoding.c:629:21
    #1 0x5666edfb in main /home/br0nzu/openexr/v3.4.13/report/20. OpenEXR v3.4.13 ILP32 DeepTiledInputFile sample count table decode OOB read/poc_artifacts/repro_ilp32_deeptiled_input_sampletable_file.cpp:20:21
    #2 0xf7963cb8  (/lib/i386-linux-gnu/libc.so.6+0x24cb8) (BuildId: 0f6a23454651f08cb9b65ff6544d658177b59947)
    #3 0xf7963d7b in __libc_start_main (/lib/i386-linux-gnu/libc.so.6+0x24d7b) (BuildId: 0f6a23454651f08cb9b65ff6544d658177b59947)
    #4 0x5663ee66 in _start (/home/br0nzu/openexr/v3.4.13/report/20. OpenEXR v3.4.13 ILP32 DeepTiledInputFile sample count table decode OOB read/repro/bin/repro_ilp32_deeptiled_input_sampletable_file_ubsan+0x3ce66) (BuildId: 85e97162032e138ff7e9ea276a516d523ddfce40)

SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /home/br0nzu/openexr/v3.4.13/sources/openexr-3.4.13/src/lib/OpenEXRCore/decoding.c:630:25 
timeout: the monitored command dumped core

logs/ubsan_core.stdout is empty:


Full ASAN C++ DeepTiledInputFile output

logs/asan_cpp.rc:

134

logs/asan_cpp.stderr:

reading /home/br0nzu/openexr/v3.4.13/report/20. OpenEXR v3.4.13 ILP32 DeepTiledInputFile sample count table decode OOB read/poc_artifacts/poc_ilp32_deeptiled_input_sampletable.exr via DeepTiledInputFile::readPixelSampleCounts()
=================================================================
==3705309==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xf5a00634 at pc 0x56964939 bp 0xffd93c78 sp 0xffd93c70
READ of size 4 at 0xf5a00634 thread T0
    #0 0x56964938 in unpack_sample_table /home/br0nzu/openexr/v3.4.13/sources/openexr-3.4.13/src/lib/OpenEXRCore/decoding.c:236:59
    #1 0x56963857 in exr_decoding_run /home/br0nzu/openexr/v3.4.13/sources/openexr-3.4.13/src/lib/OpenEXRCore/decoding.c:644:14
    #2 0x567c9ef6 in Imf_3_4::(anonymous namespace)::TileProcess::run_decode(_priv_exr_context_t const*, int, Imf_3_4::DeepFrameBuffer const*, std::vector<Imf_3_4::DeepSlice, std::allocator<Imf_3_4::DeepSlice>> const&) /home/br0nzu/openexr/v3.4.13/sources/openexr-3.4.13/src/lib/OpenEXR/ImfDeepTiledInputFile.cpp:1063:23
    #3 0x567c20cc in Imf_3_4::DeepTiledInputFile::Data::readTiles(int, int, int, int, int, int, bool) /home/br0nzu/openexr/v3.4.13/sources/openexr-3.4.13/src/lib/OpenEXR/ImfDeepTiledInputFile.cpp:968:20
    #4 0x567c6c8c in Imf_3_4::DeepTiledInputFile::readPixelSampleCounts(int, int, int, int, int, int) /home/br0nzu/openexr/v3.4.13/sources/openexr-3.4.13/src/lib/OpenEXR/ImfDeepTiledInputFile.cpp:690:16
    #5 0x567ba85c in main /home/br0nzu/openexr/v3.4.13/report/20. OpenEXR v3.4.13 ILP32 DeepTiledInputFile sample count table decode OOB read/poc_artifacts/repro_ilp32_deeptiled_input_sampletable_cpp.cpp:35:14
    #6 0xf7954cb8  (/lib/i386-linux-gnu/libc.so.6+0x24cb8) (BuildId: 0f6a23454651f08cb9b65ff6544d658177b59947)
    #7 0xf7954d7b in __libc_start_main (/lib/i386-linux-gnu/libc.so.6+0x24d7b) (BuildId: 0f6a23454651f08cb9b65ff6544d658177b59947)
    #8 0x566de1a6 in _start (/home/br0nzu/openexr/v3.4.13/report/20. OpenEXR v3.4.13 ILP32 DeepTiledInputFile sample count table decode OOB read/repro/bin/repro_ilp32_deeptiled_input_sampletable_cpp_asan+0x991a6) (BuildId: c12e906fe46d4329455538140a8f858ec1c7346b)

0xf5a00634 is located 0 bytes after 4-byte region [0xf5a00630,0xf5a00634)
allocated by thread T0 here:
    #0 0x567759d7 in malloc (/home/br0nzu/openexr/v3.4.13/report/20. OpenEXR v3.4.13 ILP32 DeepTiledInputFile sample count table decode OOB read/repro/bin/repro_ilp32_deeptiled_input_sampletable_cpp_asan+0x1309d7) (BuildId: c12e906fe46d4329455538140a8f858ec1c7346b)
    #1 0x569c878c in internal_exr_alloc /home/br0nzu/openexr/v3.4.13/sources/openexr-3.4.13/src/lib/OpenEXRCore/memory.c:39:12
    #2 0x569ca44e in internal_decode_alloc_buffer /home/br0nzu/openexr/v3.4.13/sources/openexr-3.4.13/src/lib/OpenEXRCore/coding.c:256:22
    #3 0x56962e91 in default_read_chunk /home/br0nzu/openexr/v3.4.13/sources/openexr-3.4.13/src/lib/OpenEXRCore/decoding.c:161:14
    #4 0x569636d7 in exr_decoding_run /home/br0nzu/openexr/v3.4.13/sources/openexr-3.4.13/src/lib/OpenEXRCore/decoding.c:567:10
    #5 0x567c9ef6 in Imf_3_4::(anonymous namespace)::TileProcess::run_decode(_priv_exr_context_t const*, int, Imf_3_4::DeepFrameBuffer const*, std::vector<Imf_3_4::DeepSlice, std::allocator<Imf_3_4::DeepSlice>> const&) /home/br0nzu/openexr/v3.4.13/sources/openexr-3.4.13/src/lib/OpenEXR/ImfDeepTiledInputFile.cpp:1063:23
    #6 0x567c20cc in Imf_3_4::DeepTiledInputFile::Data::readTiles(int, int, int, int, int, int, bool) /home/br0nzu/openexr/v3.4.13/sources/openexr-3.4.13/src/lib/OpenEXR/ImfDeepTiledInputFile.cpp:968:20
    #7 0x567c6c8c in Imf_3_4::DeepTiledInputFile::readPixelSampleCounts(int, int, int, int, int, int) /home/br0nzu/openexr/v3.4.13/sources/openexr-3.4.13/src/lib/OpenEXR/ImfDeepTiledInputFile.cpp:690:16
    #8 0x567ba85c in main /home/br0nzu/openexr/v3.4.13/report/20. OpenEXR v3.4.13 ILP32 DeepTiledInputFile sample count table decode OOB read/poc_artifacts/repro_ilp32_deeptiled_input_sampletable_cpp.cpp:35:14
    #9 0xf7954cb8  (/lib/i386-linux-gnu/libc.so.6+0x24cb8) (BuildId: 0f6a23454651f08cb9b65ff6544d658177b59947)

SUMMARY: AddressSanitizer: heap-buffer-overflow /home/br0nzu/openexr/v3.4.13/sources/openexr-3.4.13/src/lib/OpenEXRCore/decoding.c:236:59 in unpack_sample_table
Shadow bytes around the buggy address:
  0xf5a00380: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0xf5a00400: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0xf5a00480: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0xf5a00500: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0xf5a00580: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0xf5a00600: fa fa fa fa fa fa[04]fa fa fa 00 fa fa fa 00 fa
  0xf5a00680: fa fa 00 fa fa fa 00 fa fa fa 00 04 fa fa 00 fa
  0xf5a00700: fa fa 00 00 fa fa 02 fa fa fa fd fd fa fa 00 00
  0xf5a00780: fa fa 04 fa fa fa 00 00 fa fa fa fa fa fa fa fa
  0xf5a00800: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0xf5a00880: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==3705309==ABORTING
timeout: the monitored command dumped core

logs/asan_cpp.stdout is empty:


Impact

Applications that build OpenEXR v3.4.13 for 32-bit ILP32 targets and decode attacker-controlled deep-tiled EXR files can be crashed by a small malformed file. The demonstrated impact is denial of service via SIGSEGV in an unsanitized original binary, plus sanitizer-confirmed out-of-bounds read/null-pointer access in the OpenEXRCore deep sample-count decode path. Arbitrary code execution and out-of-bounds write were not proven. LP64 builds are not demonstrated because the vulnerable arithmetic does not truncate to 32-bit size_t there.

Updates

2026-08-11 03:50 CEST

Metadata changes:

  • Status for package openexr: “Plausible

2026-08-11 03:32 CEST

Metadata changes:

  • Status for package openexr: “New