OpenImageIO uses libFuzzer to
exercise image format readers against malformed input. A single binary,
oiio_fuzz_image, covers all compiled-in formats by dispatching at runtime
based on the OIIO_FUZZ_FORMAT environment variable. Nightly CI fuzzes every
format in parallel; crash reproducers are uploaded as GitHub Actions
artifacts.
- clang ≥ 14 with libFuzzer support (
-fsanitize=fuzzer). GCC does not support libFuzzer and will be rejected by CMake with a clear error.- On macOS, Apple's clang (from Xcode / Command Line Tools) does not
ship the libFuzzer runtime, so
-fsanitize=fuzzerfails to link. Install upstream LLVM (brew install llvm) and point CMake at it:-DCMAKE_C_COMPILER=$(brew --prefix llvm)/bin/clang -DCMAKE_CXX_COMPILER=$(brew --prefix llvm)/bin/clang++. With AppleClang, CMake warns and skips the fuzz targets (the rest of OIIO still builds).
- On macOS, Apple's clang (from Xcode / Command Line Tools) does not
ship the libFuzzer runtime, so
- CMake ≥ 3.18
- All optional format libraries you want fuzz coverage for (the same ones
used in a normal OIIO build). The
aswf/ci-oiio:2027container has all of them pre-installed.
cmake -B build -S . \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
-DCMAKE_BUILD_TYPE=Release \
-DOIIO_BUILD_FUZZ_TARGETS=ON \
-DSANITIZE=address,undefined
cmake --build build --target oiio_fuzz_image -j$(nproc)OIIO_BUILD_FUZZ_TARGETS=ON is the only new flag. SANITIZE=address,undefined
instruments the full OpenImageIO library for maximum bug detection — omitting
it produces a valid binary but with much weaker coverage.
By default the local fuzzing engine is -fsanitize=fuzzer. On OSS-Fuzz the
LIB_FUZZING_ENGINE environment variable is set to the engine's .a path and
is picked up automatically by src/fuzz/CMakeLists.txt.
build/src/fuzz/oiio_fuzz_image --list-formatsPrints one format name per line for every format compiled into this build.
This list drives the CI lint check: the build fails if any format returned
here lacks a directory under src/fuzz/corpora/.
# Fuzz JPEG for 60 seconds using the seed corpus as a starting point:
OIIO_FUZZ_FORMAT=jpeg \
build/src/fuzz/oiio_fuzz_image \
src/fuzz/corpora/jpeg/ \
-max_total_time=60
# Useful flags:
# -timeout=60 kill any single input that takes longer than 60 s
# -jobs=4 run 4 worker processes in parallel
# -runs=0 process seeds only, no mutation (useful to verify seeds)
# -max_len=65536 cap input size to 64 KB (speeds up throughput)The fuzzer writes new interesting inputs into the corpus directory and any
crash reproducers as crash_<hash> files in the current directory.
- Download the crash artifact from the failed GitHub Actions run
(named
fuzz-crashes-<format>-<run_id>). - Extract the
crash_<format>_<hash>file. - Run:
OIIO_FUZZ_FORMAT=<format> \
build/src/fuzz/oiio_fuzz_image \
crash_<format>_<hash>The AddressSanitizer or UBSan report appears on stderr. The key fields are
the error type (e.g. heap-buffer-overflow), the stack trace at the point of
the violation, and the allocation site.
Reduce a crash reproducer to the smallest input that still triggers it:
OIIO_FUZZ_FORMAT=<format> \
build/src/fuzz/oiio_fuzz_image \
-minimize_crash=1 \
-exact_artifact_path=min_crash \
crash_<format>_<hash>Commit min_crash to testsuite/fuzz-<format>/ as a regression test before
merging the fix.
When a new format plugin is added to OIIO:
- The format automatically appears in
--list-formats(no harness changes needed). - The CI lint job (
fuzz-corpus-lintin.github/workflows/ci.yml) will fail untilsrc/fuzz/corpora/<format>/exists. This is intentional — it enforces that every compiled-in format has at least a corpus directory. - Create the directory and add 1–5 representative seed files (each ≤ 100 KB):
mkdir src/fuzz/corpora/<format>/
# copy or generate seed files
cp path/to/sample.<ext> src/fuzz/corpora/<format>/
# Or generate a small synthetic seed:
oiiotool --create 64x64 3 --ch R,G,B -o src/fuzz/corpora/<format>/seed.<ext>- Verify the seeds parse cleanly:
OIIO_FUZZ_FORMAT=<format> \
build/src/fuzz/oiio_fuzz_image \
src/fuzz/corpora/<format>/ \
-runs=0- Commit both the corpus directory and any seed files.
To populate corpus seeds from testsuite and companion image repos run:
# Populate src/fuzz/corpora/<format>/ (local dev)
python3 src/fuzz/populate_corpora.py [--format <name>]
# Or write directly into a working corpus dir (as CI does):
python3 src/fuzz/populate_corpora.py --format <name> --dest corpus/The script auto-detects companion repos at ../oiio-images (sibling of the
repo root, as checked out by the fuzz CI job) or at build/testsuite/oiio-images
(fetched by oiio_setup_test_data during a regular test run). Only synthetic
seeds (formats with no existing source files, such as seed.hdr) are committed
to src/fuzz/corpora/; all other seeds are sourced from testsuite/ or
companion repos at fuzz time.
oiio_fuzz_image resolves the active format in priority order:
OIIO_FUZZ_FORMATenvironment variable — used by CI matrix jobs.basename(argv[0])stripped offuzz_prefix — used by OSS-Fuzz per-format symlinks (fuzz_jpeg → oiio_fuzz_image).--format=<name>command-line argument.- None set → the binary prints available formats and exits with an error.
libFuzzer enforces an rss_limit_mb (set in
src/fuzz/oiio_fuzz_image.options, currently 4096 MB) and reports a crash if
the process exceeds it. OIIO's own decode-bomb guards default to much larger
values (limits:imagesize_MB = 32768, limits:resolution = 1048576), so a
corrupt header claiming a multi-GB image would trip libFuzzer's OOM kill before
OIIO's guard rejects it — a false positive.
To keep the two budgets commensurate, OIIO_FUZZ_INIT (in
src/fuzz/fuzz_image.cpp) lowers OIIO's limits well under the RSS budget:
limits:imagesize_MB to 2048 (half the budget, leaving headroom for decode
scratch and process overhead) and limits:resolution to 65536. With these in
place OIIO rejects oversized headers through its normal error path instead of
allocating into an OOM kill. If you change rss_limit_mb, update these limits
to match.
- Nightly fuzz (
.github/workflows/fuzz.yml): 29-format parallel matrix, Tier 1 formats run for 1 hour, Tier 2 for 30 minutes. Evolved corpus is cached per format per branch (seedata-model.mdEvolvedCorpus for the exact cache-key scheme). - Corpus lint (
fuzz-corpus-lintjob in.github/workflows/fuzz.yml): buildsoiio_fuzz_imageon every PR, runs--list-formats, and fails if any compiled-in format lacks asrc/fuzz/corpora/<format>/directory.
Not yet onboarded. No ossfuzz/ directory exists in the repo yet — this is
deferred future work. The harness already supports what OSS-Fuzz would need
($LIB_FUZZING_ENGINE linkage, argv[0]-based per-format dispatch for
fuzz_<format> symlinks, --list-formats), so onboarding remains a small,
additive step: a build.sh would loop over --list-formats output to create
per-format symlinks and seed corpus zips automatically, so new formats would
be covered without any build.sh changes. See
specs/001-image-fuzzing/research.md §8 for the intended design.