Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
e61372b
Move README
msooseth Jun 25, 2026
235a001
Cleaner readme
msooseth Jun 25, 2026
0cd4ca5
Build OSS-Fuzz fuzzers natively, drop Docker
msooseth Jun 25, 2026
4aecc46
fsanitize=undefined
msooseth Jun 25, 2026
365da61
Update gitignore
msooseth Jun 25, 2026
f77a4fd
README: drop Docker/OSS-Fuzz-build cruft
msooseth Jun 25, 2026
3f242a0
cleanup
msooseth Jun 25, 2026
1ce4bd5
Update readme
msooseth Jun 25, 2026
ec575e0
No need for NYX
msooseth Jun 25, 2026
0fee6b7
ossfuzz cmake: de-duplicate targets, drop cruft, fix toolchain comment
msooseth Jun 25, 2026
323d22d
Add patch for libc++
msooseth Jun 25, 2026
1fe93f5
Update to use libc++
msooseth Jun 25, 2026
999e7c0
Update
msooseth Jun 25, 2026
ed76cc1
Fix build on GitHub
msooseth Jun 25, 2026
87877b1
Update README
msooseth Jun 25, 2026
c24e423
ossfuzz: stop committing generated *.pb.{cc,h}
msooseth Jun 25, 2026
b7f9f44
ossfuzz: disable libprotobuf-mutator examples
msooseth Jun 25, 2026
77f0026
Bump ubuntu
msooseth Jun 29, 2026
42faa0c
ossfuzz: build libFuzzer from source against libc++
msooseth Jun 29, 2026
cc51899
ossfuzz: run proto fuzzers under AFL++ + LPM (no libc++) — pilot
msooseth Jun 30, 2026
c35d9f7
ossfuzz: finish AFL+LPM rollout and rewrite READMEs
msooseth Jun 30, 2026
247b727
crash runners: follow AFL+LPM rollout (build_afl/ + AFL crash names)
msooseth Jun 30, 2026
fa6309f
ossfuzz README: fix corpus example for AFL proto fuzzers
msooseth Jun 30, 2026
62eeb87
run_ossfuzz_afl: add --resume to continue from the findings dir
msooseth Jun 30, 2026
b02c150
Update gitignore
msooseth Jun 30, 2026
98fa83d
ci: update build-fuzz workflow for the AFL++ build
msooseth Jun 30, 2026
032b7dd
ci: fix ineffective ccache dir and drop unused wget
msooseth Jun 30, 2026
5331957
docs: align fuzz docs with the AFL++ rollout, drop orphaned toolchain
msooseth Jun 30, 2026
a73b4f4
run_ossfuzz_afl: auto-seed a missing or empty corpus dir
msooseth Jun 30, 2026
a199498
ci: finish AFL++ rollout — update workflows, docs, ossfuzz cmake
msooseth Jun 30, 2026
21ebda8
run_ossfuzz_afl: seed must be valid text-format protobuf
msooseth Jun 30, 2026
468edb1
run_ossfuzz_afl: disable AFL trimming to stop text-proto parse flood
msooseth Jun 30, 2026
76b9bd4
Update to fix
msooseth Jul 1, 2026
2faf88c
Fix build
msooseth Jul 1, 2026
59cd13c
fixing
msooseth Jul 1, 2026
8fcc83e
Fix afl-cmin
msooseth Jul 1, 2026
262d18d
tsgen: afl-cmin into a real dir, not the (symlinked) corpus_tsgen
msooseth Jul 1, 2026
ea715c9
Add pycache to gitignore
msooseth Jul 1, 2026
3e62acd
Move ossfuzz build/run scripts into tools/ossfuzz/
msooseth Jul 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions .github/workflows/build-afl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,33 @@ on:
jobs:
build-afl:
runs-on: ubuntu-latest
container:
image: ghcr.io/${{ github.repository }}-afl:latest
env:
CCACHE_DIR: /ccache

# Pin ccache's dir so it matches the path cached below; ccache 4.x otherwise
# defaults to ~/.cache/ccache, which the cache step would miss.
env:
CCACHE_DIR: ~/.ccache

steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive

# Native build — no Docker. AFL++ needs clang + llvm-dev to build its
# LTO/llvm instrumentation passes (afl-clang-fast).
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential cmake ccache git \
clang llvm-dev \
libboost-all-dev

- name: Cache ccache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: /ccache
key: ccache-ubuntu-afl-${{ github.sha }}
restore-keys: ccache-ubuntu-afl-
path: ~/.ccache
key: ccache-ubuntu-aflrunner-${{ github.sha }}
restore-keys: ccache-ubuntu-aflrunner-

# The host build also drives the three external projects:
# AFLplusplus, afl-ts, tree-sitter-solidity. So a plain `make` here
Expand Down
36 changes: 33 additions & 3 deletions .github/workflows/build-fuzz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,43 @@ on:
jobs:
build-fuzz:
runs-on: ubuntu-latest
container:
image: ghcr.io/${{ github.repository }}-ossfuzz:latest

# Pin ccache's dir so it matches the path cached below; ccache 4.x otherwise
# defaults to ~/.cache/ccache, which the cache step would miss.
env:
CCACHE_DIR: ~/.ccache

steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive

# Native AFL++ build — no Docker, no libc++. The fuzzers are built with
# AFL++'s afl-clang-fast++ against the system libstdc++; boost, protobuf
# and abseil come from the system. Only libprotobuf-mutator is built from
# source (into deps_afl/) by tools/ossfuzz/build_ossfuzz.sh. clang + llvm-dev
# are needed to build AFL++'s LLVM-mode compiler; ninja + pkg-config +
# protoc are used by the build script.
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential cmake ccache git ninja-build pkg-config \
clang llvm-dev \
protobuf-compiler libprotobuf-dev libabsl-dev libboost-all-dev

- name: Cache ccache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.ccache
key: ccache-ubuntu-ossfuzz-${{ github.sha }}
restore-keys: ccache-ubuntu-ossfuzz-

# AFLplusplus is a submodule (checked out above); build its toolchain so
# afl-clang-fast++ and utils/aflpp_driver/libAFLDriver.a exist. Mirrors the
# `aflplusplus` cmake ExternalProject (make source-only NO_NYX=1).
- name: Build AFL++
run: make -C AFLplusplus source-only NO_NYX=1 -j$(nproc)

- name: Build fuzzers
run: scripts/build_ossfuzz.sh
run: tools/ossfuzz/build_ossfuzz.sh
55 changes: 0 additions & 55 deletions .github/workflows/publish-images.yml

This file was deleted.

27 changes: 13 additions & 14 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,28 @@ compile_commands.json
/.ccache
sol_debug_out*/
yul_debug_out*/
/corpuses
/tmp
/tmp.tar.xz
/my_corpus
/my_corpus_shuffler
/my_corpus_sol_ice_ossfuxx
crash-*
/my_corpus_no_ssa
/my_corpus_single_S
fuzz-*.log
/my_corpus_sol_roundtrip_ossfuzz
/my_corpus_yul_ast_comparator_fuzz
/my_corpus_yul_proto_ossfuzz_evmone_check_stack_alloc
/my_corpus_sol_proto_ossfuzz_evmone
/build_afl
/corpus_afl
/findings_afl
/my_corpus*
/realworld_cache
/my_run
/hang_triage
/ice_crash
/perf.data
/perf.data.old
/perf.dat*
/corpus_tsgen
/corpus_tsgen_raw
/corpus_afl_minimized
/shuffler_proto_corpus
/deps/
/deps_afl/
/dist
/findings_*

# Generated protobuf bindings — regenerated on every build by
# tools/ossfuzz/build_ossfuzz.sh (system protoc), so not committed.
/tools/ossfuzz/*.pb.cc
/tools/ossfuzz/*.pb.h
/__pycache__
47 changes: 27 additions & 20 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,34 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

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 |
| Build tree | Toolchain | Produces |
| ---------- | --------------------------------- | --------------------------------------------------------------------------------- |
| `build/` | Host compiler, cmake | `solc`, `sol_debug_runner`, `yul_debug_runner`, `stackshuffler` — for reproducing |
| `build_afl/` | AFL++ `afl-clang-fast++` + system libstdc++ | the proto fuzzers under `tools/ossfuzz/` (+ `sol_afl_diff_runner`) — 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.
Both trees build natively on the host — **no Docker.** The fuzz build uses AFL++'s `afl-clang-fast++` against the **system libstdc++** (no libc++), so `boost`, `protobuf`+`abseil` come from the system packages and `evmone` is the in-tree static archive built under the AFL toolchain. Only `libprotobuf-mutator` (LPM) is built from source, into `deps_afl/`. AFL++ is a submodule (`AFLplusplus/`) built via the top-level `CMakeLists.txt`; the proto fuzzers link the AFL++ driver (`utils/aflpp_driver/libAFLDriver.a`) as `LIB_FUZZING_ENGINE`.

### Building fuzzers (build_ossfuzz/) — Docker only
The proto grammars are mutated by LPM, not AFL's byte-level havoc: `tools/ossfuzz/build_ossfuzz.sh` builds one AFL++ custom-mutator `.so` per grammar (`tools/ossfuzz/lpm_afl_mutator.cc`), and `tools/ossfuzz/run_ossfuzz_afl.sh` wires the matching `.so` into `afl-fuzz` via `AFL_CUSTOM_MUTATOR_LIBRARY` + `AFL_CUSTOM_MUTATOR_ONLY=1`.

### Building fuzzers (build_afl/) — native AFL++

```bash
docker run --rm -v "$(pwd)":/src/solidity-fuzzing -ti solidity-ossfuzz \
/src/solidity-fuzzing/scripts/build_ossfuzz.sh
# Prereq: build the AFL++ toolchain once (afl-clang-fast++ + libAFLDriver.a):
make -C build aflplusplus # or: make -C AFLplusplus source-only NO_NYX=1

tools/ossfuzz/build_ossfuzz.sh
```

**Never 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:
Prerequisites (Arch package names): `clang` + `llvm-dev` (to build AFL++'s LLVM mode), `protobuf` + `abseil` (system, libstdc++), `boost` (static, system), `cmake`, `ninja`, `make`, `git`, `protoc`, `ccache`. The script:

```bash
docker build -t solidity-ossfuzz -f scripts/docker/Dockerfile.ubuntu.clang.ossfuzz .
```
1. builds `libprotobuf-mutator` static + PIC against the **system** protobuf into `deps_afl/` (skipped if `deps_afl/lib/libprotobuf-mutator.a` exists);
2. regenerates `*.pb.{cc,h}` from the `.proto` files with the **system** `protoc` so they match the linked system libprotobuf — these are **git-ignored** (regenerated on every build, not committed);
3. builds one LPM custom-mutator `.so` per grammar (plain `clang++` — the `.so` is loaded by `afl-fuzz` itself, so it must carry no AFL instrumentation);
4. configures `build_afl/` with `afl-clang-fast{,++}` (`-DOSSFUZZ=ON -DLPM_PREFIX=deps_afl -DLIB_FUZZING_ENGINE=…/libAFLDriver.a`) and builds the `ossfuzz_proto` + `ossfuzz_abiv2` targets.

`deps_afl/` and `build_afl/` are git-ignored. To force the LPM rebuild, delete `deps_afl/lib/libprotobuf-mutator.a`.

`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.
> **Note:** `tools/ossfuzz/CMakeLists.txt` is AFL-only — configuring `OSSFUZZ` with anything other than `afl-clang-fast++` fails fast with a `FATAL_ERROR`. (The old libc++/libFuzzer build flavour has been removed.)

### Building debug runners and `solc` (build/) — host cmake

Expand All @@ -43,7 +50,7 @@ make -j$(nproc)
- `solidity/` — git submodule; built as a subdirectory of the top-level `CMakeLists.txt` with `TESTS=OFF`. All fuzzers and runners link against the resulting `solidity`/`libsolc` libraries.
- `evmone/` — git submodule; built as an `ExternalProject`. Runners `dlopen` `libevmone.so` at runtime; its directory is baked into the runner RPATH so `LD_LIBRARY_PATH` is 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. See `tools/ossfuzz/README.md` for the per-binary breakdown.
- `tools/ossfuzz/` — the proto-fuzzer harnesses (run under AFL++ + LPM) and their proto grammars, plus `lpm_afl_mutator.cc` (the LPM→AFL custom-mutator bridge). See `tools/ossfuzz/README.md` for the per-binary breakdown.
- `tools/property/` — fuzztest-based property tests. Two build modes (see top-level `CMakeLists.txt` for the cmake option):
- **Property mode** (default, `build/` tree, any compiler) — each `FUZZ_TEST` runs as a gtest case with a ~1s random-sampling budget. Useful for CI smoke checks. `--fuzz=...` / `--fuzz_for=...` are no-ops here because the binary lacks coverage instrumentation.
- **Fuzzing mode** (`build_fuzztest/` tree, clang only) — `cmake -DFUZZTEST_FUZZING_MODE_ENABLED=ON -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ ..` applies `-fsanitize=fuzzer-no-link` + ASan + coverage flags to the whole tree (yul, solidity submodule, and the property target). The resulting binary supports `--fuzz=<Suite>.<Test> [--fuzz_for=<duration>]` for continuous coverage-guided fuzzing with mutator-driven input generation.
Expand All @@ -61,7 +68,7 @@ Most `*_ossfuzz_*` binaries share a source file and are differentiated by compil
- `sol_ice_ossfuzz` — frontend-ICE hunter. **Deliberately** lets `InternalCompilerError`, `solAssert`, and boost assertions escape; only `UnimplementedFeatureError` + `StackTooDeep*` are caught as known non-bugs. Other `sol_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 a `Node storage p` local), GRANDCHILD (`root=root.children[i].children[j]`). Primitive field types vary across `uint8..256 / int256 / address / bool / bytes32` to 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).
- `stack_shuffler_invariance_property` (`tools/property/stackShufflerInvariance.cpp`) — fuzztest-based property/regression target asserting that `trace(stack, target, spill) == trace(stack, target, spill')` for any `spill' ⊇ spill` whose extras don't appear in `target.args` or `liveOut`. Two FUZZ_TESTs: `TraceStable` (base = ∅) and `TraceStableUnderSuperset` (general `base ⊆ augmented`). Domain mirrors the existing libFuzzer shuffler harness (V/PHI/LIT/JUNK slot kinds, target top + tail set + padding). Run continuously via the fuzz-mode build:
- `stack_shuffler_invariance_property` (`tools/property/stackShufflerInvariance.cpp`) — fuzztest-based property/regression target asserting that `trace(stack, target, spill) == trace(stack, target, spill')` for any `spill' ⊇ spill` whose extras don't appear in `target.args` or `liveOut`. Two FUZZ_TESTs: `TraceStable` (base = ∅) and `TraceStableUnderSuperset` (general `base ⊆ augmented`). Domain mirrors the existing proto shuffler harness (V/PHI/LIT/JUNK slot kinds, target top + tail set + padding). Run continuously via the fuzz-mode build:
```
mkdir build_fuzztest && cd build_fuzztest
cmake .. -DCMAKE_BUILD_TYPE=RelWithDebInfo \
Expand All @@ -84,7 +91,7 @@ Most `*_ossfuzz_*` binaries share a source file and are differentiated by compil

1. Convert the protobuf input to a source string.
2. Call `runOnce()` twice with two different optimizer / viaIR settings.
3. Compare `status_code`, `output_data`, logs, storage, transient storage. Mismatches are reported via `solAssert(…)` — which throws `langutil::InternalCompilerError`, so libFuzzer records the crash.
3. Compare `status_code`, `output_data`, logs, storage, transient storage. Mismatches are reported via `solAssert(…)` — which throws `langutil::InternalCompilerError`, so the fuzzer records the crash.
4. **Compile-path failures that are either known non-bugs or ICE are caught inside `runOnce` and surfaced as `EVMC_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 the `InternalCompilerError` type with `solAssert`).

## Reproducing fuzzer findings
Expand All @@ -94,19 +101,19 @@ Crash inputs are raw protobuf; to inspect/debug, dump them to text first using e
```bash
# Sol:
PROTO_FUZZER_DUMP_PATH=bad.sol \
./build_ossfuzz/tools/ossfuzz/sol_proto_ossfuzz_evmone crash-<hash>
./build_afl/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_afl/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_afl/tools/ossfuzz/shuffler_proto_ossfuzz crash-<hash>
./build/tools/shuffler-fuzzer/stackshuffler --verbose bad.stack
```

Expand All @@ -127,7 +134,7 @@ Both runners accept `--quiet` (used by delta debuggers) and `--output-dir` (writ
./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_viair
./build_afl/tools/ossfuzz/sol_proto_ossfuzz_evmone_viair
```

Wraps `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.
Expand Down
11 changes: 10 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@ if (NOT EVMONE_LIB_DIR)
BINARY_DIR "${EVMONE_BUILD_DIR}"
CMAKE_ARGS
-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
# Force lib (not lib64) so evmone's ARCHIVE_OUTPUT_DIRECTORY,
# ${CMAKE_INSTALL_LIBDIR}, matches the ${EVMONE_BUILD_DIR}/lib
# path hardcoded below. GNUInstallDirs picks lib64 on non-Debian
# 64-bit hosts (e.g. NixOS), so without this the standalone
# archive lands in lib64/ and the harness link can't find it.
-DCMAKE_INSTALL_LIBDIR=lib
-DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}
-DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}
-DCMAKE_C_COMPILER_LAUNCHER=${CMAKE_C_COMPILER_LAUNCHER}
Expand Down Expand Up @@ -164,7 +170,10 @@ if (EXISTS "${CMAKE_SOURCE_DIR}/AFLplusplus/Makefile")
SOURCE_DIR "${CMAKE_SOURCE_DIR}/AFLplusplus"
CONFIGURE_COMMAND ""
BUILD_IN_SOURCE 1
BUILD_COMMAND make source-only
# NO_NYX=1 skips the optional Nyx (QEMU/KVM snapshot) backend. We only
# use the standard fork-server path (afl-fuzz + afl-clang-fast), and the
# Nyx sub-build pulls QEMU + Rust and fails noisily on non-Debian libdirs.
BUILD_COMMAND make source-only NO_NYX=1
INSTALL_COMMAND ""
BUILD_BYPRODUCTS
"${CMAKE_SOURCE_DIR}/AFLplusplus/afl-fuzz"
Expand Down
Loading