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, and with
clang it also adds -fsanitize=fuzzer-no-link coverage instrumentation to the
libraries so libFuzzer can steer into the format readers. Omitting it produces
a valid binary, but one that is effectively coverage-blind.
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.
Seeds are not hand-curated into src/fuzz/corpora/ per format. At fuzz
time ci-fuzztest.bash gathers every example we have for the format from the
directories listed in FORMAT_SOURCES (src/fuzz/populate_corpora.py) — the
format's own testsuite/ fixtures (valid images and malformed/regression
files) plus the companion image repos — capped only by a per-file size limit
(MAX_BYTES, 5 MB), with no cap on the number of seeds. It then runs
oiio_fuzz_image -merge=1 to distill that pile down to the coverage-increasing
subset before the timed run. Feeding in the full, size-diverse set (not just a
few tiny files) is deliberate: mutating structurally rich real images reaches
the deep decode paths where subtle bugs hide.
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/fuzz.yml) will fail untilsrc/fuzz/corpora/<format>/exists. This is intentional — it enforces that every compiled-in format has at least a corpus directory (a.gitkeepis enough). - Wire the format's seed sources into
FORMAT_SOURCESinsrc/fuzz/populate_corpora.py: itstestsuite/<format>/src(and any dedicated malformed-fixture dirs, e.g.testsuite/<format>-corrupt/src) and the relevant companion-repo path. Regression fixtures added totestsuite/are then picked up automatically — do not also copy them intosrc/fuzz/corpora/. - Only when a format has no testsuite fixture or companion source (e.g.
hdr,iff,sgi,jpegxl), commit a small synthetic seed directly:
oiiotool --create 64x64 3 --ch R,G,B -o src/fuzz/corpora/<format>/seed.<ext>- Verify the gathered seeds parse cleanly (
-runs=0processes seeds without mutating):
python3 src/fuzz/populate_corpora.py --format <format> --dest /tmp/seeds
OIIO_FUZZ_FORMAT=<format> build/src/fuzz/oiio_fuzz_image /tmp/seeds/<format>/ -runs=0populate_corpora.py 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). The committed src/fuzz/corpora/<format>/ dir holds only
synthetic seeds that have no testsuite fixture; everything else is 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 (currently 4096 MB) and reports a crash
if the process exceeds it; CI also passes -malloc_limit_mb=2048
(src/build-scripts/ci-fuzztest.bash), which reports any single allocation
over 2 GB. 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 512 and limits:resolution to 65536. The image
size limit is a quarter of the malloc limit because some readers legitimately
allocate a few times the declared image size in one buffer (TIFF's RGBA
fallback, LibRaw's working image). 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 or malloc_limit_mb, update these limits to
match.
When triaging an OOM, first check whether the declared spec passed
check_open() and was under the 1 GB check_compression_ratio() floor. If
so, and a valid file of the same size would need the same memory, the reader
is fine and the harness limits are too loose; it is not a reader bug.
- 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.