coverage-analysis について
coverage-analysisは、コーパスが実行するコードパスを測りたい開発者向けのファジングスキルです。C/C++とRustの計測ビルドやレポート作成に加え、実行されない経路の調査やキャンペーン間の比較方法を扱います。
主な機能
- 01LLVMとGCCの計測設定
- 02ファジングコーパスでのカバレッジ測定
- 03テキストまたはHTMLレポートの生成
- 04未実行経路やマジック値チェックの特定
- 05キャンペーン間のカバレッジ比較
- 06レポートからハーネスコードを除外
活用シーン
- 01ファジング開始時の基準値の作成
- 02停滞したファザーの調査
- 03ハーネス変更による到達範囲の確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ trailofbits/ skills --skill coverage-analysisインストール後、Claude Code で/coverage-analysisを入力して読み込まれたか確認します。
同梱ファイルagents/openai.yamlassets/trail-of-bits-mark.svg
詳細
カバレッジ分析
カバレッジ分析は、ファジング中にコードのどの部分が実行されているかを理解するうえで欠かせません。マジック値チェックのようなファジングの妨げを特定し、ハーネスの改善が時間の経過とともにどれほど効果を発揮しているかを追跡できます。
概要
ファジング中のコードカバレッジには、次の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 を使用する | 大規模なレポートをフラットなファイル一覧ではなく、ディレクトリ構造ごとに整理できます |
| HTML の見やすさを向上させるため、lcov 形式にエクスポートする | llvm-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/
大規模なコードベースの処理
数百のソースファイルがあるプロジェクトの場合:
-
プレフィックスでフィルターする:関連するディレクトリだけを対象にレポートを生成します
llvm-cov show ./fuzz_exec -instr-profile=fuzz.profdata /path/to/src/ -
ディレクトリカバレッジを使用する:ディレクトリごとにグループ化し、表示を整理します(LLVM 18 以降)
llvm-cov show -show-directory-coverage -format=html -output-dir html/ -
プログラムによる分析用に JSON を生成する:
llvm-cov export -format=lcov > coverage.json
差分カバレッジ
2 つのファジングキャンペーン間でカバレッジを比較します。
# 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 の計測を混在させる | 互換性のない形式によって解析に失敗します | カバレッジビルドでは、1 つのツールチェーンに統一します |
| クラッシュする入力を無視する | クラッシュによってカバレッジの生成が妨げられ、実際のカバレッジデータが隠れてしまいます | まずクラッシュを修正するか、プロセスをフォークして分離します |
| カバレッジを経時的に追跡しない | 一度だけのカバレッジ確認では、後退や改善を見落とします | カバレッジデータをタイムスタンプ付きで保存し、傾向を追跡します |
ツール別のガイダンス
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を使用しないでください(プロファイル計測と競合します) - 同じハーネス関数(
LLVMFuzzerTestOneInput)を別の main 関数とともに再利用します -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
統合のヒント:
- カバレッジには必ずナイトリーツールチェーンを使用してください
-Xdemangler=rustfiltフラグを指定すると、関数名が読みやすくなります- クレートのコードに焦点を当てるには、ソースファイル(例:
src/lib.rs)でフィルターしてください - Rust 固有の出力を改善するには、
-show-line-counts-or-regionsと-show-instantiationsを使用してください - コーパスは
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 フラグを追加してください |
| クラッシュによってカバレッジを生成できない | コーパスにクラッシュを引き起こす入力が含まれています | クラッシュをフィルターするか、フォーク方式を使って障害を切り分けてください |
| ハーネス変更後にカバレッジが低下する | ハーネスが一部のコードパスをスキップするようになりました | ハーネスのロジックを確認してください。より多くの入力形式に対応する必要がある場合があります |
| HTML レポートがフラットなファイル一覧になる | 古い LLVM バージョンを使用しています | LLVM 18 以降にアップグレードし、-show-directory-coverage を使用してください |
incompatible instrumentation | LLVM と GCC のカバレッジを混在させています | 同じツールチェーンですべてを再ビルドしてください |
関連スキル
この手法を使用するツール
| スキル | 適用方法 |
|---|---|
| libfuzzer | フィードバックに SanitizerCoverage を使用します。カバレッジ分析によってハーネスの有効性を評価できます |
| aflpp | フィードバックにエッジカバレッジを使用します。詳細な分析には別途インストルメンテーションが必要です |
| cargo-fuzz | Rust プロジェクト向けに cargo fuzz coverage コマンドを組み込みで提供します |
| honggfuzz | エッジカバレッジを使用します。標準の LLVM/GCC ツールで分析できます |
関連する手法
| スキル | 関連性 |
|---|---|
| fuzz-harness-writing | カバレッジから、ハーネスが到達するコードパスがわかり、ハーネスの改善に役立ちます |
| fuzzing-dictionaries | カバレッジから、辞書エントリーが必要なマジック値のチェックを特定できます |
| corpus-management | カバレッジ分析により、冗長なテストケースを特定してコーパスを整理できます |
| sanitizers | カバレッジによって、サニタイザーをインストルメントしたコードが実際に実行されているかを確認できます |
リソース
主要な外部リソース
LLVM のソースベースのコードカバレッジ 分岐カバレッジ、リージョンカバレッジ、既存のビルドシステムとの統合などの高度な機能を含む、LLVM のプロファイル計測に関する包括的なガイドです。コンパイラフラグ、ランタイムの動作、プロファイルデータ形式について説明しています。
llvm-cov コマンドガイド
show、report、export などの llvm-cov コマンドに関する詳細な CLI リファレンスです。すべてのフィルタリングオプション、出力形式、llvm-profdata との統合について記載しています。
gcovr ドキュメント gcov データからカバレッジレポートを生成する gcovr ツールの完全なガイドです。HTML テーマ、フィルタリングオプション、複数ディレクトリのプロジェクト、CI/CD との統合パターンについて説明しています。
SanitizerCoverage ドキュメント LLVM の SanitizerCoverage 計測に関する低レベルのドキュメントです。インライン 8 ビットカウンター、PC テーブル、ファジングツールがカバレッジフィードバックをガイダンスに利用する方法について説明しています。
ファザーの性能評価について ファジングの性能指標としてのカバレッジの限界を検証する研究論文です。単純なコードカバレッジ率にとどまらない、よりきめ細かな評価方法を提唱しています。
動画リソース
該当なし。カバレッジ分析は主にツールとワークフローに関するトピックであり、ドキュメントを読み、実践を通じて学ぶのが最適です。