Skip to content

Commit d3aa165

Browse files
authored
fix(psd): fixes against corrupt files with better validation (#5089)
1. Validate channel count against color mode to prevent heap buffer overflow. 2. Corrupted PSD files can declare a color mode (e.g. RGB, needing 3 channels) with transparency (needing +1) but report fewer channels in the header. This caused an out-of-bounds read in read_native_scanline when setup() built channel pointer arrays using mode_channel_count, which exceeded the actual m_image_data.channel_info size. 3. Additional buffer overflow protection, error checking and propagation. 4. Avoid leak by properly freeing z_stream resources in decompress_zip. Used Claude Code / Opus 4.6 to help narrow down and confirm the bugs 1 and 2. Assisted-by: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Larry Gritz <lg@larrygritz.com>
1 parent 7d16832 commit d3aa165

6 files changed

Lines changed: 109 additions & 41 deletions

File tree

src/psd.imageio/psdinput.cpp

Lines changed: 58 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ class PSDInput final : public ImageInput {
294294

295295
void set_type_desc();
296296
//Setup m_specs and m_channels
297-
void setup();
297+
bool setup();
298298
void fill_channel_names(ImageSpec& spec, bool transparency);
299299

300300
//Read a row of channel data
@@ -619,9 +619,11 @@ PSDInput::open(const std::string& name, ImageSpec& newspec)
619619
// Set m_type_desc to the appropriate TypeDesc
620620
set_type_desc();
621621
// Setup ImageSpecs and m_channels
622-
setup();
622+
bool ok = true;
623+
ok &= setup();
623624

624-
bool ok = seek_subimage(0, 0);
625+
if (ok)
626+
ok &= seek_subimage(0, 0);
625627
if (ok)
626628
newspec = spec();
627629
else
@@ -1468,7 +1470,9 @@ PSDInput::load_layers()
14681470
ok &= read_bige<int16_t>(layer_info.layer_count);
14691471
if (layer_info.layer_count < 0) {
14701472
m_image_data.transparency = true;
1471-
layer_info.layer_count = -layer_info.layer_count;
1473+
if (layer_info.layer_count == -32768)
1474+
return false; // will overflow when negated
1475+
layer_info.layer_count = -layer_info.layer_count;
14721476
}
14731477
m_layers.resize(layer_info.layer_count);
14741478
for (int16_t layer_nbr = 0; layer_nbr < layer_info.layer_count;
@@ -1639,6 +1643,7 @@ PSDInput::load_layer_channel(Layer& layer, ChannelInfo& channel_info)
16391643
channel_info.row_pos.resize(height);
16401644
channel_info.row_length = (width * m_header.depth + 7) / 8;
16411645

1646+
bool ok = true;
16421647
switch (channel_info.compression) {
16431648
case Compression_Raw:
16441649
if (height) {
@@ -1687,7 +1692,7 @@ PSDInput::load_layer_channel(Layer& layer, ChannelInfo& channel_info)
16871692
if (!ioread(compressed_data.data(), channel_info.data_length))
16881693
return false;
16891694

1690-
decompress_zip(compressed_data, channel_info.decompressed_data);
1695+
ok = decompress_zip(compressed_data, channel_info.decompressed_data);
16911696
} break;
16921697
case Compression_ZIP_Predict: {
16931698
// We subtract the compression marker from the data length
@@ -1704,16 +1709,18 @@ PSDInput::load_layer_channel(Layer& layer, ChannelInfo& channel_info)
17041709
if (!ioread(compressed_data.data(), channel_info.data_length))
17051710
return false;
17061711

1707-
decompress_zip_prediction(compressed_data,
1708-
channel_info.decompressed_data, width,
1709-
height);
1712+
ok = decompress_zip_prediction(compressed_data,
1713+
channel_info.decompressed_data, width,
1714+
height);
17101715
} break;
17111716
default:
17121717
errorfmt("[Layer Channel] unsupported compression {}",
17131718
channel_info.compression);
17141719
return false;
17151720
}
1716-
return true;
1721+
if (!ok)
1722+
errorfmt("Error during layer decompression. Possible corrupt file?");
1723+
return ok;
17171724
}
17181725

17191726

@@ -1853,7 +1860,9 @@ PSDInput::load_layers_16_32(uint64_t length)
18531860
ok &= read_bige<int16_t>(layer_info.layer_count);
18541861
if (layer_info.layer_count < 0) {
18551862
m_image_data.transparency = true;
1856-
layer_info.layer_count = -layer_info.layer_count;
1863+
if (layer_info.layer_count == -32768)
1864+
return false; // will overflow when negated
1865+
layer_info.layer_count = -layer_info.layer_count;
18571866
}
18581867
m_layers.resize(layer_info.layer_count);
18591868
for (int16_t layer_nbr = 0; layer_nbr < layer_info.layer_count;
@@ -1894,6 +1903,22 @@ PSDInput::load_image_data()
18941903
compression);
18951904
return false;
18961905
}
1906+
// Validate that the file has enough channels for its color mode.
1907+
// mode_channel_count gives the minimum channels required; if the layer
1908+
// info section indicated transparency, we need one more channel.
1909+
{
1910+
int required = (m_header.color_mode <= ColorMode_Lab)
1911+
? (int)mode_channel_count[m_header.color_mode]
1912+
: 0;
1913+
if (m_image_data.transparency)
1914+
required++;
1915+
if (m_header.channel_count < required) {
1916+
errorfmt(
1917+
"[Image Data Section] channel count {} is too few for color mode {}",
1918+
m_header.channel_count, m_header.color_mode);
1919+
return false;
1920+
}
1921+
}
18971922
m_image_data.channel_info.resize(m_header.channel_count);
18981923
// setup some generic properties and read any RLE lengths
18991924
// Image Data Section has RLE lengths for all channels stored first
@@ -1938,7 +1963,7 @@ PSDInput::load_image_data()
19381963

19391964

19401965

1941-
void
1966+
bool
19421967
PSDInput::setup()
19431968
{
19441969
// raw_channel_count is the number of channels in the file
@@ -2012,6 +2037,8 @@ PSDInput::setup()
20122037
if (layer.name.size())
20132038
spec.attribute("oiio:subimagename", layer.name);
20142039
}
2040+
2041+
return true;
20152042
}
20162043

20172044

@@ -2297,6 +2324,7 @@ bool
22972324
PSDInput::decompress_zip(span<char> src, span<char> dest)
22982325
{
22992326
z_stream stream {};
2327+
stream.zalloc = Z_NULL;
23002328
stream.zfree = Z_NULL;
23012329
stream.opaque = Z_NULL;
23022330
stream.avail_in = src.size();
@@ -2306,26 +2334,29 @@ PSDInput::decompress_zip(span<char> src, span<char> dest)
23062334

23072335
if (inflateInit(&stream) != Z_OK) {
23082336
errorfmt(
2309-
"zip compression inflate init failed with: src_size={}, dst_size={}",
2310-
src.size(), dest.size());
2337+
"zip compression inflate init failed with: src_size={}, dst_size={} {}",
2338+
src.size(), dest.size(), stream.msg ? stream.msg : "");
23112339
return false;
23122340
}
23132341

2342+
bool ok = true;
23142343
if (inflate(&stream, Z_FINISH) != Z_STREAM_END) {
23152344
errorfmt(
2316-
"unable to decode zip compressed data: src_size={}, dst_size={}",
2317-
src.size(), dest.size());
2318-
return false;
2345+
"unable to decode zip compressed data: src_size={}, dst_size={} {}",
2346+
src.size(), dest.size(), stream.msg ? stream.msg : "");
2347+
ok = false;
23192348
}
23202349

2350+
// Note: call inflateEnd even if ok == false, because we need to clean up.
23212351
if (inflateEnd(&stream) != Z_OK) {
2322-
errorfmt(
2323-
"zip compression inflate cleanup failed with: src_size={}, dst_size={}",
2324-
src.size(), dest.size());
2325-
return false;
2352+
if (ok) // message only if this was the first error
2353+
errorfmt(
2354+
"zip compression inflate cleanup failed with: src_size={}, dst_size={} {}",
2355+
src.size(), dest.size(), stream.msg ? stream.msg : "");
2356+
ok = false;
23262357
}
23272358

2328-
return true;
2359+
return ok;
23292360
}
23302361

23312362

@@ -2342,6 +2373,8 @@ PSDInput::decompress_zip_prediction(span<char> src, span<char> dest,
23422373

23432374
switch (m_header.depth) {
23442375
case 8:
2376+
if ((height - 1) * width + (width - 1) >= dest.size())
2377+
return false; // going to exceed the dest bounds
23452378
for (uint64_t y = 0; y < height; ++y) {
23462379
// Index x beginning at one since we look behind to calculate
23472380
// the offset
@@ -2355,6 +2388,8 @@ PSDInput::decompress_zip_prediction(span<char> src, span<char> dest,
23552388
// prediction decoding to work correctly
23562389
span<uint16_t> destView(reinterpret_cast<uint16_t*>(dest.data()),
23572390
dest.size() / 2);
2391+
if ((height - 1) * width + (width - 1) >= destView.size())
2392+
return false; // going to exceed the dest bounds
23582393
if (!bigendian())
23592394
byteswap_span(destView);
23602395

@@ -2373,6 +2408,8 @@ PSDInput::decompress_zip_prediction(span<char> src, span<char> dest,
23732408
for (uint64_t y = 0; y < height; ++y) {
23742409
++index;
23752410
for (uint64_t x = 1; x < (width * sizeof(float)); ++x) {
2411+
if (index >= dest.size())
2412+
return false; // going to exceed the dest bounds
23762413
uint8_t value = dest[index] + dest[index - 1];
23772414
dest[index] = value;
23782415
++index;

testsuite/psd/ref/out-linuxarm.txt

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1467,14 +1467,6 @@ src/layer-mask.psd : 10 x 10, 4 channel, uint8 psd
14671467
stEvt:instanceID: "xmp.iid:a64763c8-be7b-ff48-b857-0f1ee8e5da2b; xmp.iid:9847e4c5-ca7e-fa42-9c7f-5fe6373d31de; xmp.iid:ddf40b95-b12c-744e-a1a9-5e7724fe4ca9"
14681468
stEvt:softwareAgent: "Adobe Photoshop CC 2017 (Windows)"
14691469
stEvt:when: "2017-07-13T10:26:10+09:00; 2017-07-13T10:32:41+09:00; 2017-07-13T11:42:54+09:00"
1470-
oiiotool ERROR: read : Failed to decode Exif data
1471-
failed to open "src/crash-psd-exif-1632.psd": failed load_resources
1472-
Full command line was:
1473-
> oiiotool --info -v -a --hash src/crash-psd-exif-1632.psd
1474-
oiiotool ERROR: read : Corrupt thumbnail: 262304w * 24bpp does not match 480 width bytes
1475-
failed to open "src/crash-thumb-1626.psd": failed load_resources
1476-
Full command line was:
1477-
> oiiotool --info -v -a --hash src/crash-thumb-1626.psd
14781470
Reading src/Layers_8bit_RGB.psd
14791471
src/Layers_8bit_RGB.psd : 48 x 27, 3 channel, uint8 psd
14801472
4 subimages: 48x27 [u8,u8,u8], 48x27 [u8,u8,u8,u8], 48x27 [u8,u8,u8,u8], 48x27 [u8,u8,u8,u8]
@@ -2098,3 +2090,23 @@ src/Layers_32bit_RGB.psd : 48 x 27, 3 channel, float psd
20982090
stEvt:instanceID: "xmp.iid:68fcb000-4377-c148-974d-bdd193ca024d; xmp.iid:cbd904c5-53b4-0d4e-9ff8-37ac35429f46"
20992091
stEvt:softwareAgent: "Adobe Photoshop 23.3 (Windows)"
21002092
stEvt:when: "2024-04-01T19:35:16+02:00"
2093+
oiiotool ERROR: read : Failed to decode Exif data
2094+
failed to open "src/crash-psd-exif-1632.psd": failed load_resources
2095+
Full command line was:
2096+
> oiiotool --info -v -a --hash src/crash-psd-exif-1632.psd
2097+
oiiotool ERROR: read : Corrupt thumbnail: 262304w * 24bpp does not match 480 width bytes
2098+
failed to open "src/crash-thumb-1626.psd": failed load_resources
2099+
Full command line was:
2100+
> oiiotool --info -v -a --hash src/crash-thumb-1626.psd
2101+
oiiotool ERROR: read : failed to open "src/crash-005c.psd": failed load_layers
2102+
Full command line was:
2103+
> oiiotool --info -v -a --hash src/crash-005c.psd
2104+
oiiotool ERROR: read : [Image Data Section] channel count 3 is too few for color mode 3
2105+
failed to open "src/crash-8a15.psd": failed load_image_data
2106+
Full command line was:
2107+
> oiiotool --info -v -a --hash src/crash-8a15.psd
2108+
oiiotool ERROR: read : unable to decode zip compressed data: src_size=39, dst_size=1296
2109+
Error during layer decompression. Possible corrupt file?
2110+
failed to open "../oiio-images/psd/corrupt_20260312a.psd": failed load_global_additional
2111+
Full command line was:
2112+
> oiiotool --info -v -a --hash ../oiio-images/psd/corrupt_20260312a.psd

testsuite/psd/ref/out.txt

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1467,14 +1467,6 @@ src/layer-mask.psd : 10 x 10, 4 channel, uint8 psd
14671467
stEvt:instanceID: "xmp.iid:a64763c8-be7b-ff48-b857-0f1ee8e5da2b; xmp.iid:9847e4c5-ca7e-fa42-9c7f-5fe6373d31de; xmp.iid:ddf40b95-b12c-744e-a1a9-5e7724fe4ca9"
14681468
stEvt:softwareAgent: "Adobe Photoshop CC 2017 (Windows)"
14691469
stEvt:when: "2017-07-13T10:26:10+09:00; 2017-07-13T10:32:41+09:00; 2017-07-13T11:42:54+09:00"
1470-
oiiotool ERROR: read : Failed to decode Exif data
1471-
failed to open "src/crash-psd-exif-1632.psd": failed load_resources
1472-
Full command line was:
1473-
> oiiotool --info -v -a --hash src/crash-psd-exif-1632.psd
1474-
oiiotool ERROR: read : Corrupt thumbnail: 262304w * 24bpp does not match 480 width bytes
1475-
failed to open "src/crash-thumb-1626.psd": failed load_resources
1476-
Full command line was:
1477-
> oiiotool --info -v -a --hash src/crash-thumb-1626.psd
14781470
Reading src/Layers_8bit_RGB.psd
14791471
src/Layers_8bit_RGB.psd : 48 x 27, 3 channel, uint8 psd
14801472
4 subimages: 48x27 [u8,u8,u8], 48x27 [u8,u8,u8,u8], 48x27 [u8,u8,u8,u8], 48x27 [u8,u8,u8,u8]
@@ -2098,3 +2090,23 @@ src/Layers_32bit_RGB.psd : 48 x 27, 3 channel, float psd
20982090
stEvt:instanceID: "xmp.iid:68fcb000-4377-c148-974d-bdd193ca024d; xmp.iid:cbd904c5-53b4-0d4e-9ff8-37ac35429f46"
20992091
stEvt:softwareAgent: "Adobe Photoshop 23.3 (Windows)"
21002092
stEvt:when: "2024-04-01T19:35:16+02:00"
2093+
oiiotool ERROR: read : Failed to decode Exif data
2094+
failed to open "src/crash-psd-exif-1632.psd": failed load_resources
2095+
Full command line was:
2096+
> oiiotool --info -v -a --hash src/crash-psd-exif-1632.psd
2097+
oiiotool ERROR: read : Corrupt thumbnail: 262304w * 24bpp does not match 480 width bytes
2098+
failed to open "src/crash-thumb-1626.psd": failed load_resources
2099+
Full command line was:
2100+
> oiiotool --info -v -a --hash src/crash-thumb-1626.psd
2101+
oiiotool ERROR: read : failed to open "src/crash-005c.psd": failed load_layers
2102+
Full command line was:
2103+
> oiiotool --info -v -a --hash src/crash-005c.psd
2104+
oiiotool ERROR: read : [Image Data Section] channel count 3 is too few for color mode 3
2105+
failed to open "src/crash-8a15.psd": failed load_image_data
2106+
Full command line was:
2107+
> oiiotool --info -v -a --hash src/crash-8a15.psd
2108+
oiiotool ERROR: read : unable to decode zip compressed data: src_size=39, dst_size=1296
2109+
Error during layer decompression. Possible corrupt file?
2110+
failed to open "../oiio-images/psd/corrupt_20260312a.psd": failed load_global_additional
2111+
Full command line was:
2112+
> oiiotool --info -v -a --hash ../oiio-images/psd/corrupt_20260312a.psd

testsuite/psd/run.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,19 @@
1919
command += info_command ("src/different-mask-size.psd")
2020
command += info_command ("src/layer-mask.psd")
2121

22+
# Test more modern (Photoshop 2023 files) with 16- and 32-bit files containing multiple sublayers
23+
command += info_command ("src/Layers_8bit_RGB.psd")
24+
command += info_command ("src/Layers_16bit_RGB.psd")
25+
command += info_command ("src/Layers_32bit_RGB.psd")
26+
2227
# This file has a corrupted Exif block
2328
command += info_command ("src/crash-psd-exif-1632.psd", failureok = 1)
2429
# Corrupted thumbnail clobbered memory
2530
command += info_command ("src/crash-thumb-1626.psd", failureok = 1)
31+
# Corruption caused an integer overflow
32+
command += info_command ("src/crash-005c.psd", failureok=True)
33+
# Corruption where the file didn't have enough channels for its color mode
34+
command += info_command ("src/crash-8a15.psd", failureok=True)
35+
# Corruption where bad zip compression data caused a buffer overrun
36+
command += info_command (OIIO_TESTSUITE_IMAGEDIR + "/corrupt_20260312a.psd", failureok=True)
2637

27-
# Test more modern (Photoshop 2023 files) with 16- and 32-bit files containing multiple sublayers
28-
command += info_command ("src/Layers_8bit_RGB.psd")
29-
command += info_command ("src/Layers_16bit_RGB.psd")
30-
command += info_command ("src/Layers_32bit_RGB.psd")

testsuite/psd/src/crash-005c.psd

43.6 KB
Binary file not shown.

testsuite/psd/src/crash-8a15.psd

22.9 KB
Binary file not shown.

0 commit comments

Comments
 (0)