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— auto-detected to match the selected clang version. When a versioned compiler is chosen (e.g.CC=clang-22, or the defaultclangreports version 22), the matchingllvm-profdata-22/llvm-cov-22are used so the raw profiles merge without a version mismatch.- 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. Run with no arguments and neither default report present in the current directory, it prints the help instead of an error.
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.
Wonder which corpus entries actually exercise a particular source line? The
search command replays each input in isolation and reports the ones that
reach FILE:LINE:
cov-analysis search src/parser.c:142 -d ../afl/out -e "./cov @@"Matching input paths are printed to stdout (one per line, sorted) so the result pipes cleanly; progress and the summary go to stderr:
src/parser.c:142 is reachable; scanning...
out/queue/id:000017,...
out/queue/id:000094,...
[+] 2 of 142 inputs reach src/parser.c:142
By default only queue/corpus entries are scanned. Add --crashes to also scan
crash and timeout inputs, and -t N to parallelize:
cov-analysis search src/parser.c:142 -d ../afl/out -e "./cov @@" --crashes -t 8A fast union pre-check replays the whole corpus once first; if no input reaches
the line, search reports 0 of N immediately (and tells you whether the line
is merely unreached or not present in the coverage data at all) without the full
per-input scan.
Pipe the reaching inputs straight into another tool:
cov-analysis search src/parser.c:142 -d ../afl/out -e "./cov @@" | xargs -I{} cp {} ./hits/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.
Resilient to flaky passes: a pass whose profiles cannot be collected or
merged (e.g. a crashing input that left a truncated .profraw behind) is
skipped and the run continues with the remaining passes, as long as at
least 2 passes succeed.
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. If any pass failed to collect or merge its profiles, it is skipped and the report notes how many runs were actually analyzed.
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 4Usage: cov-analysis search FILE:LINE -d <dir> -e "<cmd>" [options]
Report which corpus entries reach a given source line. Each input is replayed
in isolation; an input "reaches" FILE:LINE when its line-execution count for
that line is > 0. Matching input paths print to stdout (sorted, one per line);
progress and the summary go to stderr.
Required:
FILE:LINE Source location, e.g. src/foo.c:123 (single line)
-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:
--crashes Also scan crash and timeout inputs (default: corpus only)
-t <num> Parallel workers for the per-input scan (default: 1)
-T <secs> Per-input replay timeout in seconds (default: 5)
--layout <kind> Force layout: 'afl' or 'flat' (default: auto-detect)
-v Verbose output
-q Quiet mode
-V Print version and exit
-h, --help Print this help and exit
cov-analysis is released under the GNU Affero General Public License 3.



