Go Fuzzing

Go 1.18 から、Go は標準ツールチェーンでファジングをサポートしています。ネイティブ 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.

ファズテストの記述

要件

ファズテストは以下のルールに従う必要があります。

  • ファズテストは、FuzzXxx のような名前の関数で、*testing.F のみを受け入れ、戻り値がない必要があります。
  • ファズテストは、実行するために *_test.go ファイルに存在する必要があります。
  • ファズターゲットは、最初のパラメータとして *testing.T を受け入れ、その後にファジング引数が続く (*testing.F).Fuzz へのメソッド呼び出しである必要があります。戻り値はありません。
  • ファズテストごとにファズターゲットは正確に 1 つである必要があります。
  • すべてのシードコーパスエントリは、ファジング引数と同一の型を持ち、同じ順序である必要があります。これは、(*testing.F).Add への呼び出しと、ファズテストの testdata/fuzz ディレクトリ内のコーパスファイルに当てはまります。
  • ファジング引数は以下の型のみ可能です
    • string, []byte
    • int, int8, int16, int32/rune, int64
    • uint, uint8/byte, uint16, uint32, uint64
    • float32, float64
    • bool

提案

ファジングを最大限に活用するための提案を以下に示します。

  • ファズターゲットは高速で決定論的である必要があります。これにより、ファジングエンジンが効率的に動作し、新しい障害やコードカバレッジを簡単に再現できます。
  • ファズターゲットは複数のワーカーで並行して、非決定論的な順序で呼び出されるため、ファズターゲットの状態は各呼び出しの終了後も持続してはならず、ファズターゲットの動作はグローバルな状態に依存してはなりません。

ファズテストの実行

ファズテストを実行するには、ユニットテストとして(デフォルトの 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

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

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

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

  • elapsed: プロセス開始からの経過時間
  • execs: ファズターゲットに対して実行された入力の総数(最後のログ行からの平均 execs/sec 付き)
  • new interesting: このファジング実行中に生成されたコーパスに追加された「興味深い」入力の総数(コーパス全体の合計サイズ付き)

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

「new interesting」の数は、コーパス内の入力がより多くのコード行をカバーするにつれて時間の経過とともに減少していくことが予想されますが、ファジングエンジンが新しいコードパスを見つけた場合は一時的に急増することがあります。

失敗した入力

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

  • コードまたはテストでパニックが発生した。
  • ファズターゲットが t.Fail を直接、または t.Errort.Fatal のようなメソッドを通じて呼び出した。
  • os.Exit やスタックオーバーフローなどの回復不能なエラーが発生した。
  • ファズターゲットの完了に時間がかかりすぎた。現在、ファズターゲットの実行のタイムアウトは 1 秒です。これはデッドロックや無限ループ、またはコード内の意図された動作が原因で失敗する可能性があります。これが、ファズターゲットを高速にするよう提案されている理由の 1 つです。

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

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

    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 を再実行して修正を検証し、新しい testdata ファイルを回帰テストとして機能させるパッチを送信する必要があります。

カスタム設定

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

$ go test -fuzz={FuzzTestName}

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

いくつか強調すると

  • -fuzztime: 終了するまでのファズターゲットが実行される合計時間またはイテレーション数。デフォルトは無期限。
  • -fuzzminimizetime: 各最小化試行中にファズターゲットが実行される時間またはイテレーション数。デフォルトは 60 秒。ファジング時に -fuzzminimizetime 0 を設定することで、最小化を完全に無効にできます。
  • -parallel: 同時に実行されるファジングプロセスの数。デフォルトは $GOMAXPROCS。現在、ファジング中に -cpu を設定しても効果はありません。

コーパスファイル形式

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

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

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 パッケージのドキュメントでは、ファズテストを記述する際に使用される testing.F 型について説明しています。
    • cmd/go パッケージのドキュメントでは、ファジングに関連するフラグについて説明しています。
  • 技術的な詳細:

用語集

コーパスエントリ: ファジング中に使用できるコーパス内の入力。これは、特殊な形式のファイル、または (*testing.F).Add への呼び出しのいずれかです。

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

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

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

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

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

ファジング引数: ファズターゲットに渡され、ミューテーターによって変更される型。

ファジングエンジン: コーパスの維持、ミューテーターの呼び出し、新しいカバレッジの特定、障害の報告など、ファジングを管理するツール。

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

ミューテーター: ファジング中に使用されるツールで、コーパスエントリをファズターゲットに渡す前にランダムに操作します。

パッケージ: 同じディレクトリ内のソースファイルのコレクションで、一緒にコンパイルされます。Go 言語仕様のパッケージセクションを参照してください。

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

テストファイル: テスト、ベンチマーク、例、ファズテストを含むことができる xxx_test.go 形式のファイル。

脆弱性: 攻撃者によって悪用される可能性がある、コード内のセキュリティ上の脆弱性。

フィードバック

問題が発生した場合、または機能のアイデアがある場合は、問題を提出してください

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