gosentry は、Go コードベースに対する最先端のファジングキャンペーンのための多数の機能を統合した、Go ツールチェーンのセキュリティ重視のフォークです。以前 go test -fuzz を使用していたなら、gosentry をその代替として使用してください。
Go ツールチェーンにはネイティブに存在しないさまざまなファジング改善とバグ検出器が含まれています。以下の TLDR を参照してください。関連するブログ記事は こちら から読むこともできます。
TLDR(機能とオプション):
struct 入力を直接ファジングします(カスタムパーサーは不要)。f.Add(Input{N: 7, S: "hi"}) でシードを追加し、f.Fuzz(func(t *testing.T, in Input) { ... }) を実行します。X + Y - Z は X / U + Z - 14 になり得ますが、X + Yè - Z にはなりませんcd src && ./make.bash # Produces ../bin/go. See GOFLAGS below.
> [!TIP]
> コントリビューター向けドキュメント: コードマップ、推奨される開発ループ、CI エントリポイント、ベンチマークスクリプトについては、`docs/gosentry/index.md` をお読みください。
> このフォークは Pull GitHub App を使用して、`golang/go:master` から `master` への PR を開き自動マージすることで、Go ツールチェーンの最新アップデートから遅れないようにしています。
## 機能 1: 構造体を意識したファジング(構造体を入力としてファズ)
#### 概要
Go のネイティブなファジング (`go test -fuzz=...`) は、ファズパラメータとして少数のスカラー型 (`[]byte`, `string`, 数値, ...) のみをサポートしています。gosentry では、それらのスカラーから構築された**複合型**(構造体、配列、スライス、ポインタ)もファズできます。
これは、コードが自然に構造化入力を受け取り、コーパスをシードしてミューテーションするためだけに独自のエンコーダ/デコーダを構築したくない場合に便利です。
例については、`test/gosentry/examples/multiargs` と `test/gosentry/examples/composite` を参照してください。
#### 簡単な例```go
type Input struct {
Data []byte
S string
N int
OK bool
}
func FuzzStructInput(f *testing.F) {
// Seed the initial corpus with a Go struct (gosentry feature).
f.Add(Input{Data: []byte("A"), S: "B", N: 7, OK: true})
f.Fuzz(func(t *testing.T, in Input) {
if in.OK && in.N == 1337 && in.S == "BOOMMOOB" && bytes.Equal(in.Data, []byte("A")) {
t.Fatalf("boom")
}
})
}
f.Add)と構造体ファジングの仕組み(作られた接着層)Go標準のファザーはstruct値を直接ファズできません(スカラー型の小さなリストしか変異できないため)。gosentryは小さな接着層を追加します。ファズターゲットが(Inputのような)複合型を使う場合、gosentryは舞台裏で単一の[]byteをファズします。実行のたびに、そのバイト列をあなたの構造体へデコードし(フィールド単位で、スライス/配列/ポインタは再帰的に処理)、デコードされた値をあなたのf.Fuzzコールバックへ渡します。シードにも同じエンコーディングが使われるため、f.Add(Input{...})はエンコードされた[]byteコーパスエントリになり、ファザーは他のシードと同様に再利用・変異できます。
ファザー(LibAFLを含む)は生のバイト列を変異させるため、どんなバイト列でも「何らかの」構造体値に変換し、処理を続行できるデコーダが必要です。JSON/gobはほとんどのランダム入力を拒否するため(カバレッジに悪影響)、また未エクスポートフィールドを設定しないため、ファジングでは不変条件を破る方が有益なことがよくあります。このカスタム形式は、小さく、高速で、決定的で、不正なデータに対して寛容です。
内部では、gosentry独自の単純なバイナリ形式(gobでもJSONでもない)を使用しています。コードはsrc/testing/libafl.goにあります:
libaflMarshalInputs / libaflAppendValuelibaflUnmarshalArgs / libaflDecodeValueエンコード規則(概要):
bool:1バイト(0または1)int/uintは8バイト)float32 = 4バイト、float64 = 8バイト)string:uvarint(len)に続けて生の文字列バイト列[]byte:uvarint(len)に続けて生のバイト列uvarint(len)に続けて各要素をエンコード0 = nil、1 = 存在する)に続けてポインタ先の値をエンコードこの作業は、以前開発された go-panikint から着想を得ています。整数演算のオーバーフロー/アンダーフロー検出と、(オプションで)整数変換時の型切り詰め検出を追加します。オーバーフローまたは切り詰めが検出されると、詳細なエラーメッセージを含むパニックが発生し、関与する特定の演算種別と整数型が示されます。
算術演算:符号付きおよび符号なし整数型の加算 +、減算 -、乗算 *、除算 / を処理します。符号付き整数では int8、int16、int32 を対象とします。符号なし整数では uint8、uint16、uint32、uint64 を対象とします。除算のケースでは、符号付き整数の MIN_INT / -1 オーバーフロー条件を特に検出します。int64 と uintptr は算術演算のチェック対象外です。
型切り詰め検出:情報を失う可能性のある整数型変換を検出します。すべての整数型(int8、int16、int32、int64、uint8、uint16、uint32、uint64)を対象とします。uintptr はプラットフォーム依存の使用法のため除外されます。これはデフォルトでは無効です。
オーバーフロー検出はデフォルトで有効です。無効にするには、./make.bash の前に GOFLAGS='-gcflags=-overflowdetect=false' を追加してください。切り詰め問題チェッカーを有効にするには、-gcflags=-truncationdetect=true を使用できます。
この機能はコンパイラのSSA生成をパッチし、整数演算と整数変換に追加のランタイムチェックを挿入します。バグが検出されると、詳細なエラーメッセージ付きのパニックを発生させるランタイム関数を呼び出します。チェックはソースコード位置に基づくフィルタリングで適用されるため、ユーザーコードだけが計測され、標準ライブラリファイルや依存関係(モジュールキャッシュおよびvendor/)はスキップされます。
関連するブログ記事は こちら で読めます。
該当する操作と同じ行、またはその直上の行にマーカーを追加すると、特定のレポートを抑制できます:
overflow_false_positivetruncation_false_positive例:```go // overflow_false_positive intentionalOverflow := a + b // truncation_false_positive x := uint8(big) sum2 := a + b // overflow_false_positive x2 := uint8(big) // truncation_false_positive
これが機能しない場合がありますが、それはGoが関数をインライン化しているためです。`// overflow_false_positive` で不十分な場合は、関数シグネチャの前に `//go:noinline` を追加してください。
## Feature 3: Panic on selected functions
ファジングの対象を調査する際、特定の関数が呼び出されたときにパニックを発生させたい場合があります。例えば、一部のソフトウェアはパニックを起こさずに `log.error` メッセージを出力する場合がありますが、そのような状況は、セキュリティ研究者がファジング中に検出したい状態を示していることがよくあります。
しかし、これらのエラーは通常、内部的に処理されるため(再試行や一時停止のメカニズム、またはログへのメッセージ出力など)、ファザーからはほぼ見えません。この機能の目的は、この問題に対処することです。
#### 使い方
gosentry をコンパイルし、`--panic-on` フラグを使用します。```bash
./bin/go test -fuzz=FuzzHarness --use-libafl --focus-on-new-code=false --catch-races=false --catch-leaks=false --panic-on="test_go_panicon.(*Logger).Warning,test_go_panicon.(*Logger).Error"
上記の例では、(*Logger).Warning または (*Logger).Error のいずれかが呼び出されるとパニックが発生します(カンマ区切りのリスト)。
LibAFL は従来の Go ファザーよりも はるかに 優れたパフォーマンスを発揮します。ファジング実行時(go test -fuzz=...)、gosentry はデフォルトで LibAFL を使用します(ランナーは golibafl/ にあります)。
安定性に関する注意: LibAFL モードでは、gosentry は GODEBUG=updatemaxprocs=0 を強制し(ランタイムの GOMAXPROCS 自動更新を無効化)、断続的に発生する Linux CI のクラッシュ("sync: inconsistent mutex state")を回避します。詳細は misc/gosentry/USE_LIBAFL.md にあります。
LibAFL(デフォルト)を使用する場合、git 対応スケジューリングを有効にするかどうかを明示的に選択する必要があります: --focus-on-new-code=true|false。詳細なドキュメントはこのMarkdownファイルにあります。
また、LibAFL 用のオプションの JSONC 設定ファイル(文法ファジングオプションを含む)を渡すこともできます。参照はこちらです。
"stop_all_fuzzers_on_panic": false の場合、LibAFL は各クラッシュを保存し、ファジングを継続するためにクライアントを再起動します。```bash
./bin/go test -fuzz=FuzzHarness --focus-on-new-code=false --catch-races=false --catch-leaks=false --libafl-config=path/to/libafl.jsonc # optional --libafl-config
`-fuzztime=1m` を使用すると、LibAFL キャンペーンを 1 分後に停止できます。
LibAFL キャンペーンのコーパスからのカバレッジレポート生成については、[機能 8](#feature-8-generate-go-coverage-reports-from-fuzzing-campaign) に記載されています。
文法ベースのファジング (Nautilus) については、[機能 7](#feature-7-grammar-based-fuzzing-nautilus) に記載されています。
<details>
<summary><strong>Go + LibAFL の連携方法</strong></summary>```text
┌───────────────────────────────────────────────────────────────────────────┐
│ 1) gosentry `go test` │
│ - captures `testing.F.Fuzz(...)` callback │
│ - generates extra source file: `_libaflmain.go` │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 2) Generated bridge: `_libaflmain.go` │
│ - provides libFuzzer-style C ABI entrypoints: │
│ LLVMFuzzerInitialize │
│ LLVMFuzzerTestOneInput │
│ - adapts bytes -> Go types -> calls the captured fuzz callback │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 3) `libharness.a` (static archive on disk) contains: │
│ - compiled objects for all test package (+ dependencies) │
│ - generated `_testmain.go` + `_libaflmain.go` │
│ - LLVMFuzzerInitialize │
│ - LLVMFuzzerTestOneInput │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 4) `golibafl/` (Rust + LibAFL) │
│ env: HARNESS_LIB=/path/to/libharness.a │
│ fuzz loop: mutate input -> LLVMFuzzerTestOneInput(data) -> observe │
└───────────────────────────────────────────────────────────────────────────┘
In --use-libafl モードでは、gosentry が libharness.a をビルドし、Rust 製の golibafl ランナーが libFuzzer エントリポイントを介してそれをプロセス内で駆動します。注: HARNESS_LIB は任意のハーネスアーカイブ名を指すことができます(たとえば --catch-races で使用される libharness_race.a など)。
--use-libafl モードでは、gosentry はカバレッジ計装を有効にして Go ハーネスをコンパイルします。これにより、プログラムのさまざまな部分が実行されたときに変化する小さなカウンタがコードに追加されます。ハーネスが golibafl 内で開始されると、Go ランタイムはこれらのカウンタを LibAFL に公開します。LibAFL は各入力後にそれらを読み取ってどのコードが実行されたかを確認し、そのカバレッジを次のミューテーションの指針として使用します。
LibAFL を使用する動機について説明します。go test -fuzz によるファジングは、最先端のファジング技術よりも はるかに 遅れています。その良い例が AFL++ の CMPLOG/Redqueen です。これらの機能により、ファザーは特定の制約を解決できます。次のスニペットを想定しましょう。```go
if input == "IMARANDOMSTRINGJUSTCMPLOGMEMAN" {
panic("this string is illegal")
}
SOTAファザー(AFL++やLibAFLなど)は、その場合パニックを即座に検出するでしょう。しかし、Goネイティブファザーはそうではありません。これは、カバレッジ探索を**大幅に**制限する大きなギャップです。
以下のベンチマークは、これらの限界を示しています。これらのベンチマークは、[gosentry-bench-libafl リポジトリ](https://github.com/kevin-valerio/gosentry-bench-libafl/tree/main)で**再現**および改善できることに注意してください。
##### ベンチマーク1:
下のチャートは、LibAFLとGoネイティブファザーを使用してGoogleの[UUID](https://github.com/google/uuid)をファジングした際にカバーされた行数の推移です。

##### ベンチマーク2:
下のチャートは、LibAFLとGoネイティブファザーを使用して[go-ethereum](https://github.com/ethereum/go-ethereum)をファジングした際にカバーされた行数の推移です。

#### 例
`test/gosentry/examples/` にあるいくつかのファジングハーネスでテストできます。```bash
cd test/gosentry/examples/reverse
../../../../bin/go test -fuzz=FuzzReverse --focus-on-new-code=false --catch-races=false --catch-leaks=false
Ctrl+C でファズキャンペーンを停止してください。
gosentry は、LibAFL のキャンペーン状態(コーパス、クラッシュなど)を、Go のファズキャッシュルート(おおよそ $(go env GOCACHE)/fuzz)の下にある、同じパッケージ + 同じファズターゲット(および同じプロジェクトルート)から導出される決定論的なディレクトリに保存します。
つまり、同じファズキャンペーンを停止(Ctrl+C)して再開すると、デフォルトでは前回の LibAFL queue/ コーパスから継続されます。
パスは実行の最後に出力されます。```text libafl output dir: /full/path/to/.../fuzz//libafl//
Notes:
- `<harness>` は、`-fuzz` が `FuzzXxx`(または `^FuzzXxx$`)のような単純な識別子である場合のファズターゲット名です。それ以外の場合は `pattern-<hash>` です。
- カバレッジ生成(`--generate-coverage`)は、正しい `queue/` コーパスを見つけるために同じルールを使用するため、同じパッケージから同じ `-fuzz=...` で実行する必要があります。
## 機能5: Git-blame指向ファジング(実験的)
#### 概要
カバレッジ誘導ファジングは新しいパスを探索するのに優れていますが、カバーされたすべてのコードを同等に興味深いものとして扱います。大規模なコードベースをファジングする際には、リグレッションやバグが発生しやすい最近変更されたコードにファザーを偏らせたい場合があります。LibAFLモードでは、gosentryは`git blame`を使用して、最近変更された行を実行する入力を優先できます(カバレッジガイダンスを主要なシグナルとして維持しつつ)。
この作業は、[LibAFL-git-aware](https://github.com/kevin-valerio/LibAFL-git-aware) の以前の作業に基づいています。技術的な詳細はすべてそこに文書化されています。
#### 使用方法
`--focus-on-new-code=true` でgit対応スケジューリングを有効にします:```bash
./bin/go test -fuzz=FuzzHarness --use-libafl --focus-on-new-code=true --catch-races=false --catch-leaks=false
このモードでは、git(git blame の実行用)と go tool addr2line(カバレッジカウンタをソースの file:line にマッピングし直すため)が必要です。
git_recency_map.bin をビルドする方法.go.fuzzcntrs は、Go の libFuzzer スタイルの 8ビットカバレッジカウンタ を保持するリンカセクションです (-gcflags=all=-d=libfuzzer で有効化)。各バイトは「この計測ポイントが何回ヒットしたか」を表します。--focus-on-new-code=true の場合、golibafl は以下の手順で git_recency_map.bin を生成します:
libharness.a から go.o を抽出する。.go.fuzzcntrs セクションのサイズを読み取り、カウンタ数 N を取得する。.text 内で .go.fuzzcntrs シンボルを参照する再配置情報をスキャンし、各カウンタインデックスのアドレスを復元する。go tool addr2line を使用して、各アドレスを file:line に解決する。git blame --line-porcelain を実行して、行ごとの committer-time を取得する。git_recency_map.bin を u64 head_time + u64 N + N * u64 timestamps (リトルエンディアン) として書き込む。未マッピングのエントリはタイムスタンプ 0 を使用する。misc/gosentry/bench_focus_on_new_code_geth.sh --trials 5 --warmup 600 --timeout 200 で実行。
git-aware results: trial 1: timeout (200000ms) trial 2: crash (87432ms) trial 3: crash (61733ms) trial 4: crash (157540ms) trial 5: crash (7122ms) git-aware crashes: 4/5 (timeouts=1, errors=0) git-aware median (capped to timeout): 87.432s
</details>
## 機能 6: ファズ時に競合状態、goroutine リーク、ハング(タイムアウト)を検出する
##### 確認済みハングの捕捉(LibAFL タイムアウト)
LibAFL でファジングを行うと、ハーネスの実行が **タイムアウト** することがあります(例: デッドロック、待機中にスタックした goroutine、または非常に遅いパス)。
誤検知を減らすため、gosentry はタイムアウトをハングの候補として扱い、タイムアウトした入力をより大きなタイムアウトで数回リプレイして確認します。ハングが確認された場合、gosentry は入力を `<libafl output dir>/hangs/` に書き込み、ファズキャンペーンを停止します(バグ/クラッシュとして扱います)。
終了前に、`golibafl` はクラッシュ/ハングする入力を最小化しようとします(ベストエフォート。ハングは合計で最大約60秒に制限されます)。
注: ハング確認は初期コーパスのインポート/生成中にも実行されるため、すべての入力でタイムアウトするターゲットでも決定的に検出できます。
これは `--libafl-config` で設定します:
- `catch_hangs` (デフォルト: `true`)
- `hang_timeout_ms` (デフォルト: `10000`)
- `hang_confirm_runs` (デフォルト: `3`)
##### データ競合の捕捉(`--catch-races`)
gosentry は、LibAFL の `queue/` ディレクトリを監視し、`GORACE=halt_on_error=1` を設定して新たに発見されたシードをリプレイする、独立した `-race` リプレイループを実行できます。
リプレイループは、リプレイ専用(ファズカバレッジ計測なし)の別の `-race` ハーネスアーカイブを構築します。
リプレイ中にデータ競合が検出されると、gosentry は `catch-races:` の要約と再現コマンドの前に、完全なレース検出レポートを出力します。
注: Go のレース検出器は、**単一のハーネス実行内** のデータ競合のみを検出します(同じプロセス内の goroutine が適切な同期なしに同じメモリにアクセスする競合)。`--catch-races` は、シードが競合する並行処理をトリガーしない場合、競合を見逃します。また、プロセス間の競合も検出しません。
<details>
<summary><strong>データ競合モードの仕組み</strong></summary>
このモードは、`go test` 内(同じ親プロセス)に小さなモニターを起動し、ファズキャンペーン全体を通じて実行します。
- タイミング: メインの LibAFL ファジングプロセスが開始される前に、gosentry はリプレイハーネス + ランナーを構築します。
- 監視: ファジング開始前に、gosentry は `<libafl output dir>/queue/` の初期内容を `seen` セットにスナップショットします。その後、goroutine が `<libafl output dir>/queue/` を約1秒ごとにポーリングし、新しく作成されたシードのみをリプレイします(ドットファイルと `*.metadata` はスキップします)。
</details>```text
Legend: output/... = <libafl output dir>/...
┌───────────────────────────────────────────────────────────────────────────┐
│ 1) Main LibAFL fuzzing run │
│ - `golibafl` writes new seeds to `output/queue/` │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 2) `--catch-races` sidecar setup │
│ - builds replay harness: `libharness_race.a` (`go test -race ...`) │
│ - builds replay runner: `golibafl-race` (linked against race harness) │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 3) Replay loop │
│ - polls `output/queue/` for new seeds │
│ - runs: `GORACE=halt_on_error=1 golibafl-race run --input <seed>` │
│ (2 workers × 3 repeats per seed) │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 4) On "DATA RACE" │
│ - prints the race detector report │
│ - copies seed to `output/races/` │
│ - stops the fuzz campaign (treat as bug/crash) │
└───────────────────────────────────────────────────────────────────────────┘
--catch-leaks)gosentryは、LibAFL queue/ ディレクトリを監視し、go.uber.org/goleak を有効にして新しく発見されたシードを再生する goleak リプレイループも実行できます。
goroutineリークが検出されると、gosentryは正確なシードパスを出力し、それを <libafl output dir>/leaks/ にコピーします。
注:goleak はgoroutineリーク用であり、メモリリーク用ではありません。
このモードでも、go test(同じ親プロセス)内に小さなモニターを起動し、ファズキャンペーン全体にわたって実行します。
<libafl output dir>/queue/ を約1秒ごとにポーリングし、GOSENTRY_LIBAFL_CATCH_LEAKS=1 を指定して各新しいシードをリプレイします(各実行後に go.uber.org/goleak を有効にします)。```text
Legend: output/... = /...┌───────────────────────────────────────────────────────────────────────────┐
│ 1) Main LibAFL fuzzing run │
│ - golibafl writes new seeds to output/queue/ │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 2) --catch-leaks sidecar setup │
│ - builds replay runner: golibafl-leak (linked against the harness) │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 3) Replay loop │
│ - polls output/queue/ for new seeds │
│ - runs: GOSENTRY_LIBAFL_CATCH_LEAKS=1 golibafl-leak run --input <seed>│
│ (enables go.uber.org/goleak checks after each execution) │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 4) On "catch-leaks: detected goroutine leak" │
│ - copies seed to output/leaks/ │
│ - stops the fuzz campaign (treat as bug/crash) │
└───────────────────────────────────────────────────────────────────────────┘
</details>
#### 使用方法
`--catch-leaks=true` で goroutine リークの検出を有効にするか、`--catch-races=true` でレースの検出を有効にします。```bash
./bin/go test -fuzz=FuzzHarness --use-libafl --focus-on-new-code=false --catch-races=true --catch-leaks=true
バイトレベルのファジングは優れていますが、パーサーやファイル形式では構造化された入力が必要なことがよくあります。--use-grammar を使用すると、gosentry は LibAFL の Nautilus 文法ミューテータを使って、ユーザーが提供した文法(JSON形式)に準拠する入力を生成・変異させ、それを通常の Go ファズハーネス(testing.F.Fuzz)に渡します。
文法モードでは、LibAFL は通常のカバレッジ誘導ループ(コーパスシードを選択 → 変異 → 実行 → カバレッジを増やす入力を保持)を引き続き実行します。ランナーは Nautilus 変異(シード → 文法ツリー → 変異 → 逆構文解析)に加えて、(デフォルトで)実行時の比較に基づいて Nautilus のリーフ終端を書き換える CMPLOG 誘導の I2S 風ステージを追加します。これにより入力は文法に準拠したままになります(文法モードでは生のバイトレベルのハボック/トークンステージは実行されません)。--libafl-config で nautilus_cmplog_i2s=false を指定すると、CMPLOG/I2S ステージを無効にできます(バイトレベルのファジングでは CMPLOG/I2S は常に有効のままです)。
[!NOTE] 文法モードは通常、バイトレベルのファジングよりも低速です。これはトレードオフです。構造が増える一方で、1秒あたりの実行回数は減ります。
最良の結果を得るには、バイトスライス([]byte)または string を受け取る1引数のファズコールバックを使用してください:```go
f.Fuzz(func(t testing.T, data []byte) { / parse data */ })
// or:
f.Fuzz(func(t testing.T, s string) { / parse s */ })
グラマーモードは、単一の入力引数(`[]byte` または `string`)で最もよく機能します。複数引数のファズコールバックでは、gosentry が基になるバイトバッファを個別の値にデコードするため、元のグラマー生成テキストはそのまま保持されません。
> [!NOTE]
> グラマーモードは依然として**バイト/文字列**を生成します。構造化された入力が必要な場合(または差分ファジングを行う場合)、`data` をドメイン値に変換(パース/アンマーシャル)するのはハーネスの役割です。(グラマーモード以外では、gosentry はバイトからデコードすることで複合 Go > 型をファズすることもできます。[機能 1](#feature-1-struct-aware-fuzzing-fuzz-structs-as-inputs) を参照してください。)
`--libafl-config`(`--use-grammar` と共にのみ使用)を介して Nautilus を調整できます:`nautilus_max_len` と `nautilus_cmplog_i2s`(`misc/gosentry/libafl.config.jsonc` を参照)。
<details>
<summary><strong>ベンチマーク:Nautilus グラマー CMPLOG/I2S ステージ(オン vs オフ)</strong></summary>
2026年2月17日に、リポジトリの JSON グラマー例(`test/gosentry/examples/grammar_json`、`FuzzGrammarJSON`、グラマー `testdata/JSON.json`)を使用して実行しました。
結果(LibAFL `UserStats`):
| モード | `nautilus_cmplog_i2s` | 実行時間 | 実行回数 | 実行/秒 | エッジ |
|---|---:|---:|---:|---:|---:|
| オン | `true` | 1m-5s | 103818 | 1.586k | 388/8008 (4%) |
| オフ | `false` | 1m-0s | 256659 | 4.251k | 388/8008 (4%) |
注:`edges` は LibAFL のカバレッジマップのエッジであり、Go ソース行ではありません。
</details>
`GOSENTRY_VERBOSE_AFL=1` を設定すると、生成された入力をいくつか表示します。`GOSENTRY_VERBOSE_AFL_ALL_INPUTS=1` を設定すると、グラマーモードの**すべての**実行を `GOLIBAFL_MUTATED_INPUT "..."` として表示します(非常にノイズが多い)。
#### グラマー作成ヘルパー
独自のターゲット形式/プロトコル用に新しい Nautilus JSON グラマーを作成する必要がある場合、gosentry には以下が同梱されています:
- LLM 対応のプロンプト:[misc/gosentry/nautilus/prompt.md](https://github.com/trailofbits/gosentry/blob/HEAD/misc/gosentry/nautilus/prompt.md)
- 小さなサンプルグラマーセット:[misc/gosentry/nautilus/examples/](https://github.com/trailofbits/gosentry/blob/HEAD/misc/gosentry/nautilus/examples/)
<details>
<summary><strong>Go ファズハーネス例(JSON)</strong></summary>```go
func FuzzGrammarJSON(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
dec := json.NewDecoder(bytes.NewReader(data))
dec.UseNumber()
var v any
if err := dec.Decode(&v); err != nil {
t.Fatalf("invalid JSON: %v", err)
}
if err := dec.Decode(&struct{}{}); err != io.EOF {
t.Fatalf("invalid JSON: trailing data")
}
})
}
差分ファジングハーネスのスケッチ(2つのパーサー):```go f.Fuzz(func(t *testing.T, data []byte) { gotA, errA := ParseA(data) gotB, errB := ParseB(data) if (errA == nil) != (errB == nil) { t.Fatalf("parser disagreement: A=%v B=%v", errA, errB) } _ = gotA _ = gotB })
</details>
<details>
<summary><strong>例: 文法を使った「実際の入力言語」のファジング(カスタムエンコーダなし)</strong></summary>
この例では、文法から**有効な式**を生成することで、小さな算術式評価器をファジングします。アドホックな「構造体からバイト列への」エンコードはありません。ファザーは、コードが通常パースするのと同じ種類の入力を生成します。
ハーネス(文法モードでは1引数の`string`入力が最適です):```go
func FuzzExprEval(f *testing.F) {
f.Add("1+2")
f.Add("(3*4)-5")
f.Fuzz(func(t *testing.T, expr string) {
// Parse+eval your language/protocol.
// You can be **sure** that `expr` will always be a valid math operation. Just decode/parse/unmarshall it afterwards.
_, _ = Eval(expr)
})
}
文法スケッチ (Nautilus JSON format):```json [ ["Expr", "{Term}"], ["Expr", "{Term}+{Expr}"], ["Expr", "{Term}-{Expr}"], ["Term", "{Factor}"], ["Term", "{Factor}*{Term}"], ["Factor", "{Num}"], ["Factor", "({Expr})"], ["Num", "0"], ["Num", "1"], ["Num", "2"], ["Num", "3"] ]
</details>
<details>
<summary><strong>Nautilus JSON 文法の例 (小さな JSON サブセット)</strong></summary>
これは `--grammar=...` で期待されるファイル形式です:
- 文法はルールの JSON 配列です: `["NonTerm", "RHS"]`。
- 非終端記号名は大文字で始まる必要があります (`Value`, `Object`, ...)。
- RHS で `{NonTerm}` を使用して別のルールを参照します。
- `{` と `}` は非終端記号の参照用に予約されています。文字通りの波括弧を出力するには、RHS 文字列で `\\{` と `\\}` を使用します。```json
[
["Json", "{Value}"],
["Value", "null"],
["Value", "{String}"],
["String", "\"{Chars}\""],
["Chars", ""],
["Chars", "{Char}{Chars}"],
["Char", "a"],
["Char", "b"]
]
┌───────────────────────────────────────────────────────────────────────────┐
│ 0) gosentry go test -fuzz=FuzzXxx (LibAFL + --use-grammar) │
│ - captures your testing.F.Fuzz callback + its parameter types │
│ - builds libharness.a (libFuzzer-style entrypoints for LibAFL) │
│ - runs golibafl fuzz ... --use-grammar --grammar ... │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 1) golibafl (Rust + LibAFL) fuzzes the Go harness in-process │
│ - loads libharness.a via HARNESS_LIB=... │
│ - observers: edges + time (+ cmplog for comparisons) │
│ - feedback/objective: coverage/time/crash (and optional hang handling) │
│ - scheduler selects a corpus seed (coverage-guided) │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 2) Nautilus (in-process, per client) │
│ - loads the JSON grammar into a Nautilus context │
│ - fuzz loop stage: parse seed -> mutate tree -> unparse to bytes │
│ - if the seed is not parseable: fall back to generation-from-scratch │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 3) Grammar mode stages │
│ - initial corpus: if input dir empty, call generate N times │
│ - fuzz loop: corpus seed -> grammar mutate -> exec harness │
│ - new coverage inputs are added to the on-disk corpus (output/queue/) │
└───────────────────────────────────────────────────────────────────────────┘
</details>
制限事項(現在のグルー):
- 文法モードは単一の入力引数で最もよく機能します。複数引数のファズターゲットは、基になるバイトバッファを個別の値にデコードします。
- 2つのコーパスシード間の文法の再結合/交叉はまだありません(変異は単一シードのみ)。
## 機能8: ファジングキャンペーンからGoカバレッジレポートを生成
LibAFLファズキャンペーンの実行後(または実行中)に、gosentryは現在のLibAFL **キューのコーパス**を再生することでGoカバレッジレポートを生成できます(ファジングなし)。```bash
# Same package + same fuzz target as your fuzz campaign:
./bin/go test -fuzz=FuzzHarness --generate-coverage .
これは <libafl output dir>/queue/ からの入力をリプレイし、cover.out と cover.html を書き出します。
これらのバグは、gosentry の文法ファジング機能を使用した差分ファジングキャンペーンによって発見されました。