Goファジング

Goは、Go 1.18以降の標準ツールチェーンでファジングをサポートしています。ネイティブGoファズテストは、OSS-Fuzzでサポートされています

Goでのファジングのチュートリアルを試してみてください。

概要

ファジングは、バグを見つけるためにプログラムへの入力を継続的に操作する自動テストの一種です。Goファジングは、カバレッジガイダンスを使用して、ファジング中のコードをインテリジェントに歩き回り、障害を検出してユーザーに報告します。人間が見逃しがちなエッジケースに到達できるため、ファジングテストはセキュリティエクスプロイトや脆弱性を見つけるのに特に役立ちます。

以下は、主なコンポーネントを強調表示したファズテストの例です。

Example code showing the overall fuzz test, with a fuzz target within
it. Before the fuzz target is a corpus addition with f.Add, and the parameters
of the fuzz target are highlighted as the fuzzing arguments. Example code showing the overall fuzz test, with a fuzz target within
it. Before the fuzz target is a corpus addition with f.Add, and the parameters
of the fuzz target are highlighted as the fuzzing arguments.

ファズテストの作成

要件

以下は、ファズテストが従う必要のあるルールです。

提案

以下は、ファジングを最大限に活用するのに役立つ提案です。

ファズテストの実行

ファズテストの実行には、ユニットテストとして(デフォルトgo test)、またはファジングありで(go test -fuzz=FuzzTestName)の2つのモードがあります。

ファズテストは、デフォルトではユニットテストとほぼ同じように実行されます。各シードコーパスエントリは、ファズターゲットに対してテストされ、終了前に障害を報告します。

ファジングを有効にするには、-fuzzフラグを指定してgo testを実行し、単一のファズテストに一致する正規表現を提供します。デフォルトでは、そのパッケージ内の他のすべてのテストは、ファジングが開始される前に実行されます。これは、ファジングが既存のテストで既にキャッチされる問題を報告しないようにするためです。

ファジングの実行時間を決定するのはユーザー次第であることに注意してください。エラーが見つからなければ、ファジングの実行が無期限に続く可能性は十分にあります。将来的にはOSS-Fuzzなどのツールを使用してこれらのファズテストを継続的に実行するサポートが提供される予定です。 Issue #50192を参照してください。

注: ファジングは、実行中にコーパスが意味のある成長を遂げ、ファジング中にさらに多くのコードをカバーできるように、カバレッジインストルメンテーション(現在AMD64およびARM64)をサポートするプラットフォームで実行する必要があります。

コマンドライン出力

ファジングが進行中の間、ファジングエンジンは新しい入力を生成し、提供されたファズターゲットに対して実行します。デフォルトでは、失敗する入力が見つかるか、ユーザーがプロセスをキャンセルする(例:Ctrl^C)まで実行し続けます。

出力は次のようになります。

~ go test -fuzz FuzzFoo
fuzz: elapsed: 0s, gathering baseline coverage: 0/192 completed
fuzz: elapsed: 0s, gathering baseline coverage: 192/192 completed, now fuzzing with 8 workers
fuzz: elapsed: 3s, execs: 325017 (108336/sec), new interesting: 11 (total: 202)
fuzz: elapsed: 6s, execs: 680218 (118402/sec), new interesting: 12 (total: 203)
fuzz: elapsed: 9s, execs: 1039901 (119895/sec), new interesting: 19 (total: 210)
fuzz: elapsed: 12s, execs: 1386684 (115594/sec), new interesting: 21 (total: 212)
PASS
ok      foo 12.692s

最初の行は、ファジングが開始される前に「ベースラインカバレッジ」が収集されることを示しています。

ベースラインカバレッジを収集するために、ファジングエンジンはシードコーパス生成されたコーパスの両方を実行して、エラーが発生していないことと、既存のコーパスが既に提供しているコードカバレッジを理解します。

後続の行は、アクティブなファジング実行に関する洞察を提供します

入力が「興味深い」と見なされるためには、既存の生成されたコーパスが到達できる範囲を超えてコードカバレッジを拡張する必要があります。新しい興味深い入力の数は、最初は急速に増加し、最終的には速度が低下し、新しい分岐が発見されると時折バーストが発生するのが一般的です。

コーパス内の入力がコードのより多くの行をカバーし始めると、時間の経過とともに「新しい興味深い」数が減少していくことが予想されます。ファジングエンジンが新しいコードパスを見つけた場合は、時折バーストが発生します。

失敗する入力

ファジング中に、いくつかの理由で障害が発生する可能性があります

エラーが発生した場合、ファジングエンジンは、エラーを生成する最小限の、最も人間が読みやすい値に入力を最小化しようとします。これを構成するには、カスタム設定セクションを参照してください。

最小化が完了すると、エラーメッセージがログに記録され、出力は次のようになります

    Failing input written to testdata/fuzz/FuzzFoo/a878c3134fe0404d44eb1e662e5d8d4a24beb05c3d68354903670ff65513ff49
    To re-run:
    go test -run=FuzzFoo/a878c3134fe0404d44eb1e662e5d8d4a24beb05c3d68354903670ff65513ff49
FAIL
exit status 1
FAIL    foo 0.839s

ファジングエンジンは、この失敗する入力をそのファズテストのシードコーパスに書き込みました。これは、バグが修正されると、デフォルトでgo testによって実行されるようになり、リグレッションテストとして機能します。

次のステップでは、問題を診断し、バグを修正し、go testを再実行して修正を検証し、新しいテストデータファイルをリグレッションテストとして機能させてパッチを送信する必要があります。

カスタム設定

デフォルトのgoコマンド設定は、ほとんどのファジングユースケースで機能するはずです。したがって、通常、コマンドラインでのファジングの実行は次のようになります

$ go test -fuzz={FuzzTestName}

ただし、goコマンドは、ファジングを実行する際にいくつかの設定を提供します。これらはcmd/goパッケージドキュメントに記載されています。

いくつか強調すると

コーパスファイル形式

コーパスファイルは特別な形式でエンコードされています。これは、シードコーパス生成されたコーパスの両方で同じ形式です。

以下は、コーパスファイルの例です

go test fuzz v1
[]byte("hello\\xbd\\xb2=\\xbc ⌘")
int64(572293)

最初の行は、ファイルのエンコードバージョンをファジングエンジンに通知するために使用されます。エンコード形式の将来のバージョンは現在計画されていませんが、設計はこの可能性をサポートする必要があります。

後続の各行は、コーパスエントリを構成する値であり、必要に応じてGoコードに直接コピーできます。

上記の例では、[]byteの後にint64があります。これらのタイプは、その順序で、ファジング引数と正確に一致する必要があります。これらのタイプのファズターゲットは次のようになります

f.Fuzz(func(*testing.T, []byte, int64) {})

独自のシードコーパス値を指定する最も簡単な方法は、(*testing.F).Addメソッドを使用することです。上記の例では、次のようになります

f.Add([]byte("hello\\xbd\\xb2=\\xbc ⌘"), int64(572293))

ただし、テストにコードとしてコピーするのではなく、testdata/fuzz/{FuzzTestName}ディレクトリの個々のシードコーパスエントリとして残しておきたい大きなバイナリファイルがある場合があります。golang.org/x/tools/cmd/file2fuzzにあるfile2fuzzツールを使用して、これらのバイナリファイルを[]byte用にエンコードされたコーパスファイルに変換できます。

このツールを使用するには

$ go install golang.org/x/tools/cmd/file2fuzz@latest
$ file2fuzz -h

リソース

用語集

コーパスエントリ: ファジング中に使用できるコーパス内の入力。これは、特別にフォーマットされたファイル、または(*testing.F).Addへの呼び出しである可能性があります。

カバレッジガイダンス: コードカバレッジの拡張を使用して、将来の使用のために保持する価値のあるコーパスエントリを決定するファジングの方法。

失敗する入力: 失敗する入力とは、ファズターゲットに対して実行されるとエラーまたはパニックを引き起こすコーパスエントリのことです。

ファズターゲット: ファジング中にコーパスエントリと生成された値に対して実行されるファズテストの関数。関数を(*testing.F).Fuzzに渡すことで、ファズテストに提供されます。

ファズテスト: func FuzzXxx(*testing.F)形式のテストファイル内の関数で、ファジングに使用できます。

ファジング (fuzzing): バグや、コードが脆弱である可能性のある脆弱性などの問題を見つけるために、プログラムへの入力を継続的に操作する自動テストの一種。

ファジング引数 (fuzzing arguments): ファズ対象に渡され、ミューテーターによって変化させられる型。

ファジングエンジン (fuzzing engine): コーパスの維持、ミューテーターの起動、新しいカバレッジの特定、およびエラーの報告など、ファジングを管理するツール。

生成されたコーパス (generated corpus): ファジング中に進行状況を追跡するために、ファジングエンジンによって時間経過とともに維持されるコーパス。$GOCACHE/fuzz に保存される。これらのエントリはファジング中にのみ使用される。

ミューテーター (mutator): ファジング中に使用され、ファズ対象に渡す前にコーパスエントリをランダムに操作するツール。

パッケージ (package): 同じディレクトリにある、一緒にコンパイルされるソースファイルの集まり。Go言語仕様のパッケージセクションを参照。

シードコーパス (seed corpus): ファジングエンジンを誘導するために使用できる、ファズテスト用にユーザーが提供するコーパス。ファズテスト内の f.Add 呼び出しによって提供されるコーパスエントリと、パッケージ内の testdata/fuzz/{FuzzTestName} ディレクトリ内のファイルで構成される。これらのエントリは、ファジングの有無に関わらず、go test でデフォルトで実行される。

テストファイル (test file): テスト、ベンチマーク、サンプル、およびファズテストを含めることができる xxx_test.go という形式のファイル。

脆弱性 (vulnerability): 攻撃者によって悪用される可能性のある、コードにおけるセキュリティ上の機密性の高い弱点。

フィードバック

問題が発生した場合や機能に関するアイデアがある場合は、問題を提出してください。

機能に関する議論や一般的なフィードバックについては、Gophers Slack の #fuzzing チャンネルにも参加できます。