Go语言精进之路:从新手到高手的编程思想、方法和技巧(2)
上QQ阅读APP看书,第一时间看更新

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。