概要

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

以下は、主なコンポーネントを強調したfuzz testの例です。

ファジングターゲットを含む全体のファジングテストを示す例のコード。ファジングターゲットの前にはf.Addによるコーパス追加があり、ファジングターゲットのパラメータはファジング引数として強調表示されています。 ファジングターゲットを含む全体のファジングテストを示す例のコード。ファジングターゲットの前にはf.Addによるコーパス追加があり、ファジングターゲットのパラメータはファジング引数として強調表示されています。

ファジングテストの作成

要件

以下は、ファジングテストが従うべきルールです。

  • ファジングテストは、FuzzXxxのように名付けられた関数でなければならず、*testing.Fのみを受け入れ、戻り値はありません。
  • ファジングテストは、実行するために*_test.goファイル内に存在しなければなりません。
  • fuzz targetは、(*testing.F).Fuzzへのメソッド呼び出しでなければならず、最初のパラメータとして*testing.Tを受け入れ、その後にファジング引数が続きます。戻り値はありません。
  • 各ファジングテストには正確に1つのファジングターゲットが必要です。
  • すべてのseed corpusエントリは、同じ順序でfuzzing argumentsと同一の型でなければなりません。これは、(*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つのモードがあります。

ファジングテストは、デフォルトではユニットテストのように実行されます。各seed corpus entryはファジングターゲットに対してテストされ、終了前に失敗が報告されます。

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

ファジングをどのくらいの時間実行するかはあなた次第です。エラーが見つからない場合、ファジングの実行が無限に続く可能性があります。将来的には、OSS-Fuzzのようなツールを使用してこれらのファジングテストを継続的に実行するサポートが提供される予定です。詳細はIssue #50192を参照してください。

注意: ファジングは、コバレッジ計測をサポートするプラットフォーム(現在はAMD64およびARM64)で実行する必要があります。これにより、実行中にコーパスが意味のある形で成長し、ファジング中により多くのコードがカバーされることができます。

コマンドライン出力

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

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

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

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

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

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

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

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

コーパス内の入力がより多くのコード行をカバーし始めるにつれて、「新しい興味深い」数が時間とともに減少することを期待するべきです。ファジングエンジンが新しいコードパスを見つけた場合には、時折急増することがあります。

失敗した入力

ファジング中に失敗が発生する理由はいくつかあります:

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

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

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

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

ファジングエンジンは、このfailing inputをそのファジングテストのseed corpusに書き込み、バグが修正された後に回帰テストとしてgo testでデフォルトで実行されます。

次のステップは、問題を診断し、バグを修正し、go testを再実行して修正を確認し、新しいテストデータファイルを回帰テストとしてパッチとして提出することです。

カスタム設定

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

  1. $ go test -fuzz={FuzzTestName}

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

いくつかを強調すると:

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

コーパスファイル形式

コーパスファイルは特別な形式でエンコードされています。これは、seed corpusgenerated corpusの両方に対して同じ形式です。

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

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

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

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

上記の例では、[]byteの後にint64があります。これらの型は、ファジング引数と正確に一致しなければなりません。その順序で。これらの型のファジングターゲットは次のようになります:

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

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

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

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

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

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

リソース

  • チュートリアル:
  • ドキュメント:
    • testingパッケージのドキュメントは、ファジングテストを書くときに使用されるtesting.F型について説明しています。
    • cmd/goパッケージのドキュメントは、ファジングに関連するフラグについて説明しています。
  • 技術的詳細:

用語集

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

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

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

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

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

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

ファジング引数: ファジングターゲットに渡され、mutatorによって変異される型。

ファジングエンジン: コーパスの管理、ミューテーターの呼び出し、新しいカバレッジの特定、失敗の報告を含むファジングを管理するツール。

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

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

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

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

テストファイル: テスト、ベンチマーク、例、およびファジングテストを含む可能性のあるxxx_test.go形式のファイル。

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

フィードバック

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

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