Coverage analysis is essential for understanding which parts of your code are exercised during fuzzing. It helps identify fuzzing blockers like magic value checks and tracks the effectiveness of harness improvements over time. Code coverage during fuzzing serves two critical purposes: 1. **Assessing harness effectiveness**: Understand which parts of your application are actually executed by your fuzzing harnesses
-fprofile-instr-generate -fcoverage-mapping-ftest-coverage -fprofile-arcscargo +nightly fuzz coverage <target>llvm-profdata merge -sparse file.profraw -o file.profdatallvm-cov report ./binary -instr-profile=file.profdatallvm-cov show ./binary -instr-profile=file.profdata -format=html -output-dir html/gcovr --html-details -o coverage.html[Fuzzing Campaign] | v [Generate Corpus] | v [Coverage Analysis] | +---> Coverage Increased? --> Continue fuzzing with larger corpus | +---> Coverage Decreased? --> Fix harness or investigate SUT changes | +---> Coverage Plateaued? --> Add dictionary entries or seed inputs
clang++ -fprofile-instr-generate -fcoverage-mapping \ -O2 -DNO_MAIN \ main.cc harness.cc execute-rt.cc -o fuzz_exec `**GCC (C/C++):**` g++ -ftest-coverage -fprofile-arcs \ -O2 -DNO_MAIN \ main.cc harness.cc execute-rt.cc -o fuzz_exec_gcov `**Rust:**` rustup toolchain install nightly --component llvm-tools-preview cargo +nightly fuzz coverage fuzz_target_1
// execute-rt.cc #include <stdio.h> #include <stdlib.h> #include <dirent.h> #include <stdint.h> extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size); void load_file_and_test(const char *filename) { FILE *file = fopen(filename, "rb"); if (file == NULL) { printf("Failed to open file: %s\n", filename); return; } fseek(file, 0, SEEK_END); long filesize = ftell(file); rewind(file); uint8_t *buffer = (uint8_t*) malloc(filesize); if (buffer == NULL) { printf("Failed to allocate memory for file: %s\n", filename); fclose(file); return; } long read_size = (long) fread(buffer, 1, filesize, file); if (read_size != filesize) { printf("Failed to read file: %s\n", filename); free(buffer); fclose(file); return; } LLVMFuzzerTestOneInput(buffer, filesize); free(buffer); fclose(file); } int main(int argc, char **argv) { if (argc != 2) { printf("Usage: %s <directory>\n", argv[0]); return 1; } DIR *dir = opendir(argv[1]); if (dir == NULL) { printf("Failed to open directory: %s\n", argv[1]); return 1; } struct dirent *entry; while ((entry = readdir(dir)) != NULL) { if (entry->d_type == DT_REG) { char filepath[1024]; snprintf(filepath, sizeof(filepath), "%s/%s", argv[1], entry->d_name); load_file_and_test(filepath); } } closedir(dir); return 0; }
LLVM_PROFILE_FILE=fuzz.profraw ./fuzz_exec corpus/./fuzz_exec_gcov corpus/cargo fuzz coverage.# Merge raw profile data llvm-profdata merge -sparse fuzz.profraw -o fuzz.profdata # Generate text report llvm-cov report ./fuzz_exec \ -instr-profile=fuzz.profdata \ -ignore-filename-regex='harness.cc|execute-rt.cc' # Generate HTML report llvm-cov show ./fuzz_exec \ -instr-profile=fuzz.profdata \ -ignore-filename-regex='harness.cc|execute-rt.cc' \ -format=html -output-dir fuzz_html/ `**GCC with gcovr:**` # Install gcovr (via pip for latest version) python3 -m venv venv source venv/bin/activate pip3 install gcovr # Generate report gcovr --gcov-executable "llvm-cov gcov" \ --exclude harness.cc --exclude execute-rt.cc \ --root . --html-details -o coverage.html `**Rust:**` # Install required tools cargo install cargo-binutils rustfilt # Create HTML generation script cat <<'EOF' > ./generate_html #!/bin/sh if [ $# -lt 1 ]; then echo "Error: Name of fuzz target is required." echo "Usage: $0 fuzz_target [sources...]" exit 1 fi FUZZ_TARGET="$1" shift SRC_FILTER="$@" TARGET=$(rustc -vV | sed -n 's|host: ||p') cargo +nightly cov -- show -Xdemangler=rustfilt \ "target/$TARGET/coverage/$TARGET/release/$FUZZ_TARGET" \ -instr-profile="fuzz/coverage/$FUZZ_TARGET/coverage.profdata" \ -show-line-counts-or-regions -show-instantiations \ -format=html -o fuzz_html/ $SRC_FILTER EOF chmod +x ./generate_html # Generate HTML report ./generate_html fuzz_target_1 src/lib.rs
// Coverage shows this block is never executed if (buf == 0x7F454C46) { // ELF magic number // start parsing buf } `**Solution**: Add magic values to dictionary file:` # magic.dict "\x7F\x45\x4C\x46"
./fuzz_exec corpus/ # Crashes on bad input, no coverage generated// Fork before executing to isolate crashes int main(int argc, char **argv) { // ... directory opening code ... while ((entry = readdir(dir)) != NULL) { if (entry->d_type == DT_REG) { pid_t pid = fork(); if (pid == 0) { // Child process - crash won't affect parent char filepath[1024]; snprintf(filepath, sizeof(filepath), "%s/%s", argv[1], entry->d_name); load_file_and_test(filepath); exit(0); } else { // Parent waits for child waitpid(pid, NULL, 0); } } } }
project(FuzzingProject) cmake_minimum_required(VERSION 3.0) # Main binary add_executable(program main.cc) # Fuzzing binary add_executable(fuzz main.cc harness.cc) target_compile_definitions(fuzz PRIVATE NO_MAIN=1) target_compile_options(fuzz PRIVATE -g -O2 -fsanitize=fuzzer) target_link_libraries(fuzz -fsanitize=fuzzer) # Coverage execution binary add_executable(fuzz_exec main.cc harness.cc execute-rt.cc) target_compile_definitions(fuzz_exec PRIVATE NO_MAIN) target_compile_options(fuzz_exec PRIVATE -O2 -fprofile-instr-generate -fcoverage-mapping) target_link_libraries(fuzz_exec -fprofile-instr-generate) `Build:` cmake -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ . cmake --build . --target fuzz_exec
-show-directory-coveragellvm-cov export -format=lcov + genhtml provides cleaner per-file reports.profdata files with timestamps to track progress over time-ignore-filename-regex to focus on SUT coverage only.gcda files across multiple runs. This is useful for tracking coverage as you add test cases:# First run ./fuzz_exec_gcov corpus_batch_1/ gcovr --html coverage_v1.html # Second run (adds to existing coverage) ./fuzz_exec_gcov corpus_batch_2/ gcovr --html coverage_v2.html # Start fresh gcovr --delete # Remove .gcda files ./fuzz_exec_gcov corpus/
llvm-cov show ./fuzz_exec -instr-profile=fuzz.profdata /path/to/src/llvm-cov show -show-directory-coverage -format=html -output-dir html/llvm-cov export -format=lcov > coverage.json# Campaign 1 LLVM_PROFILE_FILE=campaign1.profraw ./fuzz_exec corpus1/ llvm-profdata merge -sparse campaign1.profraw -o campaign1.profdata # Campaign 2 LLVM_PROFILE_FILE=campaign2.profraw ./fuzz_exec corpus2/ llvm-profdata merge -sparse campaign2.profraw -o campaign2.profdata # Compare llvm-cov show ./fuzz_exec \ -instr-profile=campaign2.profdata \ -instr-profile=campaign1.profdata \ -show-line-counts-or-regions
-O3 optimizations can eliminate code, making coverage misleading-O2 or -O0 for coverage builds-ignore-filename-regex or --exclude to filter harness filesclang++ -fprofile-instr-generate -fcoverage-mapping \ -O2 -DNO_MAIN \ main.cc harness.cc execute-rt.cc -o fuzz_exec `**Execute corpus and generate report:**` LLVM_PROFILE_FILE=fuzz.profraw ./fuzz_exec corpus/ llvm-profdata merge -sparse fuzz.profraw -o fuzz.profdata llvm-cov show ./fuzz_exec -instr-profile=fuzz.profdata -format=html -output-dir html/
-fsanitize=fuzzer for coverage builds (it conflicts with profile instrumentation)LLVMFuzzerTestOneInput) with a different main function-ignore-filename-regex flag to exclude harness code from coverage reports-show-instantiation flag for template-heavy C++ codeclang++ -fprofile-instr-generate -fcoverage-mapping \ -O2 main.cc harness.cc execute-rt.cc -o fuzz_exec `**Build for coverage with GCC:**` AFL_USE_ASAN=0 afl-gcc -ftest-coverage -fprofile-arcs \ main.cc harness.cc execute-rt.cc -o fuzz_exec_gcov `**Execute and generate report:**` # LLVM approach LLVM_PROFILE_FILE=fuzz.profraw ./fuzz_exec afl_output/queue/ llvm-profdata merge -sparse fuzz.profraw -o fuzz.profdata llvm-cov report ./fuzz_exec -instr-profile=fuzz.profdata # GCC approach ./fuzz_exec_gcov afl_output/queue/ gcovr --html-details -o coverage.html
afl-clang-fast) for coverage buildsqueue/ directory contains your corpusrustup toolchain install nightly --component llvm-tools-preview cargo install cargo-binutils rustfilt `**Generate coverage data:**` cargo +nightly fuzz coverage fuzz_target_1 `**Create HTML report script:**` cat <<'EOF' > ./generate_html #!/bin/sh FUZZ_TARGET="$1" shift SRC_FILTER="$@" TARGET=$(rustc -vV | sed -n 's|host: ||p') cargo +nightly cov -- show -Xdemangler=rustfilt \ "target/$TARGET/coverage/$TARGET/release/$FUZZ_TARGET" \ -instr-profile="fuzz/coverage/$FUZZ_TARGET/coverage.profdata" \ -show-line-counts-or-regions -show-instantiations \ -format=html -o fuzz_html/ $SRC_FILTER EOF chmod +x ./generate_html `**Generate report:**` ./generate_html fuzz_target_1 src/lib.rs
-Xdemangler=rustfilt flag makes function names readablesrc/lib.rs) to focus on crate code-show-line-counts-or-regions and -show-instantiations for better Rust-specific outputfuzz/corpus/<target>/# Use standard compiler, not honggfuzz compiler clang -fprofile-instr-generate -fcoverage-mapping \ -O2 harness.c execute-rt.c -o fuzz_exec `**Execute corpus:**` LLVM_PROFILE_FILE=fuzz.profraw ./fuzz_exec honggfuzz_workspace/
hfuzz-clang for coverage buildserror: no profile data availableLLVM_PROFILE_FILE was set and .profraw file existsFailed to load coverageno_working_dir_found error (gcovr).gcda files in unexpected location--gcov-ignore-errors=no_working_dir_found flag-show-directory-coverageincompatible instrumentationcargo fuzz coverage command for Rust projectsshow, report, and export. Documents all filtering options, output formats, and integration with llvm-profdata.