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:
Larry Gritz
2026-07-10 22:54:56 -07:00
committed by GitHub
parent 4995b25b82
commit b2d3328b3d
24 changed files with 70 additions and 24 deletions
+6
View File
@@ -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)
+1 -1
View File
@@ -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;
}
+1 -1
View File
@@ -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();
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
}
+14
View File
@@ -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
+1
View File
@@ -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;
+1 -1
View File
@@ -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;
+17
View File
@@ -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)) {
+9
View File
@@ -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;
+1 -1
View File
@@ -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) {
+1 -1
View File
@@ -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) {
+3 -4
View File
@@ -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;
+1 -1
View File
@@ -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;
}
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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
+2 -3
View File
@@ -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
+2 -3
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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 " "')