Go 1.18からGo Fuzzingの機能が追加されました。 僕もいくつかのパッケージに導入してみたのですが、予期していなかったテストパターンを見つけられて役にやっている気がします。 Fuzzテストが増えてきたので、毎回手元でFuzzテストを実行するのも大変になってきました。
簡単にFuzzテストを実行する環境を作れないかと、GitHub Actions上でFuzzテストを実行するActionを書いてみました。
使い方
Fuzzingのチュートリアルのコードで試してみましょう。
テスト対象の関数を書く
main.go
に文字列を反転させるコードを書きます。
// main.go
package main
func Reverse(s string) string {
b := []byte(s)
for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return string(b)
}
チュートリアルを進めていくとわかるのですが、このコードは「マルチバイト文字を正しく処理できない」というバグがあります。 このバグをFuzzingを使って見つけてもらいましょう。
Fuzzテストを書く
Reverse
を2回実行すると同じ文字列に戻るはずです。
このことを確認するテストを書きます。
さらに、入力がUTF-8としてValidであるなら、出力もValidであって欲しいです。 これもテストで確認します。
package main
func FuzzReverse(f *testing.F) {
testcases := []string{"Hello, world", " ", "!12345"}
for _, tc := range testcases {
f.Add(tc) // Use f.Add to provide a seed corpus
}
f.Fuzz(func(t *testing.T, orig string) {
rev := Reverse(orig)
doubleRev := Reverse(rev)
if orig != doubleRev {
t.Errorf("Before: %q, after: %q", orig, doubleRev)
}
if utf8.ValidString(orig) && !utf8.ValidString(rev) {
t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
}
})
}
Workflowを書く
shogo82148/actions-go-fuzzには「モジュール内のfunc FuzzTest
をすべて見つけるためのアクション」と「実際にFuzzingを行うアクション」の2つが含まれています。
この2つのアクションを使って、モジュール内のすべてのFuzzテストにFuzzingを行うワークフローを書きます。
name: "fuzz"
on:
workflow_dispatch:
schedule:
- cron: "36 2 * * 1,4"
permissions:
contents: write
pull-requests: write
jobs:
# モジュール内の func FuzzTest をすべて見つけるためのジョブ
list:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: "stable"
- id: list
uses: shogo82148/actions-go-fuzz/list@v0
outputs:
fuzz-tests: ${{steps.list.outputs.fuzz-tests}}
# 実際にFuzzingを行うジョブ
fuzz:
runs-on: ubuntu-latest
timeout-minutes: 360
needs: list
strategy:
fail-fast: false
matrix:
include: ${{fromJson(needs.list.outputs.fuzz-tests)}}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: "stable"
- uses: shogo82148/actions-go-fuzz/run@v0
with:
packages: ${{ matrix.package }}
fuzz-regexp: ${{ matrix.func }}
fuzz-time: "355m"
実行する
実行するとすぐにfailing inputを見つけてくれます。 アクションはfailing inputを見つけると、その内容をコミットして、プルリクエストを作ってくれます。
From a61ed4471d7058448f3985aa939bda29dcd51040 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Mon, 3 Jul 2023 14:53:24 +0000
Subject: [PATCH] Add a new fuzz input data for FuzzReverse in example/fuzz.
`go test -run=FuzzReverse/636ca16883e1fa24 example/fuzz` failed with the following output:
```
--- FAIL: FuzzReverse (0.00s)
--- FAIL: FuzzReverse/636ca16883e1fa24 (0.00s)
reverse_test.go:40: Reverse produced invalid UTF-8 string "\xa2\xd4"
FAIL
FAIL example/fuzz 0.003s
FAIL
```
This fuzz data is generated by [actions-go-fuzz](https://github.com/shogo82148/actions-go-fuzz).
---
.../testdata/fuzz/testdata/fuzz/FuzzReverse/636ca16883e1fa24 | 2 ++
1 file changed, 2 insertions(+)
create mode 100644 testdata/fuzz/testdata/fuzz/FuzzReverse/636ca16883e1fa24
diff --git a/testdata/fuzz/testdata/fuzz/FuzzReverse/636ca16883e1fa24 b/testdata/fuzz/testdata/fuzz/FuzzReverse/636ca16883e1fa24
new file mode 100644
index 0000000..8106293
--- /dev/null
+++ b/testdata/fuzz/testdata/fuzz/FuzzReverse/636ca16883e1fa24
@@ -0,0 +1,2 @@
+go test fuzz v1
+string("Ԣ")
プルリクエストには「エラーを再現するためのコマンド」と「その実行結果」が含まれているので、 手元にpullしてコマンドを叩けばすぐに再現できます。
$ go test -run=FuzzReverse/636ca16883e1fa24 example/fuzz
--- FAIL: FuzzReverse (0.00s)
--- FAIL: FuzzReverse/636ca16883e1fa24 (0.00s)
reverse_test.go:40: Reverse produced invalid UTF-8 string "\xa2\xd4"
FAIL
FAIL example/fuzz 0.003s
FAIL
あとはこのテストが通るよう直すだけです。
セキュリティー上の注意
一般的にfailing inputは脆弱性に関わる情報なので、一般公開されることはありません。 悪い人に見つかって悪用される可能性があるからです。 しかし、公開レポジトリ上でshogo82148/actions-go-fuzzを実行すると、 failing inputがプルリクエストとして公開されてしまいます。
GitHub上で完結させるためにプルリクエストを選びましたが、 本来であれば非公開でメンテナーに通知がされるべきです。 そこで、Slackで通知するオプションも追加しました。
- uses: shogo82148/actions-go-fuzz/run@v0
with:
packages: ${{ matrix.package }}
fuzz-regexp: ${{ matrix.func }}
fuzz-time: "355m"
report-method: "slack"
webhook-url: ${{ secrets.SLACK_INCOMING_WEBHOOK }}
Incoming Webhook発行の手間が増えますが、セキュリティーが気になる方はご利用ください。
本当にセキュリティーを気にするならOSS-Fuzzに参加してもいいかもしれませんね。 (参加の仕方知らないけど・・・)
実行スケジュール
shogo82148/actions-go-fuzzは検索効率を高めるために、generated corpusをキャッシュに保存します。
GitHub Actionsのキャッシュ保存期間は1週間なので、週に最低一度は実行しないとキャッシュが消えてしまいます。
先の例で"36 2 * * 1,4"
と週に2回実行するスケジュールになっているのは、キャッシュ削除を防ぐためです。
まとめ
GitHub Actions上でGo Fuzzingを実行するアクションを作成しました。
レポート方法はGitHubのプルリクエストかSlack通知を選べます。 GitHubで完結すると利用する側は楽なんですが・・・何かいい方法があったら教えてください。