This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This repo produces two sets of binaries from two different build directories. They are not interchangeable.
| Build tree | Toolchain | Produces |
|---|---|---|
build/ |
Host compiler, cmake | solc, sol_debug_runner, yul_debug_runner, stackshuffler — for reproducing |
build_ossfuzz/ |
clang + libc++ in Docker | libFuzzer fuzzers under tools/ossfuzz/ — for fuzzing |
Fuzzing binaries must link against libc++ (MemorySanitizer requires it; libc++ is instrumented). That is why the fuzz build only works inside the OSS-Fuzz Docker image — it pulls in the exact compiler/toolchain OSS-Fuzz uses upstream.
docker run --rm -v "$(pwd)":/src/solidity-fuzzing -ti solidity-ossfuzz \
/src/solidity-fuzzing/scripts/build_ossfuzz.shNever run cmake/make directly on the host to build anything under build_ossfuzz/. It will link against the wrong libc++/toolchain and either fail or silently produce a broken fuzzer. If the docker image is missing, build it first:
docker build -t solidity-ossfuzz -f scripts/docker/Dockerfile.ubuntu.clang.ossfuzz .scripts/build_ossfuzz.sh regenerates *.pb.{cc,h} from the .proto files before building. The proto bindings are committed (so that LSP / IDE works) but are refreshed on every fuzz build.
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)solidity/— git submodule; built as a subdirectory of the top-levelCMakeLists.txtwithTESTS=OFF. All fuzzers and runners link against the resultingsolidity/libsolclibraries.evmone/— git submodule; built as anExternalProject. Runnersdlopenlibevmone.soat runtime; its directory is baked into the runner RPATH soLD_LIBRARY_PATHis not needed.tools/common/EVMHost.{cpp,h}— fuzz-specific extensions of solidity's EVMHost (m_subCallOutOfGas,m_contractCreationOrder). Everything links against this copy, not the one in the solidity submodule.tools/ossfuzz/— libFuzzer harnesses and their proto grammars. Seetools/ossfuzz/README.mdfor the per-binary breakdown.tools/runners/— standalone reproducers (sol_debug_runner,yul_debug_runner,sol_crash_backtrace.py,check_diversity_and_errors.sh).tools/shuffler-fuzzer/— standalonestackshufflerCLI.tools/afl/— AFL-specific harnesses.cmake/— overrides forfmtlib,nlohmann-json,range-v3, submodules. Prepended toCMAKE_MODULE_PATHbecause solidity's cmake modules useCMAKE_SOURCE_DIR, which points at us when built as a subdir.
Most *_ossfuzz_* binaries share a source file and are differentiated by compile definitions (see tools/ossfuzz/CMakeLists.txt and the table in tools/ossfuzz/README.md):
sol_proto_ossfuzz_evmoneandsol_proto_ossfuzz_evmone_viair— both built fromsolProtoFuzzer2.cpp; the_viairvariant adds-DFUZZER_MODE_VIAIR.yul_proto_ossfuzz_evmone{,_ssacfg,_check_stack_alloc,_no_ssa}andyul_proto_ossfuzz_evmone_single_pass_<abbr>(one per pass inc S L M s r D) — all built fromyulProtoFuzzerEvmone.cppwithFUZZER_MODE_*defines. The single-pass variants additionally setFUZZER_SINGLE_PASS_CHAR="<abbr>"so the target pass is baked in at compile time (no env var).sol_ice_ossfuzz— frontend-ICE hunter. Deliberately letsInternalCompilerError,solAssert, and boost assertions escape; onlyUnimplementedFeatureError+StackTooDeep*are caught as known non-bugs. Othersol_proto_*fuzzers should ignore ICE and leave it to this one.sol_recstruct_alias_ossfuzz— narrow harness for report #1392 (recursive struct storage-copy aliasing). Uses a dedicated grammar (solRecStructAliasProto.proto+protoToSolRecStructAlias.cpp) that emits three aliasing shapes: DIRECT (root=root.children[i]), VIA_POINTER (through aNode storage plocal), GRANDCHILD (root=root.children[i].children[j]). Primitive field types vary acrossuint8..256 / int256 / address / bool / bytes32to stress storage packing. Non-differential —test()returns a bitmask of mismatching fields; harness asserts zero. Both legacy and IR carry the bug, so cross-config differential would not flag it.sol_roundtrip_ossfuzz— identity-oracle fuzzer (solRoundtripProto.proto+protoToSolRoundtrip.cpp+solRoundtripFuzzer.cpp). Each proto is a list of probes; each probe picks a type T, an op, and a seed. Ops: ABI round-trip, storage↔memory round-trip, delete-default, integer cast ladder. Same bitmask oracle: any violated identity sets a bit; harness asserts zero. Catches codegen/encoder bugs that corrupt the same way on both codegens (so differential fuzzers miss them).
protoToSol.cpp/protoToSol.h+solProto.proto— used by the legacysol_proto_ossfuzz_nondiff.protoToSol2.cpp/protoToSol2.h+sol2Proto.proto— newer grammar used by the differentialsol_proto_ossfuzz_evmone*and bysol_ice_ossfuzz.protoToSolRecStructAlias.cpp/.h+solRecStructAliasProto.proto— aliasing-shape grammar forsol_recstruct_alias_ossfuzz.protoToSolRoundtrip.cpp/.h+solRoundtripProto.proto— identity-probe grammar forsol_roundtrip_ossfuzz.protoToYul.cpp+yulProto.proto— Yul grammar.
- Convert the protobuf input to a source string.
- Call
runOnce()twice with two different optimizer / viaIR settings. - Compare
status_code,output_data, logs, storage, transient storage. Mismatches are reported viasolAssert(…)— which throwslangutil::InternalCompilerError, so libFuzzer records the crash. - Compile-path failures that are either known non-bugs or ICE are caught inside
runOnceand surfaced asEVMC_INTERNAL_ERROR, which the caller skips. These must never be caught at the outer scope — doing so would silently swallow real differential mismatches (they share theInternalCompilerErrortype withsolAssert).
Crash inputs are raw protobuf; to inspect/debug, dump them to text first using env vars the fuzzer recognises, then replay with the appropriate runner:
# Sol:
PROTO_FUZZER_DUMP_PATH=bad.sol \
./build_ossfuzz/tools/ossfuzz/sol_proto_ossfuzz_evmone crash-<hash>
./build/tools/runners/sol_debug_runner bad.sol
# Yul (also supports optimizer sequence dump):
PROTO_FUZZER_DUMP_PATH=bad.yul PROTO_FUZZER_DUMP_SEQ_PATH=bad.seq \
./build_ossfuzz/tools/ossfuzz/yul_proto_ossfuzz_evmone crash-<hash>
./build/tools/runners/yul_debug_runner bad.yul \
--optimizer-sequence "<from bad.seq>" \
--optimizer-cleanup-sequence "<from bad.seq>"
# Stack shuffler (dumps to a special .stack format):
PROTO_FUZZER_DUMP_PATH=bad.stack \
./build_ossfuzz/tools/ossfuzz/shuffler_proto_ossfuzz crash-<hash>
./build/tools/shuffler-fuzzer/stackshuffler --verbose bad.stack| Code | Meaning |
|---|---|
| 0 | All match — no bug |
| 1 | Differential mismatch found |
| 2 | Normal compilation failure / file error |
| 3 | Internal compiler error (assertion failure, crash) |
Both runners accept --quiet (used by delta debuggers) and --output-dir (write per-config .bytecode.hex and .log).
./tools/runners/check_diversity_and_errors.sh my_corpus_sol_proto_ossfuzz_evmone 300
# Or specify a non-default fuzzer binary:
./tools/runners/check_diversity_and_errors.sh my_corpus_sol_proto_ossfuzz_evmone_viair 300 \
./build_ossfuzz/tools/ossfuzz/sol_proto_ossfuzz_evmone_viairWraps check_sol_proto_files.py — dumps N random corpus entries via the given fuzzer binary, compiles each with ./build/solidity/solc/solc, and tallies language-feature coverage. Requires both build trees.
There is one binary per target pass — yul_proto_ossfuzz_evmone_single_pass_<abbr> — each with the pass baked in at compile time via FUZZER_SINGLE_PASS_CHAR. Currently built: c S L M s r D. To add another, extend the foreach(pass …) in tools/ossfuzz/CMakeLists.txt. See tools/ossfuzz/README.md for a tmux-based parallel launcher.