ImplementationFuzzing

Overview

The fuzzer drives the full lex -> parse -> VM pipeline against mutated input, looking for panics, arithmetic overflow, and memory faults. It lives in compiler/fuzz-afl/ and is built on cargo-afl (AFL++), which instruments via AFL++‘s LLVM passes and therefore runs on stable Rust, no nightly toolchain required.

The target runs the VM under the sandbox profile so runaway loops and allocations become a VmErr instead of a hang, and any real crash is a genuine bug rather than resource exhaustion. The harness tightens one field — Limits { ops: 100_000, ..Limits::sandbox() } — because the default 100M-op budget, while bounded, takes long enough in an unoptimized build that AFL would flag a legitimately-terminating loop (or wide recursion) as a hang; the smaller budget keeps each execution inside AFL’s hang timeout while still reaching deep into the language. It also sets strict_input = true so input() raises instead of blocking on real stdin, which AFL feeds through shared memory. See Limits and errors.

Running it

cd compiler/fuzz-afl
./seeds.sh # generate corpus + dictionary from vm.json (once)
cargo afl build # instrument on stable, no nightly
cargo afl fuzz -i in -o out -x edge.dict target/debug/afl-pipeline # runs until Ctrl-C; add -V 300 to stop after 300s
 
cargo afl whatsup out # status summary of the ./out campaign; run in another terminal while fuzzing

For a parallel run across most of the host cores, ./deploy.sh builds, regenerates seeds, and launches one -M plus N-1 -S instances sharing one out/ (default CPU_PERCENT=75; DURATION and FRESH are optional overrides). The same target also runs on a daily schedule in CI via .github/workflows/fuzzer.yml, which fails the run on any saved crash.

Reusing the same out/ resumes the campaign: AFL recalibrates the saved queue (the dry-run pass) before fuzzing, so execs sits at 0 for a while; delete it with rm -rf out for a clean start. Under WSL, prefix the fuzz command with AFL_SKIP_CPUFREQ=1 AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1 to bypass the core-pattern and CPU-governor checks. Crashes and hangs land in out/default/. Reproduce one by piping it back into the target:

./target/debug/afl-pipeline < out/default/crashes/<id>

Inputs are generated, not committed

The seed corpus (in/) and the token dictionary (edge.dict) are derived from a single source of truth, tests/cases/vm.json, so they are gitignored and regenerated by seeds.sh:

  • in/: one file per unique program src in the VM test fixtures, giving AFL valid starting points that already exercise most of the language.
  • edge.dict: keywords, operators, and common builtins, so the byte mutator splices real tokens instead of discovering them blindly.

Only four files are tracked: Cargo.toml, src/main.rs, seeds.sh, and deploy.sh. The corpus, dictionary, AFL output, and build artifacts are all reproducible.