From bd9967ebb49606fc26554359be8d717de4d78a30 Mon Sep 17 00:00:00 2001 From: Larry Gritz Date: Sun, 26 Jul 2026 15:10:00 -0700 Subject: [PATCH] fix: unify decompression-bomb and extent guards across image readers Apply check_open() and/or check_compression_ratio() consistently across all the format readers that previously lacked them. This helps ensure that a small, malformed file cannot drive a large pixel allocation before any pixel data is read, or other chaos that might result from the headers claiming resolutions that the file formats themselves (or common sense) disallow. Also add a new validity test to check_open: verify that tile sizes are non-negative and not larger than the largest allowed image size for that format. Readers guarded here: cineon, dds, dpx, fits, gif, hdr, heif, ico, iff, jpeg, null, openexr (both the C++ and C-API readers), png, pnm, rla, sgi, softimage, webp, zfile. Apologies for making changes to many formats in one commit, but since the changes to the readers are short and are nearly identical, it seemed like less reviewer burden to do it all in one PR rather than breaking it up per-format. Also adds self-contained decompression-bomb / extent regression tests (targa, png-bomb, openexr-bomb, sgi, dpx, cineon, iff, webp, hdr, ico, softimage, pnm) and registers the new seed corpora with the fuzz harness. Assisted-by: GitHub Copilot / Claude Opus 4.8 Signed-off-by: Larry Gritz --- src/build-scripts/ci-test.bash | 5 ++++ src/cineon.imageio/cineoninput.cpp | 5 +++- src/cmake/testing.cmake | 5 ++++ src/dds.imageio/ddsinput.cpp | 7 +++++ src/dpx.imageio/dpxinput.cpp | 3 ++- src/fits.imageio/fitsinput.cpp | 13 +++++++++ src/fuzz/populate_corpora.py | 33 ++++++++++++++++++----- src/gif.imageio/gifinput.cpp | 3 ++- src/hdr.imageio/hdrinput.cpp | 3 ++- src/heif.imageio/heifinput.cpp | 11 ++++++++ src/ico.imageio/icoinput.cpp | 3 ++- src/iff.imageio/iffinput.cpp | 7 +++-- src/jpeg.imageio/jpeginput.cpp | 7 +++++ src/libOpenImageIO/imageinput.cpp | 19 +++++++++++++ src/null.imageio/nullimageio.cpp | 8 ++++++ src/openexr.imageio/exrinput.cpp | 7 +++++ src/openexr.imageio/exrinput_c.cpp | 7 +++++ src/png.imageio/pnginput.cpp | 9 +++++++ src/pnm.imageio/pnminput.cpp | 7 +++++ src/rla.imageio/rlainput.cpp | 5 ++++ src/sgi.imageio/sgiinput.cpp | 3 ++- src/softimage.imageio/softimageinput.cpp | 3 ++- src/webp.imageio/webpinput.cpp | 10 ++++--- src/zfile.imageio/zfile.cpp | 6 +++++ testsuite/cineon/ref/out.txt | 3 +++ testsuite/cineon/run.py | 4 +++ testsuite/cineon/src/bomb-46341.cin | Bin 0 -> 2048 bytes testsuite/dpx/ref/out.txt | 3 +++ testsuite/dpx/run.py | 5 ++++ testsuite/dpx/src/bomb-46341.dpx | Bin 0 -> 21248 bytes testsuite/gif/ref/out.txt | 2 +- testsuite/hdr/ref/out.txt | 3 +++ testsuite/hdr/run.py | 5 ++++ testsuite/hdr/src/bomb-40000.hdr | 4 +++ testsuite/ico/ref/out.txt | 1 + testsuite/ico/run.py | 3 +++ testsuite/ico/src/bomb-png-46340.ico | Bin 0 -> 375 bytes testsuite/iff/ref/out.txt | 3 +++ testsuite/iff/run.py | 4 +++ testsuite/iff/src/bomb-46341.iff | Bin 0 -> 336 bytes testsuite/openexr-bomb/ref/out-exr22.txt | 6 +++++ testsuite/openexr-bomb/ref/out.txt | 6 +++++ testsuite/openexr-bomb/run.py | 18 +++++++++++++ testsuite/openexr-bomb/src/bomb.exr | Bin 0 -> 464 bytes testsuite/png-bomb/ref/out.txt | 3 +++ testsuite/png-bomb/run.py | 14 ++++++++++ testsuite/png-bomb/src/bomb.png | Bin 0 -> 353 bytes testsuite/pnm/ref/out.txt | 3 +++ testsuite/pnm/run.py | 5 ++++ testsuite/pnm/src/.gitattributes | 1 + testsuite/pnm/src/bomb-65000.pgm | 3 +++ testsuite/sgi/ref/out.txt | 1 + testsuite/sgi/run.py | 5 ++++ testsuite/sgi/src/bomb-65535.sgi | Bin 0 -> 512 bytes testsuite/softimage/ref/out.txt | 1 + testsuite/softimage/run.py | 4 +++ testsuite/softimage/src/bomb-40000.pic | Bin 0 -> 118 bytes testsuite/targa/ref/out.txt | 3 +++ testsuite/targa/run.py | 2 ++ testsuite/targa/src/crash-bomb-65535.tga | Bin 0 -> 18 bytes testsuite/webp/ref/out-webp1.1.txt | 3 +++ testsuite/webp/run.py | 8 ++++++ testsuite/webp/src/bomb-16383.webp | Bin 0 -> 76 bytes 63 files changed, 294 insertions(+), 21 deletions(-) create mode 100644 testsuite/cineon/src/bomb-46341.cin create mode 100644 testsuite/dpx/src/bomb-46341.dpx create mode 100644 testsuite/hdr/src/bomb-40000.hdr create mode 100644 testsuite/ico/src/bomb-png-46340.ico create mode 100644 testsuite/iff/src/bomb-46341.iff create mode 100644 testsuite/openexr-bomb/ref/out-exr22.txt create mode 100644 testsuite/openexr-bomb/ref/out.txt create mode 100644 testsuite/openexr-bomb/run.py create mode 100644 testsuite/openexr-bomb/src/bomb.exr create mode 100644 testsuite/png-bomb/ref/out.txt create mode 100644 testsuite/png-bomb/run.py create mode 100644 testsuite/png-bomb/src/bomb.png create mode 100644 testsuite/pnm/src/.gitattributes create mode 100644 testsuite/pnm/src/bomb-65000.pgm create mode 100644 testsuite/sgi/src/bomb-65535.sgi create mode 100644 testsuite/softimage/src/bomb-40000.pic create mode 100644 testsuite/targa/src/crash-bomb-65535.tga create mode 100644 testsuite/webp/src/bomb-16383.webp diff --git a/src/build-scripts/ci-test.bash b/src/build-scripts/ci-test.bash index 89f3f5138b..79b1e502e8 100755 --- a/src/build-scripts/ci-test.bash +++ b/src/build-scripts/ci-test.bash @@ -47,6 +47,11 @@ echo "Default timeout ${CTEST_TEST_TIMEOUT}" echo "Test exclusions '${CTEST_EXCLUSIONS}'" echo "CTEST_ARGS '${CTEST_ARGS}'" +# Adjustments to OPENIMAGEIO_OPTIONS +# - limits:imagesize_MB=32768 forces the same limit regardless of physical +# memory size of the CI runner, for the sake of uniform error messages. +export OPENIMAGEIO_OPTIONS="limits:imagesize_MB=32768,${OPENIMAGEIO_OPTIONS}" + pushd build time ctest -C ${CMAKE_BUILD_TYPE} --force-new-ctest-process --output-on-failure \ -E "${CTEST_EXCLUSIONS}" --timeout ${CTEST_TEST_TIMEOUT} ${CTEST_ARGS} diff --git a/src/cineon.imageio/cineoninput.cpp b/src/cineon.imageio/cineoninput.cpp index cf76059cba..f7704e4b42 100644 --- a/src/cineon.imageio/cineoninput.cpp +++ b/src/cineon.imageio/cineoninput.cpp @@ -7,6 +7,7 @@ #include "libcineon/Cineon.h" #include +#include #include #include #include @@ -130,7 +131,9 @@ CineonInput::open(const std::string& name, ImageSpec& newspec) m_spec = ImageSpec(m_cin.header.Width(), m_cin.header.Height(), nchannels, typedesc); - if (!check_open(m_spec, { 0, 1 << 30, 0, 1 << 30, 0, 1, 0, 8 })) { + if (!check_open(m_spec, { 0, 1 << 30, 0, 1 << 30, 0, 1, 0, 8 }) + || !check_compression_ratio(m_spec, Filesystem::file_size(name))) { + close(); return false; } diff --git a/src/cmake/testing.cmake b/src/cmake/testing.cmake index 15605b35c2..8aa6ca0bf1 100644 --- a/src/cmake/testing.cmake +++ b/src/cmake/testing.cmake @@ -432,6 +432,9 @@ macro (oiio_add_all_tests) # Regression test (compiles its own helper and generates its own image) # for a partial edge-tile heap overflow in the OpenEXR readers. oiio_add_tests (openexr-partialtile) + # Self-contained decompression-bomb regression (ships its own tiny fixture); + # exercises both the C++ and C-API readers via the openexr:core attribute. + oiio_add_tests (openexr-bomb) # if (NOT DEFINED ENV{${PROJECT_NAME}_CI}) # oiio_add_tests (openexr-damaged # IMAGEDIR openexr-images @@ -446,6 +449,8 @@ macro (oiio_add_all_tests) oiio_add_tests (png png-damaged ENABLEVAR ENABLE_PNG IMAGEDIR oiio-images/png) + # Self-contained decompression-bomb regression (ships its own tiny fixture). + oiio_add_tests (png-bomb ENABLEVAR ENABLE_PNG) oiio_add_tests (pnm ENABLEVAR ENABLE_PNM IMAGEDIR oiio-images) diff --git a/src/dds.imageio/ddsinput.cpp b/src/dds.imageio/ddsinput.cpp index 4bf4806438..355cddd3b2 100644 --- a/src/dds.imageio/ddsinput.cpp +++ b/src/dds.imageio/ddsinput.cpp @@ -944,6 +944,13 @@ DDSInput::seek_subimage(int subimage, int miplevel) return false; } + // Decompression-bomb guard: reject a tiny file that declares a huge + // decoded image before internal_readimg() commits to the m_buf allocation. + // For compressed formats the declared bytes are the decoded (RGBA/half) + // size; genuine BCn ratios are well under the default cap. + if (!check_compression_ratio(m_spec, ioproxy()->size())) + return false; + m_subimage = subimage; m_miplevel = miplevel; return true; diff --git a/src/dpx.imageio/dpxinput.cpp b/src/dpx.imageio/dpxinput.cpp index 63934f6450..878552fad8 100644 --- a/src/dpx.imageio/dpxinput.cpp +++ b/src/dpx.imageio/dpxinput.cpp @@ -213,7 +213,8 @@ DPXInput::seek_subimage(int subimage, int miplevel) m_spec = ImageSpec(m_dpx.header.Width(), m_dpx.header.Height(), m_dpx.header.ImageElementComponentCount(subimage), typedesc); - if (!check_open(m_spec, { 0, 1 << 30, 0, 1 << 30, 0, 1 << 16, 0, 8 })) + if (!check_open(m_spec, { 0, 1 << 30, 0, 1 << 30, 0, 1 << 16, 0, 8 }) + || !check_compression_ratio(m_spec, m_filesize)) return false; // xOffset/yOffset are defined as unsigned 32-bit integers, but m_spec.x/y are signed diff --git a/src/fits.imageio/fitsinput.cpp b/src/fits.imageio/fitsinput.cpp index ec36509fa7..648c697996 100644 --- a/src/fits.imageio/fitsinput.cpp +++ b/src/fits.imageio/fitsinput.cpp @@ -191,6 +191,19 @@ FitsInput::set_spec_info() m_spec.set_format(TypeDesc::FLOAT); else if (m_bitpix == -64) m_spec.set_format(TypeDesc::DOUBLE); + + // Validate dimensions and implied size for non-empty images. FITS permits + // an empty (0x0) primary HDU, which we pass through as metadata-only. + if (m_spec.width > 0 && m_spec.height > 0) { + if (m_spec.format == TypeDesc::UNKNOWN) { + errorfmt("Unsupported FITS BITPIX value {}", m_bitpix); + return false; + } + if (!check_open(m_spec, { 0, 1 << 20, 0, 1 << 20, 0, 1 << 16, 0, 4 })) + return false; + if (!check_compression_ratio(m_spec, Filesystem::file_size(m_filename))) + return false; + } return true; } diff --git a/src/fuzz/populate_corpora.py b/src/fuzz/populate_corpora.py index 4c85905a7c..8468b58a8e 100644 --- a/src/fuzz/populate_corpora.py +++ b/src/fuzz/populate_corpora.py @@ -56,6 +56,7 @@ def _find_images_root() -> Path: ("../bmpsuite", ["bmp", "BMP"]), ], "cineon": [ + ("testsuite/cineon/src", ["cin"]), ("../oiio-images/cineon", ["cin"]), ("../oiio-images", ["cin"]), ], @@ -68,25 +69,32 @@ def _find_images_root() -> Path: ("testsuite/dicom/src", ["dcm"]), ], "dpx": [ + ("testsuite/dpx/src", ["dpx"]), ("../oiio-images/dpx", ["dpx"]), ("../dpx-images-spi", ["dpx"]), ], "openexr": [ + ("testsuite/openexr-bomb/src", ["exr"]), ("../oiio-images", ["exr"]), ("testsuite/oiio-images", ["exr"]), ], "ffmpeg": [ ("testsuite/ffmpeg/ref", ["mkv", "mov", "mp4", "avi"]), + ("testsuite/ffmpeg/src", ["mkv", "mov", "mp4", "avi"]), ], "fits": [ + ("testsuite/fits/src", ["fits", "fit"]), ("../fits-images/ftt4b", ["fits", "fit"]), ("../fits-images/pg93", ["fits", "fit"]), ], "gif": [ + ("testsuite/gif/src", ["gif"]), ("../oiio-images/gif", ["gif"]), ], # hdr: synthetic seed committed; no further sources needed - "hdr": [], + "hdr": [ + ("testsuite/hdr/src", ["hdr", "rgbe"]), + ], "heif": [ ("../oiio-images/heif", ["heif", "heic", "avif"]), ("../heif-images", ["heif", "heic", "avif"]), @@ -96,22 +104,30 @@ def _find_images_root() -> Path: ("../oiio-images/ico", ["ico"]), ], # iff: synthetic seed committed; no further sources needed - "iff": [], + "iff": [ + ("testsuite/iff/src", ["iff", "z"]), + ], "jpeg": [ + ("testsuite/jpeg-corrupt/src", ["jpg", "jpeg"]), ("../oiio-images/jpeg", ["jpg", "jpeg"]), ("testsuite/jpeg/src", ["jpg", "jpeg"]), ("../oiio-images", ["jpg", "jpeg"]), ], "jpeg2000": [ + ("testsuite/htj2k/src", ["j2c"]), ("../oiio-images/jpeg2000", ["jp2", "j2k"]), ("../j2kp4files_v1_5/codestreams_profile0", ["j2k"]), ], - # jpegxl: synthetic seed committed; no further sources needed - "jpegxl": [], + "jpegxl": [ + ("testsuite/jxl/src", ["jxl"]), + ], "openvdb": [ ("testsuite/openvdb/src", ["vdb"]), + ("testsuite/openvdb-damaged/src", ["vdb"]), ], "png": [ + ("testsuite/png-bomb/src", ["png"]), + ("testsuite/png-damaged/src", ["png"]), ("../oiio-images/png", ["png"]), ], "pnm": [ @@ -126,6 +142,7 @@ def _find_images_root() -> Path: ("testsuite/ptex/src", ["ptx", "ptex"]), ], "raw": [ + ("testsuite/raw/src", ["dng"]), ("../oiio-images/raw", ["cr2", "nef", "arw", "raf", "rw2", "orf", "CR2", "NEF", "ARW", "RAF", "RW2", "ORF"]), ], @@ -133,8 +150,10 @@ def _find_images_root() -> Path: ("testsuite/rla/src", ["rla"]), ("../oiio-images/rla", ["rla"]), ], - # sgi: synthetic seed committed; no further sources needed - "sgi": [], + "sgi": [ + ("testsuite/sgi/src", ["sgi", "rgb", "rgba", "bw", "int", "inta"]), + ("../oiio-images", ["sgi", "rgb"]), + ], "softimage": [ ("testsuite/softimage/src", ["pic"]), ("../oiio-images/softimage", ["pic"]), @@ -149,10 +168,12 @@ def _find_images_root() -> Path: ("testsuite/tiff-suite/src", ["tif", "tiff"]), ], "webp": [ + ("testsuite/webp/src", ["webp"]), ("../oiio-images/webp", ["webp"]), ], # zfile: use the reference output produced by the testsuite (a valid zfile) "zfile": [ + ("testsuite/zfile/src", ["zfile"]), ("testsuite/zfile/ref", ["zfile"]), ], } diff --git a/src/gif.imageio/gifinput.cpp b/src/gif.imageio/gifinput.cpp index 4fa9c5e842..a13ffa45af 100644 --- a/src/gif.imageio/gifinput.cpp +++ b/src/gif.imageio/gifinput.cpp @@ -460,7 +460,8 @@ GIFInput::seek_subimage(int subimage, int miplevel) m_spec.full_width = m_spec.width; m_spec.full_depth = m_spec.depth; - if (!check_open(m_spec, { 0, 32768, 0, 32768, 0, 1, 0, 4 })) { + if (!check_open(m_spec, { 0, 32768, 0, 32768, 0, 1, 0, 4 }) + || !check_compression_ratio(m_spec, ioproxy()->size())) { return false; } diff --git a/src/hdr.imageio/hdrinput.cpp b/src/hdr.imageio/hdrinput.cpp index d6ae224369..22a8d7e5a2 100644 --- a/src/hdr.imageio/hdrinput.cpp +++ b/src/hdr.imageio/hdrinput.cpp @@ -256,7 +256,8 @@ HdrInput::open(const std::string& name, ImageSpec& newspec) m_spec.full_width = m_spec.width; m_spec.full_height = m_spec.height; // Validation of resolution - if (!check_open(m_spec, { 0, 65535, 0, 65535, 0, 1, 0, 4 })) { + if (!check_open(m_spec, { 0, 65535, 0, 65535, 0, 1, 0, 4 }) + || !check_compression_ratio(m_spec, ioproxy()->size())) { close(); return false; } diff --git a/src/heif.imageio/heifinput.cpp b/src/heif.imageio/heifinput.cpp index fa703b63bb..b135f4b8fe 100644 --- a/src/heif.imageio/heifinput.cpp +++ b/src/heif.imageio/heifinput.cpp @@ -299,6 +299,17 @@ HeifInput::seek_subimage(int subimage, int miplevel) : heif_channel_interleaved; const int nchannels = is_monochrome ? 1 : m_has_alpha ? 4 : 3; + // Validate the handle's declared dimensions against OIIO's limits BEFORE + // asking libheif to decode (and allocate) the full image. libheif has its + // own internal security limits, but this additionally enforces OIIO's + // limits:* policy and rejects degenerate (zero/negative) dimensions. + m_spec = ImageSpec(m_ihandle.get_width(), m_ihandle.get_height(), nchannels, + (m_bitdepth > 8) ? TypeUInt16 : TypeUInt8); + if (!check_open(m_spec, { 0, 1 << 18, 0, 1 << 18, 0, 1, 0, 4 })) { + m_ctx.reset(); + return false; + } + #if 0 try { m_himage = m_ihandle.decode_image(heif_colorspace_RGB, chroma); diff --git a/src/ico.imageio/icoinput.cpp b/src/ico.imageio/icoinput.cpp index f25740415d..946ceee911 100644 --- a/src/ico.imageio/icoinput.cpp +++ b/src/ico.imageio/icoinput.cpp @@ -243,7 +243,8 @@ ICOInput::seek_subimage(int subimage, int miplevel) bool ok = PNG_pvt::read_info(m_png, m_info, m_bpp, m_color_type, m_interlace_type, m_bg, m_spec, true); if (!ok || m_err - || !check_open(m_spec, { 0, 1 << 30, 0, 1 << 30, 0, 1, 0, 4 })) { + || !check_open(m_spec, { 0, 1 << 30, 0, 1 << 30, 0, 1, 0, 4 }) + || !check_compression_ratio(m_spec, ioproxy()->size())) { return false; } diff --git a/src/iff.imageio/iffinput.cpp b/src/iff.imageio/iffinput.cpp index e965dfa298..602a4cf7b1 100644 --- a/src/iff.imageio/iffinput.cpp +++ b/src/iff.imageio/iffinput.cpp @@ -283,10 +283,9 @@ IffInput::open(const std::string& name, ImageSpec& spec) // we save this position - it will be helpful in read_native_tile m_tbmp_start = m_header.tbmp_start; - // Validity check resolutions. Width and height are a uint16, but this is - // an old format, we are guessing that there are no Maya IFF files that - // are > 64k pixels per side. - if (!check_open(m_spec, { 0, 1 << 16, 0, 1 << 16, 0, 1, 0, 5 })) + // Validity check resolutions and check for decompression bombs. + if (!check_open(m_spec, { 0, 1 << 16, 0, 1 << 16, 0, 1, 0, 5 }) + || !check_compression_ratio(m_spec, ioproxy()->size())) return false; spec = m_spec; diff --git a/src/jpeg.imageio/jpeginput.cpp b/src/jpeg.imageio/jpeginput.cpp index cacd06d337..53f136f711 100644 --- a/src/jpeg.imageio/jpeginput.cpp +++ b/src/jpeg.imageio/jpeginput.cpp @@ -274,9 +274,16 @@ JpgInput::open(const std::string& name, ImageSpec& newspec) m_spec = ImageSpec(m_cinfo.output_width, m_cinfo.output_height, nchannels, TypeDesc::UINT8); + // Validity check resolutions. if (!check_open(m_spec, { 0, 1 << 16, 0, 1 << 16, 0, 1, 0, 3 })) return false; + // check_open's size cap still admits dimensions that are absurd for a tiny + // file, so also bound the declared-vs-compressed ratio. + imagesize_t filesize = m_io ? m_io->size() : Filesystem::file_size(name); + if (!check_compression_ratio(m_spec, filesize)) + return false; + // Assume JPEG is in sRGB unless the Exif or XMP tags say otherwise. m_spec.set_colorspace("srgb_rec709_scene"); diff --git a/src/libOpenImageIO/imageinput.cpp b/src/libOpenImageIO/imageinput.cpp index e400d6eff2..2e3c87dc36 100644 --- a/src/libOpenImageIO/imageinput.cpp +++ b/src/libOpenImageIO/imageinput.cpp @@ -1725,6 +1725,25 @@ ImageInput::check_open(const ImageSpec& spec, ROI range, uint64_t /*flags*/) return false; } + // Check for sensible tile sizes. A tile dimension of 0 means "not + // tiled", which is always fine; only reject negative sizes and tile + // dimensions that exceed the same per-format ceiling already applied + // to the image resolution above. + if (spec.tile_width < 0 || spec.tile_height < 0 || spec.tile_depth < 0) { + errorfmt( + "{} tile size may not be negative, but was {}x{}x{}. Possible corrupt input?", + format_name(), spec.tile_width, spec.tile_height, spec.tile_depth); + return false; + } + if (spec.tile_width > range.width() || spec.tile_height > range.height() + || spec.tile_depth > range.depth()) { + errorfmt( + "{} tile size may not exceed {}x{}x{}, but was {}x{}x{}. Possible corrupt input?", + format_name(), range.width(), range.height(), range.depth(), + spec.tile_width, spec.tile_height, spec.tile_depth); + return false; + } + // Check for invalid full_* values for sensibility if (spec.full_width == 0 && spec.full_height == 0 && spec.full_depth == 0 && supports("noimage")) { diff --git a/src/null.imageio/nullimageio.cpp b/src/null.imageio/nullimageio.cpp index 89afc9c0c6..917c1684e4 100644 --- a/src/null.imageio/nullimageio.cpp +++ b/src/null.imageio/nullimageio.cpp @@ -319,6 +319,14 @@ NullInput::open(const std::string& name, ImageSpec& newspec, } } + // The query arguments above are parsed input, even though no file is + // ever read. Without this, "foo.null?CHANNELS=-1" sign-extended its way + // into a std::length_error that nothing catches, "RES=-4x-4" handed the + // caller a negative-sized spec, and a large enough CHANNELS hung building + // channel names. check_open() also rejects a negative or oversized TILE. + if (!check_open(m_topspec, ROI(0, 1 << 20, 0, 1 << 20, 0, 1 << 20, 0, 1024))) + return false; + m_value.resize(m_topspec.pixel_bytes()); // default fills with 0's if (fvalue.size()) { // Convert float to the native type diff --git a/src/openexr.imageio/exrinput.cpp b/src/openexr.imageio/exrinput.cpp index 31dcb516a2..c2015b85f6 100644 --- a/src/openexr.imageio/exrinput.cpp +++ b/src/openexr.imageio/exrinput.cpp @@ -1137,6 +1137,13 @@ OpenEXRInput::seek_subimage(int subimage, int miplevel) if (!check_open(m_spec, { 0, 1 << 30, 0, 1 << 30, 0, 1, 0, 1 << 12 })) return false; + // check_open's size cap still admits a dataWindow that is absurd for a tiny + // compressed file, so also bound the declared-vs-compressed ratio. + imagesize_t filesize = m_io ? m_io->size() + : Filesystem::file_size(m_filename); + if (!check_compression_ratio(m_spec, filesize)) + return false; + if (miplevel == 0 && part.levelmode == Imf::ONE_LEVEL) { return true; } diff --git a/src/openexr.imageio/exrinput_c.cpp b/src/openexr.imageio/exrinput_c.cpp index 8933d3e77e..f4f7b6a8cc 100644 --- a/src/openexr.imageio/exrinput_c.cpp +++ b/src/openexr.imageio/exrinput_c.cpp @@ -1121,6 +1121,13 @@ OpenEXRCoreInput::seek_subimage(int subimage, int miplevel) if (!check_open(m_spec, { 0, 1 << 30, 0, 1 << 30, 0, 1, 0, 1 << 12 })) return false; + // check_open's size cap still admits a dataWindow that is absurd for a tiny + // compressed file, so also bound the declared-vs-compressed ratio. + imagesize_t filesize = m_userdata.m_io ? m_userdata.m_io->size() + : Filesystem::file_size(m_filename); + if (!check_compression_ratio(m_spec, filesize)) + return false; + if (miplevel == 0 && part.levelmode == EXR_TILE_ONE_LEVEL) { return true; } diff --git a/src/png.imageio/pnginput.cpp b/src/png.imageio/pnginput.cpp index 02841868b8..55afc89ee8 100644 --- a/src/png.imageio/pnginput.cpp +++ b/src/png.imageio/pnginput.cpp @@ -183,6 +183,15 @@ PNGInput::open(const std::string& name, ImageSpec& newspec) return false; } + // check_open's size cap still admits dimensions that are absurd for a tiny + // compressed file, so also bound the declared-vs-compressed ratio. + imagesize_t filesize = ioproxy() ? ioproxy()->size() + : Filesystem::file_size(m_filename); + if (!check_compression_ratio(m_spec, filesize)) { + close(); + return false; + } + string_view colorspace = m_spec.get_string_attribute("oiio:ColorSpace", "srgb_rec709_scene"); const ColorConfig& colorconfig(ColorConfig::default_colorconfig()); diff --git a/src/pnm.imageio/pnminput.cpp b/src/pnm.imageio/pnminput.cpp index 959d3a749e..6d1cb90bf5 100644 --- a/src/pnm.imageio/pnminput.cpp +++ b/src/pnm.imageio/pnminput.cpp @@ -515,6 +515,13 @@ PNMInput::open(const std::string& name, ImageSpec& newspec) if (!check_open(m_spec)) // check for apparently invalid values return false; + // Reject a tiny file that declares a huge image before the caller + // allocates the full (declared) pixel buffer. Binary PNM data must be + // present in the file and ASCII values cost >=2 bytes each, so a + // legitimate file never has an extreme ratio. + if (!check_compression_ratio(m_spec, m_io->size())) + return false; + m_remaining = append_remainder_to_buffer(m_file_contents, m_io, m_remaining); m_after_header = m_remaining; diff --git a/src/rla.imageio/rlainput.cpp b/src/rla.imageio/rlainput.cpp index 68ef176f9f..9f73542f34 100644 --- a/src/rla.imageio/rlainput.cpp +++ b/src/rla.imageio/rlainput.cpp @@ -331,6 +331,11 @@ RLAInput::seek_subimage(int subimage, int miplevel) 3 + 3 + 256 })) return false; + // Check for uncompressed size that is wildly out of proportion to + // file size. + if (!check_compression_ratio(m_spec, ioproxy()->size())) + return false; + // set channel formats and stride int z_channel = -1; m_stride = 0; diff --git a/src/sgi.imageio/sgiinput.cpp b/src/sgi.imageio/sgiinput.cpp index 9b6efa6843..2c1364628e 100644 --- a/src/sgi.imageio/sgiinput.cpp +++ b/src/sgi.imageio/sgiinput.cpp @@ -159,7 +159,8 @@ SgiInput::open(const std::string& name, ImageSpec& spec) m_sgi_header.bpc == 1 ? TypeDesc::UINT8 : TypeDesc::UINT16); - if (!check_open(m_spec, { 0, 65535, 0, 65535, 0, 1, 0, 4 })) { + if (!check_open(m_spec, { 0, 65535, 0, 65535, 0, 1, 0, 4 }) + || !check_compression_ratio(m_spec, ioproxy()->size())) { close(); return false; } diff --git a/src/softimage.imageio/softimageinput.cpp b/src/softimage.imageio/softimageinput.cpp index 69c9956b0e..47b0336285 100644 --- a/src/softimage.imageio/softimageinput.cpp +++ b/src/softimage.imageio/softimageinput.cpp @@ -212,7 +212,8 @@ SoftimageInput::open(const std::string& name, ImageSpec& spec) m_spec = ImageSpec(m_pic_header.width, m_pic_header.height, nchannels, widest); - if (!check_open(m_spec, { 0, 65535, 0, 65535, 0, 1, 0, 4 })) { + if (!check_open(m_spec, { 0, 65535, 0, 65535, 0, 1, 0, 4 }) + || !check_compression_ratio(m_spec, Filesystem::file_size(m_filename))) { close(); return false; } diff --git a/src/webp.imageio/webpinput.cpp b/src/webp.imageio/webpinput.cpp index ae2b36c57d..d30b568f68 100644 --- a/src/webp.imageio/webpinput.cpp +++ b/src/webp.imageio/webpinput.cpp @@ -241,6 +241,13 @@ WebpInput::open(const std::string& name, ImageSpec& spec, } } + // Validate the declared canvas extents and the implied uncompressed size + // BEFORE allocating the decoded-image buffer, so a malformed header cannot + // drive a large allocation. + if (!check_open(m_spec, { 0, (1 << 14) - 1, 0, (1 << 14) - 1, 0, 1, 0, 4 }) + || !check_compression_ratio(m_spec, m_image_size)) + return false; + // Make space for the decoded image m_decoded_image.reset(new uint8_t[m_spec.image_bytes()]); @@ -250,9 +257,6 @@ WebpInput::open(const std::string& name, ImageSpec& spec, m_spec.attribute("oiio:UnassociatedAlpha", 1); } - if (!check_open(m_spec, { 0, (1 << 14) - 1, 0, (1 << 14) - 1, 0, 1, 0, 4 })) - return false; - seek_subimage(0, 0); spec = m_spec; return true; diff --git a/src/zfile.imageio/zfile.cpp b/src/zfile.imageio/zfile.cpp index d2e326aa4a..56273a9197 100644 --- a/src/zfile.imageio/zfile.cpp +++ b/src/zfile.imageio/zfile.cpp @@ -189,6 +189,12 @@ ZfileInput::open(const std::string& name, ImageSpec& newspec) } m_spec = ImageSpec(header.width, header.height, 1, TypeDesc::FLOAT); + // The reader had no open-time validation: header.width/height are signed + // 16-bit and could be negative or zero, and a tiny file could declare a + // huge (gzip-bomb) image. Reject both before the caller allocates. + if (!check_open(m_spec, { 0, 32767, 0, 32767, 0, 1, 0, 1 }) + || !check_compression_ratio(m_spec, Filesystem::file_size(m_filename))) + return false; if (m_spec.channelnames.size() == 0) m_spec.channelnames.resize(1); m_spec.channelnames[0] = std::string("z"); diff --git a/testsuite/cineon/ref/out.txt b/testsuite/cineon/ref/out.txt index 07f06a1928..e1a2ef796f 100644 --- a/testsuite/cineon/ref/out.txt +++ b/testsuite/cineon/ref/out.txt @@ -39,3 +39,6 @@ Full command line was: oiiotool ERROR: read : "src/broken_bitdepth2.cin": Invalid bitdepth in channel 0: 26 bits Full command line was: > oiiotool --info --hash src/broken_bitdepth2.cin +oiiotool ERROR: read : "src/bomb-46341.cin": cineon header claims a 12288 MB image from a 2048 byte file; probably a corrupt or malicious header +Full command line was: +> oiiotool --info --hash src/bomb-46341.cin diff --git a/testsuite/cineon/run.py b/testsuite/cineon/run.py index 9e01f91ed5..a8bc24b3d7 100755 --- a/testsuite/cineon/run.py +++ b/testsuite/cineon/run.py @@ -17,5 +17,9 @@ # Regression test for a per-channel bit depth libcineon doesn't recognize # (used to assert/abort inside CineonHeader.cpp instead of erroring out). command += info_command ("src/broken_bitdepth2.cin", verbose=False, failureok=True) +# Regression test: a 2 KB Cineon header declaring a ~12 GB image +# (46341x46341x3) -- a decompression bomb the compression-ratio guard must +# reject before any large allocation. +command += info_command ("src/bomb-46341.cin", verbose=False, failureok=True) outputs = [ "out.txt" ] diff --git a/testsuite/cineon/src/bomb-46341.cin b/testsuite/cineon/src/bomb-46341.cin new file mode 100644 index 0000000000000000000000000000000000000000..c7a5abe2dda727da87b15d618b6089e61a2a880a GIT binary patch literal 2048 zcmZqBioedl(7?dJzyie;3=E8k3}GgEra%D({k+r?{fzwF)RfGkV*Q+S{gTw;620Wi zJdh-nfRTZbk(Hr=m7zHU1Bf!RGBDLPG$77&=Kud07`cFIx3WSoRNH}i7a+@lfeDWs zGafnOY^Rf||9%V%_%NNEF_6{(W0x2}kMxSm9nM9mi6yBi3gww4847--*{KSKMtVlL zRAY*Rv|x%NLpv2}uM$`+fEE1rD=*3{DM`&!NU8+;l90Ah N`Oy#<4S_)#0su$WQL_L5 literal 0 HcmV?d00001 diff --git a/testsuite/dpx/ref/out.txt b/testsuite/dpx/ref/out.txt index d4c13b5d2e..df2109bf80 100644 --- a/testsuite/dpx/ref/out.txt +++ b/testsuite/dpx/ref/out.txt @@ -219,3 +219,6 @@ src/crash-1chan-10bit-filled-methodA.dpx : 80 x 60, 3 channel, uint10 dpx oiio:BitsPerSample: 10 oiio:subimages: 2 smpte:TimeCode: 00:00:00:00 +oiiotool ERROR: read : "src/bomb-46341.dpx": dpx header claims a 12288 MB image from a 21248 byte file; probably a corrupt or malicious header +Full command line was: +> oiiotool --info -v -a --no-metamatch DateTime|Software|OriginatingProgram|ImageHistory --hash src/bomb-46341.dpx diff --git a/testsuite/dpx/run.py b/testsuite/dpx/run.py index 1acaee89e7..1f6c46a2db 100755 --- a/testsuite/dpx/run.py +++ b/testsuite/dpx/run.py @@ -45,3 +45,8 @@ # remained for the last (partial) group in the scanline, writing/reading # one uint16 past the end of the caller's scanline buffer. command += info_command("src/crash-1chan-10bit-filled-methodA.dpx", safematch=True) + +# Regression test: a 21 KB DPX whose header declares a ~12 GB image +# (46341x46341x3) -- a decompression bomb the compression-ratio guard must +# reject before any large allocation. +command += info_command("src/bomb-46341.dpx", safematch=True, failureok=True) diff --git a/testsuite/dpx/src/bomb-46341.dpx b/testsuite/dpx/src/bomb-46341.dpx new file mode 100644 index 0000000000000000000000000000000000000000..da9413b8f56aeff717d95f051a1100506d2a386f GIT binary patch literal 21248 zcmeI1O=uHA6vrn4(>5x2=%J!W1O=gzY6K4@eicD0#iU7F3wmgwYfMd>AffTlnn*qN zP@&%Hts-K?c<>gR_;FBCPp$M2K}Ezv586gcc!gszvD>wa~Gl;QTFexK6rvK_V`u{#Eo z5!;n;IMmwG5{X3YL3Q6x_x(Y8@S1Pk>%#-}Cs0&}{#ms$n#h)?q%^6b{Ooj;Sx1pr zOUFPxBW#TLm58l>*cVV*Z2{$SnE9Edv2>^>$i>@|le;jzmSs}p5fd^RA90nk-gK0Z zbkwcNow6YY)v@o!Mz=A^?Unb zCQIUlwfS}P<79ZQ^NgsMTKQ;eqH+KL literal 0 HcmV?d00001 diff --git a/testsuite/gif/ref/out.txt b/testsuite/gif/ref/out.txt index 30ce27631e..aebb422f76 100644 --- a/testsuite/gif/ref/out.txt +++ b/testsuite/gif/ref/out.txt @@ -109,6 +109,6 @@ tahoe-tiny.gif : 128 x 96, 4 channel, uint8 gif oiiotool ERROR: read : "src/crash_4163.gif": Wrong record type detected Full command line was: > oiiotool -nostderr -oiioattrib try_all_readers 0 src/crash_4163.gif -o test.exr -oiiotool ERROR: read : "src/gif_idx_overflow_32768x16385_top16384_1x1.gif": GIF image canvas is too large +oiiotool ERROR: read : "src/gif_idx_overflow_32768x16385_top16384_1x1.gif": gif header claims a 2048 MB image from a 35 byte file; probably a corrupt or malicious header Full command line was: > oiiotool --info -oiioattrib try_all_readers 0 src/gif_idx_overflow_32768x16385_top16384_1x1.gif diff --git a/testsuite/hdr/ref/out.txt b/testsuite/hdr/ref/out.txt index e4fbc081ee..f8d8c37169 100644 --- a/testsuite/hdr/ref/out.txt +++ b/testsuite/hdr/ref/out.txt @@ -25,3 +25,6 @@ MtTamWest.hdr : 1214 x 732, 3 channel, float hdr oiiotool ERROR: read : "src/crash-res.hdr": hdr image resolution may not exceed 65535x65535, but the file appears to be 8888888x4. Possible corrupt input? Full command line was: > oiiotool -nostderr -oiioattrib try_all_readers 0 src/crash-res.hdr -o test.exr +oiiotool ERROR: read : "src/bomb-40000.hdr": hdr header claims a 18310 MB image from a 53 byte file; probably a corrupt or malicious header +Full command line was: +> oiiotool -nostderr -oiioattrib try_all_readers 0 -info src/bomb-40000.hdr diff --git a/testsuite/hdr/run.py b/testsuite/hdr/run.py index 3c21d3d05b..14102337a3 100755 --- a/testsuite/hdr/run.py +++ b/testsuite/hdr/run.py @@ -14,3 +14,8 @@ # Regression test: invalid resolution command += oiiotool ("-nostderr -oiioattrib try_all_readers 0 src/crash-res.hdr -o test.exr", failureok = True) + +# Regression test: a 53-byte header declaring a ~18 GB image (40000x40000). +# The compression-ratio guard must reject it before the caller allocates the +# full pixel buffer. +command += oiiotool ("-nostderr -oiioattrib try_all_readers 0 -info src/bomb-40000.hdr", failureok=True) diff --git a/testsuite/hdr/src/bomb-40000.hdr b/testsuite/hdr/src/bomb-40000.hdr new file mode 100644 index 0000000000..e8d2f7e946 --- /dev/null +++ b/testsuite/hdr/src/bomb-40000.hdr @@ -0,0 +1,4 @@ +#?RADIANCE +FORMAT=32-bit_rle_rgbe + +-Y 40000 +X 40000 diff --git a/testsuite/ico/ref/out.txt b/testsuite/ico/ref/out.txt index e3fd37e584..e7da25958b 100644 --- a/testsuite/ico/ref/out.txt +++ b/testsuite/ico/ref/out.txt @@ -50,3 +50,4 @@ iconvert ERROR copying "src/crash-002c.ico" to "out.tif" : Read error: hit end of file in ico reader iconvert ERROR copying "src/bad-palette.ico" to "out.tif" : Possible corruption: index exceeds palette size +iconvert ERROR: ico header claims a 6143 MB image from a 375 byte file; probably a corrupt or malicious header diff --git a/testsuite/ico/run.py b/testsuite/ico/run.py index d4f5cf8ba7..93487d6ea8 100755 --- a/testsuite/ico/run.py +++ b/testsuite/ico/run.py @@ -12,4 +12,7 @@ command += iconvert ("src/crash-002c.ico out.tif") command += iconvert ("src/bad-not-8x-wide.ico out.tif") command += iconvert ("src/bad-palette.ico out.tif") +# ICO embedding a PNG whose IHDR declares a ~6 GB image (46340x46340). The +# compression-ratio guard must reject it before the PNG buffer is allocated. +command += iconvert ("src/bomb-png-46340.ico out.tif") diff --git a/testsuite/ico/src/bomb-png-46340.ico b/testsuite/ico/src/bomb-png-46340.ico new file mode 100644 index 0000000000000000000000000000000000000000..11914a3a495447450ff6e3e2c55e86726304aa03 GIT binary patch literal 375 zcmZQzU<5)CU}R8WNCYy)fb7lyKX+a(DJ~$B*VDr#h=E}%3kY*C0Y$2qcE1OyFAj2d zVm3eS8w}*Iq&xaLGB9lH=l+w(3gjy!dj$Ccm8vl?G_)`<`~nI!ykKA`HDF+PmB7GY zHG_dcykO3*KpUV$OlpK@ny)W|7Le`bUyz#TnVXoN>glgwtY@TWpl6hlTBcy7;B08< zY;11m;$~rB;OgpPXz1qRYG7{WVrF1rZ0zD@;%4aN>SktY0dyFnAp^*rPC(27#0;PQ znSBRRocV5U#UMTy+|t^14#@B-ag8Vm&QB{TPb^AhpxE}~SGR5jn#$wp;uyj)GdV#) Y7RWPUU{rQ)E(KZZ>FVdQ&MBb@03&u&JOBUy literal 0 HcmV?d00001 diff --git a/testsuite/iff/ref/out.txt b/testsuite/iff/ref/out.txt index 25960a1142..51e226716a 100644 --- a/testsuite/iff/ref/out.txt +++ b/testsuite/iff/ref/out.txt @@ -19,3 +19,6 @@ oiiotool ERROR: read : "src/zbuffer_only.iff": IFF error ZBUFFER-only (Z-depth) IFF error could not read header Full command line was: > oiiotool --info -v -a --hash src/zbuffer_only.iff +oiiotool ERROR: read : "src/bomb-46341.iff": iff header claims a 8192 MB image from a 336 byte file; probably a corrupt or malicious header +Full command line was: +> oiiotool --info -v -a --hash src/bomb-46341.iff diff --git a/testsuite/iff/run.py b/testsuite/iff/run.py index 9a0c940b46..395740718c 100755 --- a/testsuite/iff/run.py +++ b/testsuite/iff/run.py @@ -20,3 +20,7 @@ # ImageSpec while the internal pixel size stayed 32-bit, causing # read_native_tile to write past the caller's tile buffer. Must be rejected. command += info_command("src/zbuffer_only.iff", hash=True, failureok=True) +# Regression test: a 336-byte IFF whose TBHD declares a ~8 GB image +# (46341x46341x4) -- a decompression bomb the compression-ratio guard must +# reject before any large allocation. +command += info_command("src/bomb-46341.iff", hash=True, failureok=True) diff --git a/testsuite/iff/src/bomb-46341.iff b/testsuite/iff/src/bomb-46341.iff new file mode 100644 index 0000000000000000000000000000000000000000..c3fc83b8df03cdb3193070bb7a2405c1bb9a8c6f GIT binary patch literal 336 zcmZ?s4>Dn3VDxbI^mPw$@^E2bU{GLS*vbmVj0}uGJ~I%5Xb=ViH;5jQ5GUV&Aa^H6 QpcDsK!k)qYKf}m|05h0~$N&HU literal 0 HcmV?d00001 diff --git a/testsuite/openexr-bomb/ref/out-exr22.txt b/testsuite/openexr-bomb/ref/out-exr22.txt new file mode 100644 index 0000000000..f9eea90c8e --- /dev/null +++ b/testsuite/openexr-bomb/ref/out-exr22.txt @@ -0,0 +1,6 @@ +oiiotool ERROR: read : OpenEXR exception: Cannot read image file "src/bomb.exr". Unexpected end of file. +Full command line was: +> oiiotool -oiioattrib openexr:core 0 --info -v src/bomb.exr +oiiotool ERROR: read : "src/bomb.exr": openexr header claims a 12287 MB image from a 464 byte file; probably a corrupt or malicious header +Full command line was: +> oiiotool -oiioattrib openexr:core 1 --info -v src/bomb.exr diff --git a/testsuite/openexr-bomb/ref/out.txt b/testsuite/openexr-bomb/ref/out.txt new file mode 100644 index 0000000000..df5f642624 --- /dev/null +++ b/testsuite/openexr-bomb/ref/out.txt @@ -0,0 +1,6 @@ +oiiotool ERROR: read : "src/bomb.exr": openexr header claims a 12287 MB image from a 464 byte file; probably a corrupt or malicious header +Full command line was: +> oiiotool -oiioattrib openexr:core 0 --info -v src/bomb.exr +oiiotool ERROR: read : "src/bomb.exr": openexr header claims a 12287 MB image from a 464 byte file; probably a corrupt or malicious header +Full command line was: +> oiiotool -oiioattrib openexr:core 1 --info -v src/bomb.exr diff --git a/testsuite/openexr-bomb/run.py b/testsuite/openexr-bomb/run.py new file mode 100644 index 0000000000..aa2de94ca6 --- /dev/null +++ b/testsuite/openexr-bomb/run.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO + + +failureok = True +redirect = ' >> out.txt 2>&1 ' + +# src/bomb.exr is a tiny (464-byte) EXR whose dataWindow declares a ~12 GB +# image: a decompression bomb. Both the C++ reader (openexr:core=0) and the +# C-API core reader (openexr:core=1) must reject it with a bounded error +# instead of attempting the enormous allocation. +command += oiiotool("-oiioattrib openexr:core 0 --info -v src/bomb.exr", + failureok=True) +command += oiiotool("-oiioattrib openexr:core 1 --info -v src/bomb.exr", + failureok=True) diff --git a/testsuite/openexr-bomb/src/bomb.exr b/testsuite/openexr-bomb/src/bomb.exr new file mode 100644 index 0000000000000000000000000000000000000000..a6d501ed93fcaa621e7129570d20046fc713388f GIT binary patch literal 464 zcmZ`z!A`*>J!TFNiWX2w61p(%Vs(B++>6rqR%6UM6^*ar={bO_|JP!`KaD7C7D@_C1+)54eN^=Y6yF;&cMl^_OAAgU^ mQoxGM;o|TI@v&tl3tRq)fbH(ke&dp8^CZgm8-TCR{@XV~zGw^p literal 0 HcmV?d00001 diff --git a/testsuite/png-bomb/ref/out.txt b/testsuite/png-bomb/ref/out.txt new file mode 100644 index 0000000000..a56cf0f4e0 --- /dev/null +++ b/testsuite/png-bomb/ref/out.txt @@ -0,0 +1,3 @@ +oiiotool ERROR: read : "src/bomb.png": png header claims a 6143 MB image from a 353 byte file; probably a corrupt or malicious header +Full command line was: +> oiiotool --info -v src/bomb.png diff --git a/testsuite/png-bomb/run.py b/testsuite/png-bomb/run.py new file mode 100644 index 0000000000..368d36bb37 --- /dev/null +++ b/testsuite/png-bomb/run.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python + +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO + + +failureok = True +redirect = ' >> out.txt 2>&1 ' + +# src/bomb.png is a tiny (353-byte) PNG whose IHDR declares a ~6 GB image +# (46340x46340x3): a decompression bomb. The reader must reject it with a +# bounded error instead of attempting the enormous allocation. +command += oiiotool("--info -v src/bomb.png", failureok=True) diff --git a/testsuite/png-bomb/src/bomb.png b/testsuite/png-bomb/src/bomb.png new file mode 100644 index 0000000000000000000000000000000000000000..50b5c8321dc30cff8d7a39b1b61e45c10def354a GIT binary patch literal 353 zcmeAS@N?(olHy`uVBq!ia0y~y*vbOJ985rwDyH4GJ`ElQ1AcrO0(btiI zVPik{pF~z5Um@8e$QP(oje((|g@NH0P^jSr14F3+1H-EX1_rAc3=HB0b9M#V03~8l zBRtc5eHpZXY%l+U)I87J#Pn28e+6SbBRvB>qm oiiotool --info -v -a --hash --oiioattrib try_all_readers 0 --printstats src/bad-4553.pgm +oiiotool ERROR: read : "src/bomb-65000.pgm": pnm header claims a 4029 MB image from a 19 byte file; probably a corrupt or malicious header +Full command line was: +> oiiotool --info -v -a --no-metamatch DateTime|Software|OriginatingProgram|ImageHistory --hash src/bomb-65000.pgm diff --git a/testsuite/pnm/run.py b/testsuite/pnm/run.py index be6a094e82..7729debcba 100755 --- a/testsuite/pnm/run.py +++ b/testsuite/pnm/run.py @@ -23,3 +23,8 @@ files = [ "src/bad-4552.pgm", "src/bad-4553.pgm" ] for f in files: command += info_command (f, extraargs="--oiioattrib try_all_readers 0 --printstats", failureok=True) + +# Decompression bomb: a 19-byte header declaring a ~4 GB image (65000x65000). +# The compression-ratio guard must reject it before the caller allocates the +# full pixel buffer. +command += info_command ("src/bomb-65000.pgm", failureok=True, safematch=True) diff --git a/testsuite/pnm/src/.gitattributes b/testsuite/pnm/src/.gitattributes new file mode 100644 index 0000000000..72c3181b54 --- /dev/null +++ b/testsuite/pnm/src/.gitattributes @@ -0,0 +1 @@ +bomb-65000.pgm binary diff --git a/testsuite/pnm/src/bomb-65000.pgm b/testsuite/pnm/src/bomb-65000.pgm new file mode 100644 index 0000000000..5f63882f7e --- /dev/null +++ b/testsuite/pnm/src/bomb-65000.pgm @@ -0,0 +1,3 @@ +P5 +65000 65000 +255 diff --git a/testsuite/sgi/ref/out.txt b/testsuite/sgi/ref/out.txt index 63f1b50ff1..ca8c0019e6 100644 --- a/testsuite/sgi/ref/out.txt +++ b/testsuite/sgi/ref/out.txt @@ -49,3 +49,4 @@ src/broken_rle16_len1.sgi : 1 x 1, 1 channel, uint16 sgi channel list: Y compression: "rle" iinfo ERROR: "src/broken_rle16_len1.sgi" : Corrupt RLE data +iinfo ERROR: "src/bomb-65535.sgi" : sgi header claims a 16383 MB image from a 512 byte file; probably a corrupt or malicious header diff --git a/testsuite/sgi/run.py b/testsuite/sgi/run.py index c15f74cdde..0cc96db5ee 100755 --- a/testsuite/sgi/run.py +++ b/testsuite/sgi/run.py @@ -28,3 +28,8 @@ # broken_rle16_len1.sgi tests a 16 bit RLE file that has an odd byte count. command += info_command ("--stats src/broken_rle16_len1.sgi", info_program="iinfo", failureok=True) +# bomb-65535.sgi declares a ~16 GB image (65535x65535x4) from a 512-byte +# header: a decompression bomb the compression-ratio guard must reject before +# any large allocation. +command += info_command ("--stats src/bomb-65535.sgi", + info_program="iinfo", failureok=True) diff --git a/testsuite/sgi/src/bomb-65535.sgi b/testsuite/sgi/src/bomb-65535.sgi new file mode 100644 index 0000000000000000000000000000000000000000..1442f8ded2c957807db385f7d980aaa3f7092b61 GIT binary patch literal 512 zcmZR)#mLCO{2vGySQsGSKZw%P(<{o$&q;|7cK3`ANv$X;ElQ1dE=nxUh<7SYFG?-Q pFDi*QFpP0YO-n4zDTxnAEG~}sP0Y;GGt?_i&x9H^N)7f9008jU9S;Bi literal 0 HcmV?d00001 diff --git a/testsuite/softimage/ref/out.txt b/testsuite/softimage/ref/out.txt index 471e196acf..255fa1078d 100644 --- a/testsuite/softimage/ref/out.txt +++ b/testsuite/softimage/ref/out.txt @@ -44,6 +44,7 @@ src/broken01.pic : 16 x 4, 1 channel, uint8 softimage Stats FiniteCount: 64 Constant: No Monochrome: Yes +iinfo ERROR: "src/bomb-40000.pic" : softimage header claims a 6103 MB image from a 118 byte file; probably a corrupt or malicious header src/mixed-bitdepth.pic : 2 x 1, 2 channel, uint16 softimage SHA-1: 52D6B72AAD7084E2ECF1550D500A66C42F408ED1 channel list: R, G diff --git a/testsuite/softimage/run.py b/testsuite/softimage/run.py index 8c80afe8d6..a31841ff4a 100755 --- a/testsuite/softimage/run.py +++ b/testsuite/softimage/run.py @@ -13,6 +13,10 @@ # Regression testing of error handling and corrupt files command += info_command ("--stats src/broken01.pic", info_program="iinfo", failureok=True) +# A 118-byte header declaring a ~6 GB image (40000x40000): the +# compression-ratio guard must reject it before the caller allocates. +command += info_command ("--stats src/bomb-40000.pic", + info_program="iinfo", failureok=True) # A file whose channel packets use different bit depths (here a 16-bit R # packet and an 8-bit G packet): narrower channels get promoted to the # widest depth present. This used to overrun the scanline buffer because a diff --git a/testsuite/softimage/src/bomb-40000.pic b/testsuite/softimage/src/bomb-40000.pic new file mode 100644 index 0000000000000000000000000000000000000000..d8eb5e2823551f8a29c921cdd249c4de847fc029 GIT binary patch literal 118 zcmWG;_-10?z`)>}S&^EeP>`6Mom!%hlv$#il3Gxbp^#seT9lTPU#<|4@61372=H_c bnd30W0c17^FbXg oiiotool --oiioattrib try_all_readers 0 src/crash-20250423.tga -o out.null +oiiotool ERROR: read : "src/crash-bomb-65535.tga": TGA header claims image size 12884508675 bytes, implausible for 0 bytes of remaining file data +Full command line was: +> oiiotool --oiioattrib try_all_readers 0 src/crash-bomb-65535.tga -o out.null Reading src/1x1.tga src/1x1.tga : 1 x 1, 3 channel, uint2 targa SHA-1: DB9237F28F9622F7B7E73B783A8822B63BDB3708 diff --git a/testsuite/targa/run.py b/testsuite/targa/run.py index ca4219cbc8..13f0b6f92f 100755 --- a/testsuite/targa/run.py +++ b/testsuite/targa/run.py @@ -28,6 +28,8 @@ "--oiioattrib try_all_readers 0 " "src/crash3952.tga -o crash3952.exr", failureok = True) command += oiiotool("--oiioattrib try_all_readers 0 src/crash-20250423.tga -o out.null", failureok=True); +# Tiny file declaring a huge image (decompression bomb) is rejected +command += oiiotool("--oiioattrib try_all_readers 0 src/crash-bomb-65535.tga -o out.null", failureok=True); # Test odds and ends, unusual files command += rw_command("src", "1x1.tga") diff --git a/testsuite/targa/src/crash-bomb-65535.tga b/testsuite/targa/src/crash-bomb-65535.tga new file mode 100644 index 0000000000000000000000000000000000000000..09ba4a62f1be6e590d5d207fac4d67fcd5dc77cb GIT binary patch literal 18 RcmZQzU}AuQ|3DzY000tD1Q!4R literal 0 HcmV?d00001 diff --git a/testsuite/webp/ref/out-webp1.1.txt b/testsuite/webp/ref/out-webp1.1.txt index edbdaa0cf1..76ec534165 100644 --- a/testsuite/webp/ref/out-webp1.1.txt +++ b/testsuite/webp/ref/out-webp1.1.txt @@ -29,3 +29,6 @@ short-exif-len4.webp-ok short-exif-len5.webp-ok short-exif-len6.webp-ok short-exif-len13.webp-ok +oiiotool ERROR: read : "src/bomb-16383.webp": Couldn't decode +Full command line was: +> oiiotool --info -v -a --no-metamatch DateTime|Software|OriginatingProgram|ImageHistory --hash src/bomb-16383.webp diff --git a/testsuite/webp/run.py b/testsuite/webp/run.py index 091accdf1e..dbd1108e20 100755 --- a/testsuite/webp/run.py +++ b/testsuite/webp/run.py @@ -4,6 +4,8 @@ # SPDX-License-Identifier: Apache-2.0 # https://github.com/AcademySoftwareFoundation/OpenImageIO +redirect = ' >> out.txt 2>&1 ' + files = [ "1.webp", "2.webp", "3.webp", "4.webp" ] for f in files: command += info_command (OIIO_TESTSUITE_IMAGEDIR + "/" + f) @@ -27,3 +29,9 @@ ] for f in short_exif_files: command += iconvert(f + " out.null", successmessage=f + "-ok") + +# Regression test: a 76-byte WebP whose VP8X header declares a 16383x16383 +# canvas inconsistent with its tiny frame. Must be rejected cleanly (libwebp's +# demux catches the mismatch; the open-time check_open/compression-ratio guard +# now runs before the decoded-image allocation regardless). +command += info_command ("src/bomb-16383.webp", failureok=True, safematch=True) diff --git a/testsuite/webp/src/bomb-16383.webp b/testsuite/webp/src/bomb-16383.webp new file mode 100644 index 0000000000000000000000000000000000000000..e193a0f613036251718288f0d1c2f496b8b77a8f GIT binary patch literal 76 zcmWIYbaQiIU|