Replacing afl-cov and libfuzzer-cov with modern coverage gathering and great features!
Version: 1.0.0
cov-analysis generates LLVM source-based code coverage reports from a fuzzing corpus. It auto-detects the on-disk layout used by AFL++ (queue/crashes/timeouts directories, single or parallel), libFuzzer and libafl (flat corpus dir plus crash-*/leak-*/oom-* artifacts), and honggfuzz (flat corpus plus SIG*.fuzz crash files). It replays each input through a coverage-instrumented binary, merges the raw profiles, and produces HTML, text, and JSON reports via llvm-profdata and llvm-cov.
This is a rewrite of the original cov-analysis. Key changes in 1.0.0:
- New: diff reports comparing coverage between two runs
- New: stability analysis identifying source lines with non-deterministic hit counts
- Replaced gcov/lcov/genhtml with LLVM source-based coverage (
-fprofile-instr-generate,llvm-profdata,llvm-cov) - faster, more accurate under optimization cov-analysis buildsets compiler flags and builds the target;cov-analysis driveremits a ready-to-usecoverage_driver.cforLLVMFuzzerTestOneInputharnessescov-analysis diffgenerates an HTML diff report comparing coverage between two JSON exports- Rewritten in bash (was Python)
clang(any version down to 11)llvm-profdataandllvm-cov(matching the clang version; auto-detected)- AFL++ (
afl-fuzz), libafl, libfuzzer, Honggfuzz, ... - only needed to produce the corpus, not to runcov-analysis
| Fuzzer | Detected by | Input files replayed |
|---|---|---|
| AFL++ | <dir>/queue/ or <dir>/*/queue/ exists |
queue/id:*, crashes/id:*, timeouts/id:* |
| libFuzzer | flat directory of files, no queue/ |
all files except crash-*/leak-*/oom-*/timeout-*/slow-unit-* |
| libafl | flat directory of files, no queue/ |
all files except crash-*/leak-*/oom-*/timeout-*/slow-unit-* |
| honggfuzz | flat directory of files, no queue/ |
all files except SIG*.fuzz and HONGGFUZZ.REPORT.TXT |
For libFuzzer, libafl and honggfuzz, crash-like files (above) are still replayed, but under the -T timeout so a hanging input can't stall the run.
Override auto-detection with --layout afl|flat.
Use cov-analysis build to set the correct compiler flags and build your target:
# Set up a coverage build (run once per build step)
cd /path/to/project-cov/
cov-analysis build ./configure --disable-shared
cov-analysis build make -j$(nproc)cov-analysis build sets:
CC=clang CXX=clang++
CFLAGS="-fprofile-instr-generate -fcoverage-mapping -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1"
LDFLAGS="-fprofile-instr-generate"
Important: FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1 must match what was used during fuzzing - it disables the same checksums/HMACs that AFL++ bypassed.
Generate a replay driver and link it against your coverage-instrumented library:
cov-analysis driver -o coverage_driver.c
clang -fprofile-instr-generate -fcoverage-mapping \
-c coverage_driver.c -o coverage_driver.o
clang -fprofile-instr-generate \
coverage_driver.o -L./build -ltarget -o covThe driver loops over all file arguments, calls LLVMFuzzerTestOneInput for each, and installs a crash handler that flushes profiling data so crashing inputs still contribute to the report.
This step produces an llvm-cov coverage report with regions and branches:
cd /path/to/project-cov/
cov-analysis -d /path/to/afl-fuzz-output/ -e "./cov @@"To replay coverage with multiple workers, add -t:
cov-analysis -d /path/to/afl-fuzz-output/ -e "./cov @@" -t 8cov-analysis will for AFL++:
- Replay all
queue/id:*files in batch (fast) - Replay
crashes/id:*andtimeouts/id:*one-by-one with a timeout - Merge
.profrawprofiles withllvm-profdata - Generate reports in
/path/to/afl-fuzz-output/cov/
For libfuzzer/libafl/Honggfuzz cov-analysis will:
- Replay all files in the directory
- Crash files are replayed one-by-one with a timeout
Output:
/path/to/afl-fuzz-output/cov/
html/index.html ← browse this for annotated source coverage
text/ ← text format, suitable for automated analysis
summary.txt ← per-file line/branch/function percentages
coverage.json ← machine-readable export
coverage.profdata ← merged profile (baseline for iterative improvement)
For stdin-based targets (binary reads from stdin, no file argument):
cov-analysis -d /path/to/afl-fuzz-output/ -e "./target"cov-analysis -d /path/to/libfuzzer-corpus/ -e "./cov @@"Corpus files are replayed in batch mode. If your libFuzzer run used -artifact_prefix=./crashes/, point a second run at that directory to cover crash inputs too — or move artifacts into the corpus dir beforehand.
cov-analysis -d /path/to/hfuzz-workdir/ -e "./cov @@"SIG*.fuzz crash files are replayed under the -T timeout. The HONGGFUZZ.REPORT.TXT metadata file is ignored automatically.
Compare coverage between two llvm-cov JSON exports and generate an HTML diff report:
cov-analysis diff coverage_old.json coverage_new.jsonIf you use the same output directory for a subsequent run, cov-analysis renames the existing coverage.json to coverage_old.json automatically, so cov-analysis diff works with no arguments.
The report is written to <report-dir>/coverage_diff.html and shows:
- Newly covered and no-longer-covered lines per file
- Newly covered and lost functions
- Source code snippets annotated with coverage change
If the JSON paths are omitted, cov-analysis diff defaults to <report-dir>/coverage_old.json and <report-dir>/coverage.json.
The HTML diff report looks like this:
Ever wondered which source lines cause AFL++ or libafl to report instability in a fuzz target?
The stability command identifies them.
cov-analysis stability -d ../afl/out -e "./cov @@"This will give you the exact lines that are problematic, e.g.:
Stability Report
--------------------------------------------------------
Corpus size : 2 inputs
Runs : 8
Stability : 74.0% (91/123 executed lines stable)
~~ Variable-count lines (32 lines):
Lines with varying hit counts:
/prg/cov-analysis/tests/unstable.c:35-37
/prg/cov-analysis/tests/unstable.c:43
/prg/cov-analysis/tests/unstable.c:46-48
/prg/cov-analysis/tests/unstable.c:51-52
/prg/cov-analysis/tests/unstable.c:55-61
/prg/cov-analysis/tests/unstable.c:64-66
/prg/cov-analysis/tests/unstable.c:69-70
/prg/cov-analysis/tests/unstable.c:75-85
[!] Unstable coverage detected.
For parallel AFL runs (afl-fuzz -o sync_dir), point -d at the top-level sync directory. cov-analysis automatically discovers all fuzzer instance subdirectories:
cov-analysis -d /path/to/sync_dir/ -e "./cov @@"Usage: cov-analysis [report] [options]
Required:
-d <dir> Fuzzing output directory (AFL++, libFuzzer, libafl, or honggfuzz)
-e <cmd> Coverage command. Use @@ as input file placeholder.
Omit @@ to feed input via stdin instead.
Optional:
-o <dir> Report output directory (default: <afl-dir>/cov)
-t <num> Parallel replay workers/forks (default: 1)
-T <secs> Timeout for crash/timeout replay (default: 5)
--layout <kind> Force layout: 'afl' or 'flat' (default: auto-detect)
--ignore-regex <r> Filename regex to exclude from llvm-cov reports
(default: /usr/include/)
-v Verbose output
-q Quiet mode
-V Print version and exit
-h, --help Print this help and exit
Usage: cov-analysis build <build-command> [args...]
Sets CC/CXX/CFLAGS/CXXFLAGS/LDFLAGS for LLVM source-based coverage and
runs the given build command.
Usage: cov-analysis driver [-o output.c]
Emits coverage_driver.c source to stdout (or to -o FILE).
Use this for LLVMFuzzerTestOneInput harnesses to replay corpus files.
The driver loops over all file arguments, calls LLVMFuzzerTestOneInput
for each, and installs a crash handler that flushes profiling data so
crashing inputs still contribute to the coverage report.
Options:
-o <file> Write driver source to FILE instead of stdout
Usage: cov-analysis diff [<OLD_JSON> <NEW_JSON>]
Compare coverage between two llvm-cov JSON exports and generate an
HTML diff report showing newly covered, lost, and still-uncovered
lines and functions.
Defaults to <report-dir>/coverage_old.json and <report-dir>/coverage.json.
Usage: cov-analysis stability [options]
Run each corpus input N times with LLVM coverage, collect per-line hit
counts, and flag lines where counts vary across runs as "unstable."
Reports a stability percentage. If instability is found with the default
4 runs, reruns for a total of 8 to confirm.
Required:
-d <dir> Fuzzing output directory (AFL++, libFuzzer, libafl, or honggfuzz)
-e <cmd> Coverage command. Use @@ as input file placeholder.
Omit @@ to feed input via stdin instead.
Optional:
-n <num> Number of runs per corpus pass (default: 4)
-s <prefix> Only consider source lines whose file path contains
this prefix (e.g. -s src/)
-t <num> Parallel replay workers (default: 1)
--layout <kind> Force layout: 'afl' or 'flat' (default: auto-detect)
-v Verbose output
-q Quiet mode (suppress all [+] output)
-V Print version and exit
-h, --help Print this help and exit
The command outputs a Stability Report showing corpus size, number of runs, and the stability percentage (stable executed lines / total executed lines). If unstable lines are found, they are listed with file paths and line number ranges.
Examples:
cov-analysis stability -d out/ -e "./cov @@"
cov-analysis stability -d out/ -e "./cov @@" -n 8 -s src/
cov-analysis stability -d ./corpus -e "./cov @@" -t 4cov-analysis is released under the GNU Affero General Public License 3.



