みなさん Fuzz testing ってご存じでしょうか。
人間が作る物は必ずといっていいほどバグが存在します。そしてそのコードをテストする人間も必ずバグを見逃します。
想定していなかった境界値テスト等、人間には先入観という物があり、それが邪魔をして簡単にバグを見逃します。昨今、この様な誰も気付かなかったバグの隙間を突く様な脆弱性が沢山見つかっています。
物によっては重大インシデントに発展する物まであります。
こういった人間では想定できない様なバグを見付けてくれるのが Fuzz testing です。Fuzz testing を実施する事で、ソフトウェアは頑丈になり安全にもなりえます。
本日、Go の master ブランチに Fuzz testing の機能が入りました。
[dev.fuzz] Merge remote-tracking branch 'origin/dev.fuzz' into merge-… · golang/go@6e81f78 · GitHubこれまで Go でテストを書いた事がある皆さんであれば、Go のテストは
+1 −0 api/except.txt +36 −0 api/next.txt +63 −15 src/cmd/go/alldocs.go +10 −0 src/cmd/go/internal/ca...
https://github.com/golang/go/commit/6e81f78c0f1653ea140e6c8d008700ddad1fa0a5
xxx_test.go
というファイルに、 Test
から始まり引数に *testing.T
を持つ関数を定義される事はご存じだと思います。今回入る Fuzz はこれに似ており先頭が Fuzz
で引数に *testing.F
を持つ関数が実行対象となります。
package foo_test
import "testing"
func FuzzFoo(f *testing.F) {
f.Log("all good here")
f.Fuzz(func(*testing.T, []byte) {})
}
例えば以下の関数があったとしましょう。b のゼロ長さチェックはしていますが、2バイト必要かどうかのテストが抜けています。
func doSomething(s string) bool {
if len(s) > 0 && s[0] == 0xff && s[1] == 0xff {
return true
}
return false
}
以下の様に、f.Fuzz
の第二引数に受け取りたい fuzz 値を得られる様にして実装します。
package foo_test
import "testing"
func doSomething(s string) bool {
if len(s) > 0 && s[0] == 0xff && s[1] == 0xff {
return true
}
return false
}
func FuzzFoo(f *testing.F) {
f.Log("all good here")
f.Fuzz(func(f *testing.T, b []byte) {
doSomething(string(b))
})
}
コマンドラインから以下の様に実行します。
$ go test -fuzz=.
すると以下の様に落ちる箇所を見付けてくれます。
gathering baseline coverage, elapsed: 0s, workers: 4, left: 2
fuzz: found a 31-byte crash input; minimizing...
FAIL
fuzz: elapsed: 1s, execs: 9807 (14334/sec), workers: 4, interesting: 0
--- FAIL: FuzzFoo (0.69s)
foo_test.go:13: all good here
foo_test.go:13: all good here
--- FAIL: FuzzFoo (0.00s)
testing.go:1244: panic: runtime error: index out of range [1] with length 1
goroutine 3187 [running]:
runtime/debug.Stack()
C:/go/src/runtime/debug/stack.go:24 +0x90
testing.tRunner.func1()
C:/go/src/testing/testing.go:1244 +0x545
panic({0x65c2e0, 0xc000014138})
C:/go/src/runtime/panic.go:814 +0x207
github.com/mattn/go-fuzz-example_test.doSomething(...)
C:/Users/mattn/go/src/github.com/mattn/go-fuzz-example/foo_test.go:6
github.com/mattn/go-fuzz-example_test.FuzzFoo.func1(0x0, {0xc000180000, 0x0, 0x4f37d9})
C:/Users/mattn/go/src/github.com/mattn/go-fuzz-example/foo_test.go:15 +0xfb
reflect.Value.call({0x63c8e0, 0x67a178, 0x30}, {0x66a8b0, 0x4}, {0xc008d80090, 0x2, 0x18})
C:/go/src/reflect/value.go:542 +0x814
reflect.Value.Call({0x63c8e0, 0x67a178, 0x5a33c0}, {0xc008d80090, 0x2, 0x2})
C:/go/src/reflect/value.go:338 +0xc5
testing.(*F).Fuzz.func1.1(0x0)
C:/go/src/testing/fuzz.go:412 +0x20b
testing.tRunner(0xc008d961a0, 0xc008d98090)
C:/go/src/testing/testing.go:1352 +0x102
created by testing.(*F).Fuzz.func1
C:/go/src/testing/fuzz.go:401 +0x4df
--- FAIL: FuzzFoo (0.00s)
testing.go:1244: panic: runtime error: index out of range [1] with length 1
goroutine 3189 [running]:
runtime/debug.Stack()
C:/go/src/runtime/debug/stack.go:24 +0x90
testing.tRunner.func1()
C:/go/src/testing/testing.go:1244 +0x545
panic({0x65c2e0, 0xc000014180})
C:/go/src/runtime/panic.go:814 +0x207
github.com/mattn/go-fuzz-example_test.doSomething(...)
C:/Users/mattn/go/src/github.com/mattn/go-fuzz-example/foo_test.go:6
github.com/mattn/go-fuzz-example_test.FuzzFoo.func1(0x0, {0xc008d820b0, 0x0, 0x4f37d9})
C:/Users/mattn/go/src/github.com/mattn/go-fuzz-example/foo_test.go:15 +0xfb
reflect.Value.call({0x63c8e0, 0x67a178, 0x30}, {0x66a8b0, 0x4}, {0xc008d80180, 0x2, 0x18})
C:/go/src/reflect/value.go:542 +0x814
reflect.Value.Call({0x63c8e0, 0x67a178, 0x5a33c0}, {0xc008d80180, 0x2, 0x2})
C:/go/src/reflect/value.go:338 +0xc5
testing.(*F).Fuzz.func1.1(0x0)
C:/go/src/testing/fuzz.go:412 +0x20b
testing.tRunner(0xc008d96340, 0xc008d98120)
C:/go/src/testing/testing.go:1352 +0x102
created by testing.(*F).Fuzz.func1
C:/go/src/testing/fuzz.go:401 +0x4df
Crash written to testdata\fuzz\FuzzFoo\e3443030b91d09bbdd7fbe67dbbbc3093401b277aa711c82dd97ee80606f56ab
To re-run:
go test github.com/mattn/go-fuzz-example -run=FuzzFoo/e3443030b91d09bbdd7fbe67dbbbc3093401b277aa711c82dd97ee80606f56ab
FAIL
exit status 1
FAIL github.com/mattn/go-fuzz-example 1.012s
バイト列以外の mutate に関しては現在は TODO の様です。
実装しておくだけで勝手にバグを見付けてくれるのですから、とても便利ですね。ぜひ活用していきましょう。