Shogo's Blog

Jul 5, 2023 - 2 minute read - github go golang

GitHub Actions上でFuzzingを実行するアクションを書いた

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で完結すると利用する側は楽なんですが・・・何かいい方法があったら教えてください。

参考