From ab3793dadd9f97cfacdc640b57cdabd0eb46c4ea Mon Sep 17 00:00:00 2001 From: Larry Gritz Date: Fri, 24 Jul 2026 01:03:19 -0700 Subject: [PATCH] fix(tiff): hardening: prevent OOB read, overflow protection In the raw-strip read path of read_native_scanlines(), a failed TIFFReadRawStrip() returns csize < 0. Bail out of the strip loop for this case before running the decompress, matching the existing tiled path (read_native_tiles already breaks on this condition), and guard the leftover-scanline loop with ok so it doesn't run after the bail. Add regression seed to the TIFF corpora -- a valid DEFLATE TIFF with StripOffsets[0] pointing past EOF that the nightly ASan fuzz job exercises via full-image reads. The reader now rejects it with a clean error instead of reading out of bounds. Also size scanline scratch with 64-bit arithmetic, for overflow protection. Assisted-by: Claude Code / Claude Opus 4.8 Signed-off-by: Larry Gritz --- src/fuzz/populate_corpora.py | 1 + src/tiff.imageio/tiffinput.cpp | 5 +++-- testsuite/tiff-misc/ref/out-libtiff403-b.txt | 3 +++ testsuite/tiff-misc/ref/out-libtiff403-c.txt | 3 +++ testsuite/tiff-misc/ref/out-libtiff403.txt | 3 +++ testsuite/tiff-misc/ref/out-libtiff409.txt | 3 +++ testsuite/tiff-misc/ref/out-libtiff410.txt | 3 +++ testsuite/tiff-misc/ref/out-libtiff430.txt | 3 +++ testsuite/tiff-misc/ref/out-libtiff470-b.txt | 3 +++ testsuite/tiff-misc/ref/out-libtiff470-c.txt | 3 +++ testsuite/tiff-misc/ref/out-libtiff470.txt | 3 +++ testsuite/tiff-misc/ref/out.txt | 3 +++ testsuite/tiff-misc/run.py | 5 +++++ .../src/crash-rawstrip-offset-past-eof.tif | Bin 0 -> 959 bytes 14 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 testsuite/tiff-misc/src/crash-rawstrip-offset-past-eof.tif diff --git a/src/fuzz/populate_corpora.py b/src/fuzz/populate_corpora.py index 4c85905a7c..afcac7f4bb 100644 --- a/src/fuzz/populate_corpora.py +++ b/src/fuzz/populate_corpora.py @@ -144,6 +144,7 @@ def _find_images_root() -> Path: ("../oiio-images/targa", ["tga", "TGA"]), ], "tiff": [ + ("testsuite/tiff-misc/src", ["tif", "tiff"]), ("../oiio-images", ["tif", "tiff"]), ("../oiio-images/libtiffpic",["tif", "tiff"]), ("testsuite/tiff-suite/src", ["tif", "tiff"]), diff --git a/src/tiff.imageio/tiffinput.cpp b/src/tiff.imageio/tiffinput.cpp index a9f90a91fc..fdfcd4e77d 100644 --- a/src/tiff.imageio/tiffinput.cpp +++ b/src/tiff.imageio/tiffinput.cpp @@ -1962,7 +1962,7 @@ TIFFInput::read_native_scanline_locked(int subimage, int miplevel, int y, } // Make sure there's enough scratch space - int nvals = m_spec.width * m_inputchannels; + imagesize_t nvals = imagesize_t(m_spec.width) * m_inputchannels; if (m_photometric == PHOTOMETRIC_PALETTE && m_bitspersample > 8) m_scratch.resize(nvals * 2); // special case for 16 bit palette else @@ -2279,6 +2279,7 @@ TIFFInput::read_native_scanlines(int subimage, int miplevel, int ybegin, read_raw_strips ? "Raw" : "Encoded", y, err.size() ? err.c_str() : "unknown error"); ok = false; + break; // failed strip read -- bail, don't decompress } auto out = this; auto uncompress_etc = [=, &ok](int /*id*/) { @@ -2345,7 +2346,7 @@ TIFFInput::read_native_scanlines(int subimage, int miplevel, int ybegin, // If we have left over scanlines, read them serially m_next_scanline = y - m_spec.y; - for (; y < yend; ++y) { + for (; ok && y < yend; ++y) { if (!read_native_scanline_locked(subimage, miplevel, y, data)) { ok = false; break; diff --git a/testsuite/tiff-misc/ref/out-libtiff403-b.txt b/testsuite/tiff-misc/ref/out-libtiff403-b.txt index 64188cf31d..3bd70e1779 100644 --- a/testsuite/tiff-misc/ref/out-libtiff403-b.txt +++ b/testsuite/tiff-misc/ref/out-libtiff403-b.txt @@ -58,5 +58,8 @@ src/crash-cmyk-1bit.tif : 73 x 43, 3 channel, uint1 tiff tiff:PhotometricInterpretation: 5 tiff:PlanarConfiguration: 1 tiff:RowsPerStrip: 28 +oiiotool ERROR: read : "src/crash-rawstrip-offset-past-eof.tif": TIFFReadRawStrip failed reading line y=0: Read error at scanline 4294967295; got 0 bytes, expected 28 +Full command line was: +> oiiotool --oiioattrib try_all_readers 0 src/crash-rawstrip-offset-past-eof.tif -o out.exr Comparing "check1.tif" and "ref/check1.tif" PASS diff --git a/testsuite/tiff-misc/ref/out-libtiff403-c.txt b/testsuite/tiff-misc/ref/out-libtiff403-c.txt index 0add5ff244..de0d58c72d 100644 --- a/testsuite/tiff-misc/ref/out-libtiff403-c.txt +++ b/testsuite/tiff-misc/ref/out-libtiff403-c.txt @@ -58,5 +58,8 @@ src/crash-cmyk-1bit.tif : 73 x 43, 3 channel, uint1 tiff tiff:PhotometricInterpretation: 5 tiff:PlanarConfiguration: 1 tiff:RowsPerStrip: 28 +oiiotool ERROR: read : "src/crash-rawstrip-offset-past-eof.tif": TIFFReadRawStrip failed reading line y=0: Read error at scanline 4294967295; got 0 bytes, expected 28 +Full command line was: +> oiiotool --oiioattrib try_all_readers 0 src/crash-rawstrip-offset-past-eof.tif -o out.exr Comparing "check1.tif" and "ref/check1.tif" PASS diff --git a/testsuite/tiff-misc/ref/out-libtiff403.txt b/testsuite/tiff-misc/ref/out-libtiff403.txt index 643ca1b8cf..0837fc8a11 100644 --- a/testsuite/tiff-misc/ref/out-libtiff403.txt +++ b/testsuite/tiff-misc/ref/out-libtiff403.txt @@ -58,5 +58,8 @@ src/crash-cmyk-1bit.tif : 73 x 43, 3 channel, uint1 tiff tiff:PhotometricInterpretation: 5 tiff:PlanarConfiguration: 1 tiff:RowsPerStrip: 28 +oiiotool ERROR: read : "src/crash-rawstrip-offset-past-eof.tif": TIFFReadRawStrip failed reading line y=0: Read error at scanline 4294967295; got 0 bytes, expected 28 +Full command line was: +> oiiotool --oiioattrib try_all_readers 0 src/crash-rawstrip-offset-past-eof.tif -o out.exr Comparing "check1.tif" and "ref/check1.tif" PASS diff --git a/testsuite/tiff-misc/ref/out-libtiff409.txt b/testsuite/tiff-misc/ref/out-libtiff409.txt index 8cd8867ea7..678ab158e8 100644 --- a/testsuite/tiff-misc/ref/out-libtiff409.txt +++ b/testsuite/tiff-misc/ref/out-libtiff409.txt @@ -58,5 +58,8 @@ src/crash-cmyk-1bit.tif : 73 x 43, 3 channel, uint1 tiff tiff:PhotometricInterpretation: 5 tiff:PlanarConfiguration: 1 tiff:RowsPerStrip: 28 +oiiotool ERROR: read : "src/crash-rawstrip-offset-past-eof.tif": TIFFReadRawStrip failed reading line y=0: Read error at scanline 4294967295; got 0 bytes, expected 28 +Full command line was: +> oiiotool --oiioattrib try_all_readers 0 src/crash-rawstrip-offset-past-eof.tif -o out.exr Comparing "check1.tif" and "ref/check1.tif" PASS diff --git a/testsuite/tiff-misc/ref/out-libtiff410.txt b/testsuite/tiff-misc/ref/out-libtiff410.txt index 5f08284b0c..567ac0d5af 100644 --- a/testsuite/tiff-misc/ref/out-libtiff410.txt +++ b/testsuite/tiff-misc/ref/out-libtiff410.txt @@ -58,5 +58,8 @@ src/crash-cmyk-1bit.tif : 73 x 43, 3 channel, uint1 tiff tiff:PhotometricInterpretation: 5 tiff:PlanarConfiguration: 1 tiff:RowsPerStrip: 28 +oiiotool ERROR: read : "src/crash-rawstrip-offset-past-eof.tif": TIFFReadRawStrip failed reading line y=0: Read error at scanline 4294967295; got 0 bytes, expected 28 +Full command line was: +> oiiotool --oiioattrib try_all_readers 0 src/crash-rawstrip-offset-past-eof.tif -o out.exr Comparing "check1.tif" and "ref/check1.tif" PASS diff --git a/testsuite/tiff-misc/ref/out-libtiff430.txt b/testsuite/tiff-misc/ref/out-libtiff430.txt index 73d139c156..65307292ff 100644 --- a/testsuite/tiff-misc/ref/out-libtiff430.txt +++ b/testsuite/tiff-misc/ref/out-libtiff430.txt @@ -73,5 +73,8 @@ src/crash-cmyk-1bit.tif : 73 x 43, 3 channel, uint1 tiff tiff:PhotometricInterpretation: 5 tiff:PlanarConfiguration: 1 tiff:RowsPerStrip: 28 +oiiotool ERROR: read : "src/crash-rawstrip-offset-past-eof.tif": TIFFReadRawStrip failed reading line y=0: Read error at scanline 4294967295; got 0 bytes, expected 28 +Full command line was: +> oiiotool --oiioattrib try_all_readers 0 src/crash-rawstrip-offset-past-eof.tif -o out.exr Comparing "check1.tif" and "ref/check1.tif" PASS diff --git a/testsuite/tiff-misc/ref/out-libtiff470-b.txt b/testsuite/tiff-misc/ref/out-libtiff470-b.txt index 6bebe2b474..168a14b43c 100644 --- a/testsuite/tiff-misc/ref/out-libtiff470-b.txt +++ b/testsuite/tiff-misc/ref/out-libtiff470-b.txt @@ -73,5 +73,8 @@ src/crash-cmyk-1bit.tif : 73 x 43, 3 channel, uint1 tiff tiff:PhotometricInterpretation: 5 tiff:PlanarConfiguration: 1 tiff:RowsPerStrip: 28 +oiiotool ERROR: read : "src/crash-rawstrip-offset-past-eof.tif": TIFFReadRawStrip failed reading line y=0: Read error at scanline 4294967295; got 0 bytes, expected 28 +Full command line was: +> oiiotool --oiioattrib try_all_readers 0 src/crash-rawstrip-offset-past-eof.tif -o out.exr Comparing "check1.tif" and "ref/check1.tif" PASS diff --git a/testsuite/tiff-misc/ref/out-libtiff470-c.txt b/testsuite/tiff-misc/ref/out-libtiff470-c.txt index 61dc38ee9d..f00e9c69b7 100644 --- a/testsuite/tiff-misc/ref/out-libtiff470-c.txt +++ b/testsuite/tiff-misc/ref/out-libtiff470-c.txt @@ -73,5 +73,8 @@ src/crash-cmyk-1bit.tif : 73 x 43, 3 channel, uint1 tiff tiff:PhotometricInterpretation: 5 tiff:PlanarConfiguration: 1 tiff:RowsPerStrip: 28 +oiiotool ERROR: read : "src/crash-rawstrip-offset-past-eof.tif": TIFFReadRawStrip failed reading line y=0: Read error at scanline 4294967295; got 0 bytes, expected 28 +Full command line was: +> oiiotool --oiioattrib try_all_readers 0 src/crash-rawstrip-offset-past-eof.tif -o out.exr Comparing "check1.tif" and "ref/check1.tif" PASS diff --git a/testsuite/tiff-misc/ref/out-libtiff470.txt b/testsuite/tiff-misc/ref/out-libtiff470.txt index a86c32046d..ec4bcd9899 100644 --- a/testsuite/tiff-misc/ref/out-libtiff470.txt +++ b/testsuite/tiff-misc/ref/out-libtiff470.txt @@ -73,5 +73,8 @@ src/crash-cmyk-1bit.tif : 73 x 43, 3 channel, uint1 tiff tiff:PhotometricInterpretation: 5 tiff:PlanarConfiguration: 1 tiff:RowsPerStrip: 28 +oiiotool ERROR: read : "src/crash-rawstrip-offset-past-eof.tif": TIFFReadRawStrip failed reading line y=0: Read error at scanline 4294967295; got 0 bytes, expected 28 +Full command line was: +> oiiotool --oiioattrib try_all_readers 0 src/crash-rawstrip-offset-past-eof.tif -o out.exr Comparing "check1.tif" and "ref/check1.tif" PASS diff --git a/testsuite/tiff-misc/ref/out.txt b/testsuite/tiff-misc/ref/out.txt index 707d956136..d641064155 100644 --- a/testsuite/tiff-misc/ref/out.txt +++ b/testsuite/tiff-misc/ref/out.txt @@ -58,5 +58,8 @@ src/crash-cmyk-1bit.tif : 73 x 43, 3 channel, uint1 tiff tiff:PhotometricInterpretation: 5 tiff:PlanarConfiguration: 1 tiff:RowsPerStrip: 28 +oiiotool ERROR: read : "src/crash-rawstrip-offset-past-eof.tif": TIFFReadRawStrip failed reading line y=0: Read error at scanline 4294967295; got 0 bytes, expected 28 +Full command line was: +> oiiotool --oiioattrib try_all_readers 0 src/crash-rawstrip-offset-past-eof.tif -o out.exr Comparing "check1.tif" and "ref/check1.tif" PASS diff --git a/testsuite/tiff-misc/run.py b/testsuite/tiff-misc/run.py index 434223de79..4dd434f37c 100755 --- a/testsuite/tiff-misc/run.py +++ b/testsuite/tiff-misc/run.py @@ -40,4 +40,9 @@ # causing a heap buffer overflow. command += info_command ("src/crash-cmyk-1bit.tif", safematch=True) +# Regression: a valid DEFLATE TIFF whose StripOffsets[0] points past EOF must +# be rejected cleanly during the pixel read (not read out of bounds). Placed +# last so its output appends at the tail of every libtiff-version ref variant. +command += oiiotool ("--oiioattrib try_all_readers 0 src/crash-rawstrip-offset-past-eof.tif -o out.exr", failureok = True) + outputs = [ "check1.tif", "out.txt" ] diff --git a/testsuite/tiff-misc/src/crash-rawstrip-offset-past-eof.tif b/testsuite/tiff-misc/src/crash-rawstrip-offset-past-eof.tif new file mode 100644 index 0000000000000000000000000000000000000000..f2726fc6a817200f2b23116eb8b1f6955924c3d6 GIT binary patch literal 959 zcmds#O^ee&7{@2=qKhED;JP06FpM{`naQL{Hbb+EHYpaY6<5%krs-_A)Vz>J6MNB{ z;Aik4_%RejKY%Acf>+U-2QNMoV_CsZ;0*udmuH@rnI|(GUO}4(p|c2SZKMG`2WiK+ z1>6SPCs4bwtB4Z~w}8)S8+Dd1z~>vg%C*(7ioe(}R6^UpD#vHg&M|*OYoY64I|acU z_yX`vV6X8}wGMz)z74RyYiZ~iydQ$p*sSLc`g=ZjsiB_`x(#6_Xbf6_UVz?!w0e#+ znGEA_CWm8e>Re|!pUNj#;DO&CbWL~$z7Yhz={nrv18xVl%Pq&|%yt9EHhshyw*_+q z?_$P;1;9~nzskaBAqzZ_vw7l?Z*SidJfC{xo;6~ltS^t|H&(ekSlzlCtrn3xCHs50 zSBWalVp)W^iXSCQQF$bsrV}ZEm6;M;yA%tLTvH49{^&ODr@6$IZgnEYI5u>nyURGg zia9elWezp0jp+Fl0Ggx0vr8(f;d~JO_q9n8a+y5T7?-kbf!)e zXQH3Wu#i5;Bmd|pp?etjXtQJ3J6(4QKGwaK2{gEbPFAnJe$Y