Fuzzing#
onnx-light ships a set of libFuzzer
instrumented C++ harnesses
that exercise the public API surface from random / malformed inputs.
They live in the top-level fuzz/ directory and are intended to be
driven by OSS-Fuzz for
long-running coverage-guided campaigns, while a short smoke campaign
is also run in CI to catch regressions in the harnesses themselves.
The harnesses are written directly against the onnx-light C++ API,
mirroring the upstream ONNX harnesses introduced in
onnx/onnx#8052 but using
LLVMFuzzerTestOneInput instead of a Python (atheris) wrapper, so
they can be built with the standard clang + -fsanitize=fuzzer,...
toolchain that OSS-Fuzz expects for C++ projects.
Harnesses#
File |
Entry point fuzzed |
Input path |
|---|---|---|
|
|
Raw bytes → protobuf parser |
|
|
Raw bytes → protobuf parser |
|
|
UTF-8 text (ONNX text format) |
|
Raw bytes → protobuf parser |
|
|
Raw bytes → protobuf parser |
|
|
Raw bytes → protobuf parser |
|
|
|
Raw bytes → ORT flatbuffer parser |
|
(seed generator, not a fuzzer) |
Writes seed files for OSS-Fuzz / libFuzzer |
Building the harnesses#
The harnesses are compiled as standalone libFuzzer executables when
ONNX_LIGHT_BUILD_FUZZERS=ON is passed to CMake. Clang is required
because libFuzzer (-fsanitize=fuzzer,...) ships with Clang.
CC=clang CXX=clang++ cmake -S . -B build-fuzz \
-DONNX_LIGHT_BUILD_FUZZERS=ON \
-DONNX_LIGHT_BUILD_PYTHON=OFF
cmake --build build-fuzz -j
The build produces one executable per fuzz/fuzz_*.cc source file
(fuzz_checker, fuzz_model_loader, fuzz_parser,
fuzz_shape_inference, fuzz_optim_shape_inference,
fuzz_version_converter, fuzz_ort_flatbuffers) plus the
make_seed_corpus helper.
The default sanitizer set is address (so the actual link line is
-fsanitize=fuzzer,address). Pass -DONNX_LIGHT_FUZZER_SANITIZERS=...
to override it (for example undefined or memory); the
fuzzer sanitizer is always added automatically.
Running a harness locally#
Each harness takes the standard libFuzzer command line. To run a short smoke campaign:
./build-fuzz/fuzz_checker -runs=1000
./build-fuzz/fuzz_model_loader -runs=1000
./build-fuzz/fuzz_parser -runs=1000
./build-fuzz/fuzz_shape_inference -runs=1000
./build-fuzz/fuzz_optim_shape_inference -runs=1000
./build-fuzz/fuzz_version_converter -runs=1000
./build-fuzz/fuzz_ort_flatbuffers -runs=1000
To generate the seed corpora that OSS-Fuzz uses as starting inputs:
./build-fuzz/make_seed_corpus \
/tmp/fuzz_seeds/version_converter \
/tmp/fuzz_seeds/parser \
/tmp/fuzz_seeds/shape_inference
Each directory is created if needed and filled with one seed file per model. The harnesses accept a seed-corpus directory as a positional argument:
./build-fuzz/fuzz_shape_inference /tmp/fuzz_seeds/shape_inference -runs=1000
How OSS-Fuzz uses these files#
The companion OSS-Fuzz infrastructure clones this repository and
builds each fuzz/fuzz_*.cc target with its standard C++ pipeline.
The OSS-Fuzz build.sh should:
Configure the project with
-DONNX_LIGHT_BUILD_FUZZERS=ON -DONNX_LIGHT_BUILD_PYTHON=OFF.Build each
fuzz_*target.Run
make_seed_corpusand zip each directory into the matching$OUT/fuzz_<target>_seed_corpus.zip.
Continuous fuzzing in CI#
The .github/workflows/fuzz.yml workflow builds the harnesses with
Clang + libFuzzer and runs a short smoke campaign (-runs=2000
per harness) on a weekly schedule (Mondays at 06:00 UTC), on manual
workflow_dispatch, and on pull requests that touch fuzz/**,
.github/workflows/fuzz.yml or CMakeLists.txt. It is meant to
catch regressions in the harnesses themselves and obvious shallow
bugs; long-running coverage-guided campaigns are still expected to be
driven by OSS-Fuzz.
Design notes#
Why catch (...) { return 0; }?#
Fuzz targets must never crash on expected errors — only on unexpected
ones (memory corruption, hangs, sanitizer reports). All protobuf
parse failures, ValidationError, InferenceError,
ConvertError, etc. are expected when the fuzzer feeds random
bytes. Catching them lets libFuzzer keep searching for inputs that
cause real bugs.
Why LLVMFuzzerTestOneInput?#
LLVMFuzzerTestOneInput is the standard libFuzzer entry point.
Each harness defines it with extern "C" so libFuzzer’s runtime can call it
directly without name mangling.
Adding a new harness#
Create
fuzz/fuzz_<name>.ccfollowing the pattern of an existing harness (singleextern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)entry point that catches every exception).CMake picks the new file up automatically because the fuzzer target list is globbed from
fuzz/fuzz_*.cc.If the fuzzer benefits from seed inputs, add them to
fuzz/make_seed_corpus.ccand wire up the matching output directory in the OSS-Fuzzbuild.sh.Open a PR here; once merged, update the OSS-Fuzz
build.shif a new seed directory was added.