Goファジング
Goは、Go 1.18以降の標準ツールチェーンでファジングをサポートしています。ネイティブGoファズテストは、OSS-Fuzzでサポートされています。
Goでのファジングのチュートリアルを試してみてください。
概要
ファジングは、バグを見つけるためにプログラムへの入力を継続的に操作する自動テストの一種です。Goファジングは、カバレッジガイダンスを使用して、ファジング中のコードをインテリジェントに歩き回り、障害を検出してユーザーに報告します。人間が見逃しがちなエッジケースに到達できるため、ファジングテストはセキュリティエクスプロイトや脆弱性を見つけるのに特に役立ちます。
以下は、主なコンポーネントを強調表示したファズテストの例です。
ファズテストの作成
要件
以下は、ファズテストが従う必要のあるルールです。
- ファズテストは、
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/秒を含む)
- new interesting: このファジング実行中に生成されたコーパスに追加された「興味深い」入力の合計数(コーパス全体の合計サイズを含む)
入力が「興味深い」と見なされるためには、既存の生成されたコーパスが到達できる範囲を超えてコードカバレッジを拡張する必要があります。新しい興味深い入力の数は、最初は急速に増加し、最終的には速度が低下し、新しい分岐が発見されると時折バーストが発生するのが一般的です。
コーパス内の入力がコードのより多くの行をカバーし始めると、時間の経過とともに「新しい興味深い」数が減少していくことが予想されます。ファジングエンジンが新しいコードパスを見つけた場合は、時折バーストが発生します。
失敗する入力
ファジング中に、いくつかの理由で障害が発生する可能性があります
- コードまたはテストでパニックが発生しました。
- ファズターゲットは、直接または
t.Error
やt.Fatal
などのメソッドを介してt.Fail
を呼び出しました。 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
を再実行して修正を検証し、新しいテストデータファイルをリグレッションテストとして機能させてパッチを送信する必要があります。
カスタム設定
デフォルトの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
リソース
- チュートリアル:
- 新しい概念を深く掘り下げるには、Goでのファジングのチュートリアルを試してみてください。
- Goでのファジングのより短い入門チュートリアルについては、ブログ投稿をご覧ください。
- ドキュメント:
- 技術的な詳細:
用語集
コーパスエントリ: ファジング中に使用できるコーパス内の入力。これは、特別にフォーマットされたファイル、または(*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 チャンネルにも参加できます。