From 5fcd891a327f05a5f67722fd9a4c4f624096df29 Mon Sep 17 00:00:00 2001 From: Larry Gritz Date: Thu, 25 Jun 2026 11:49:33 -0500 Subject: [PATCH] fix(psd): identify corruptions of layer resolutions, eof in strings * Find corruptions by validating layer resolutions. * Find corruptions when eof is hit in the middle of a string. Signed-off-by: Larry Gritz --- src/psd.imageio/psdinput.cpp | 117 +++++++++++++++----------- testsuite/psd/ref/out-linuxarm.txt | 11 ++- testsuite/psd/ref/out.txt | 11 ++- testsuite/psd/run.py | 5 +- testsuite/psd/src/crash-eofstring.psd | Bin 0 -> 18289 bytes testsuite/psd/src/crash-layerres.psd | Bin 0 -> 23484 bytes 6 files changed, 89 insertions(+), 55 deletions(-) create mode 100644 testsuite/psd/src/crash-eofstring.psd create mode 100644 testsuite/psd/src/crash-layerres.psd diff --git a/src/psd.imageio/psdinput.cpp b/src/psd.imageio/psdinput.cpp index e22617db63..eb9cb3a0ff 100644 --- a/src/psd.imageio/psdinput.cpp +++ b/src/psd.imageio/psdinput.cpp @@ -227,6 +227,7 @@ class PSDInput final : public ImageInput { GlobalMaskInfo m_global_mask_info; ImageDataSection m_image_data; ImageBuf m_thumbnail; + uint32_t m_maxres = 30000; // Maximum resolution based on header version //Reset to initial state void init(); @@ -236,6 +237,7 @@ class PSDInput final : public ImageInput { bool read_header(); bool validate_header(); static bool validate_signature(const char signature[4]); + bool validate_resolution(string_view name, uint32_t width, uint32_t height); //Color Mode Data bool load_color_data(); @@ -436,7 +438,8 @@ class PSDInput final : public ImageInput { return true; } - int read_pascal_string(std::string& s, uint16_t mod_padding); + bool read_pascal_string(std::string& s, uint16_t mod_padding, + int* bytes_read = nullptr); // Swap a planar bytespan representing the bytes of a float vector to its // interleaved byte order. This is per scanline @@ -966,6 +969,22 @@ PSDInput::validate_signature(const char signature[4]) +bool +PSDInput::validate_resolution(string_view name, uint32_t width, uint32_t height) +{ + if (width < 1 || width > m_maxres) { + errorfmt("[{}] invalid image width {}", name, width); + return false; + } + if (height < 1 || height > m_maxres) { + errorfmt("[{}] invalid image height {}", name, height); + return false; + } + return true; +} + + + bool PSDInput::validate_header() { @@ -981,32 +1000,13 @@ PSDInput::validate_header() errorfmt("[Header] invalid channel count"); return false; } - switch (m_header.version) { - case 1: - // PSD - // width/height range: [1,30000] - if (m_header.height < 1 || m_header.height > 30000) { - errorfmt("[Header] invalid image height"); - return false; - } - if (m_header.width < 1 || m_header.width > 30000) { - errorfmt("[Header] invalid image width"); - return false; - } - break; - case 2: - // PSB (Large Document Format) - // width/height range: [1,300000] - if (m_header.height < 1 || m_header.height > 300000) { - errorfmt("[Header] invalid image height {}", m_header.height); - return false; - } - if (m_header.width < 1 || m_header.width > 300000) { - errorfmt("[Header] invalid image width {}", m_header.width); - return false; - } - break; - } + if (m_header.version == 2 /* PSB - Large Document Format */) + m_maxres = 300000; + else /* PSD */ + m_maxres = 30000; + if (!validate_resolution("Header", m_header.height, m_header.width)) + return false; + // Valid depths are 1,8,16,32 if (m_header.depth != 1 && m_header.depth != 8 && m_header.depth != 16 && m_header.depth != 32) { @@ -1205,7 +1205,10 @@ PSDInput::load_resource_1006(uint32_t length) int32_t bytes_remaining = length; std::string name; while (bytes_remaining >= 2) { - bytes_remaining -= read_pascal_string(name, 1); + int b = 0; + if (!read_pascal_string(name, 1, &b)) + return false; + bytes_remaining -= b; m_alpha_names.push_back(name); } return true; @@ -1524,6 +1527,8 @@ PSDInput::load_layer(Layer& layer) layer.width = std::abs((int)layer.right - (int)layer.left); layer.height = std::abs((int)layer.bottom - (int)layer.top); + if (!validate_resolution("Layer Record", layer.width, layer.height)) + return false; layer.channel_info.resize(layer.channel_count); for (uint16_t channel = 0; channel < layer.channel_count; channel++) { ChannelInfo& channel_info = layer.channel_info[channel]; @@ -1589,7 +1594,10 @@ PSDInput::load_layer(Layer& layer) if (!ok) return false; - extra_remaining -= read_pascal_string(layer.name, 4); + int b = 0; + if (!read_pascal_string(layer.name, 4, &b)) + return false; + extra_remaining -= b; while (ok && extra_remaining >= 12) { layer.additional_info.emplace_back(); Layer::AdditionalInfo& info = layer.additional_info.back(); @@ -1654,6 +1662,8 @@ PSDInput::load_layer_channel(Layer& layer, ChannelInfo& channel_info) width = layer.width; height = layer.height; } + if (!validate_resolution("layer_channel", width, height)) + return false; channel_info.width = width; channel_info.height = height; @@ -2226,33 +2236,40 @@ PSDInput::set_type_desc() -int -PSDInput::read_pascal_string(std::string& s, uint16_t mod_padding) +bool +PSDInput::read_pascal_string(std::string& s, uint16_t mod_padding, + int* bytes_read) { + if (bytes_read) + *bytes_read = 0; + int bytes = 0; s.clear(); uint8_t length; - int bytes = 0; - if (ioread((char*)&length, 1)) { - bytes = 1; - if (length == 0) { - if (ioseek(mod_padding - 1, SEEK_CUR)) - bytes += mod_padding - 1; - } else { - s.resize(length); - if (ioread(&s[0], length)) { - bytes += length; - if (mod_padding > 0) { - for (int padded_length = length + 1; - padded_length % mod_padding != 0; padded_length++) { - if (!ioseek(1, SEEK_CUR)) - break; - bytes++; - } - } + if (!ioread((char*)&length, 1)) + return false; + bytes = 1; + if (length == 0) { + if (ioseek(mod_padding - 1, SEEK_CUR)) + bytes += mod_padding - 1; + else + return false; + } else { + s.resize(length); + if (!ioread(&s[0], length)) + return false; + bytes += length; + if (mod_padding > 0) { + for (int padded_length = length + 1; + padded_length % mod_padding != 0; padded_length++) { + if (!ioseek(1, SEEK_CUR)) + return false; + bytes++; } } } - return bytes; + if (bytes_read) + *bytes_read = bytes; + return true; } diff --git a/testsuite/psd/ref/out-linuxarm.txt b/testsuite/psd/ref/out-linuxarm.txt index 799e1d0e29..50a050ea38 100644 --- a/testsuite/psd/ref/out-linuxarm.txt +++ b/testsuite/psd/ref/out-linuxarm.txt @@ -2105,8 +2105,15 @@ oiiotool ERROR: read : [Image Data Section] channel count 3 is too few for color failed to open "src/crash-8a15.psd": failed load_image_data Full command line was: > oiiotool --info -v -a --hash src/crash-8a15.psd -oiiotool ERROR: read : unable to decode zip compressed data: src_size=39, dst_size=1296 -Error during layer decompression. Possible corrupt file? +oiiotool ERROR: read : [Layer Record] invalid image width 16777264 failed to open "../oiio-images/psd/corrupt_20260312a.psd": failed load_global_additional Full command line was: > oiiotool --info -v -a --hash ../oiio-images/psd/corrupt_20260312a.psd +oiiotool ERROR: read : [Layer Record] invalid image height 1493172215 +failed to open "src/crash-layerres.psd": failed load_layers +Full command line was: +> oiiotool --info -v -a --hash src/crash-layerres.psd +oiiotool ERROR: read : Read error: hit end of file in psd reader +failed to open "src/crash-eofstring.psd": failed load_resources +Full command line was: +> oiiotool --info -v -a --hash src/crash-eofstring.psd diff --git a/testsuite/psd/ref/out.txt b/testsuite/psd/ref/out.txt index 8d43514ac3..fb8bc906e5 100644 --- a/testsuite/psd/ref/out.txt +++ b/testsuite/psd/ref/out.txt @@ -2105,8 +2105,15 @@ oiiotool ERROR: read : [Image Data Section] channel count 3 is too few for color failed to open "src/crash-8a15.psd": failed load_image_data Full command line was: > oiiotool --info -v -a --hash src/crash-8a15.psd -oiiotool ERROR: read : unable to decode zip compressed data: src_size=39, dst_size=1296 -Error during layer decompression. Possible corrupt file? +oiiotool ERROR: read : [Layer Record] invalid image width 16777264 failed to open "../oiio-images/psd/corrupt_20260312a.psd": failed load_global_additional Full command line was: > oiiotool --info -v -a --hash ../oiio-images/psd/corrupt_20260312a.psd +oiiotool ERROR: read : [Layer Record] invalid image height 1493172215 +failed to open "src/crash-layerres.psd": failed load_layers +Full command line was: +> oiiotool --info -v -a --hash src/crash-layerres.psd +oiiotool ERROR: read : Read error: hit end of file in psd reader +failed to open "src/crash-eofstring.psd": failed load_resources +Full command line was: +> oiiotool --info -v -a --hash src/crash-eofstring.psd diff --git a/testsuite/psd/run.py b/testsuite/psd/run.py index 031b3ec215..33700e5ee5 100755 --- a/testsuite/psd/run.py +++ b/testsuite/psd/run.py @@ -34,4 +34,7 @@ command += info_command ("src/crash-8a15.psd", failureok=True) # Corruption where bad zip compression data caused a buffer overrun command += info_command (OIIO_TESTSUITE_IMAGEDIR + "/corrupt_20260312a.psd", failureok=True) - +# Corruption where invalid layer resolution caused a huge allocation +command += info_command ("src/crash-layerres.psd", failureok=True) +# Corruption where eof was hit in the end of a string read +command += info_command ("src/crash-eofstring.psd", failureok=True) diff --git a/testsuite/psd/src/crash-eofstring.psd b/testsuite/psd/src/crash-eofstring.psd new file mode 100644 index 0000000000000000000000000000000000000000..a816e64ec52d8ef564b72457eaa84ff1140351c5 GIT binary patch literal 18289 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 literal 0 HcmV?d00001 diff --git a/testsuite/psd/src/crash-layerres.psd b/testsuite/psd/src/crash-layerres.psd new file mode 100644 index 0000000000000000000000000000000000000000..1514c5898ea036d4cca25d255bc6367509ea8a01 GIT binary patch literal 23484 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 Nwxp%KYeN_6`yVj$ys7{I literal 0 HcmV?d00001