Move the decompression bomb detection in the TIFF reader into into
ImageInput::check_compression_ratio() so it can be used uniformly for
other input types as well.
Assisted-by: Claude Code / Claude Sonnet 5
Signed-off-by: Larry Gritz <lg@larrygritz.com>
A Softimage PIC file stores a chain of channel packets, each carrying
its own bit depth (8 or 16). The reader assumed one uniform per-channel
size for the whole image, so a file mixing depths (e.g. 16-bit R with
8-bit G) wrote past the end of the scanline buffer.
Fix: always expose one uniform pixel format, the widest depth among the
file's packets, and promote narrower channels to it on read via exact
bit replication (v*257 for 8->16), which preserves the normalized value
exactly. This keeps the pixel layout naturally aligned; representing
channels with their true differing formats instead would pack them at
non-uniform, sometimes misaligned byte offsets.
Assisted-by: Claude Code / claude-sonnet-5
Signed-off-by: Larry Gritz <lg@larrygritz.com>
read_layer_record computed a layer's width/height as `std::abs((int)right
- (int)left)`. The rectangle coordinates are signed 32-bit values (layers
can have negative offsets), but were stored in unsigned (uint32_t)
struct fields, and subtracting two ints near the extremes overflows --
undefined behavior that UBSan flagged on a fuzzed file (1615681375 -
-1605526113 cannot be represented in type 'int').
Retype Layer's and MaskData's top/left/bottom/right fields as int32_t,
matching what they actually hold, instead of reinterpreting bits at the
point of use. Widen to int64_t before subtracting so the computation
can't overflow; the absolute difference of two int32 values always fits
in uint32, and validate_resolution still rejects implausibly large
extents.
The mask data rect (used for layer masks) had the identical
std::abs((int)right - (int)left) overflow hazard in load_layer_channel
and gets the same fix.
Assisted-by: Claude Code / Sonnet 5
Signed-off-by: Larry Gritz <lg@larrygritz.com>
Read10bitFilled() unpacks a scanline of 10-bit "filled" (Method A/B)
samples backwards, and for 1-channel images applies a workaround that
swaps the first and third datum of each group of 3 packed samples (so
column order comes out right). The swap didn't check that a full group
of 3 remained in bounds. Guard the swap so it only fires when a complete
group of 3 remains.
Add a regression test in testsuite/dpx.
Assisted-by: Claude Code / Sonnet 5
---------
Signed-off-by: Larry Gritz <lg@larrygritz.com>
Find OpenCV via its exported CMake config (PREFER_CONFIG) so OpenCV 5 is
located correctly.
Modify our imagebufalgo_opencv.h to work with OpenCV 5. But how to test
whether we did it correctly?
We never had an actual test for the OpenCV interop. So set one up.
Assisted-by: Claude Code / Sonnet 5
Signed-off-by: Larry Gritz <lg@larrygritz.com>
One small improvement in how decode_exif detects corruption, to ensure
that the initial header examination doesn't read past the array bounds
-- by starting off with a check that the exif blob's size is at least a
big as a TIFFHeader, otherwise parsing it is pointless.
Then the rest of the changes are finding places where readers call
decode_exif but ignore the results, effectively ignoring corrupt Exif
blobs. That is in fact the default intended behavior (if the rest of the
image file can be read properly), but we are supposed to be checking the
global "imageinput:strict" attribute and considering it a hard error
when in strict mode. There were several places where we didn't handle
that case, in the JPEG, PNG, WebP, and Heif readers.
---------
Signed-off-by: Larry Gritz <lg@larrygritz.com>
Adds a hint "raw:bad_pixels" which passes a filename for bad pixel data
to libraw.
I haven't added any tests, can't think of a good way of testing this.
Open to suggestions.
This functionality has been tested in a client code.
Signed-off-by: Anton Dukhovnikov <antond@wetafx.co.nz>
read_native_tiles() on a tiled EXR whose dimensions aren't a multiple of
the tile size wrote past the caller's buffer when reading an edge tile
range: both readers used a row stride padded up to a whole number of
tiles (nxtiles*tile_width) instead of the caller's actual rectangle
width, overrunning the buffer whenever the last tile column is partial.
Fixed in both exrinput_c.cpp (Core reader) and exrinput.cpp (classic
reader) to use the requested rectangle width as the destination row
stride instead. exrinput.cpp additionally needs the pre-clamp width
specifically, since read_native_tile() forwards a tile-aligned xend past
the image edge for ordinary full-tile reads.
Adds testsuite/openexr-partialtile, a compiled regression test that
calls read_native_tiles() directly for both readers -- the only path
that exercises this code -- and checks pixels and buffer bounds.
Assisted-by: Claude Code / Claude Opus 4.8
Signed-off-by: Larry Gritz <lg@larrygritz.com>
(The same problem and set of fixes were applied to both ImageInput and
ImageOutput. For brevity, we will only discuss ImageInput below.)
ImageInput error messages are stored in a thread_local map keyed by a
unique per-instance id (m_id). The destructor's erase of that entry was
commented out (over caution about the static destruction order fiasco),
so every ImageInput that accumulated an error and was destroyed without
its error being cleared via geterror() leaked one map entry. The id is
monotonically increasing, so entries never get reused and the map grows
without bound.
How did we find this? A fuzzer opening hundreds of thousands of corrupt
files -- each of which errors, and each ImageInput::create() attempt
tries several readers -- grew the map to tens of MB and eventually
OOMed. The leak was traced to this issue.
So we re-enable the erase, guarded against the destruction order fiasco:
the thread_local map is wrapped in a struct whose destructor clears an
'alive' flag, and the ImageInput destructor only touches the map while
it is alive. Each ImageInput removes only its own entry (keyed by its
unique id); other inputs' errors are untouched.
Cross-thread cleanup is still a known limitation -- errors accumulated
in a different thread than the one that descroys the ImageInput are
still leaked, noted in a TODO. However, this should be exceptionally
rare, since not only do you need the two-thread situation, but the one
that made the call that produced an error needs to not check for errors
and return/clear the message. So only badly behaving user code that
fails to check and retrieve errors should have this minor leak. And we
are in the process of adding compile-time warnings for not capturing the
error status values from our APIs.
Assisted-by: Claude Code / Claude Opus 4.8
Signed-off-by: Larry Gritz <lg@larrygritz.com>
A fuzzed JPEG2000 caused a heap-buffer-overflow (caught by ASan) when
copy_scanline read past the end of openjpeg's decoded component buffer.
The file's image canvas was 32x32, but one of its three components had a
horizontal subsampling factor dx=249 (larger than the entire image).
OIIO derives the ImageSpec size from the union of each component's
x0+w*dx / y0+h*dy window, so that bogus component inflated the spec to
249x32. copy_scanline then iterated x over the inflated width while
indexing a component whose data array only held 1 column, walking off
the end. The per-sample guard was also broken: it compared the component
row index against reference-grid offsets (wrong units) and used `>`
instead of `>=` on the column.
Two fixes:
1. Detect the corruption at open(): the JPEG2000 canvas (x1-x0, y1-y0)
is the authoritative image size, and a component's subsampling factor
must be nonzero and no larger than the canvas. Reject files that violate
this instead of decoding a nonsensical, inflated image.
2. Harden copy_scanline by bounds-checking each access against the
actual component array geometry and skip the channel (zero-fill) when
comp.data is null or a subsampling factor is zero, which also closes a
latent divide-by-zero on comp.dx/comp.dy.
Assisted-by: Claude Code / Claude Opus 4.8
Signed-off-by: Larry Gritz <lg@larrygritz.com>
Previously, we did not have a PtexInput::valid_file() method, and our
open() method just directly called PtexTexture::open(), which is really
bad with validity checks and can crash or OOM with corrupted files.
But Ptex has a fixed 64 byte header (cheap to read and inspect) that has
lots of fields that let us more accurately check for reasonable file
validity. Therefore, we both implement valid_file, and also have open()
do these checks (rejecting many forms of corruption) before turning the
job over to the Ptex library itself.
Assisted-by: Claude Code / opus-4.8
---------
Signed-off-by: Larry Gritz <lg@larrygritz.com>
Change several build-time options from `option()` to our `set_option()`,
which mostly has the effect of allowing them to be overridden by
environment variables of the same name, which is really handy for CI
variants.
In the process, realized that we had some real problems with handling of
EMBEDPLUGINS=0, it probably hasn't worked properly for years and went
unnoticed because the one CI job variant that was trying to test
EMBEDPLUGINS=0 actually *was not* because it was trying to do it by
setting the environment variable, unaware that it could not be used to
set the option until this patch. So there is a bit of cmake and linkage
fixing that needed to be done to get it squared away.
Assisted-by: Claude Code / opus-4.8
Signed-off-by: Larry Gritz <lg@larrygritz.com>
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>
Validate channel-row byte availability before interleaving scanlines in
PSD reads, and fail gracefully with an error instead of risking
out-of-bounds access on malformed files.
Assisted-by: GitHub Copilot / GPT-5.3-Codex
Signed-off-by: Larry Gritz <lg@larrygritz.com>
- Add recently published security advisories to SECURITY.md
- Revision of security policy and definitions of vulnerability.
- Also some minor forward-ports and adjustments of CHANGES.
The substantive change is explaining that our policy is to only consider
vulnerabilities if they (a) allow remote code execution, privilege
escalation, and other very severe problems (not crashes, OOM, or most
"DoS" scenaries), and (b) are exploitable in practice by untrusted
actors merely by presenting malicious routine input such as image files
(misuse of APIs, for example, is not a vulnerability).
Everything else should be reported through the usual Issue/PR mechanism,
and not use the security vulnerability reporting regime, which is
massively more overhead for the maintainers.
Signed-off-by: Larry Gritz <lg@larrygritz.com>
PRs #5260 and #5254 interact: #5254 exposed the default type arg on the
module getattribute (`"type"_a = TypeUnknown`), so a one-arg call now
returns None instead of raising TypeError. #5260's test still expected
the old pybind behavior. Update the test and ref to the intended new
behavior.
Assisted-by: Claude Code / Claude Opus 4.8
Signed-off-by: Larry Gritz <lg@larrygritz.com>
Exploring an idea Anton had about unifiying two python backends into
single source since they are so similar.
## Summary
This PR unifies the early nanobind migration with the existing pybind11
Python bindings by compiling **shared sources** from `src/python/` for
both backends, instead of maintaining parallel copies under
`src/python-nanobind/`.
Migrated modules so far: `ROI`, `TypeDesc`, `ImageSpec`, `ParamValue` /
`ParamValueList`, plus the core `OpenImageIO` module helpers in
`py_oiio.cpp`. Backend differences are isolated in `py_backend.h` (type
aliases, binding API macros like `.OIIO_PY_RW(...)`, and small
`oiio_py::` helpers). Shared Python↔C++ conversion logic lives in
`py_oiio.h` with `#if` only where the backends genuinely differ.
CMake is updated so that when `-DOIIO_PYTHON_BINDINGS_BACKEND=nanobind`
is selected, the module is built from the unified sources and installed
as the main `OpenImageIO` package in the expected location. With `both`,
pybind11 remains the primary module and nanobind builds as
`PyOpenImageIONanobind` for side-by-side testing.
Duplicate nanobind-only `.cpp` files are removed (~1.8k lines deleted
net). Testsuite coverage is extended with explicit roundtrip checks for
shared helper paths (attributes, buffers, tuples).
---
## Pros
- **Single source of truth** for migrated bindings — no more keeping two
copies of `py_roi.cpp`, `py_imagespec.cpp`, etc. in sync by hand.
- **Lower risk of backend drift** — a fix or API tweak in a migrated
module automatically applies to both pybind11 and nanobind.
- **Net code reduction** — removes a large duplicated tree under
`src/python-nanobind/` while centralizing shared logic in one place.
- **Clearer migration path** — `MIGRATION_STATUS.md` documents what is
fully migrated, partially migrated, and still pybind-only.
- **Better nanobind-only build layout** — on `main`, choosing nanobind
does not install into the same package layout as pybind11; this PR fixes
that for the migrated surface.
- **Side-by-side validation** — `OIIO_PYTHON_BINDINGS_BACKEND=both`
builds both modules so existing `testsuite/python-*` and `*.nanobind`
variants can be run against the same C++ sources.
- **Explicit parity tests** — new/extended tests cover shared conversion
paths (typed attributes, buffer→vector, tuple roundtrips) rather than
relying on import smoke tests alone.
- **Maintainable binding style** — method-name macros
(`.OIIO_PY_RW(...)`) keep clang-format happy without `clang-format off`
regions.
---
## Cons
- **Added abstraction in the pybind11 path** — even pybind-only builds
now go through `py_backend.h`, macros, and shared helpers; slightly more
indirection than plain `.def_readwrite(...)`.
- **Regression risk for pybind11** — adapting existing pybind code to
work for both backends could subtly change pybind11 behavior at the
edges (init, casting, error paths, ownership).
- **Incomplete migration** — most of the Python API (`ImageBuf`,
`ImageInput`/`ImageOutput`, `ImageBufAlgo`, texture system, etc.) is
still pybind-only; this PR is a foundation, not full nanobind coverage.
- **Known nanobind gaps remain** — module-level exports and helpers
listed in `MIGRATION_STATUS.md` are still missing or intentionally
different vs pybind11.
- **Higher review and test burden** — reviewers need to think about two
backends; CI/local testing should cover both pybind and `.nanobind`
variants for touched code.
- **Conditional compilation complexity** — `#if
defined(OIIO_PY_BACKEND_NANOBIND)` remains in a few places (e.g.
`ParamValue` constructors, buffer dispatch); easy to get wrong if
extended carelessly.
- **Slightly less obvious binding code** — `.OIIO_PY_RW("x", ...)` reads
less directly than `.def_readwrite("x", ...)` until you know the macro
map.
---------
Signed-off-by: Aleksandr Motsjonov <soswow@gmail.com>
Run check_open on JXL basic-info dimensions/channels before decode
output allocation to prevent oversized allocation paths on malformed
input.
Also update ref output for libjxl 0.12.0.
Assisted-by: GitHub Copilot / GPT-5.3-Codex
---------
Signed-off-by: Larry Gritz <lg@larrygritz.com>
* Find corruptions by validating layer resolutions.
* Find corruptions when eof is hit in the middle of a string.
Signed-off-by: Larry Gritz <lg@larrygritz.com>
Truncating a corrupt uint32 length_tab entry into `int` could yield a
huge negative value, causing a runaway allocation and crash in
uncompress_rle_channel. Validate offset/length against the file size
instead.
Assisted-by: Claude Code / Sonnet 5
Signed-off-by: Larry Gritz <lg@larrygritz.com>
This aligns FFmpeg/libswscale destination image buffers and validates
the buffer-size, frame-fill, and swscale return paths before decoded
pixels are consumed.
The change prevents narrow high-bit-depth VP9/GBR inputs from writing
past OIIO's destination buffer during swscale conversion, and adds a
regression case for the width-1 high-bit-depth FFmpeg path.
Signed-off-by: Dongju Lee <dongpago2@gmail.com>
This handles malformed GIF files whose logical canvas dimensions can
overflow legacy int-indexed canvas calculations.
The patch rejects oversized canvas dimensions before scanline/canvas
indexing and adds a regression test for the malformed oversized-canvas
case.
Signed-off-by: Dongju Lee <dongpago2@gmail.com>
This treats non-positive tiff:RowsPerStrip metadata as absent before
using it to align generic scanline read/write chunks. That avoids
feeding invalid chunking metadata into round_to_multiple while
preserving normal positive RowsPerStrip behavior.
The patch adds regression coverage for an OpenEXR file carrying
RowsPerStrip=0 through iinfo --hash, iinfo --stats, and an oiiotool
conversion path.
---------
Signed-off-by: Dongju Lee <dongpago2@gmail.com>
Signed-off-by: Larry Gritz <lg@larrygritz.com>
Co-authored-by: Larry Gritz <lg@larrygritz.com>
GifSplitPalette() computed `numPixels * (splitElt - firstElt)` as a
32-bit int before dividing. For any image over roughly 16.9M pixels
(e.g. 4117x4117), that intermediate multiplication overflows INT_MAX and
is undefined behavior; in practice it produced a bogus negative/garbage
split count that could drive later indexing into `image` out of its
bounds.
Do the multiply in 64 bits and narrow back afterward -- the quotient is
always <= numPixels, so the narrowing itself is safe.
Assisted-by: Claude Code / Sonnet 5
---------
Signed-off-by: Larry Gritz <lg@larrygritz.com>
TBHD width/height/channel fields were never cross-checked against how
much tile data the file actually contains, so a corrupt header could
claim an arbitrarily large image (e.g. 16k x 16k) backed by only a few
hundred bytes of real data. readimg() would then resize() a
multi-gigabyte buffer for a tiny malformed file, hanging (or very slowly
succeeding) instead of failing -- a resource-exhaustion DoS on untrusted
input, found via fuzzing.
Bound the claimed image size against how much tile data remains in the
file, generously allowing for RLE's worst-case 128x expansion ratio, and
fail fast if it's implausible.
Assisted-by: Claude Code / claude-sonnet-5
---------
Signed-off-by: Larry Gritz <lg@larrygritz.com>
CineonInput::open() accepted any per-channel bit depth in [1,32], but
libcineon's ComponentDataSize()/ComponentByteCount() only recognize
{8,10,12,16,32,64} and assert(0) on anything else. A crafted file with
an unsupported bit depth (e.g. 26) passed the OIIO-side check and then
aborted deep inside the vendored library instead of failing cleanly.
Tighten the check to libcineon's actual whitelist so bogus bit depths
are rejected with a proper errorfmt() before ever reaching the vendored
code. Adds a regression fixture (broken_bitdepth2.cin) to the cineon
testsuite.
Assisted-by: Claude Code / Sonnet 5
Signed-off-by: Larry Gritz <lg@larrygritz.com>
open_raw set imgdata.rawparams.max_raw_memory_mb *after* calling
m_processor->unpack(), so LibRaw's allocation cap never protected the
unpack step -- which is exactly where LibRaw allocates the raw image
buffer. A corrupt raw file claiming huge dimensions could therefore
allocate unbounded and OOM (showing up on the fuzzer as a 0-length crash
artifact from an out-of-memory trip rather than a normal reproducer).
Move the max_raw_memory_mb assignment to immediately after open_file
succeeds, before unpack(), so the cap (default 2048 MB) actually bounds
the allocation. LibRaw then fails the file cleanly instead of OOMing.
Assisted-by: Claude Code / Claude Opus 4.8
Signed-off-by: Larry Gritz <lg@larrygritz.com>
While working on
https://github.com/AcademySoftwareFoundation/OpenImageIO/pull/5254,
where we're introducing changes that could surface bugs in pybind11 code
that's in production today, I figured I'd try adding some tests.
This was a fairly loose-handed effort. To be upfront: most of it was
LLM-generated. I did some research into which modules were lacking
coverage the most, and those are what this PR targets. I haven't gone
through every line of the generated code.
I hate being asked to review something that was clearly vibe-coded with
no real effort put in, so I'm saying that here rather than pretending
otherwise. I can try to review it properly, but without deep codebase
knowledge it'll be slow and painful. Since these are just tests, my
thinking was: even if there are silly bits here and there, it shouldn't
hurt much, as long as things go green and we cover more logic and
branches. Worth offering as-is.
---------
Signed-off-by: Aleksandr Motsjonov <soswow@gmail.com>
We had missed a case where bit_convert needed outputs to go to the
scratch buffer: PHOTOMETRIC_SEPARATED, but not requesting raw color.
(Because the real number of channels differ from what we are presenting
to the caller.)
Assisted-by: Claude Code / Sonnet 5
Signed-off-by: Larry Gritz <lg@larrygritz.com>
10MB was conservatively chosen as big enough that anything larger is
definitely bogus for an ICC, Exif, or XMP block in a PSD file.
Signed-off-by: Larry Gritz <lg@larrygritz.com>
read_native_scanlines declared task_set before the locals its queued
decompression lambdas capture (ok, compressed_scratch). On the
leftover-scanline early-return path those locals were destroyed before
~task_set() waited for the queued work, so workers could write through a
dangling ok and hand a freed compressed buffer to zlib.
Declare task_set after the captured locals so its destructor wait runs
while they are still alive, and propagate async decompression failures
via return ok instead of unconditionally returning true.
Assisted-by: Claude Code / opus-4.8
---------
Signed-off-by: Larry Gritz <lg@larrygritz.com>
validate_header() let the oiio:RawColor/psd:RawData path return true
before the switch that rejects unknown m_header.color_mode values, so an
out-of-range or "hole" (undocumented) color mode reached setup(), which
unconditionally indexes the fixed 10-entry mode_channel_count /
mode_channel_names tables with the attacker-controlled value -- a global
out-of-bounds read (and, per external report, a path to unbounded/bogus
allocation) reachable via the public RawColor config attribute on a
crafted PSD.
Move the "is this a color mode we know about at all" check ahead of the
RawColor early return so it always runs, and keep the existing
Duotone/Lab-unsupported-without-raw check as a second pass after the
early return, preserving current behavior for every valid mode.
Assisted-by: Claude Code / claude-sonnet-5
---------
Signed-off-by: Larry Gritz <lg@larrygritz.com>
An 18-byte TGA file consisting of just a header (no pixel data) can
claim an arbitrary width/height/bpp -- e.g. a 1472x12000 24bpp image --
and readimg() would allocate the full ~53MB buffer and only discover the
truncation once the first read hits EOF. Worse, the resulting
partially-formed ImageSpec could still propagate to a downstream writer
before the read failure was acted on.
Cross-check the header's width/height/bpp against how many bytes of
pixel data the file could plausibly contain before committing to the
allocation, mirroring the existing pattern in iff.imageio and
jpeg2000.imageio. The check compares against the *on-disk* pixel size
(palette/16-bit storage is smaller per pixel than the decoded, unpacked
ImageSpec) and allows up to 128x expansion for RLE-compressed images (a
1-byte run-length count can encode up to 128 output pixels).
Updated testsuite/targa/ref/out.txt: several existing corrupt-file
regression cases (crash1.tga, crash6.tga, crash3952.tga) are now caught
earlier with clearer messages, and two real oiio-images TGA files
(UCM8.TGA, UTC16.TGA) that use palette/16-bit storage smaller than their
decoded size continue to read correctly.
Assisted-by: Claude Code / Sonnet 5
Signed-off-by: Larry Gritz <lg@larrygritz.com>
PngReadCallback passed input->geterror(false).c_str() to
png_chunk_error() when a PNG read failed. png_chunk_error() does not
return -- it longjmp's back to libpng's setjmp point, which skips C++
destructors, so the temporary std::string returned by geterror() was
never freed. The fuzzer surfaced this as a small, intermittent leak (a
few KB across hundreds of thousands of runs).
Copy the message into a plain stack buffer (no destructor) before
calling png_chunk_error, so nothing heap-allocated is alive across the
longjmp.
Assisted-by: Claude Code / Claude Opus 4.8
Signed-off-by: Larry Gritz <lg@larrygritz.com>
The mask-based uncompressed decode path copies m_Bpp bytes into a
uint32_t pixel variable. m_Bpp is validated up to 16 (for 128-bit DXGI
formats), but the legacy fmt.masks[] are uint32_t and can only address
32 bits. A corrupt file with m_Bpp > 4 caused a stack-buffer-overflow.
Clamp the copy to sizeof(pixel) = 4 bytes, which is all the masks can
reference anyway.
Assisted-by: Claude Code / claude-sonnet-4-6
Signed-off-by: Larry Gritz <lg@larrygritz.com>
A DDS file with a garbage pixel-format bpp field (e.g. fmt.bpp ~2.5e9)
yields a native bytes-per-pixel (m_Bpp) of hundreds of millions.
internal_readimg() then does `new uint8_t[width * m_Bpp]`, a multi-GB
allocation that OOMs. The core resolution/imagesize limits do not catch
this: they only see the small output ImageSpec, not the native pixel
size, and the existing fmt.bpp check is gated on RGB/LUM/YUV/ALPHA flags
that a corrupt header can simply omit.
Validate m_Bpp for uncompressed formats right after it is computed in
open(): the largest real uncompressed DDS/DXGI pixel is 128-bit (16
bytes), so anything outside [1,16] is a corrupt header and is rejected
before any allocation. The check is confined to uncompressed formats
because m_Bpp is neither set nor used for compressed (block) formats,
which size their data via GetStorageRequirements.
Assisted-by: Claude Code / Claude Opus 4.8
Signed-off-by: Larry Gritz <lg@larrygritz.com>
Detect and reject corrupt chunk sizes, flags, and channel configurations
in the header.
Assisted-by: Claude Code / Claude Opus 4.8
---------
Signed-off-by: Larry Gritz <lg@larrygritz.com>
Somehow the check_open validation was only in exrinput.cpp (using the
old C++ AP), but not exrinput_c.cpp (using the C "Core API").
Also adjust the allowable z resolution to reflect that OpenEXR doesn't
support volumetric images. (This was obviously a cut and paste error
long ago.)
Signed-off-by: Larry Gritz <lg@larrygritz.com>