Artifact of LEAFuzz
Authors/Creators
Description
LEAFuzz
LEAFuzz is a modular energy-aware fuzzer built on AFL++. It attaches a seed-level cost to every queue entry and lets that cost steer scheduling, so the fuzzer prefers inputs that are informative but cheap to execute.
Three interchangeable cost backends produce that signal:
| backend | signal | needs |
|
|
measured package + DRAM energy, through cppJoules | RAPL access (bare metal, root or CAP_SYS_ADMIN) |
tti_path (LEAFuzz-TTI) |
compiler-assigned TTI path cost, accumulated along the executed path | nothing beyond the instrumented binary |
tti_hwpc (LEAFuzz-Hybrid) |
the TTI path cost plus LLC misses and failed speculation | perf_event_open (CAP_PERFMON) and a one-time scaling step |
The cost reaches scheduling at two sites: favoured-seed selection, where it replaces execution time in the edge-owner score, and the alias table, where it multiplies each seed's sampling weight. AFL++'s mutation budget is left untouched.
Table of Contents
- Project Structure
- Dependencies
- Build Instructions
- Running a Campaign
- Testing
- AFLPlusPlus Integration
- Analyses
- Cleaning the Build
- Notes on Modifications
Project Structure
.
├── AFLPlusPlus # AFL++ 4.34a: cost-aware scheduling + the TTI pass
├── configs
│ └── cells.tsv # the campaign matrix: code -> backend/sampling/policy/binary
├── cppJoules # RAPL reader used by the preload library
├── EcoFuzz # AFL 2.52b baseline (submodule)
├── initial-seeds # frozen corpora, one corpus.lock each
├── Makefile
├── results # seed replays and the per-target cost scales
├── scripts
│ ├── build # per-target build scripts and buildlib.sh
│ ├── fuzzer # run_campaign.sh, the sweeps, corpus replay
│ ├── proxy # the preliminary scaling step for tti_hwpc
│ ├── probes # availability, RAPL resolution, seed generators
│ └── analyses
│ └── final-analyses # rebuilds every table and figure in the paper
├── src
│ ├── tti # path-cost runtime linked into the _tti binaries
│ ├── hwpc # perf_event helper the hybrid backend calls
│ ├── preload # LD_PRELOAD energy tracker for the RAPL backend
│ ├── oss-fuzz
│ └── tests
└── targets # public.zip and MANIFEST per target
AFLPlusPlus/: the fuzzer. Carries the cost backends, the two scheduling sites, andafl-llvm-tti-cost-optimised-pass.so, the LLVM pass that weights basic blocks.src/tti/,src/hwpc/: the measuring code linked into the_ttibinaries. Both are compiled without AFL++'s instrumentation and without the TTI pass, so they enter neither the coverage map nor the path cost.src/preload/:energy_preload.cpp, the LD_PRELOAD library that reads RAPL in the forkserver child and publishes the deltas through shared memory.initial-seeds/<target>/minimised/: the corpus campaigns actually read. Frozen once;corpus.lockrecords what it was built from.build/: every binary,<target>_fuzzerand<target>_fuzzer_tti, plus the preload libraries.
Prerequisites
Set the power governor to performance before fuzzing:
cd /sys/devices/system/cpu
echo performance | sudo tee cpu*/cpufreq/scaling_governor
The tti_hwpc backend and the perf probes need unprivileged counter access:
echo "kernel.perf_event_paranoid=-1" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
The scaling step (make scale_pilot) must run on an idle machine: the speculation counter's median shifts under load.
Dependencies
clangandllvm-config17 or newer — the TTI cost pass needs itGCC/G++newer than 11.0,Make,CMake(>= 3.24)CPPJoules, linked into the preload libraryperf(linux-tools-generic)
make setup_environment installs all of them; it uses sudo, which is why it is a separate step and not part of make setup. To install CPPJoules by hand, follow the instructions here, or:
curl https://raw.githubusercontent.com/rishalab/CPPJoules/main/installer.sh | bash
source ~/.bashrc
If llvm-config is not on PATH, point the build at one:
make LLVM_CONFIG=llvm-config-17 local_afl
Build Instructions
Build Everything
make setup
This runs, in order: local_afl (AFL++ and the TTI pass), preload_all (the four preload variants), targets and targets_tti (both binaries per target), seeds -> freeze -> lock (fetch, afl-cmin once, record), and verify.
make all stops after the binaries and skips the corpus pipeline.
Fuzz Targets
make targets # build/<target>_fuzzer, plain instrumentation
make targets_tti # build/<target>_fuzzer_tti, plus the path-cost pass
Targets are libarchive sqlite3 brotli quickjs harfbuzz. Every configuration shares the same coverage instrumentation; only tti_path and tti_hwpc fuzz the _tti binary.
Seed Corpora
make seeds freeze lock
freeze runs afl-cmin once per target and then refuses to redo it, since re-minimising against a rebuilt binary would change which seeds get fuzzed and make campaigns from either side incomparable. FREEZE_ARGS=--force overrides.
Preliminary Scaling Step
make scale_pilot # all targets, ~30 min each, IDLE machine only
make scale_pilot_sqlite3 # one target
Only tti_hwpc needs this. It runs a 30-minute AFL++ campaign, replays its queue on the _tti binary, and writes the per-target scale factors that put the counter terms on the same median scale as the path cost.
Preload Library Variants
make preload # build/energy.so direct execution
make preload_afl # build/energy_afl.so the RAPL backend uses this
make preload_print # build/energy_print.so prints measurements
make preload_print_afl # build/energy_print_afl.so both
Running a Campaign
One campaign is one cell. scripts/fuzzer/run_campaign.sh resolves a code from configs/cells.tsv into the fuzzer, backend, sampling point and policy, and records everything it used in cell.meta:
scripts/fuzzer/run_campaign.sh --target sqlite3 \
--bin build/sqlite3_fuzzer_tti \
--corpus initial-seeds/sqlite3/minimised \
--out /path/to/cell_dir --code t1 --duration 12h
DRY_RUN=1 prints the command without running it.
| code | configuration | binary | |---|---|---| | c1 | AFL++ (baseline) | plain | | x1 | EcoFuzz (baseline) | plain | | c4 | LEAFuzz-RAPL | plain + energy_afl.so | | t1 | LEAFuzz-TTI | _tti | | h1 | LEAFuzz-Hybrid | _tti + scale file |
The codes map onto the environment AFL++ parses strictly — AFL_COST_BACKEND, AFL_COST_SAMPLING, AFL_COST_POLICY — so an unknown value or an incoherent combination aborts the campaign instead of silently degrading to a different configuration. The scheduling levers are separate: AFL_COST_SITES (favored, alias, budget), AFL_COST_K_SEL, AFL_COST_SEL_BAND and AFL_COST_CENTER; AFL_COST_CONFIG points tti_hwpc at its scale file.
scripts/fuzzer/run_sweep_blocked.sh runs whole blocks: every configuration back-to-back on one node, in a Latin square, which is the design the paper's 500 campaigns use.
Testing
make verify
Checks that every <target>_fuzzer and <target>_fuzzer_tti exists, that each _tti binary actually carries the runtime (SIG_AFL_TTI_RUNTIME), that every target has a public corpus and a frozen one, and that each corpus.lock still matches what is on disk. It is the last step of make setup and worth re-running after any rebuild.
make hello builds src/tests/hello.cpp, a minimal program for checking the preload library on its own:
LD_PRELOAD=build/energy.so ./build/tests/hello
AFLPlusPlus Integration
make local_afl
builds afl-fuzz, afl-clang-fast and the TTI cost pass from the local AFLPlusPlus/ tree. Targets are always compiled with that tree's afl-clang-fast, never a system-wide AFL++.
Analyses
scripts/analyses/final-analyses/ rebuilds everything the paper reports. The per-campaign tables it needs ship with it, so none of it requires the raw sweeps:
scripts/analyses/final-analyses
├── statistics # descriptive, paired and rank statistics; the scaling-step tables
├── power # wall-socket meter against RAPL; the scaling-step energy
├── plots # coverage against wall-clock time
├── horizons # 2 h / 4 h / 12 h cuts, saturation, energy to a coverage goal
└── paper-numbers # numbers quoted only in the text, with the inputs they need
- Tables 3 and 5:
cd statistics && python3 tables.py --no-baseline --out total-coverage - Table 4:
cd paper-numbers && python3 friedman_table.py - Table 6:
cd statistics && python3 setup_amortization.py --no-baseline --tex-k 1 2 3 5 20 --long-names --out total-coverage - Figure 4:
cd plots && python3 coverage_time.py --out . - Figure 5:
cd horizons && python3 energy_to_coverage.py --libsql <sweep> --hqb <sweep>
paper-numbers/ holds the one-off scripts behind claims that have no table of their own: the paired coverage comparisons (cov_table.py), the same comparisons repeated at 0.5-12 h (cross.py), throughput and campaign energy (axes.py), the percentages quoted with LEAFuzz first (rev2.py), and the scaling-factor reuse check (lam_buckets.py, lam_spread.py), whose 100 scaling steps and seed replays are bundled in paper-numbers/inputs/.
--no-baseline selects the paper's metric, total coverage per marginal kJ; without it the same scripts report the coverage gain over the seed corpus, and the conclusions change. scripts/analyses/final-analyses/README.md maps every table and figure to the command that produces it.
Cleaning the Build
make clean # remove build/ entirely
make clean_targets # only the per-target scratch trees, keep the binaries
Neither touches initial-seeds/: the frozen corpora are experiment inputs, not build artefacts.
Notes on Modifications
Against upstream AFL++ 4.34a:
- Cost collection. Each backend measures during AFL++'s calibration stage; a seed's cost is the mean over its valid measurements. A measurement that the backend cannot take is recorded as invalid, never as zero.
- Two scheduling sites.
favoredreplacesexec_us * lenwithcost * lenin the edge-owner comparison;aliasmultiplies each seed's sampling weight by a bounded power of its cost relative to the queue median. Thebudgetsite exists but is off in the paper's configurations. - Deterministic arithmetic. Scale factors are stored as rationals and the cost is evaluated in 128-bit integers, so scheduling does not depend on floating-point rounding.
- Runtime interface. Backend, sampling point and scale file are chosen through the environment, so one build of the fuzzer carries all three backends.
- Instrumentation. The TTI pass is a compile-time LLVM plugin; the runtime it calls is compiled with the plain compiler so that the measuring code stays out of both the coverage map and the path cost.
Files
Additional details
Dates
- Created
-
2026-09-25