45.3 go-fuzz使用方法
1. 安装go-fuzz
使用go-fuzz需要安装两个重要工具:go-fuzz-build和go-fuzz。通过标准go get就可以安装它们:
$ go get github.com/dvyukov/go-fuzz/go-fuzz $ go get github.com/dvyukov/go-fuzz/go-fuzz-build
go get会自动将两个工具安装到$GOROOT/bin或$GOPATH/bin下,因此你需要确保你的Path环境变量下包含这两个路径。
2. 带有模糊测试的项目组织
假设待测试的Go包名为foo,包源文件路径为$GOPATH/src/github.com/bigwhite/fuzzexamples/foo。为了应用go-fuzz为包foo建立模糊测试,我们一般会在foo下创建fuzz.go源文件,其内容模板如下:
// +build gofuzz package foo func Fuzz(data []byte) int { ... }
go-fuzz-build在构建用于go-fuzz命令输入的二进制文件时,会搜索带有“+build gofuzz”指示符的Go源文件以及其中的Fuzz函数。如果foo包下没有这样的文件,在执行go-fuzz-build时,你会得到类似如下的错误日志:
$go-fuzz-build github.com/bigwhite/fuzzexamples/foo failed to execute go build: exit status 2 $go-fuzz-main /var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-fuzz-build641745751/src/go-fuzz-main/main.go:10: undefined: foo.Fuzz
有时候,待测试包的包内功能很多,一个Fuzz函数不够用,我们可以在fuzztest下建立多个目录来应对:
github.com/bigwhite/fuzzexamples/foo/fuzztest]$tree . ├── fuzz1 │ ├── corpus │ ├── fuzz.go │ └── gen │ └── main.go └── fuzz2 ├── corpus ├── fuzz.go └── gen └── main.go ...
其中的fuzz1, fuzz2, …, fuzzN各自为一个go-fuzz单元,如果要应用go-fuzz,则可像下面这样执行:
$ cd fuzz1 $ go-fuzz-build github.com/bigwhite/fuzzexamples/foo/fuzztest/fuzz1 $ go-fuzz -bin=./foo-fuzz.zip -workdir=./ ... $ cd fuzz2 $ go-fuzz-build github.com/bigwhite/fuzzexamples/foo/fuzztest/fuzz2 $ go-fuzz -bin=./foo-fuzz.zip -workdir=./
我们看到,在每个go-fuzz测试单元下有一套“固定”的目录组合。以fuzz1目录为例:
├── fuzz1 │ ├── corpus │ ├── fuzz.go │ └── gen │ └── main.go
其中:
- corpus为存放输入数据语料的目录,在go-fuzz执行之前,可以放入初始语料;
- fuzz.go为包含Fuzz函数的源码文件;
- gen目录中包含手工生成初始语料的main.go代码。
在后续的示例中,我们会展示细节。
3. go-fuzz-build
go-fuzz-build会根据Fuzz函数构建一个用于go-fuzz执行的zip包(PACKAGENAME-fuzz.zip),包里包含了用途不同的三个文件:
cover.exe metadata sonar.exe
按照go-fuzz作者的解释,这三个二进制程序的功能分别如下。
- cover.exe:被注入了代码测试覆盖率桩设施的二进制文件。
- sonar.exe:被注入了sonar统计桩设施的二进制文件。
- metadata:包含代码覆盖率统计、sonar的元数据以及一些整型、字符串字面值。
不过作为使用者,我们不必过于关心它们,点到为止。
4. 执行go-fuzz
一旦生成了foo-fuzz.zip,我们就可以执行针对fuzz1的模糊测试。
$cd fuzz1 $go-fuzz -bin=./foo-fuzz.zip -workdir=./ 2019/12/08 17:51:48 workers: 4, corpus: 8 (1s ago), crashers: 0, restarts: 1/0, execs: 0 (0/sec), cover: 0, uptime: 3s 2019/12/08 17:51:51 workers: 4, corpus: 9 (2s ago), crashers: 0, restarts: 1/3851, execs: 11553 (1924/sec), cover: 143, uptime: 6s 2019/12/08 17:51:54 workers: 4, corpus: 9 (5s ago), crashers: 0, restarts: 1/3979, execs: 47756 (5305/sec), cover: 143, uptime: 9s ...
如果corpus目录中没有初始语料数据,那么go-fuzz也会自行生成相关数据传递给Fuzz函数,并且采用遗传算法,不断基于corpus中的语料生成新的输入语料。go-fuzz作者建议corpus初始时放入的语料越多越好,而且要有足够的多样性,这样基于这些初始语料施展遗传算法,效果才会更佳。go-fuzz在执行过程中还会将一些新语料持久化成文件放在corpus中,以供下次模糊测试执行时使用。
前面说过,go-fuzz执行时是一个无限循环,上面的测试需要手动停下来。go-fuzz会在指定的workdir中创建另两个目录:crashers和suppressions。顾名思义,crashers中存放的是代码崩溃时的相关信息,包括引起崩溃的输入用例的二进制数据、输入数据的字符串形式(xxx.quoted)以及基于这个数据的输出数据(xxx.output)。suppressions目录中则保存着崩溃时的栈跟踪信息,方便开发人员快速定位bug。