6675 Commits
Author SHA1 Message Date
Larry Gritz d9e0610989 refactor(imageinput): consolidate decompression-bomb guard (#5328)
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>
2026-07-19 21:43:06 -07:00
Larry Gritz 51f99b8f14 admin: update SECURITY with new CVE assignments (#5329)
Also typo fix in CONTRIBUTING

Signed-off-by: Larry Gritz <lg@larrygritz.com>
2026-07-17 18:35:46 -07:00
Larry Gritz ba4bb4920b fix(softimage): support channel packets with differing bit depths (#5309)
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>
2026-07-17 11:06:40 -07:00
Larry Gritz 4500a872b9 fix(psd): avoid signed overflow computing layer/mask extents (#5306)
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>
2026-07-17 10:54:31 -07:00
Larry Gritz ac7e5d48cf fix(dpx): fix heap overflow in 1-channel 10-bit filled scanline swap (#5298)
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>
2026-07-16 20:01:51 -07:00
Larry Gritz 5b4a231634 build(opencv): support OpenCV 5, add standalone header smoke test (#5326)
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>
2026-07-16 19:54:25 -07:00
Larry Gritz c5e085eee3 fix: beef up Exif error detection (#5322)
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>
2026-07-15 23:41:20 -07:00
Anton Dukhovnikov a9a170244c raw: pass the bad pixels param to libraw (#5323)
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>
2026-07-15 22:05:09 -07:00
Larry Gritz 526beeb7f6 fix(tiff): fix some int32 overflows, guard against decompression bomb (#5319)
Fixes #5316

Assisted-by: Claude Code / Sonnet 5

Signed-off-by: Larry Gritz <lg@larrygritz.com>
2026-07-15 21:38:12 -07:00
Larry Gritz a9f5e9240d fix(sgi): deal with corrupt 16 bit RLE runs with odd byte counts (#5321)
Signed-off-by: Larry Gritz <lg@larrygritz.com>
2026-07-15 21:34:22 -07:00
Larry Gritz b3e38593b1 build: FORTIFY and sanitize/debug are not compatible (#5318)
Signed-off-by: Larry Gritz <lg@larrygritz.com>
2026-07-15 21:22:24 -07:00
Larry Gritz a0d5e27dae admin: very minor fixes to README & SECURITY (#5320)
Signed-off-by: Larry Gritz <lg@larrygritz.com>
2026-07-15 18:00:49 -07:00
Larry Gritz 7303134f13 fix(exr): use rectangle row stride for partial edge tile reads (#5295)
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>
2026-07-15 17:59:25 -07:00
Larry Gritz ce1be97495 fix: avoid leaks in the per-ImageInput/Output error message (#5276)
(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>
2026-07-14 13:51:52 -07:00
Larry Gritz f70367d3de fix(jpeg2000): reject corrupt component geometry and subsampling (#5270)
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>
2026-07-11 11:52:26 -07:00
Larry Gritz e3c14a0736 ptex: More comprehensive valid_file and validation of header (#5265)
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>
2026-07-10 22:55:43 -07:00
Larry Gritz d33cb2a0e0 fix(build): fix EMBEDPLUGINS=0, make many build options env-override (#5272)
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>
2026-07-10 22:55:26 -07:00
Larry Gritz b2d3328b3d 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>
2026-07-10 22:54:56 -07:00
Larry Gritz 4995b25b82 fix(psd): guard row interleave bounds on corrupt data (#5307)
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>
2026-07-10 17:11:15 -07:00
Larry Gritz 0adaf3a56c admin: SECURITY.md updates (#5311)
- 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>
2026-07-10 15:04:36 -07:00
Larry Gritz 4ee7fa0ea4 Update CHANGES
Signed-off-by: Larry Gritz <lg@larrygritz.com>
2026-07-09 18:01:21 -07:00
Larry Gritz 7a08623f52 fix(python): update getattribute one-arg test for unified bindings (#5308)
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>
2026-07-09 11:30:57 -07:00
Aleksandr Motsjonov 667365f45b Unify pybind11 and nanobind into single-source bindings (#5254)
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>
2026-07-09 10:26:04 -07:00
Larry Gritz fed212a115 fix(jxl): guard corrupt basic-info dimensions early (#5305)
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>
2026-07-08 16:36:59 -07:00
Larry Gritz 0670f1f314 fix(psd): identify corruptions of layer resolutions, eof in strings (#5259)
* 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>
2026-07-08 10:14:20 -07:00
Larry Gritz b590a26a0a fix(sgi): prevent oversized-allocation crash from bogus RLE length/offset table (#5303)
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>
2026-07-07 21:02:57 -07:00
Nick D'Ademo 14e30d8abb build: wasm build (#5304) 2026-07-07 19:36:18 -07:00
Larry Gritz 2ecbde6e5d ci: Add vfx2027 container tests (#5302)
One gcc and one clang based test in the new containers.

Signed-off-by: Larry Gritz <lg@larrygritz.com>
2026-07-06 22:43:47 -07:00
Larry Gritz 0494901b00 build: fix certain clang varieties warning about giflib headers (#5263)
Assisted-by: Claude Code / sonnet-4.6

Signed-off-by: Larry Gritz <lg@larrygritz.com>
2026-07-06 18:20:21 -07:00
Dongju Lee d9fc577ab0 fix(ffmpeg): align swscale output buffers (#5301)
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>
2026-07-06 18:14:58 -07:00
Dongju Lee 4e4a1df806 fix(gif): guard canvas index overflow (#5299)
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>
2026-07-06 17:45:40 -07:00
Dongju LeeandLarry Gritz 21a293a319 fix(imageio): ignore invalid RowsPerStrip chunking (#5300)
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>
2026-07-06 17:28:48 -07:00
Larry Gritz 134977da62 fix(gif): avoid int32 overflow in palette-split pixel-count math (#5292)
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>
2026-07-05 17:51:40 -07:00
Larry Gritz 002ab7a7b8 fix(iff): reject implausible image dimensions before allocating tile buffer (#5284)
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>
2026-07-05 17:04:03 -07:00
Larry Gritz 6f2b2e85b3 fix(cineon): validate bit depth against libcineon's supported set (#5283)
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>
2026-07-05 17:02:48 -07:00
Larry Gritz 0c5352be3b fix(iff): add resolution validity checking (#5285)
Signed-off-by: Larry Gritz <lg@larrygritz.com>
2026-07-05 17:02:01 -07:00
Larry Gritz f717fc73db fix(raw): apply LibRaw memory cap before unpack to prevent OOM (#5275)
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>
2026-07-05 17:00:04 -07:00
Aleksandr Motsjonov c8f2c70b54 python/testing: Improve python test coverage (#5260)
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>
2026-07-04 18:32:26 -07:00
Larry Gritz 6e9b86ee4c fix(tiff): fix heap overflow unpacking sub-8-bit contig CMYK samples (#5296)
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>
2026-07-04 15:57:32 -07:00
Larry Gritz 72cea5a2cf fix(psd): detect implausibly large ICC, Exif, XMP before allocating (#5288)
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>
2026-07-03 20:55:42 -07:00
Larry Gritz 31201c22df fix(tiff): avoid use-after-scope in multithreaded scanline reads (#5294)
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>
2026-07-03 20:53:11 -07:00
Larry Gritz 50481b0f90 fix(psd): validate color_mode before RawColor early return (#5282)
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>
2026-07-03 20:29:46 -07:00
Larry Gritz 73e32c096c fix(targa): reject implausible image dimensions before allocating (#5293)
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>
2026-07-03 19:21:23 -07:00
Larry Gritz 13ffc025e8 fix(ico,png): avoid leaking error string across png_chunk_error longjmp (#5289)
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>
2026-07-03 13:06:41 -07:00
Larry Gritz a9f7a359a3 fix(ico): Better error checking for PNG-in-ICO files (#5264)
Signed-off-by: Larry Gritz <lg@larrygritz.com>
2026-07-02 13:16:03 -07:00
Larry Gritz d11941efe7 fix(dds): clamp pixel memcpy to sizeof(uint32_t) to prevent stack overflow (#5287)
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>
2026-07-02 13:15:34 -07:00
Larry Gritz 94b73bd1ed fix(dds): reject corrupt bytes-per-pixel before it forces a huge allocation (#5286)
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>
2026-07-02 13:14:57 -07:00
Larry Gritz 9cda48b150 fix(iff): detect corrupt chunk sizes, flags, channel configs (#5268)
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>
2026-07-02 09:09:14 -07:00
Larry Gritz 30cb21ac8c webp: Improve valid_file to check the header for magic words (#5266)
This is MUCH faster than asking libwebp to open the file!

Signed-off-by: Larry Gritz <lg@larrygritz.com>
2026-06-30 19:09:26 -07:00
Larry Gritz 6912562187 fix(openexr): Add check_open to exrinput_c (#5280)
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>
2026-06-30 19:08:49 -07:00