There is a newer version of the record available.

Published September 23, 2026 | Version v1.0.1
Software Restricted

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

rapl(LEAFuzz-RAPL)

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

  1. Project Structure
  2. Dependencies
  3. Build Instructions
  4. Running a Campaign
  5. Testing
  6. AFLPlusPlus Integration
  7. Analyses
  8. Cleaning the Build
  9. 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, and afl-llvm-tti-cost-optimised-pass.so, the LLVM pass that weights basic blocks.
  • src/tti/, src/hwpc/: the measuring code linked into the _tti binaries. 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.lock records what it was built from.
  • build/: every binary, <target>_fuzzer and <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

  • clang and llvm-config 17 or newer — the TTI cost pass needs it
  • GCC/G++ newer than 11.0, Make, CMake (>= 3.24)
  • CPPJoules, linked into the preload library
  • perf (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. favored replaces exec_us * len with cost * len in the edge-owner comparison; alias multiplies each seed's sampling weight by a bounded power of its cost relative to the queue median. The budget site 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

Restricted

The record is publicly accessible, but files are restricted. Log in to check if you have access.

Additional details

Dates

Created
2026-09-25