feat: add "limits:resolution" global attribute to cap per-dimension size (#5297)
The existing "limits:imagesize_MB" rejects files whose total uncompressed size exceeds a cap, but it cannot catch an image that is tiny in one dimension yet absurdly large in another (e.g. an image claiming 32 x 50331680, which slips under the 32 GB default). Add a companion global attribute, "limits:resolution", that caps the maximum number of pixels along any single dimension. Like imagesize_MB, this is enforced only in ImageInput::check_open(), intended for validity checks when reading untrusted files. Applications may set the attribute larger or smaller than this default, depending on their need to deal with images larger than these limits, and/or willingness to risk mistaking a corrupted or maliciously crafted image for a legitimate one on this basis. Now then, with this in place, we can clean up some of the values we used in passing valid resolution ranges to check_open(). That was originally meant to express the true limits of what was valid in the file formats, but had in many cases been conflated with common sense "what value is so big that it's probably bogus". Now we can disentangle these ideas -- the value passed to check_open is the definitional limit of the file format itself, and the "limits:resolution" is the overrideable global policy for what we consider more likely to be malicious/corrupted than merely legit big. Assisted-by: Claude Code / Claude Opus 4.8 Signed-off-by: Larry Gritz <lg@larrygritz.com>
This commit is contained in:
@@ -34,6 +34,12 @@ Release 3.2 (target: Sept 2026?) -- compared to 3.1
|
||||
* *ImageCache/TextureSystem*:
|
||||
- *api/TS*: `IBA::make_texture()` now honors "maketx:threads" hint [#5014](https://github.com/AcademySoftwareFoundation/OpenImageIO/pull/5014) (3.2.0.0, 3.1.10.0)
|
||||
* New global attribute queries via OIIO::getattribute():
|
||||
- `limits:resolution` (default: 1048576) is a new settable/queryable global
|
||||
attribute that caps the maximum number of pixels along any single image
|
||||
dimension. `ImageInput::check_open` rejects files exceeding it. This
|
||||
complements `limits:imagesize_MB` to catch corrupt headers that are tiny
|
||||
in one dimension but absurdly large in another, which can defeat the
|
||||
total-pixel-memory check.
|
||||
* Miscellaneous API changes:
|
||||
- *api*: Versioned namespace to preserve ABI compatibility between minor releases [#4869](https://github.com/AcademySoftwareFoundation/OpenImageIO/pull/4869) (3.2.0.0)
|
||||
- *fmath.h*: `degrees()` and `radians()` are now `constexpr`. [#5151](https://github.com/AcademySoftwareFoundation/OpenImageIO/pull/5151) (3.2.0.1, 3.1.13.0)
|
||||
|
||||
@@ -130,7 +130,7 @@ 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 << 20, 0, 1 << 20, 0, 1, 0, 8 })) {
|
||||
if (!check_open(m_spec, { 0, 1 << 30, 0, 1 << 30, 0, 1, 0, 8 })) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@ DICOMInput::seek_subimage(int subimage, int miplevel)
|
||||
|
||||
m_spec = ImageSpec(m_img->getWidth(), m_img->getHeight(), nchannels,
|
||||
format);
|
||||
if (!check_open(m_spec, { 0, 1 << 20, 0, 1 << 20, 0, 1 << 16, 0, 1 << 16 }))
|
||||
if (!check_open(m_spec, { 0, 1 << 30, 0, 1 << 30, 0, 1 << 16, 0, 1 << 16 }))
|
||||
return false;
|
||||
|
||||
m_bitspersample = m_img->getDepth();
|
||||
|
||||
@@ -210,7 +210,7 @@ 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 << 20, 0, 1 << 20, 0, 1 << 16, 0, 8 }))
|
||||
if (!check_open(m_spec, { 0, 1 << 30, 0, 1 << 30, 0, 1 << 16, 0, 8 }))
|
||||
return false;
|
||||
|
||||
// xOffset/yOffset are defined as unsigned 32-bit integers, but m_spec.x/y are signed
|
||||
|
||||
@@ -176,7 +176,7 @@ DPXOutput::open(const std::string& name, const ImageSpec& userspec,
|
||||
if (is_opened())
|
||||
close(); // Close any already-opened file
|
||||
|
||||
if (!check_open(mode, userspec, { 0, 1 << 20, 0, 1 << 20, 0, 1, 0, 256 }))
|
||||
if (!check_open(mode, userspec, { 0, 1 << 30, 0, 1 << 30, 0, 1, 0, 256 }))
|
||||
return false;
|
||||
|
||||
// From here out, all the heavy lifting is done for Create
|
||||
|
||||
@@ -200,7 +200,7 @@ bool
|
||||
HdrOutput::open(const std::string& name, const ImageSpec& newspec,
|
||||
OpenMode mode)
|
||||
{
|
||||
if (!check_open(mode, newspec, { 0, 1 << 20, 0, 1 << 20, 0, 1, 0, 3 },
|
||||
if (!check_open(mode, newspec, { 0, 1 << 30, 0, 1 << 30, 0, 1, 0, 3 },
|
||||
uint64_t(OpenChecks::Disallow1or2Channel)))
|
||||
return false;
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ bool
|
||||
HeifOutput::open(const std::string& name, const ImageSpec& newspec,
|
||||
OpenMode mode)
|
||||
{
|
||||
if (!check_open(mode, newspec, { 0, 1 << 20, 0, 1 << 20, 0, 1, 0, 4 },
|
||||
if (!check_open(mode, newspec, { 0, 1 << 30, 0, 1 << 30, 0, 1, 0, 4 },
|
||||
uint64_t(OpenChecks::Disallow2Channel)))
|
||||
return false;
|
||||
|
||||
|
||||
@@ -243,7 +243,7 @@ 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 << 20, 0, 1 << 20, 0, 1, 0, 4 })) {
|
||||
|| !check_open(m_spec, { 0, 1 << 30, 0, 1 << 30, 0, 1, 0, 4 })) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -2295,6 +2295,8 @@ protected:
|
||||
/// implied by `range`.
|
||||
/// * Whether the channel count is within the `"limits:channels"` OIIO
|
||||
/// attribute.
|
||||
/// * Whether any single dimension (width, height, depth) is within the
|
||||
/// `"limits:resolution"` OIIO attribute.
|
||||
/// * The total uncompressed pixel data size is expected to be within the
|
||||
/// `"limits:imagesize_MB"` OIIO attribute.
|
||||
/// * The full_{width,height,depth} are valid and within the range.
|
||||
@@ -3904,6 +3906,18 @@ OIIO_API std::string geterror(bool clear = true);
|
||||
/// you should raise this limit. Setting the limit to 0 means having no
|
||||
/// limit.
|
||||
///
|
||||
/// - `int limits:resolution` (1048576)
|
||||
///
|
||||
/// When nonzero, the maximum number of pixels allowed along any single
|
||||
/// dimension (width, height, or depth) of an image. Images whose headers
|
||||
/// indicate a larger dimension might be assumed to be corrupted or
|
||||
/// malicious files. This complements `limits:imagesize_MB` by catching a
|
||||
/// header that is small in one dimension but absurdly large in another,
|
||||
/// which could otherwise slip under the total-size limit. The default is
|
||||
/// 1048576 (2^20). In situations when images with a larger single
|
||||
/// dimension are expected to be encountered, you should raise this limit.
|
||||
/// Setting the limit to 0 means having no limit. (Added in version 3.2.)
|
||||
///
|
||||
/// - `int log_times` (0)
|
||||
///
|
||||
/// When the `"log_times"` attribute is nonzero, `ImageBufAlgo` functions
|
||||
|
||||
@@ -46,6 +46,7 @@ extern int png_linear_premult;
|
||||
extern int enable_hwy;
|
||||
extern int limit_channels;
|
||||
extern int limit_imagesize_MB;
|
||||
extern int limit_resolution;
|
||||
extern int imagebuf_print_uncaught_errors;
|
||||
extern int imagebuf_use_imagecache;
|
||||
extern int imageinput_strict;
|
||||
|
||||
@@ -185,7 +185,7 @@ bool
|
||||
Jpeg2000Output::open(const std::string& name, const ImageSpec& spec,
|
||||
OpenMode mode)
|
||||
{
|
||||
if (!check_open(mode, spec, { 0, 1 << 20, 0, 1 << 20, 0, 1, 0, 4 },
|
||||
if (!check_open(mode, spec, { 0, 1 << 30, 0, 1 << 30, 0, 1, 0, 4 },
|
||||
uint64_t(OpenChecks::Disallow2Channel)))
|
||||
return false;
|
||||
|
||||
|
||||
@@ -1585,6 +1585,23 @@ ImageInput::check_open(const ImageSpec& spec, ROI range, uint64_t /*flags*/)
|
||||
spec.nchannels, OIIO::pvt::limit_channels);
|
||||
return false;
|
||||
}
|
||||
if (OIIO::pvt::limit_resolution
|
||||
&& (spec.width > OIIO::pvt::limit_resolution
|
||||
|| spec.height > OIIO::pvt::limit_resolution
|
||||
|| spec.depth > OIIO::pvt::limit_resolution)) {
|
||||
if (spec.depth > 1) {
|
||||
errorfmt(
|
||||
"{} image dimension {}x{}x{} exceeds \"limits:resolution\" = {} for a single dimension. Possible corrupt input?\nIf you're sure this is a valid file, raise the OIIO global attribute \"limits:resolution\".",
|
||||
format_name(), spec.width, spec.height, spec.depth,
|
||||
OIIO::pvt::limit_resolution);
|
||||
} else {
|
||||
errorfmt(
|
||||
"{} image dimension {}x{} exceeds \"limits:resolution\" = {} for a single dimension. Possible corrupt input?\nIf you're sure this is a valid file, raise the OIIO global attribute \"limits:resolution\".",
|
||||
format_name(), spec.width, spec.height,
|
||||
OIIO::pvt::limit_resolution);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (OIIO::pvt::limit_imagesize_MB
|
||||
&& spec.image_bytes(true)
|
||||
> OIIO::pvt::limit_imagesize_MB * imagesize_t(1024 * 1024)) {
|
||||
|
||||
@@ -64,6 +64,7 @@ int enable_hwy = 0; // Not enabled at build time
|
||||
int limit_channels(1024);
|
||||
int limit_imagesize_MB(std::min(32 * 1024,
|
||||
int(Sysutil::physical_memory() >> 20)));
|
||||
int limit_resolution(1 << 20); // max pixels along any single dimension
|
||||
int imageinput_strict(0);
|
||||
ustring font_searchpath(Sysutil::getenv("OPENIMAGEIO_FONTS"));
|
||||
ustring plugin_searchpath(OIIO_DEFAULT_PLUGIN_SEARCHPATH);
|
||||
@@ -431,6 +432,10 @@ attribute(string_view name, TypeDesc type, const void* val)
|
||||
limit_imagesize_MB = *(const int*)val;
|
||||
return true;
|
||||
}
|
||||
if (name == "limits:resolution" && type == TypeInt) {
|
||||
limit_resolution = *(const int*)val;
|
||||
return true;
|
||||
}
|
||||
if (name == "oiio:print_uncaught_errors" && type == TypeInt) {
|
||||
oiio_print_uncaught_errors = *(const int*)val;
|
||||
return true;
|
||||
@@ -625,6 +630,10 @@ getattribute(string_view name, TypeDesc type, void* val)
|
||||
*(int*)val = limit_imagesize_MB;
|
||||
return true;
|
||||
}
|
||||
if (name == "limits:resolution" && type == TypeInt) {
|
||||
*(int*)val = limit_resolution;
|
||||
return true;
|
||||
}
|
||||
if (name == "tiff:multithread" && type == TypeInt) {
|
||||
*(int*)val = tiff_multithread;
|
||||
return true;
|
||||
|
||||
@@ -1125,7 +1125,7 @@ OpenEXRInput::seek_subimage(int subimage, int miplevel)
|
||||
// if (m_miplevel == 0 && part.nmiplevels > 1)
|
||||
// m_spec.attribute("oiio:miplevels", part.nmiplevels);
|
||||
|
||||
if (!check_open(m_spec, { 0, 1 << 20, 0, 1 << 20, 0, 1, 0, 1 << 12 }))
|
||||
if (!check_open(m_spec, { 0, 1 << 30, 0, 1 << 30, 0, 1, 0, 1 << 12 }))
|
||||
return false;
|
||||
|
||||
if (miplevel == 0 && part.levelmode == Imf::ONE_LEVEL) {
|
||||
|
||||
@@ -1118,7 +1118,7 @@ OpenEXRCoreInput::seek_subimage(int subimage, int miplevel)
|
||||
// if (m_miplevel == 0 && part.nmiplevels > 1)
|
||||
// m_spec.attribute("oiio:miplevels", part.nmiplevels);
|
||||
|
||||
if (!check_open(m_spec, { 0, 1 << 20, 0, 1 << 20, 0, 1, 0, 1 << 12 }))
|
||||
if (!check_open(m_spec, { 0, 1 << 30, 0, 1 << 30, 0, 1, 0, 1 << 12 }))
|
||||
return false;
|
||||
|
||||
if (miplevel == 0 && part.levelmode == EXR_TILE_ONE_LEVEL) {
|
||||
|
||||
@@ -207,11 +207,10 @@ private:
|
||||
|
||||
bool copy_and_check_spec(const ImageSpec& srcspec, ImageSpec& dstspec)
|
||||
{
|
||||
// Arbitrarily limit res to 1M x 1M and 4k channels, assuming anything
|
||||
// beyond that is more likely to be a mistake than a legit request. We
|
||||
// may have to come back to this if these assumptions are wrong.
|
||||
// Limit channels to 4k; actual per-dimension resolution limits are
|
||||
// enforced separately via the "limits:resolution" attribute.
|
||||
if (!check_open(Create, srcspec,
|
||||
{ 0, 1 << 20, 0, 1 << 20, 0, 1, 0, 1 << 12 }))
|
||||
{ 0, 1 << 30, 0, 1 << 30, 0, 1, 0, 1 << 12 }))
|
||||
return false;
|
||||
if (&dstspec != &m_spec)
|
||||
dstspec = m_spec;
|
||||
|
||||
@@ -178,7 +178,7 @@ PNGInput::open(const std::string& name, ImageSpec& newspec)
|
||||
m_interlace_type, m_bg, m_spec,
|
||||
m_keep_unassociated_alpha);
|
||||
if (!ok || m_err
|
||||
|| !check_open(m_spec, { 0, 1 << 20, 0, 1 << 20, 0, 1, 0, 4 })) {
|
||||
|| !check_open(m_spec, { 0, 1 << 30, 0, 1 << 30, 0, 1, 0, 4 })) {
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ bool
|
||||
PNGOutput::open(const std::string& name, const ImageSpec& userspec,
|
||||
OpenMode mode)
|
||||
{
|
||||
if (!check_open(mode, userspec, { 0, 1 << 20, 0, 1 << 20, 0, 1, 0, 4 }))
|
||||
if (!check_open(mode, userspec, { 0, 1 << 30, 0, 1 << 30, 0, 1, 0, 4 }))
|
||||
return false;
|
||||
|
||||
// If not uint8 or uint16, default to uint8
|
||||
|
||||
@@ -1021,7 +1021,7 @@ TIFFInput::seek_subimage(int subimage, int miplevel)
|
||||
return false;
|
||||
}
|
||||
if (!check_open(m_spec,
|
||||
{ 0, 1 << 20, 0, 1 << 20, 0, 1 << 16, 0, 1 << 16 }))
|
||||
{ 0, 1 << 30, 0, 1 << 30, 0, 1 << 16, 0, 1 << 16 }))
|
||||
return false;
|
||||
m_subimage = orig_subimage;
|
||||
m_miplevel = miplevel;
|
||||
|
||||
@@ -475,7 +475,7 @@ TIFFOutput::open(const std::string& name, const ImageSpec& userspec,
|
||||
closetif();
|
||||
|
||||
if (!check_open(mode, userspec,
|
||||
{ 0, 1 << 20, 0, 1 << 20, 0, 1 << 16, 0, 1 << 16 }))
|
||||
{ 0, 1 << 30, 0, 1 << 30, 0, 1 << 16, 0, 1 << 16 }))
|
||||
return false;
|
||||
|
||||
// Check for things this format doesn't support
|
||||
|
||||
@@ -87,8 +87,7 @@ tahoe-cicp-displayp3.jxl : 128 x 96, 3 channel, uint8 jpegxl
|
||||
ICCProfile:profile_version: "4.4.0"
|
||||
ICCProfile:rendering_intent: "Perceptual"
|
||||
oiio:ColorSpace: "srgb_p3d65_scene"
|
||||
oiiotool ERROR: read : "src/crash-bfd2220.jxl": Uncompressed image size 822997745664.0 MB exceeds the 16384 MB limit.
|
||||
Image claimed to be 328438676x164219338, 4-channel float. Possible corrupt input?
|
||||
If this is a valid file, raise the OIIO attribute "limits:imagesize_MB".
|
||||
oiiotool ERROR: read : "src/crash-bfd2220.jxl": jpegxl image dimension 328438676x164219338 exceeds "limits:resolution" = 1048576 for a single dimension. Possible corrupt input?
|
||||
If you're sure this is a valid file, raise the OIIO global attribute "limits:resolution".
|
||||
Full command line was:
|
||||
> oiiotool -info -oiioattrib limits:imagesize_MB 16384 src/crash-bfd2220.jxl
|
||||
|
||||
@@ -87,8 +87,7 @@ tahoe-cicp-displayp3.jxl : 128 x 96, 3 channel, uint8 jpegxl
|
||||
ICCProfile:profile_version: "4.4.0"
|
||||
ICCProfile:rendering_intent: "Perceptual"
|
||||
oiio:ColorSpace: "srgb_p3d65_scene"
|
||||
oiiotool ERROR: read : "src/crash-bfd2220.jxl": Uncompressed image size 822997745664.0 MB exceeds the 16384 MB limit.
|
||||
Image claimed to be 328438676x164219338, 4-channel float. Possible corrupt input?
|
||||
If this is a valid file, raise the OIIO attribute "limits:imagesize_MB".
|
||||
oiiotool ERROR: read : "src/crash-bfd2220.jxl": jpegxl image dimension 328438676x164219338 exceeds "limits:resolution" = 1048576 for a single dimension. Possible corrupt input?
|
||||
If you're sure this is a valid file, raise the OIIO global attribute "limits:resolution".
|
||||
Full command line was:
|
||||
> oiiotool -info -oiioattrib limits:imagesize_MB 16384 src/crash-bfd2220.jxl
|
||||
|
||||
@@ -112,6 +112,7 @@ oiiotool ERROR: -set : Invalid variable name "3"
|
||||
Full command line was:
|
||||
> oiiotool -echo "This should make an error:" -set 3 5
|
||||
Expr getattribute(limits:channels) = 1024
|
||||
Expr getattribute(limits:resolution) = 1.04858e+06
|
||||
Testing if with true cond (expect output):
|
||||
inside if clause, i=42
|
||||
done
|
||||
|
||||
@@ -141,6 +141,7 @@ command += oiiotool ('-echo "This should make an error:" ' +
|
||||
|
||||
# Test getattribute in an expression
|
||||
command += oiiotool ('-echo "Expr getattribute(\"limits:channels\") = {getattribute(\"limits:channels\")}"')
|
||||
command += oiiotool ('-echo "Expr getattribute(\"limits:resolution\") = {getattribute(\"limits:resolution\")}"')
|
||||
|
||||
# Test --if --else --endif
|
||||
command += oiiotool ('-echo "Testing if with true cond (expect output):" -set i 42 -if "{i}" -echo " inside if clause, i={i}" -endif -echo " done" -echo " "')
|
||||
|
||||
Reference in New Issue
Block a user