mcpField
C
coverage-analysis

coverage-analysis

github.com/trailofbits测试与质量7.9k5.5k 次安装今天更新
代码仓库

关于 coverage-analysis

coverage-analysis 为需要衡量语料触达代码路径的开发者提供模糊测试覆盖率分析方法。内容涵盖 C/C++ 与 Rust 的插桩构建、报告生成、跨轮次比较及未触达路径排查。

主要功能

  1. 01配置 LLVM 和 GCC 插桩
  2. 02使用模糊测试语料运行覆盖率分析
  3. 03生成文本或 HTML 报告
  4. 04定位未覆盖路径和魔数检查
  5. 05比较不同轮次的覆盖率
  6. 06从报告中排除 harness 代码

适用场景

  1. 01建立模糊测试活动的覆盖率基线
  2. 02排查停滞不前的模糊测试器
  3. 03验证 harness 修改是否扩大代码触达

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/trailofbits/skills --skill coverage-analysis

安装后在 Claude Code 中输入/coverage-analysis确认已加载。

附带文件agents/openai.yamlassets/trail-of-bits-mark.svg

详细介绍

覆盖率分析

覆盖率分析对于了解模糊测试期间代码的哪些部分被执行至关重要。它有助于找出魔术值检查等模糊测试阻碍因素,并跟踪改进测试框架后的效果。

概述

模糊测试期间的代码覆盖率有两个关键用途:

  1. 评估测试框架的有效性:了解应用程序中哪些部分确实由模糊测试测试框架执行
  2. 跟踪模糊测试进展:监控更新测试框架、模糊测试器或被测系统(SUT)后覆盖率的变化

覆盖率可以作为模糊测试器能力和性能的替代指标。虽然覆盖率并非衡量模糊测试器性能的理想指标,但它能可靠地表明测试框架在特定配置下是否有效。

核心概念
概念描述
覆盖率插桩用于跟踪执行了哪些代码路径的编译器标志
语料库覆盖率运行模糊测试语料库中的所有测试用例后达到的覆盖率
魔术值检查难以发现、会阻碍模糊测试器进展的条件检查
覆盖率引导模糊测试优先使用能够发现新代码路径的输入的模糊测试策略
覆盖率报告已执行与未执行代码的可视化或文本表示

适用时机

在以下情况下应用此技术:

  • 启动新的模糊测试活动,以建立基准
  • 模糊测试器似乎陷入停滞,无法找到新路径
  • 修改测试框架后,验证改进效果
  • 在不同模糊测试器之间迁移时
  • 确定需要添加字典条目或种子输入的区域
  • 调试某些代码路径为何无法触达

在以下情况下跳过此技术:

  • 模糊测试活动正在积极发现崩溃
  • 尚未设置覆盖率基础设施
  • 处理规模极大的代码库,无法实际生成完整覆盖率报告
  • 模糊测试器的内部覆盖率指标已足以满足需求

快速参考

任务命令/模式
LLVM 覆盖率插桩(C/C++)-fprofile-instr-generate -fcoverage-mapping
GCC 覆盖率插桩-ftest-coverage -fprofile-arcs
cargo-fuzz 覆盖率(Rust)cargo +nightly fuzz coverage <target>
生成 LLVM 配置文件数据llvm-profdata merge -sparse file.profraw -o file.profdata
LLVM 覆盖率报告llvm-cov report ./binary -instr-profile=file.profdata
LLVM HTML 报告llvm-cov show ./binary -instr-profile=file.profdata -format=html -output-dir html/
gcovr 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

核心原则:使用每次模糊测试活动结束后生成的语料库计算覆盖率,而不要使用模糊测试器的实时统计数据。这样可以在不同模糊测试工具之间获得可复现、可比较的测量结果。

分步操作

步骤 1:使用覆盖率插桩进行构建

根据工具链选择插桩方法:

LLVM/Clang(C/C++):

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
步骤 2:创建执行运行时(仅限 C/C++)

对于 C/C++ 项目,请创建一个运行时来执行语料库:

// 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;
}
步骤 3:在语料库上执行

LLVM(C/C++):

LLVM_PROFILE_FILE=fuzz.profraw ./fuzz_exec corpus/

GCC(C/C++):

./fuzz_exec_gcov corpus/

Rust: 运行 cargo fuzz coverage 时会自动生成覆盖率数据。

步骤 4:处理覆盖率数据

LLVM:

# 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/

使用 gcovr 的 GCC:

# Install gcovr (uv manages the isolated environment)
uv tool install gcovr
export PATH="$HOME/.local/bin:$PATH"   # uv's tool bin, if not already present

# 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
第 5 步:分析结果

查看覆盖率报告,识别以下内容:

  • 未覆盖的代码块:可能需要更好的种子输入或字典条目的区域
  • 魔数检查:包含硬编码值、会阻止程序继续执行的条件语句
  • 死代码:可能无法通过测试框架触达的函数
  • 覆盖率变化:与基线比较,以跟踪改进或退化

常见模式

模式:识别魔数

问题:模糊测试器无法发现受魔数检查保护的路径。

覆盖率揭示了:

// Coverage shows this block is never executed
if (buf == 0x7F454C46) {  // ELF magic number
    // start parsing buf
}

解决方案:将魔数添加到字典文件:

# 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);
            }
        }
    }
}
模式:CMake 集成

使用场景:为 CMake 项目添加覆盖率构建。

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)

构建:

cmake -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ .
cmake --build . --target fuzz_exec

高级用法

提示与技巧
提示帮助原因
将 LLVM 18+ 与 -show-directory-coverage 配合使用按目录结构整理大型报告,而不是使用扁平的文件列表
导出为 lcov 格式,以生成更好的 HTMLllvm-cov export -format=lcov + genhtml 可生成更清晰的逐文件报告
比较不同轮模糊测试的覆盖率为 .profdata 文件添加时间戳并保存,以跟踪长期进展
从报告中过滤测试框架代码使用 -ignore-filename-regex,仅关注 SUT 覆盖率
在 CI/CD 中自动执行覆盖率分析在计划的模糊测试运行后自动生成覆盖率报告
在 Clang 14+ 中使用 gcovr 5.1+较旧版本的 gcovr 与近期的 LLVM 存在兼容性问题
增量覆盖率更新

GCC 的 gcov 插桩会在多次运行中增量更新 .gcda 文件。这在添加测试用例时有助于跟踪覆盖率:

# 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/
处理大型代码库

对于包含数百个源文件的项目:

  1. 按前缀筛选:仅为相关目录生成报告

    llvm-cov show ./fuzz_exec -instr-profile=fuzz.profdata /path/to/src/
    
  2. 使用目录覆盖率:按目录分组,以减少杂乱(LLVM 18+)

    llvm-cov show -show-directory-coverage -format=html -output-dir html/
    
  3. 生成 JSON 以便进行程序化分析:

    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

反模式

反模式问题正确做法
使用模糊测试器报告的覆盖率进行比较不同模糊测试器计算覆盖率的方式不同,因此跨工具比较没有意义使用专用覆盖率工具(llvm-cov、gcovr)进行可复现的测量
使用优化选项生成覆盖率-O3 优化可能会消除代码,使覆盖率产生误导覆盖率构建使用 -O2 或 -O0
不过滤测试框架代码测试框架覆盖率会抬高数值,并掩盖 SUT 覆盖率使用 -ignore-filename-regex 或 --exclude 过滤测试框架文件
混用 LLVM 和 GCC 插桩不兼容的格式会导致解析失败覆盖率构建使用同一工具链
忽略导致崩溃的输入崩溃会阻止覆盖率生成,掩盖真实覆盖率数据先修复崩溃,或使用进程分叉隔离崩溃
不跟踪一段时间内的覆盖率一次性的覆盖率检查无法发现退化和改进保存带时间戳的覆盖率数据并跟踪趋势

针对特定工具的指南

libFuzzer

libFuzzer 默认使用 LLVM 的 SanitizerCoverage 来指导模糊测试,但生成报告时需要单独的插桩。

为覆盖率构建:

clang++ -fprofile-instr-generate -fcoverage-mapping \
  -O2 -DNO_MAIN \
  main.cc harness.cc execute-rt.cc -o fuzz_exec

执行语料库并生成报告:

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(它与 profile 插桩冲突)
  • 使用不同的主函数,复用同一测试框架函数(LLVMFuzzerTestOneInput)
  • 使用 -ignore-filename-regex 标志,从覆盖率报告中排除测试框架代码
  • 对模板较多的 C++ 代码,可考虑使用 llvm-cov 的 -show-instantiation 标志
AFL++

AFL++ 提供自己的覆盖率反馈机制,但如需详细报告,请使用标准 LLVM/GCC 工具。

使用 LLVM 构建覆盖率版本:

clang++ -fprofile-instr-generate -fcoverage-mapping \
  -O2 main.cc harness.cc execute-rt.cc -o fuzz_exec

使用 GCC 构建覆盖率版本:

AFL_USE_ASAN=0 afl-gcc -ftest-coverage -fprofile-arcs \
  main.cc harness.cc execute-rt.cc -o fuzz_exec_gcov

执行并生成报告:

# 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++ 的插桩工具(afl-clang-fast)
  • 改用带覆盖率标志的标准编译器
  • AFL++ 的 queue/ 目录包含你的语料库
  • AFL++ 内置的覆盖率统计适合实时监控,但不适合详细分析
cargo-fuzz(Rust)

cargo-fuzz 使用 LLVM 工具内置生成覆盖率数据的功能。

安装先决条件:

rustup toolchain install nightly --component llvm-tools-preview
cargo install cargo-binutils rustfilt

生成覆盖率数据:

cargo +nightly fuzz coverage fuzz_target_1

创建 HTML 报告脚本:

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_html fuzz_target_1 src/lib.rs

集成提示:

  • 覆盖率分析始终使用 nightly 工具链
  • -Xdemangler=rustfilt 标志可使函数名称更易读
  • 按源文件(例如 src/lib.rs)筛选,以聚焦于 crate 代码
  • 使用 -show-line-counts-or-regions 和 -show-instantiations,以获得更适合 Rust 的输出
  • 语料库位于 fuzz/corpus/<target>/
honggfuzz

honggfuzz 可与标准 LLVM/GCC 覆盖率插桩配合使用。

构建覆盖率版本:

# Use standard compiler, not honggfuzz compiler
clang -fprofile-instr-generate -fcoverage-mapping \
  -O2 harness.c execute-rt.c -o fuzz_exec

执行语料库:

LLVM_PROFILE_FILE=fuzz.profraw ./fuzz_exec honggfuzz_workspace/

集成提示:

  • 构建覆盖率版本时,不要使用 hfuzz-clang
  • honggfuzz 语料库通常位于工作区目录中
  • 使用与 libFuzzer 相同的 LLVM 工作流

故障排除

问题原因解决方案
error: no profile data available未生成分析数据,或路径错误确认已设置 LLVM_PROFILE_FILE,且 .profraw 文件存在
Failed to load coverage二进制文件与分析数据不匹配使用执行时相同的标志重新构建二进制文件
覆盖率报告显示 0%用于生成报告的二进制文件有误使用经过插桩的二进制文件,而不是模糊测试二进制文件
no_working_dir_found 错误(gcovr).gcda 文件位于意外位置添加 --gcov-ignore-errors=no_working_dir_found 标志
崩溃导致无法生成覆盖率数据语料库包含会导致崩溃的输入筛除崩溃样例,或使用派生进程方式隔离故障
更改 harness 后覆盖率下降harness 现在跳过了某些代码路径检查 harness 逻辑;可能需要支持更多输入格式
HTML 报告是扁平的文件列表使用了较旧的 LLVM 版本升级到 LLVM 18+ 并使用 -show-directory-coverage
incompatible instrumentation混用了 LLVM 和 GCC 覆盖率工具使用相同的工具链重新构建所有内容

相关技能

使用此技术的工具
技能应用方式
libfuzzer使用 SanitizerCoverage 提供反馈;覆盖率分析可评估 harness 的有效性
aflpp使用边覆盖率提供反馈;详细分析需要单独进行插桩
cargo-fuzz为 Rust 项目内置了 cargo fuzz coverage 命令
honggfuzz使用边覆盖率;使用标准 LLVM/GCC 工具进行分析
相关技术
技能关联
fuzz-harness-writing覆盖率可显示 harness 能到达哪些代码路径,并指导 harness 改进
fuzzing-dictionaries覆盖率可识别需要加入字典的魔数检查
corpus-management覆盖率分析可识别冗余测试用例,帮助整理语料库
sanitizers覆盖率有助于验证经过 sanitizer 插桩的代码是否确实被执行

资源

关键外部资源

LLVM 基于源代码的代码覆盖率 LLVM 配置文件插桩的综合指南,涵盖分支覆盖率、区域覆盖率以及与现有构建系统集成等高级功能。介绍编译器标志、运行时行为和配置文件数据格式。

llvm-cov 命令指南 llvm-cov 命令的详细 CLI 参考,包括 show、report 和 export。介绍所有筛选选项、输出格式以及与 llvm-profdata 的集成。

gcovr 文档 使用 gcovr 工具根据 gcov 数据生成覆盖率报告的完整指南。涵盖 HTML 主题、筛选选项、多目录项目以及 CI/CD 集成模式。

SanitizerCoverage 文档 LLVM SanitizerCoverage 插桩的底层文档。介绍内联 8 位计数器、PC 表,以及模糊测试工具如何利用覆盖率反馈进行引导。

关于模糊测试工具性能评估 一篇研究论文,探讨覆盖率作为模糊测试性能指标的局限性。主张采用更全面的评估方法,而非仅看简单的代码覆盖率百分比。

视频资源

不适用——覆盖率分析主要涉及工具和工作流,最适合通过阅读文档和动手实践来学习。

评论

支持 Markdown