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

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

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

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

スポンサーリンク

背景

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

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

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

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

コーパスファイルのフォーマット

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

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

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/file2fuzzfile2fuzzツールを使用して、これらのバイナリファイルを[]byte用にエンコードされたコーパスファイルに変換できます。

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

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

解説

コーパスファイルの中身を理解する

ファジングで使われる「コーパスファイル」は、特殊なテキストフォーマットで保存されています。このフォーマットを理解すると、手動でテストケースを追加したり、ファジングが何を保存しているか確認できるようになります。


コーパスファイルの基本構造

実際のファイルを見てみる

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

このファイルは3行で構成されています:

1行目: バージョン情報

go test fuzz v1

意味: このファイルは「Go fuzzing フォーマット バージョン1」です

なぜ必要? 将来、フォーマットが変更される可能性があるため、ファジングエンジンがどのバージョンか判断できるようにしています。

例え:

ファイルの先頭に「このファイルはPDF 1.7形式です」と書いてあるようなもの
→ アプリケーションが適切に読み込める

2行目以降: テストデータ

[]byte("hello\\xbd\\xb2=\\xbc ⌘")
int64(572293)

これらは、ファズターゲットに渡される引数です。

各行は、Goのコードとして直接コピーできる形式になっています:

f.Fuzz(func(t *testing.T, data []byte, num int64) {
    // ↑ この2つの引数に対応
})

コーパスファイルの型の対応

コーパスファイルの値は、ファズターゲットの引数と完全に一致しなければなりません。

例1: 単純な文字列

コーパスファイル:

go test fuzz v1
string("hello")

対応するファズターゲット:

f.Fuzz(func(t *testing.T, s string) {
    // sには"hello"が渡される
})

例2: 複数の引数

コーパスファイル:

go test fuzz v1
string("alice")
int(25)
bool(true)

対応するファズターゲット:

f.Fuzz(func(t *testing.T, name string, age int, active bool) {
    // name="alice", age=25, active=true
})

❌ 型が一致しない例

コーパスファイル:

go test fuzz v1
string("hello")
int64(100)

ファズターゲット:

// ❌ エラー: 引数は1つだけなのに、コーパスには2つある
f.Fuzz(func(t *testing.T, s string) {})
// ❌ エラー: 型が違う(int64 vs int)
f.Fuzz(func(t *testing.T, s string, n int) {})
// ✅ 正しい: 型と順序が一致
f.Fuzz(func(t *testing.T, s string, n int64) {})

コーパスファイルが保存される場所

myproject/
├── parse.go
├── parse_test.go
└── testdata/
    └── fuzz/
        └── FuzzParse/          ← ファズテスト名
            ├── seed1           ← 手動で追加したシード
            ├── seed2
            └── a8f3d4c2...     ← ファジングが自動生成したコーパス

2種類のコーパス:

  1. シードコーパス: 開発者が手動で追加したテストデータ
  2. 生成コーパス: ファジングが自動的に見つけた「興味深い」入力

コーパスファイルの作り方

方法1: f.Add()を使う(推奨)

これが最も簡単で推奨される方法です:

func FuzzParse(f *testing.F) {
    // テストコードに直接書く
    f.Add([]byte("hello\\xbd\\xb2=\\xbc ⌘"), int64(572293))
    f.Add([]byte("test"), int64(12345))
    
    f.Fuzz(func(t *testing.T, data []byte, num int64) {
        // テストコード
    })
}

メリット:

  • ✅ コードと一緒に管理できる
  • ✅ 型チェックがある(コンパイル時にエラーが分かる)
  • ✅ 簡単で分かりやすい

デメリット:

  • ❌ 大きなバイナリファイルには不向き

方法2: 手動でファイルを作る

コーパスファイルを作成: testdata/fuzz/FuzzParse/manual_test

go test fuzz v1
[]byte("test data")
int64(42)

実行すると:

$ go test
=== RUN   FuzzParse
=== RUN   FuzzParse/manual_test  ← 自動的に読み込まれる
--- PASS: FuzzParse (0.00s)

メリット:

  • ✅ 大きなファイルを扱える
  • ✅ 外部ツールで生成したデータを使える

デメリット:

  • ❌ 手動でフォーマットを合わせる必要がある
  • ❌ 型の間違いに気づきにくい

特殊文字のエスケープ

コーパスファイルでは、特殊なバイトはエスケープされます。

[]byte("hello\\xbd\\xb2=\\xbc ⌘")

エスケープの意味:

エスケープ意味
\\xbdバイト値0xBD(16進数)
\\n改行文字
\\tタブ文字
\\バックスラッシュ
\"ダブルクォート
そのまま(UTF-8文字)

実例:

// Goコードで
f.Add([]byte("hello\xbd\xb2=\xbc ⌘"))

// コーパスファイルでは
[]byte("hello\\xbd\\xb2=\\xbc ⌘")

大きなバイナリファイルを扱う: file2fuzzツール

問題: 画像やPDFをテストしたい

大きなバイナリファイルをテストしたい場合、コードに直接書くのは現実的ではありません:

// ❌ これは無理...
f.Add([]byte{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, /* 数千バイト続く... */})

解決策: file2fuzzツール

このツールは、バイナリファイルをコーパスファイルに変換してくれます。

インストール

$ go install golang.org/x/tools/cmd/file2fuzz@latest

使い方

# ヘルプを見る
$ file2fuzz -h
Usage of file2fuzz:
  file2fuzz [options] files...

# 画像ファイルをコーパスに変換
$ file2fuzz sample.png

# 出力先を指定
$ file2fuzz -o testdata/fuzz/FuzzImage/sample sample.png

実践例

ステップ1: バイナリファイルを準備

myproject/
├── testdata/
│   └── sample.png  ← テストしたい画像ファイル
└── image_test.go

ステップ2: コーパスファイルに変換

$ file2fuzz -o testdata/fuzz/FuzzImage/sample testdata/sample.png

生成されたファイル: testdata/fuzz/FuzzImage/sample

go test fuzz v1
[]byte("\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR...")

ステップ3: テストを実行

func FuzzImage(f *testing.F) {
    // file2fuzzで変換したファイルが自動的に読み込まれる
    
    f.Fuzz(func(t *testing.T, data []byte) {
        img, err := png.Decode(bytes.NewReader(data))
        if err != nil {
            return  // 無効な画像はスキップ
        }
        
        // 画像処理のテスト
        _ = processImage(img)
    })
}
$ go test -fuzz=FuzzImage
=== FUZZ  FuzzImage
=== RUN   FuzzImage/sample  ← file2fuzzで追加したファイル
fuzz: elapsed: 0s, gathering baseline coverage: 1/1 completed

コーパスファイルの実例集

例1: 文字列のみ

ファイル: testdata/fuzz/FuzzReverse/unicode

go test fuzz v1
string("こんにちは🎉")

使用:

func FuzzReverse(f *testing.F) {
    f.Fuzz(func(t *testing.T, s string) {
        reversed := Reverse(s)
        // テスト
    })
}

例2: 複数の型

ファイル: testdata/fuzz/FuzzUser/valid_user

go test fuzz v1
string("alice")
int(25)
bool(true)

使用:

func FuzzUser(f *testing.F) {
    f.Fuzz(func(t *testing.T, name string, age int, active bool) {
        user := User{Name: name, Age: age, Active: active}
        // テスト
    })
}

例3: バイナリデータ

ファイル: testdata/fuzz/FuzzParse/binary_data

go test fuzz v1
[]byte("\x00\x01\x02\xff\xfe\xfd")

使用:

func FuzzParse(f *testing.F) {
    f.Fuzz(func(t *testing.T, data []byte) {
        result, err := Parse(data)
        // テスト
    })
}

トラブルシューティング

エラー: “malformed corpus file”

原因: ファイルのフォーマットが間違っています

// ❌ 1行目が間違っている
fuzz test go v1  ← これは間違い

// ✅ 正しい
go test fuzz v1

エラー: “wrong number of values”

原因: コーパスファイルの引数の数が、ファズターゲットと一致していません

// コーパスファイル: 2つの引数
go test fuzz v1
string("hello")
int(42)

// ❌ ファズターゲット: 1つの引数
f.Fuzz(func(t *testing.T, s string) {})

// ✅ 正しい
f.Fuzz(func(t *testing.T, s string, n int) {})

エラー: “type mismatch”

原因: 型が一致していません

// コーパスファイル
go test fuzz v1
int64(123)

// ❌ ファズターゲット
f.Fuzz(func(t *testing.T, n int) {})  // int ≠ int64

// ✅ 正しい
f.Fuzz(func(t *testing.T, n int64) {})

まとめ

コーパスファイルのフォーマット:

go test fuzz v1        ← バージョン(必須)
型1(値1)               ← 1つ目の引数
型2(値2)               ← 2つ目の引数
...

コーパスの追加方法:

方法使い所
f.Add()小さなテストデータ(推奨)
手動作成複雑なケース
file2fuzz大きなバイナリファイル

file2fuzzの使い方:

# インストール
go install golang.org/x/tools/cmd/file2fuzz@latest

# 変換
file2fuzz -o testdata/fuzz/FuzzName/file input.bin

重要なルール:

  • ✅ コーパスファイルの型と順序は、ファズターゲットと完全一致
  • ✅ 1行目は必ず go test fuzz v1
  • ✅ バイナリファイルはfile2fuzzで変換

これらを理解すれば、手動でテストケースを追加したり、ファジングが保存したデータを確認できるようになります!

おわりに 

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

よっしー
よっしー

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

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

コメント

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