Fuzzing infrastructure for the Solidity compiler. Contains OSS-Fuzz harnesses, fuzzers, and debug runners to debug & reproduce findings.
The repo supports three independent fuzzing workflows. Each uses its own toolchain and its own out-of-tree build directory; they never share object files, so you can rebuild any one without touching the others.
| Tree | Compiler | Workflow / artefacts |
|---|---|---|
build/ |
host gcc/clang | solc, debug runners (sol_debug_runner, yul_debug_runner), for reproducing crashes |
build_ossfuzz/ |
clang + libc++ (in Docker) | libFuzzer harnesses (sol_proto_ossfuzz_*, yul_proto_ossfuzz_*, …) |
build_afl/ |
afl-clang-fast |
AFL++ differential fuzzer (sol_afl_diff_runner) |
The fuzz build must go through Docker — libFuzzer + MemorySanitizer require an instrumented libc++ that only the OSS-Fuzz Docker image ships. Sections below cover each tree in turn.
git clone --recurse-submodules https://github.com/argotorg/solidity-fuzzing.git
cd solidity-fuzzing
# Or if already cloned without submodules:
git submodule update --init --recursiveMake sure to have the following installed:
- gcc / g++ (C++20 support required, i.e. GCC 10+)
- cmake (>= 3.13)
- make
- libboost-dev, libboost-program-options-dev, libboost-filesystem-dev
- linux-perf
- gdb
- protobuf-compiler (protoc)
- ccache
- docker
We'll need a full solidity build along with debug tools (sol_debug_runner,
yul_debug_runner) built with a standard CMake workflow. They link against the
solidity libraries built from the submodule.
mkdir -p build && cd build
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_FLAGS="-fno-omit-frame-pointer" -DCMAKE_C_FLAGS="-fno-omit-frame-pointer" ..
make -j$(nproc)
cd ..This builds the following debug tools:
sol_debug_runner— reproducessol_proto_ossfuzz_evmone*findings (and, with--afl, AFL crashes fromsol_afl_diff_runner)yul_debug_runner— reproducesyul_proto_ossfuzz_evmone*findings
docker build -t solidity-ossfuzz -f scripts/docker/Dockerfile.ubuntu.clang.ossfuzz .docker run --rm -v "$(pwd)":/src/solidity-fuzzing -ti solidity-ossfuzz \
/src/solidity-fuzzing/scripts/build_ossfuzz.shThis builds all relevant fuzzer targets under build_ossfuzz.
The most important are the libfuzzer-based protobuf targets to be ran standalone:
sol_proto_ossfuzz_*— Solidity differential fuzzersyul_proto_ossfuzz_*— Yul differential fuzzers
./build_ossfuzz/tools/ossfuzz/sol_proto_ossfuzz_evmone corpus_dir_sol
./build_ossfuzz/tools/ossfuzz/yul_proto_ossfuzz_evmone corpus_dir_yulsol_afl_diff_runner reads a single .sol file, compiles + deploys it under
two optimiser settings (minimal vs standard), calls each with deterministic
calldata, and solAsserts that status / output / logs / storage match. On any
mismatch the unhandled exception triggers SIGABRT and AFL++ records a crash.
It works under afl-fuzz and standalone (file path or stdin).
Two binaries get built — same source, different toolchain:
| Path | Toolchain | When to use |
|---|---|---|
build/tools/afl/sol_afl_diff_runner |
host gcc | one-off check on a single .sol; reproducing crashes |
build_afl/tools/afl/sol_afl_diff_runner |
afl-clang-fast |
real fuzzing campaigns under afl-fuzz |
AFL++ itself, the afl-ts AST-aware custom mutator, and the
tree-sitter-solidity grammar are all vendored as submodules — no system
AFL++ install needed. The grammar builds in the default make target;
AFL++ + afl-ts are opt-in (they need clang + llvm-dev that the
regular host build doesn't).
# Build solc + host harness + grammar + AFL toolchain (needs clang + llvm-dev).
# Apply local patches against vendored submodules (idempotent — skip if already applied).
for p in patches/*.patch; do
git apply --reverse --check "$p" 2>/dev/null || git apply "$p"
done
sudo apt-get install libprotobuf-dev rustup
rustup toolchain install nightly
mkdir -p build && cd build && cmake .. && make -j$(nproc) && cd .. # Build solc + host harness + grammar.
make -C build -j$(nproc) aflplusplus afl_ts # Build the AFL toolchain (needs clang + llvm-dev).
tools/afl/build_afl.sh
# Build the AFL-instrumented harness in build_afl/.
tools/afl/build_instrumented.sh
# Setup: pull ~15 real-world projects + build the merged seed corpus + expand corpus
tools/afl/fetch_realworld.sh # pull real-world projects
tools/afl/build_corpus.sh # writes corpus_afl/ (~8700 files)
tools/afl/build_corpus_tsgen.sh # grammar-driven extra surface via tsgen
cp corpus_tsgen/*.sol corpus_afl/
# One-time system setup: AFL++ requires this kernel setting.
echo core | sudo tee /proc/sys/kernel/core_pattern
# Launch — coverage-guided AFL++ + afl-ts AST mutation, all from submodules:
tools/afl/run_afl_parallel.sh -j 8 # multi-threaded
tools/afl/run_afl.sh # single-threaded
# Repro a crash still with the exact harness AFL ran
build/tools/afl/sol_afl_diff_runner some.sol; echo $? # 0 = no diff, 134 = mismatch
# Replay an AFL crash with full per-config diagnostics, must use --afl
build/tools/runners/sol_debug_runner --afl findings_afl/default/crashes/id:000000,...See tools/afl/README.md for details on the harness, corpus, mutator integration, and follow-up TODOs.
tools/afl/tests/ holds a small suite of .sol inputs that pin past
harness false-positive fixes (e.g. self-bytecode introspection via
extcodecopy(address(),...) / extcodesize(address())). Each input is
expected to complete with exit code 0 — either passes the differential
or is legitimately skipped. A non-zero exit means the runner crashed
via solAssert / SIGABRT, i.e. a regression in the skip logic.
make -C build -j$(nproc) sol_afl_diff_runner # ensure host runner is built
tools/afl/tests/run.sh # runs every inputs/*.solTo add a regression input, drop a .sol under tools/afl/tests/inputs/
with a short header comment explaining what it exercises and why.
# Reproduce a sol ProtoBuf EVMOne finding:
./build/sol_debug_runner crash.sol
# Reproduce an AFL finding (".sol" file contains also calldata):
./build/sol_debug_runner --afl crash.sol
# Reproduce a Yul Protobuf EVMOne finding:
./build/yul_debug_runner crash.yulrun_yul_crashes.py -p <pass> dumps each crash-<hash> in <pass>_crash/ to
.yul and replays through yul_debug_runner with the matching single-step
optimizer, writing output to .out. Valid passes: c S L M s r D.
run_ice_crashes.py dumps each crash-<hash> in ice_crash/ to .sol via
sol_ice_ossfuzz and recompiles with solc (default args: --via-ir --optimize, override with --solc-args), writing output to .out.
-
Fuzzing binaries must link against libc++ and not libstdc++ This is [because][2] (1) MemorySanitizer (which flags uses of uninitialized memory) depends on libc++; and (2) because libc++ is instrumented (to check for memory and type errors) and libstdc++ not, the former may find more bugs.
-
Linking against libc++ requires us to compile everything solidity depends on from source (and link these against libc++ as well)
-
To reproduce the compiler versions used by upstream oss-fuzz bots, we need to reuse their docker image containing the said compiler versions
-
Some fuzzers depend on libprotobuf, libprotobuf-mutator, libevmone etc. which may not be available locally; even if they were they might not be the right versions