Go言語入門:Fuzzing -Vol.4-

スポンサーリンク
Go言語入門:Fuzzing -Vol.4- ノウハウ
Go言語入門:Fuzzing -Vol.4-
この記事は約9分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

本日は、Go言語のFuzzingついて解説しています。

スポンサーリンク

背景

Goでテストを書いていると、「Fuzzing」という言葉を目にすることがあるかもしれません。公式ドキュメントを開いてみたものの、英語で書かれていて「coverage guidance」「edge cases」といった専門用語が並び、正直なところ「何をするものなのか、よくわからない…」と感じた方も多いのではないでしょうか。

実は私も最初は同じでした。「自動テスト」「入力を操作」「セキュリティ脆弱性の発見」と聞いても、具体的にどう使えばいいのか、なぜ必要なのかがピンと来なかったんです。

そこでこの記事では、Go公式ドキュメントの「Fuzzing」について、丁寧な日本語訳はもちろん、初心者の方にもわかるように補足説明や具体例を交えながら解説していきます。Fuzzingは一見難しそうに見えますが、実は「人間が思いつかないようなテストケースを自動で試してくれる便利な仕組み」なんです。

テストコードをより強固にしたい、セキュリティに配慮したコードを書きたいと考えている方にとって、Fuzzingは強力な味方になってくれます。一緒に学んでいきましょう!

ファズテストの実行

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

ファズテストは、デフォルトではユニットテストと同様に実行されます。各シードコーパスエントリがファズターゲットに対してテストされ、終了前に失敗があれば報告されます。

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

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

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


解説

ファズテストには2つの実行モードがある

ファズテストは、普通のテストのように実行することも、本格的にファジングすることもできます。この違いを理解しましょう。


モード1: 通常のテストとして実行(デフォルト)

実行方法

go test

これは普通のgo testコマンドです。特別なオプションは不要です。

何が起こるか?

準備したシードコーパス(初期データ)だけをテストします。

func FuzzReverse(f *testing.F) {
    // これらのデータだけがテストされる
    f.Add("hello")
    f.Add("world")
    f.Add("")
    
    f.Fuzz(func(t *testing.T, input string) {
        reversed := Reverse(input)
        // テストコード
    })
}

実行すると:

$ go test
=== RUN   FuzzReverse
=== RUN   FuzzReverse/seed#0  ← "hello"をテスト
=== RUN   FuzzReverse/seed#1  ← "world"をテスト
=== RUN   FuzzReverse/seed#2  ← ""をテスト
--- PASS: FuzzReverse (0.00s)
PASS

使い所: CI/CDパイプラインや、素早く動作確認したいとき


モード2: 本格的にファジング実行

実行方法

go test -fuzz=FuzzTestName

例:

go test -fuzz=FuzzReverse

何が起こるか?

ランダムな入力を生成して、永遠にテストし続けます(手動で止めるまで)。

$ go test -fuzz=FuzzReverse
=== FUZZ  FuzzReverse
fuzz: elapsed: 0s, gathering baseline coverage: 0/3 completed
fuzz: elapsed: 0s, gathering baseline coverage: 3/3 completed, now fuzzing with 8 workers
fuzz: elapsed: 3s, execs: 125847 (41946/sec), new interesting: 11 (3/sec)
fuzz: elapsed: 6s, execs: 251693 (41948/sec), new interesting: 18 (2/sec)
fuzz: elapsed: 9s, execs: 377539 (41949/sec), new interesting: 22 (1/sec)
...
^C  ← Ctrl+Cで停止

使い所: じっくりバグを探したいとき、セキュリティテストをしたいとき


ファジング実行の詳しい流れ

ステップ1: 他のテストを先に実行

ファジングを始める前に、パッケージ内の普通のテストを全部実行します。

$ go test -fuzz=FuzzReverse
=== RUN   TestReverse          ← 普通のテストを先に実行
--- PASS: TestReverse (0.00s)
=== RUN   TestValidate
--- PASS: TestValidate (0.00s)
=== FUZZ  FuzzReverse          ← その後、ファジング開始
...

なぜ? 既知のバグがあるのに、ファジングで時間を無駄にしたくないからです。

ステップ2: ベースラインカバレッジの収集

シードコーパスを使って、どのコードが実行されるか確認します。

fuzz: elapsed: 0s, gathering baseline coverage: 3/3 completed

ステップ3: 並列でファジング開始

複数のワーカー(CPUコア数に応じる)が並列で入力を生成し続けます。

now fuzzing with 8 workers  ← 8つの並列実行

-fuzzフラグの使い方

特定のファズテストを指定

# FuzzReverseだけを実行
go test -fuzz=FuzzReverse

# Fuzzで始まる全テストを実行(マッチが1つだけの場合のみ)
go test -fuzz=Fuzz

# 正規表現も使える
go test -fuzz=FuzzReverse$

⚠️ 重要: -fuzzにマッチするファズテストは1つだけでなければなりません。

# ❌ エラー: 複数マッチする
$ go test -fuzz=Fuzz
testing: will not fuzz, -fuzz matches more than one fuzz test: [FuzzReverse FuzzValidate]

# ✅ OK: 1つだけマッチ
$ go test -fuzz=FuzzReverse

ファジングを停止する方法

ファジングは自動では止まりません。手動で止める必要があります。

方法1: Ctrl+Cで止める

$ go test -fuzz=FuzzReverse
fuzz: elapsed: 30s, execs: 1258470 (41949/sec)
^C  ← Ctrl+Cを押す

方法2: 時間制限を設定する

# 30秒間だけファジング
go test -fuzz=FuzzReverse -fuzztime=30s

# 100万回実行したら止める
go test -fuzz=FuzzReverse -fuzztime=1000000x

実例:

$ go test -fuzz=FuzzReverse -fuzztime=10s
=== FUZZ  FuzzReverse
fuzz: elapsed: 0s, gathering baseline coverage: 3/3 completed, now fuzzing with 8 workers
fuzz: elapsed: 3s, execs: 125847 (41946/sec), new interesting: 11 (3/sec)
fuzz: elapsed: 6s, execs: 251693 (41948/sec), new interesting: 18 (2/sec)
fuzz: elapsed: 9s, execs: 377539 (41949/sec), new interesting: 22 (1/sec)
--- PASS: FuzzReverse (10.00s)  ← 10秒で自動停止

プラットフォームの制限

ファジングはカバレッジ計装という機能が必要です。

サポートされているプラットフォーム

  • ✅ AMD64(Intel/AMD 64bit)
  • ✅ ARM64(Apple Silicon、AWS Gravitonなど)

サポートされていないプラットフォーム

  • ❌ 32bit システム
  • ❌ その他のアーキテクチャ

なぜ重要?

カバレッジ計装がないと、ファジングエンジンが「どのコードが実行されたか」を知ることができません。すると、賢く新しい入力を生成できず、ただのランダムテストになってしまいます。

# サポートされていない環境で実行すると警告が出る
$ go test -fuzz=FuzzReverse
warning: fuzzing on this platform is not optimized

実践的な使い分け

開発中: 素早く確認

# 普通のテストとして実行(速い)
go test

コミット前: 軽くファジング

# 10秒だけファジング
go test -fuzz=FuzzReverse -fuzztime=10s

週末や夜間: じっくりファジング

# 1時間ファジング
go test -fuzz=FuzzReverse -fuzztime=1h

CI/CD: シードコーパスのみ

# CIでは普通のテストとして実行(時間制限のため)
go test

将来の展望: 継続的ファジング

現在、GoチームはOSS-Fuzzなどのツールとの統合を進めています。

OSS-Fuzzとは? Googleが提供する、オープンソースプロジェクト向けの継続的ファジングサービスです。

あなたのコード → GitHub
       ↓
   OSS-Fuzz(24時間365日ファジング)
       ↓
  バグ発見 → 自動で通知

これが実現すれば、寝ている間もファジングが動き続けてバグを見つけてくれます!

参考: Issue #50192


まとめ

2つの実行モード:

go test                      # モード1: シードコーパスのみテスト(速い)
go test -fuzz=FuzzTestName   # モード2: 継続的にファジング(徹底的)

時間制限の設定:

go test -fuzz=FuzzReverse -fuzztime=30s    # 30秒
go test -fuzz=FuzzReverse -fuzztime=1000x  # 1000回実行

プラットフォーム:

  • AMD64またはARM64で実行しましょう

使い分け:

  • 日常: go test(速い確認)
  • 重要な変更前: -fuzztime=10s(軽くファジング)
  • じっくり: -fuzztime=1h以上(徹底的にテスト)

ファジングは時間をかければかけるほど、より深いバグを見つけられます!

おわりに 

本日は、Go言語のFuzzingについて解説しました。

よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました