From 3a613fb461420c19db39fcb6e83f7631e37d586a Mon Sep 17 00:00:00 2001 From: Larry Gritz Date: Thu, 16 Jul 2026 20:22:15 -0700 Subject: [PATCH 1/2] fix(jpeg2000): guard against oversized and decompression-bomb headers before decode check_open() validated the resolution only after opj_decode() had already run, so a corrupt or malicious header could make openjpeg spend a long time and allocate a huge buffer before OIIO ever rejected the file. Add two pre-decode guards right after opj_read_header(): 1. Build a provisional ImageSpec from the header's canvas size and per-component precision (channel count can still grow later from palette expansion, but that only makes the image larger, so the header-based estimate is a safe lower bound), and run it through check_open() before calling opj_decode(). 2. That absolute-size check alone isn't enough: a 171-byte fuzzed codestream declared a 32 x 50331680 image (9216 MB), under the default limits:imagesize_MB (32 GB), so opj_decode still hung allocating ~9 GB and decoding 1.6 billion pixels. Add a decompression-bomb check that rejects when the claimed uncompressed size both exceeds 1 GB and dwarfs the actual file size by more than 10000x -- a ratio far beyond anything a real JPEG2000 codestream produces. The existing post-decode check_open() call stays in place as the authoritative check once the final channel count is known. Verified the openjpeg conformance suite still decodes correctly, so neither guard rejects real files. Also drop dead commented-out debug std::cout lines and an unused chantypes vector left over from earlier work. Assisted-by: Claude Code / Claude Opus 4.8 Signed-off-by: Larry Gritz --- src/jpeg2000.imageio/jpeg2000input.cpp | 70 +++++++++++++++++--------- 1 file changed, 47 insertions(+), 23 deletions(-) diff --git a/src/jpeg2000.imageio/jpeg2000input.cpp b/src/jpeg2000.imageio/jpeg2000input.cpp index 561c120b00..f93337406f 100644 --- a/src/jpeg2000.imageio/jpeg2000input.cpp +++ b/src/jpeg2000.imageio/jpeg2000input.cpp @@ -571,24 +571,59 @@ Jpeg2000Input::open(const std::string& name, ImageSpec& p_spec) || has_error()) { if (!has_error()) errorfmt("Could not read Jpeg2000 header"); + close(); + return false; } - if (!has_error()) { - if (!opj_decode(m_codec, m_stream, m_image)) { - if (!has_error()) - errorfmt("Could not decode Jpeg2000 data"); + + // Check the header-claimed size before the expensive opj_decode. Channel + // count can still grow during decode (palette expansion), but that only + // makes the image larger, so this is a safe lower-bound estimate. + { + int w = (m_image->x1 > m_image->x0) ? int(m_image->x1 - m_image->x0) + : 0; + int h = (m_image->y1 > m_image->y0) ? int(m_image->y1 - m_image->y0) + : 0; + uint32_t prov_prec = 0; + for (uint32_t c = 0; c < m_image->numcomps; ++c) + prov_prec = std::max(prov_prec, m_image->comps[c].prec); + ImageSpec provspec(w, h, int(m_image->numcomps), + prov_prec <= 8 ? TypeDesc::UINT8 : TypeDesc::UINT16); + provspec.full_width = m_image->x1; + provspec.full_height = m_image->y1; + if (!check_open(provspec, + { 0, std::numeric_limits::max(), 0, + std::numeric_limits::max(), 0, 1, 0, 16384 })) { + close(); + return false; } - } - destroy_decompressor(); - destroy_stream(); + // Also guard against decompression bombs: a tiny codestream claiming + // an implausibly large uncompressed size, still under the absolute + // limit above. + const imagesize_t bomb_ratio = 10000; + imagesize_t uncompressed = provspec.image_bytes(true); + int64_t filesize = ioproxy() ? ioproxy()->size() : 0; + if (uncompressed > (imagesize_t(1) << 30) && filesize > 0 + && uncompressed > imagesize_t(filesize) * bomb_ratio) { + errorfmt("JPEG2000 header claims a {} MB image from a {} byte file; " + "probably a corrupt or malicious header", + uncompressed >> 20, filesize); + close(); + return false; + } + } - if (has_error()) { + if (!opj_decode(m_codec, m_stream, m_image) || has_error()) { + if (!has_error()) + errorfmt("Could not decode Jpeg2000 data"); close(); return false; } - OIIO_ASSERT(m_image != nullptr); + destroy_decompressor(); + destroy_stream(); - // we support only one, three or four components in image + // we support only one, three or four components in image (final only + // after decode, since palette expansion can change it) const int channelCount = m_image->numcomps; if (channelCount != 1 && channelCount != 3 && channelCount != 4) { errorfmt( @@ -633,7 +668,6 @@ Jpeg2000Input::open(const std::string& name, ImageSpec& p_spec) ROI datawindow; m_bpp.clear(); m_bpp.reserve(channelCount); - std::vector chantypes(channelCount, TypeDesc::UINT8); for (int i = 0; i < channelCount; i++) { const opj_image_comp_t& comp(m_image->comps[i]); m_bpp.push_back(comp.prec); @@ -641,17 +675,7 @@ Jpeg2000Input::open(const std::string& name, ImageSpec& p_spec) ROI roichan(comp.x0, comp.x0 + comp.w * comp.dx, comp.y0, comp.y0 + comp.h * comp.dy); datawindow = roi_union(datawindow, roichan); - // std::cout << " chan " << i << "\n"; - // std::cout << " dx=" << comp.dx << " dy=" << comp.dy - // << " x0=" << comp.x0 << " y0=" << comp.y0 - // << " w=" << comp.w << " h=" << comp.h - // << " prec=" << comp.prec << " bpp=" << comp.bpp << "\n"; - // std::cout << " sgnd=" << comp.sgnd << " resno_decoded=" << comp.resno_decoded << " factor=" << comp.factor << "\n"; - // std::cout << " roichan=" << roichan << "\n"; - } - // std::cout << "overall x0=" << m_image->x0 << " y0=" << m_image->y0 - // << " x1=" << m_image->x1 << " y1=" << m_image->y1 << "\n"; - // std::cout << "color_space=" << m_image->color_space << "\n"; + } TypeDesc format = (maxPrecision <= 8) ? TypeDesc::UINT8 : TypeDesc::UINT16; m_spec = ImageSpec(datawindow.width(), datawindow.height(), channelCount, format); @@ -662,7 +686,7 @@ Jpeg2000Input::open(const std::string& name, ImageSpec& p_spec) m_spec.full_width = m_image->x1; m_spec.full_height = m_image->y1; - // Validation of resolution + // Validation of resolution, now authoritative with the final channel count if (!check_open(m_spec, { 0, std::numeric_limits::max(), 0, std::numeric_limits::max(), 0, 1, 0, 16384 })) { From e1824226705186b130f1b4a85a56483ae6d8701f Mon Sep 17 00:00:00 2001 From: Larry Gritz Date: Thu, 16 Jul 2026 22:10:11 -0700 Subject: [PATCH 2/2] jpeg2000: Use check_compression_ratio Signed-off-by: Larry Gritz --- src/jpeg2000.imageio/jpeg2000input.cpp | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/jpeg2000.imageio/jpeg2000input.cpp b/src/jpeg2000.imageio/jpeg2000input.cpp index f93337406f..6913ef3e0a 100644 --- a/src/jpeg2000.imageio/jpeg2000input.cpp +++ b/src/jpeg2000.imageio/jpeg2000input.cpp @@ -597,17 +597,11 @@ Jpeg2000Input::open(const std::string& name, ImageSpec& p_spec) return false; } - // Also guard against decompression bombs: a tiny codestream claiming - // an implausibly large uncompressed size, still under the absolute - // limit above. - const imagesize_t bomb_ratio = 10000; - imagesize_t uncompressed = provspec.image_bytes(true); - int64_t filesize = ioproxy() ? ioproxy()->size() : 0; - if (uncompressed > (imagesize_t(1) << 30) && filesize > 0 - && uncompressed > imagesize_t(filesize) * bomb_ratio) { - errorfmt("JPEG2000 header claims a {} MB image from a {} byte file; " - "probably a corrupt or malicious header", - uncompressed >> 20, filesize); + // Guard against decompression bombs / corrupt headers: a tiny + // codestream claiming a multi-gigabyte image, which opj_decode + // would then hang and allocate gigabytes trying to produce. + imagesize_t filesize = ioproxy() ? ioproxy()->size() : 0; + if (!check_compression_ratio(provspec, filesize)) { close(); return false; }