Skip to content

PSD RawColor indexed image out-of-bounds read in `interleave_row`

Moderate
lgritz published GHSA-x877-h4xx-5m5j Aug 1, 2026

Package

OpenImageIO

Affected versions

v3.1.14.1

Patched versions

>= v3.1.16.0

Description

OpenImageIO PSD RawColor indexed image out-of-bounds read in interleave_row

Summary

A crafted indexed PSD with transparency metadata triggers a channel-count mismatch when decoded with oiio:RawColor=1 or psd:RawData=1. PSDInput::read_native_scanline() allocates channel_buffers from the stored PSD channel count, but the raw decode branch passes spec.nchannels to interleave_row(). When spec.nchannels is larger than the stored channel count, interleave_row() indexes past channel_buffers.

Affected versions tested

  • OpenImageIO v3.1.14.1, commit 559efc6f3cbddfec25c7bcca177c9f0c656f6da8: non-sanitized oiiotool crashes with SIGSEGV.
  • OpenImageIO main, commit 5beff968edaf66b7730d1f960f98e7cc0f290cfa after fetching origin/main on July 1, 2026: non-sanitized oiiotool aborts with span.h:221 Contract assertion failed: idx < m_size.

Preconditions and impact

  • Attacker-controlled input: PSD file.
  • Required decode mode: raw PSD color output via oiio:RawColor=1 or psd:RawData=1.
  • Impact: availability loss from process crash/abort. ASan also confirms a heap out-of-bounds read in the vulnerable channel-buffer access.

PoC

  • File: poc.psd
  • Archive: poc.zip

poc.zip

Run from the OpenImageIO test root:

report/02.\ psd-rawcolor-indexed-channel-oob/reproduce.sh

Minimal command:

LD_LIBRARY_PATH=install/orig/lib timeout 20s \
  install/orig/bin/oiiotool --iconfig oiio:RawColor 1 \
  -i report/02.\ psd-rawcolor-indexed-channel-oob/poc.psd \
  -o artifacts/repro-psd-orig.tif

Verification results

Build Result
orig (v3.1.14.1, non-sanitized) SIGSEGV, rc=139.
asan (v3.1.14.1) AddressSanitizer: heap-buffer-overflow at PSDInput::interleave_row<unsigned char>(), psdinput.cpp:2150.
ubsan (v3.1.14.1) SEGV at PSDInput::interleave_row<unsigned char>(), psdinput.cpp:2152.
main-orig (main, non-sanitized) span.h:221 Contract assertion failed: idx < m_size, rc=134.
main-orig-highrisk (main, non-sanitized) Same contract assertion, rc=134.
orig without RawColor Control run succeeds, rc=0.

Root cause

In src/psd.imageio/psdinput.cpp:

  1. read_native_scanline() creates channel_buffers with m_channels[subimage].size() entries.
  2. The raw decode branch calls interleave_row(..., spec.nchannels).
  3. interleave_row() loops over c < nchans and reads channel_buffers[c].
  4. For the PoC, spec.nchannels is larger than the number of stored channel buffers, so channel_buffers[c] is out of bounds.

Relevant lines in both tested trees: psdinput.cpp:787-817 and psdinput.cpp:2145-2152.

Suggested fix

  • Keep the invariant m_channels[subimage].size() >= spec.nchannels before raw interleaving, or use the stored channel count for raw interleave and keep the advertised raw channel count consistent with available channel buffers.
  • For indexed PSD plus transparency metadata, do not advertise a raw output channel that has no backing PSD channel buffer.
  • Add a regression test for raw PSD decode of this PoC; it should either decode safely or fail cleanly without out-of-bounds access.

ASan log

== asan ==
=================================================================
==933079==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x5030004f0f18 at pc 0x754b551d20d9 bp 0x7ffdc052e860 sp 0x7ffdc052e858
READ of size 8 at 0x5030004f0f18 thread T0
    #0 0x754b551d20d8 in std::vector<unsigned char, std::allocator<unsigned char>>::data() const /usr/lib/gcc/x86_64-linux-gnu/13/../../../../include/c++/13/bits/stl_vector.h:1266:42
    #1 0x754b5533d357 in void OpenImageIO::v3_1::PSDInput::interleave_row<unsigned char>(unsigned char*, OpenImageIO::v3_1::span<std::vector<unsigned char, std::allocator<unsigned char>> const, 18446744073709551615ul>, int, int) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/psd.imageio/psdinput.cpp:2150:71
    #2 0x754b55336e24 in OpenImageIO::v3_1::PSDInput::read_native_scanline(int, int, int, int, void*) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/psd.imageio/psdinput.cpp:816:13
    #3 0x754b54bee901 in OpenImageIO::v3_1::ImageInput::read_native_scanlines(int, int, int, int, int, void*) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/libOpenImageIO/imageinput.cpp:491:19
    #4 0x754b54bef5d9 in OpenImageIO::v3_1::ImageInput::read_native_scanlines(int, int, int, int, int, int, int, void*) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/libOpenImageIO/imageinput.cpp:512:16
    #5 0x754b54bec819 in OpenImageIO::v3_1::ImageInput::read_native_scanlines(int, int, int, int, int, int, OpenImageIO::v3_1::span<std::byte, 18446744073709551615ul>) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/libOpenImageIO/imageinput.cpp:590:12
    #6 0x754b54be246f in OpenImageIO::v3_1::ImageInput::read_scanlines(int, int, int, int, int, int, int, OpenImageIO::v3_1::TypeDesc, void*, long, long) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/libOpenImageIO/imageinput.cpp:428:15
    #7 0x754b54beb3e3 in OpenImageIO::v3_1::ImageInput::read_image(int, int, int, int, OpenImageIO::v3_1::TypeDesc, void*, long, long, long, bool (*)(void*, float), void*) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/libOpenImageIO/imageinput.cpp:1168:23
    #8 0x754b54a298ed in OpenImageIO::v3_1::ImageBufImpl::read(int, int, int, int, bool, OpenImageIO::v3_1::TypeDesc, bool (*)(void*, float), void*, OpenImageIO::v3_1::DoLock) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/libOpenImageIO/imagebuf.cpp:1462:27
    #9 0x754b54ae34f4 in OpenImageIO::v3_1::ImageBuf::read(int, int, int, int, bool, OpenImageIO::v3_1::TypeDesc, bool (*)(void*, float), void*) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/libOpenImageIO/imagebuf.cpp:1537:20
    #10 0x65332254abd1 in OpenImageIO::v3_1::OiioTool::ImageRec::read(OpenImageIO::v3_1::OiioTool::ReadPolicy, OpenImageIO::v3_1::basic_string_view<char, std::char_traits<char>>) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/oiiotool/imagerec.cpp:356:27
    #11 0x653322569008 in OpenImageIO::v3_1::OiioTool::Oiiotool::read(std::shared_ptr<OpenImageIO::v3_1::OiioTool::ImageRec>, OpenImageIO::v3_1::OiioTool::ReadPolicy, OpenImageIO::v3_1::basic_string_view<char, std::char_traits<char>>) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/oiiotool/oiiotool.cpp:430:20
    #12 0x6533225b8042 in OpenImageIO::v3_1::OiioTool::Oiiotool::read(OpenImageIO::v3_1::OiioTool::ReadPolicy, OpenImageIO::v3_1::basic_string_view<char, std::char_traits<char>>) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/oiiotool/oiiotool.h:212:20
    #13 0x6533228558da in output_file(OpenImageIO::v3_1::OiioTool::Oiiotool&, OpenImageIO::v3_1::span<char const* const, 18446744073709551615ul>) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/oiiotool/oiiotool.cpp:5903:13
    #14 0x6533228ea90d in std::function<void (OpenImageIO::v3_1::span<char const* const, 18446744073709551615ul>)>::operator()(OpenImageIO::v3_1::span<char const* const, 18446744073709551615ul>) const /usr/lib/gcc/x86_64-linux-gnu/13/../../../../include/c++/13/bits/std_function.h:591:9
    #15 0x754b529883b1 in std::function<void (OpenImageIO::v3_1::ArgParse::Arg&, OpenImageIO::v3_1::span<char const* const, 18446744073709551615ul>)>::operator()(OpenImageIO::v3_1::ArgParse::Arg&, OpenImageIO::v3_1::span<char const* const, 18446744073709551615ul>) const /usr/lib/gcc/x86_64-linux-gnu/13/../../../../include/c++/13/bits/std_function.h:591:9
    #16 0x754b528d97b6 in OpenImageIO::v3_1::ArgParse::Impl::parse_args(int, char const**) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/libutil/argparse.cpp:537:25
    #17 0x754b528d6bb7 in OpenImageIO::v3_1::ArgParse::parse_args(int, char const**) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/libutil/argparse.cpp:431:21
    #18 0x6533225da351 in OpenImageIO::v3_1::OiioTool::Oiiotool::getargs(int, char**) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/oiiotool/oiiotool.cpp:7478:12
    #19 0x6533228ee125 in main /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/oiiotool/oiiotool.cpp:7873:12
    #20 0x754b5202a1c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #21 0x754b5202a28a in __libc_start_main csu/../csu/libc-start.c:360:3
    #22 0x653322350f44 in _start (/home/br0nzu/OpenImageIO/v3.1.14.1/install/asan/bin/oiiotool+0x186f44) (BuildId: 9dec75854dc7a091)

0x5030004f0f18 is located 0 bytes after 24-byte region [0x5030004f0f00,0x5030004f0f18)
allocated by thread T0 here:
    #0 0x65332242a431 in operator new(unsigned long) (/home/br0nzu/OpenImageIO/v3.1.14.1/install/asan/bin/oiiotool+0x260431) (BuildId: 9dec75854dc7a091)
    #1 0x754b553501b6 in std::vector<std::vector<unsigned char, std::allocator<unsigned char>>, std::allocator<std::vector<unsigned char, std::allocator<unsigned char>>>>::_M_default_append(unsigned long) /usr/lib/gcc/x86_64-linux-gnu/13/../../../../include/c++/13/bits/vector.tcc:663:34
    #2 0x754b55335d07 in OpenImageIO::v3_1::PSDInput::read_native_scanline(int, int, int, int, void*) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/psd.imageio/psdinput.cpp:789:21
    #3 0x754b54bee901 in OpenImageIO::v3_1::ImageInput::read_native_scanlines(int, int, int, int, int, void*) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/libOpenImageIO/imageinput.cpp:491:19
    #4 0x754b54bef5d9 in OpenImageIO::v3_1::ImageInput::read_native_scanlines(int, int, int, int, int, int, int, void*) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/libOpenImageIO/imageinput.cpp:512:16
    #5 0x754b54bec819 in OpenImageIO::v3_1::ImageInput::read_native_scanlines(int, int, int, int, int, int, OpenImageIO::v3_1::span<std::byte, 18446744073709551615ul>) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/libOpenImageIO/imageinput.cpp:590:12
    #6 0x754b54be246f in OpenImageIO::v3_1::ImageInput::read_scanlines(int, int, int, int, int, int, int, OpenImageIO::v3_1::TypeDesc, void*, long, long) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/libOpenImageIO/imageinput.cpp:428:15
    #7 0x754b54beb3e3 in OpenImageIO::v3_1::ImageInput::read_image(int, int, int, int, OpenImageIO::v3_1::TypeDesc, void*, long, long, long, bool (*)(void*, float), void*) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/libOpenImageIO/imageinput.cpp:1168:23
    #8 0x754b54a298ed in OpenImageIO::v3_1::ImageBufImpl::read(int, int, int, int, bool, OpenImageIO::v3_1::TypeDesc, bool (*)(void*, float), void*, OpenImageIO::v3_1::DoLock) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/libOpenImageIO/imagebuf.cpp:1462:27
    #9 0x754b54ae34f4 in OpenImageIO::v3_1::ImageBuf::read(int, int, int, int, bool, OpenImageIO::v3_1::TypeDesc, bool (*)(void*, float), void*) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/libOpenImageIO/imagebuf.cpp:1537:20
    #10 0x65332254abd1 in OpenImageIO::v3_1::OiioTool::ImageRec::read(OpenImageIO::v3_1::OiioTool::ReadPolicy, OpenImageIO::v3_1::basic_string_view<char, std::char_traits<char>>) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/oiiotool/imagerec.cpp:356:27
    #11 0x653322569008 in OpenImageIO::v3_1::OiioTool::Oiiotool::read(std::shared_ptr<OpenImageIO::v3_1::OiioTool::ImageRec>, OpenImageIO::v3_1::OiioTool::ReadPolicy, OpenImageIO::v3_1::basic_string_view<char, std::char_traits<char>>) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/oiiotool/oiiotool.cpp:430:20
    #12 0x6533225b8042 in OpenImageIO::v3_1::OiioTool::Oiiotool::read(OpenImageIO::v3_1::OiioTool::ReadPolicy, OpenImageIO::v3_1::basic_string_view<char, std::char_traits<char>>) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/oiiotool/oiiotool.h:212:20
    #13 0x6533228558da in output_file(OpenImageIO::v3_1::OiioTool::Oiiotool&, OpenImageIO::v3_1::span<char const* const, 18446744073709551615ul>) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/oiiotool/oiiotool.cpp:5903:13
    #14 0x6533228ea90d in std::function<void (OpenImageIO::v3_1::span<char const* const, 18446744073709551615ul>)>::operator()(OpenImageIO::v3_1::span<char const* const, 18446744073709551615ul>) const /usr/lib/gcc/x86_64-linux-gnu/13/../../../../include/c++/13/bits/std_function.h:591:9
    #15 0x754b529883b1 in std::function<void (OpenImageIO::v3_1::ArgParse::Arg&, OpenImageIO::v3_1::span<char const* const, 18446744073709551615ul>)>::operator()(OpenImageIO::v3_1::ArgParse::Arg&, OpenImageIO::v3_1::span<char const* const, 18446744073709551615ul>) const /usr/lib/gcc/x86_64-linux-gnu/13/../../../../include/c++/13/bits/std_function.h:591:9
    #16 0x754b528d97b6 in OpenImageIO::v3_1::ArgParse::Impl::parse_args(int, char const**) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/libutil/argparse.cpp:537:25
    #17 0x754b528d6bb7 in OpenImageIO::v3_1::ArgParse::parse_args(int, char const**) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/libutil/argparse.cpp:431:21
    #18 0x6533225da351 in OpenImageIO::v3_1::OiioTool::Oiiotool::getargs(int, char**) /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/oiiotool/oiiotool.cpp:7478:12
    #19 0x6533228ee125 in main /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/oiiotool/oiiotool.cpp:7873:12
    #20 0x754b5202a1c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #21 0x754b5202a28a in __libc_start_main csu/../csu/libc-start.c:360:3
    #22 0x653322350f44 in _start (/home/br0nzu/OpenImageIO/v3.1.14.1/install/asan/bin/oiiotool+0x186f44) (BuildId: 9dec75854dc7a091)

SUMMARY: AddressSanitizer: heap-buffer-overflow /home/br0nzu/OpenImageIO/v3.1.14.1/src/OpenImageIO-v3.1.14.1/src/psd.imageio/psdinput.cpp:2150:71 in void OpenImageIO::v3_1::PSDInput::interleave_row<unsigned char>(unsigned char*, OpenImageIO::v3_1::span<std::vector<unsigned char, std::allocator<unsigned char>> const, 18446744073709551615ul>, int, int)
Shadow bytes around the buggy address:
  0x5030004f0c80: fa fa fd fd fd fd fa fa fd fd fd fa fa fa fd fd
  0x5030004f0d00: fd fa fa fa fd fd fd fa fa fa fd fd fd fa fa fa
  0x5030004f0d80: fd fd fd fa fa fa fd fd fd fa fa fa fd fd fd fd
  0x5030004f0e00: fa fa fd fd fd fd fa fa fd fd fd fa fa fa 00 00
  0x5030004f0e80: 00 fa fa fa 00 00 00 00 fa fa fd fd fd fd fa fa
=>0x5030004f0f00: 00 00 00[fa]fa fa fa fa fa fa fa fa fa fa fa fa
  0x5030004f0f80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x5030004f1000: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x5030004f1080: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x5030004f1100: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x5030004f1180: 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
==933079==ABORTING
timeout: the monitored command dumped core
02. psd-rawcolor-indexed-channel-oob/reproduce.sh: line 5: 933076 Aborted                 env LD_LIBRARY_PATH="$lib:${LD_LIBRARY_PATH:-}" "$@" timeout 20s "$bin" --iconfig oiio:RawColor 1 -i "$POC" -o "$ROOT/artifacts/repro-psd-$label.tif"
rc=134

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Local
Attack complexity
Low
Privileges required
None
User interaction
Required
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H

CVE ID

CVE-2026-63420

Weaknesses

Out-of-bounds Read

The product reads data past the end, or before the beginning, of the intended buffer. Learn more on MITRE.

Improper Validation of Array Index

The product uses untrusted input when calculating or using an array index, but the product does not validate or incorrectly validates the index to ensure the index references a valid position within the array. Learn more on MITRE.

Credits