From 56d183b24003a4ba1cdbcd66a1b6f74d5fd59f9c Mon Sep 17 00:00:00 2001 From: Larry Gritz Date: Tue, 30 Jun 2026 00:29:41 -0400 Subject: [PATCH] fix(psd): guard row interleave bounds on corrupt data Validate channel-row byte availability before interleaving scanlines in PSD reads, and fail gracefully with an error instead of risking out-of-bounds access on malformed files. Assisted-by: GitHub Copilot / GPT-5.3-Codex Signed-off-by: Larry Gritz --- src/psd.imageio/psdinput.cpp | 54 +++++++++++++++------ testsuite/psd/ref/out-linuxarm.txt | 6 +++ testsuite/psd/ref/out.txt | 6 +++ testsuite/psd/run.py | 2 + testsuite/psd/src/crash-rowbounds-f999.psd | Bin 0 -> 13439 bytes 5 files changed, 54 insertions(+), 14 deletions(-) create mode 100644 testsuite/psd/src/crash-rowbounds-f999.psd diff --git a/src/psd.imageio/psdinput.cpp b/src/psd.imageio/psdinput.cpp index 28395bebab..37cd0ecf03 100644 --- a/src/psd.imageio/psdinput.cpp +++ b/src/psd.imageio/psdinput.cpp @@ -306,7 +306,7 @@ class PSDInput final : public ImageInput { // Interleave channels (RRRGGGBBB -> RGBRGBRGB) while copying from // channel_buffers[0..nchans-1] to dst. template - static void + static bool interleave_row(T* dst, cspan> channel_buffers, int width, int nchans); @@ -808,16 +808,25 @@ PSDInput::read_native_scanline(int subimage, int miplevel, int y, int /*z*/, || m_header.color_mode == ColorMode_Grayscale) { switch (bps) { case 4: - interleave_row((float*)dst, channel_buffers, spec.width, - spec.nchannels); + if (!interleave_row((float*)dst, channel_buffers, spec.width, + spec.nchannels)) { + errorfmt("Corrupt PSD channel data for row {}", y + spec.y); + return false; + } break; case 2: - interleave_row((unsigned short*)dst, channel_buffers, spec.width, - spec.nchannels); + if (!interleave_row((unsigned short*)dst, channel_buffers, + spec.width, spec.nchannels)) { + errorfmt("Corrupt PSD channel data for row {}", y + spec.y); + return false; + } break; default: - interleave_row((unsigned char*)dst, channel_buffers, spec.width, - spec.nchannels); + if (!interleave_row((unsigned char*)dst, channel_buffers, + spec.width, spec.nchannels)) { + errorfmt("Corrupt PSD channel data for row {}", y + spec.y); + return false; + } break; } } else if (m_header.color_mode == ColorMode_CMYK) { @@ -825,8 +834,11 @@ PSDInput::read_native_scanline(int subimage, int miplevel, int y, int /*z*/, switch (bps) { case 4: { std::unique_ptr cmyk(new float[cmyklen]); - interleave_row(cmyk.get(), channel_buffers, spec.width, - channel_count); + if (!interleave_row(cmyk.get(), channel_buffers, spec.width, + channel_count)) { + errorfmt("Corrupt PSD channel data for row {}", y + spec.y); + return false; + } cmyk_to_rgb(spec.width, make_cspan(cmyk.get(), cmyklen), channel_count, make_span((float*)dst, spec.width * spec.nchannels), @@ -835,8 +847,11 @@ PSDInput::read_native_scanline(int subimage, int miplevel, int y, int /*z*/, } case 2: { std::unique_ptr cmyk(new unsigned short[cmyklen]); - interleave_row(cmyk.get(), channel_buffers, spec.width, - channel_count); + if (!interleave_row(cmyk.get(), channel_buffers, spec.width, + channel_count)) { + errorfmt("Corrupt PSD channel data for row {}", y + spec.y); + return false; + } cmyk_to_rgb(spec.width, make_cspan(cmyk.get(), cmyklen), channel_count, make_span((uint16_t*)dst, spec.width * spec.nchannels), @@ -845,8 +860,11 @@ PSDInput::read_native_scanline(int subimage, int miplevel, int y, int /*z*/, } default: { std::unique_ptr cmyk(new unsigned char[cmyklen]); - interleave_row(cmyk.get(), channel_buffers, spec.width, - channel_count); + if (!interleave_row(cmyk.get(), channel_buffers, spec.width, + channel_count)) { + errorfmt("Corrupt PSD channel data for row {}", y + spec.y); + return false; + } cmyk_to_rgb(spec.width, make_cspan(cmyk.get(), cmyklen), channel_count, make_span((uint8_t*)dst, spec.width * spec.nchannels), @@ -2191,16 +2209,24 @@ PSDInput::read_channel_row(ChannelInfo& channel_info, uint32_t row, char* data) template -void +bool PSDInput::interleave_row(T* dst, cspan> channel_buffers, int width, int nchans) { + if (!dst || width < 0 || nchans < 0 + || span_size_t(channel_buffers.size()) < size_t(nchans)) + return false; + + const uint64_t row_bytes = uint64_t(width) * sizeof(T); for (int c = 0; c < nchans; ++c) { + if (channel_buffers[c].size() < row_bytes) + return false; const T* cbuf = reinterpret_cast(channel_buffers[c].data()); for (int x = 0; x < width; ++x) dst[nchans * x + c] = cbuf[x]; } + return true; } diff --git a/testsuite/psd/ref/out-linuxarm.txt b/testsuite/psd/ref/out-linuxarm.txt index 50a050ea38..1c514321e8 100644 --- a/testsuite/psd/ref/out-linuxarm.txt +++ b/testsuite/psd/ref/out-linuxarm.txt @@ -2117,3 +2117,9 @@ 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 +Reading src/crash-rowbounds-f999.psd +oiiotool ERROR: -info : SHA-1: Corrupt PSD channel data for row 0 +Full command line was: +> oiiotool --info -v -a --hash src/crash-rowbounds-f999.psd +src/crash-rowbounds-f999.psd : 10 x 111, 3 channel, uint8 psd + channel list: R, G, B diff --git a/testsuite/psd/ref/out.txt b/testsuite/psd/ref/out.txt index fb8bc906e5..ee9877bdbb 100644 --- a/testsuite/psd/ref/out.txt +++ b/testsuite/psd/ref/out.txt @@ -2117,3 +2117,9 @@ 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 +Reading src/crash-rowbounds-f999.psd +oiiotool ERROR: -info : SHA-1: Corrupt PSD channel data for row 0 +Full command line was: +> oiiotool --info -v -a --hash src/crash-rowbounds-f999.psd +src/crash-rowbounds-f999.psd : 10 x 111, 3 channel, uint8 psd + channel list: R, G, B diff --git a/testsuite/psd/run.py b/testsuite/psd/run.py index 33700e5ee5..83bab5d2e3 100755 --- a/testsuite/psd/run.py +++ b/testsuite/psd/run.py @@ -38,3 +38,5 @@ 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) +# Corruption where short per-channel rows could overrun interleaving +command += info_command ("src/crash-rowbounds-f999.psd", failureok=True) diff --git a/testsuite/psd/src/crash-rowbounds-f999.psd b/testsuite/psd/src/crash-rowbounds-f999.psd new file mode 100644 index 0000000000000000000000000000000000000000..1479c7b75ff0f686489e350e375f78e329c25ac3 GIT binary patch literal 13439 zcmeI1&u^SY6vs)EL*-L1Ii*q&X{1CcJHW#7L+nP$I<2FM%8D(e>TPxyywtl41?*k> z+#dTMM9Mw?LJvLn-2bAN9((No%M`n;G`N-;TZ4@JJoBA5yw7{{K(g~<|3JDUNquiqa@68(`X z?W0Qj$?w0t`+EHIACG_9v%dfF{@bV3+~?(-``&-HP9ZwRg*?MYaWYo^{N-0gj>EC? z#tO6`o#J`Ccb?&c^KTB(`7v@s<=JG%Uwi9iy2J&P*ULqcd+V_RLo&l2waa0k$d#lx z9xKnw3HjS#U!IZ-%hq6|B7Nw{wmZ-**D=g5WJA+OKy!g^s+#U;uBYkp=29j*)bg`1 z@?P(Lx!I2XHCE(&|TLBngI+$r3y7aOA5GFll)=rr17(hbCktt z5tBqN{lXbJDaMLYZp5xlT+5Vgl;ndd#sMNru!bqn2O8*F6e6}JJ;@f;qlF0I1zzH$ z$mu%0a~(w*1Z-*NcFSew%Di}aN+Z}1>0Fr~o~0PP#yL635Po?|pX6cJ!>4mnkbF+I z#LcGb=&R(^%zXOz-ZaCoAlV@yi%FgL{l=d>otAmF<;QR0BqXc+i4QI>xsU;tiYpn{ z=xY1^Vw3G`I%aMZE_zB>+i1t)SBK4msO%pTR2DoAeQ;s4BhbyVILQl`ApB}~LW_S8 z$Ds#Drac-WN1b7NrbdxTEyuD|Ez&V|uoXgM*7$EN-@eCcj+04w)Ktw@_2HqedB(`o zwWnoc)f!ioYP*x1M8yhb`1uj-6g+3S25@oNW%oWP`|TobwT&8W)!!F!)s9n`<+$ut zW2I?V9Er%`>3>Uu>zFn+k)N=Np}NS9R4c-xp*;+TdWai#S53pv@J#*xPXlGX zDMvJi$q^1Gpoy#*xJj7{!^qTTuBFa&1F5!YVioGpRV{4Wh8fYMLbr+UYMIkL)9@_w z7GpklUBjEosW)hSYE|y)QTicA zzaatph-~fD?zGozUoPouc3-;_oAko^LEzGQJ@OXu2NB7ZXX`gCBI(6T;1alc5$Gir z)g^HCBG5}Ls!QPNMWB~hRF}Zji$E{2s4ju47lB^l8(&@JzxV!$_{N9BLJ^TL6g-B& z#Y2f{1TKbx#}K%9C^3z|#Zd4V0v8V@rV+Rp3LZn?;-SPe0vAKUV+dS4l$b`~VkmeF zfs2O{(+FG)1&<+c@lax#+k=aSbbEpkk+2HoF$69iN=zegF%&$8z{Nv}X|4~~-w$}s An*aa+ literal 0 HcmV?d00001