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

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

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

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

スポンサーリンク

背景

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

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

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

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

コマンドライン出力

ファジングが進行中、ファジングエンジンは新しい入力を生成し、提供されたファズターゲットに対して実行します。デフォルトでは、失敗する入力が見つかるか、ユーザーがプロセスをキャンセルする(例: 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」の数が時間とともに減少することを期待すべきですが、ファジングエンジンが新しいコードパスを見つけた場合には時折バーストが発生します。


解説

ファジング実行中に画面に表示される内容を理解しよう

ファジングを実行すると、リアルタイムで進捗状況が表示されます。この出力を読めるようになると、ファジングが何をしているのか、順調に進んでいるのかが分かります。


出力の読み方: ステップバイステップ

実際の出力例を見ながら、一行ずつ解説します。

~ go test -fuzz FuzzFoo

↑ ファジング開始のコマンド


フェーズ1: ベースラインカバレッジの収集

fuzz: elapsed: 0s, gathering baseline coverage: 0/192 completed
fuzz: elapsed: 0s, gathering baseline coverage: 192/192 completed, now fuzzing with 8 workers

何が起こっている?

ファジングを始める前に、既存のテストデータ(コーパス)でどこまでコードをカバーできているかを確認しています。

数字の意味:

  • 0/192 → 192個のテストデータのうち、0個を実行済み
  • 192/192 completed → 全192個のテストデータを実行完了
  • now fuzzing with 8 workers → 8つの並列実行でファジング開始

レストランの例え:

料理を始める前の準備:
- 既存のレシピ192個を全部確認
- 8人のシェフが準備完了
→ これから新しい料理の実験を始めます!

フェーズ2: アクティブなファジング

fuzz: elapsed: 3s, execs: 325017 (108336/sec), new interesting: 11 (total: 202)

それぞれの数字の意味を見ていきましょう。

elapsed: 3s(経過時間)

ファジングを開始してから3秒経過しました。

シンプルですね。時間が進むにつれて、この数字が増えていきます。

execs: 325017 (108336/sec)(実行回数)

実行回数の意味:

  • 325017 → これまでに32万5017回のテストを実行
  • (108336/sec) → 1秒あたり約10万8336回のテストを実行

すごい速度!

たった3秒で30万回以上のテストを実行しています。これが自動テストの力です。

比較してみると:

人間が手動でテスト:
- 1つのテストに10秒かかる
- 1時間で360個のテスト

ファジング:
- 1秒で10万個以上のテスト!
- 3秒で32万個のテスト
new interesting: 11 (total: 202)(新しい発見)

これが最も重要な数字です。

数字の意味:

  • new interesting: 11 → この3秒間で11個の「興味深い」入力を発見
  • (total: 202) → コーパス全体で合計202個の入力データがある

「興味深い(interesting)」とは?

新しいコードの領域を実行できた入力のことです。


「興味深い」入力を詳しく理解する

コードカバレッジで考える

あなたのコードがこんな関数だとします:

func Process(input string) {
    if len(input) > 0 {           // 分岐A
        if input[0] == 'a' {      // 分岐B
            // 処理1
        } else if input[0] == 'z' { // 分岐C
            // 処理2
        }
    }
}

既存のコーパス:

  • "" → 分岐Aに到達しない
  • "hello" → 分岐A、分岐Cを通る

この時点でのカバレッジ:

✅ 分岐A
❌ 分岐B
✅ 分岐C

ファジングが新しい入力を試す:

  • "abc" を試す → 分岐A、分岐Bを通る ← 新しいパスを発見!

この "abc"興味深い入力として保存されます。

✅ 分岐A
✅ 分岐B ← 新たにカバー!
✅ 分岐C

出力の推移を読み解く

実際の出力の流れを見てみましょう:

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)

パターン1: 最初は急成長

3秒目:  new interesting: 11
6秒目:  new interesting: 12  ← +1個だけ

何が起こっている?

最初の3秒で、簡単に到達できるコードパスをどんどん発見しています。その後は発見ペースが落ちます。

宝探しの例え:

最初: 地面に落ちている宝物を拾う(簡単)
後半: 隠れた宝物を探す(難しい)

パターン2: 突然のバースト

6秒目:  new interesting: 12 (total: 203)
9秒目:  new interesting: 19 (total: 210)  ← 突然7個増えた!

何が起こっている?

新しいコードパス(例: 深い条件分岐)を発見し、そこから連鎖的に新しい領域に到達できるようになりました。

迷路の例え:

しばらく行き止まりが続く
 ↓
ついに隠し扉を発見!
 ↓
新しいエリアに一気にアクセス可能に

パターン3: 収束(taper off)

12秒目: new interesting: 21 (total: 212)
15秒目: new interesting: 21 (total: 212)  ← 増えていない
18秒目: new interesting: 21 (total: 212)  ← まだ増えない

何が起こっている?

コードのほとんどの領域をカバーし終えました。これ以上の新しいパスを見つけるのが難しくなっています。

これは正常です!


実行速度(execs/sec)の見方

fuzz: elapsed: 3s, execs: 325017 (108336/sec)

良い速度:

  • 10,000回/秒以上 → ✅ 高速
  • 1,000〜10,000回/秒 → ✅ 普通
  • 1,000回/秒未満 → ⚠️ 遅い(ファズターゲットを最適化すべき)

速度が遅い原因:

  • ファイルI/O
  • ネットワーク通信
  • 重い計算処理
  • スリープ処理

正常終了の例

fuzz: elapsed: 12s, execs: 1386684 (115594/sec), new interesting: 21 (total: 212)
PASS
ok      foo 12.692s
  • PASS → バグが見つからなかった(良いニュース!)
  • ok foo 12.692s → 合計12.692秒かかった

エラーが見つかった場合の例

fuzz: elapsed: 5s, execs: 542319 (108463/sec), new interesting: 15 (total: 206)
--- FAIL: FuzzReverse (5.23s)
    --- FAIL: FuzzReverse (0.00s)
        reverse_test.go:15: Reverse(Reverse("🎉")) = "", want "🎉"
    
    Failing input written to testdata/fuzz/FuzzReverse/a8f3d...
FAIL
exit status 1
FAIL    example 5.234s

バグ発見!入力は自動的に保存されます。


実際の使用例と期待値

短時間のファジング(10秒)

$ go test -fuzz=FuzzValidate -fuzztime=10s
fuzz: elapsed: 3s, execs: 325017, new interesting: 25 (total: 220)
fuzz: elapsed: 6s, execs: 680218, new interesting: 28 (total: 223)
fuzz: elapsed: 9s, execs: 1039901, new interesting: 30 (total: 225)
PASS

期待: 100万回前後の実行、新しい発見が徐々に減少

長時間のファジング(1時間)

$ go test -fuzz=FuzzParse -fuzztime=1h
fuzz: elapsed: 30s, new interesting: 45 (total: 240)
fuzz: elapsed: 1m0s, new interesting: 52 (total: 247)
fuzz: elapsed: 5m0s, new interesting: 67 (total: 262)
fuzz: elapsed: 30m0s, new interesting: 78 (total: 273)
fuzz: elapsed: 1h0m0s, new interesting: 82 (total: 277)
PASS

期待: 数千万〜億回の実行、新しい発見は時間とともにほぼ止まる


まとめ

出力の見方:

項目意味良い値
elapsed経過時間
execs総実行回数多いほど良い
execs/sec実行速度10,000以上が理想
new interesting新発見の数最初は増える、後で減る
totalコーパス総数徐々に増える

正常なパターン:

  1. 最初: new interestingが急増
  2. 中盤: 増加が減速
  3. 後半: ほぼ横ばい(時々バースト)

警告サイン:

  • execs/secが極端に低い → ファズターゲットを最適化
  • new interestingが全く増えない → コーパスが不足しているかも

この出力を読めるようになれば、ファジングの進捗を正しく把握し、効果的にバグを見つけられます!

おわりに 

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

よっしー
よっしー

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

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

コメント

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