标签 GopherCon 下的文章

Go语言随机测试工具go-fuzz

Go 1.5发布时,前Intel Black Belt级工程师,现Google工程师Dmitry Vyukov同时发布了Go语言随机测试工具go-fuzz。在 GopherCon2015大会上,Dmitry Vyukov在其名为“[Go Dynamic Tools]”的presentation中着重介绍了go-fuzz

go-fuzz是一款随机测试(Random testing)工具。对于随机测试想必很多人都比较陌生,我也不例外。至少在接触go-fuzz之前,我从未在golang或其他编程语言中使用过类似的测试工具(c/c++开发者可以使用afl-fuzz)。按照维基百科的说法:随机测试就是指半自动或自动地为程序提供非法的、非预期、随机的数据,并监控程序在这些输入数据 下的crash、内置断言、内存泄露等情况。随机测试的研究始于1988年的Barton Miller,到目前为止已经有许多理论支撑,不过这里不会涉及,有兴趣的、想深入的朋友可以跟随维基百科中的链接自行学习。

在开始go-fuzz之前,我们需要认识到随机测试的位置和意义:
* 首先它是软件测试技术的一个重要分支,与单元测试等互为补充;
* 其次随机测试不是什么银弹,它有其适用的范围。随机测试最适合那些处理复杂输入数据的程序,比如文件格式解析、网络协议解析、人机交互界面入口等。
* 最后,并非所有编程语言都有类似的工具支撑,gopher很幸运,Dmitry Vyukov为我们带来了go-fuzz。

接下来就让我们回到go-fuzz这个正题上来。

一、Why go-fuzz

go-fuzz之所以吸引眼球,源于Dmitry Vyukov在使用go-fuzz对go标准库以及其他第三方开源库进行测试后的“惊人的战果”。Dmitry在其slide中展示了这些战果:

60 tests
137 bugs in std lib (70 fixed)
165 elsewhere (47 in gccgo, 30 in golang.org/x, 42 in freetype-go, protobuf, http2, bson)

Dmitry Vyukov的go-fuzz实际上也是基于前面提到的afl-fuzz的逻辑 的基础上设计和实现的。不同的是在使用的时候,afl-fuzz对于每个input case都会fork一个process,而go-fuzz则是通过将input case中的data传给一个Fuzz函数:

func Fuzz(data []byte) int

这样就无需反复重启程序。

go-fuzz进一步完善了go开发测试工具集,很多一线公司(比如cloudflare)已经开始使用go-fuzz来测试自己的产品,提高产品质量了。

二、原理

Dmitry在其slide中将go-fuzz的工作流程归纳如下:

 -> 生成随机数据
 -> 输入给程序
 -> 观察是否有crash
 -> 如果发现crash,则获益
  之后开发者根据crash的结果,尝试fix bug,并
  添加针对这个bug的单元测试case。

go-fuzz一旦运行起来,将会是一个infinite loop(一种遗传算法),该loop的伪代码在slide也有给出:

Instrument program for code coverage
Collect initial corpus of inputs  //收集初始输入数据语料(位于workdir的corpus目录下)
for {
    //从corpus中读取语料并随机变化
    Randomly mutate an input from the corpus

    //执行Fuzz,收集覆盖范围
    Execute and collect coverage

    //如果输入数据提供了新的coverage,则将该数据存入语料库(corpus)
    If the input gives new coverage, add it to corpus
}

go-fuzz内部实现了多种对初始语料库中输入数据的mutation策略:

* Insert/remove/duplicate/copy a random range of random bytes.
* Bit flip.
* Swap 2 bytes.
* Set a byte to a random value.
* Add/subtract from a byte/uint16/uint32/uint64 (le/be).
* Replace a byte/uint16/uint32 with an interesting value (le/be).
* Replace an ascii digit/number with another digit/number.
* Splice another input.
* Insert a part of another input.
* Insert a string/int literal.
* Replace with string/int literal.

三、使用方法

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-fuzz并未使用go 1.5引入的vendor机制, 而其依赖的一些包却在墙外,因此可能会遇到些麻烦。

go get自动安装两个工具到$GOROOT/bin或$GOPATH/bin,因此你需要确保你的Path环境变量下包含了这两个路径。

2、带有fuzz test的项目组织

假设我们的待测试的go包名为foo,路径为$GOPATH/src/github.com/bigwhite/fuzzexamples/foo。为了应用go- fuzz,我们一般会在foo下创建fuzz.go源文件,其内容模板如下:

// +build gofuzz

package foo

func Fuzz(data []byte) int {
    ... ...
}

go-fuzz在构建用于执行fuzz test的驱动binary文件时,会搜索带有”+build gofuzz” directive的源文件以及其中的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函数不够,我们可以参考go-fuzz中example中的目录组织形式来应对:

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
│   ├── 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),包里包含了用途不同的三 个文件:

-rw-r--r--   1 tony  staff  3902136 12 31  1979 cover.exe
-rw-r--r--   1 tony  staff  3211816 12 31  1979 metadata
-rw-r--r--   1 tony  staff  5031496 12 31  1979 sonar.exe

按照作者slide中的说法,各个二进制程序的功能如下:
cover.exe – coverage instrumented binary
sonar.exe – sonar instrumented binary
metadata – coverage and sonar metadata, int and string literals

不过对于使用者来说,我们不必过于关心它们,点到为止。

4、执行go-fuzz

一旦生成了foo-fuzz.zip,我们就可以执行针对fuzz1的fuzz test。

$ cd fuzz1
$ go-fuzz -bin=./foo-fuzz.zip -workdir=./
2015/12/08 17:51:48 slaves: 4, corpus: 8 (1s ago), crashers: 0, restarts: 1/0, execs: 0 (0/sec), cover: 0, uptime: 3s
2015/12/08 17:51:51 slaves: 4, corpus: 9 (2s ago), crashers: 0, restarts: 1/3851, execs: 11553 (1924/sec), cover: 143, uptime: 6s
2015/12/08 17:51:54 slaves: 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中,以供下次restart使用。

前面说过,go-fuzz是一个infinite loop,上面的测试需要手工停下来。go-fuzz会在workdir中创建另外两个目录:crashers和suppressions。顾名思义,crashers中存放的是代码crash时的相关数据,包括引起crash的case的输入二进制数据、输入的数据的字符串形式(xxx.quoted)以及基于这个数据的输出数据(xxx.output)。suppressions中保存着crash时的stack trace信息。

四、一个简单示例

gocmpp是一个cmpp协议库的go实现,这里打算用其中的unpack做一个最简单的fuzz test demo。

gocmpp中的每种协议包都实现了Packer接口,其中的Unpack尤其适合fuzz test。由于协议包众多,我们在gocmpp下专门建立fuzztest目录,用于存放fuzz test的代码,将各个协议包的fuzz test分到各个子目录中:

github.com/bigwhite/gocmpp/fuzztest]$tree
.
├── fwd
│   ├── corpus
│   │   └── 0
│   ├── fuzz.go
│   └── gen
│       └── main.go
└── submit
       ├── corpus
       │   ├── 0
       ├── fuzz.go
       └── gen
           └── main.go

先说说每个fuzz test单元(比如fwd或submit)下的gen/main.go,这是一个用于生成初始语料的可执行程序,我们以submit/gen/main.go为例:

package main

import (
    "github.com/dvyukov/go-fuzz/gen"
)

func main() {
    data := []byte{
        0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x74, 0x65, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x02, 0x31, 0x33, 0x35, 0x30, 0x30, 0x30, 0x30, 0x32, 0x36, 0x39, 0x36, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x39, 0x30, 0x30, 0x30, 0x30,
        0x31, 0x30, 0x32, 0x31, 0x30, 0x00, 0x00, 0x00, 0x00, 0x31, 0x35, 0x31, 0x31, 0x30, 0x35, 0x31,
        0x33, 0x31, 0x35, 0x35, 0x35, 0x31, 0x30, 0x31, 0x2b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x39, 0x30, 0x30, 0x30, 0x30,
        0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x01, 0x31, 0x33, 0x35, 0x30, 0x30, 0x30, 0x30, 0x32, 0x36, 0x39, 0x36, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x6d, 0x4b, 0x8b, 0xd5, 0x00, 0x67, 0x00, 0x6f, 0x00,
        0x63, 0x00, 0x6d, 0x00, 0x70, 0x00, 0x70, 0x00, 0x20, 0x00, 0x73, 0x00, 0x75, 0x00, 0x62, 0x00,
        0x6d, 0x00, 0x69, 0x00, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    }

    gen.Emit(data, nil, true)
}

在这个main.go中,我们借用submit包的单元测试中的数据作为fuzz test的初始语料数据,通过go-fuzz提供的gen包将数据输出到文件中:

$cd submit/gen
$go run main.go -out ../corpus/
$ll ../corpus/
total 8
drwxr-xr-x  3 tony  staff  102 12  7 22:00 ./
drwxr-xr-x  5 tony  staff  170 12  7 21:42 ../
-rw-r--r--  1 tony  staff  181 12  7 22:00 0

该程序在corpus下生成了一个文件“0”,作为submit fuzz test的初始语料。

接下来我们看看submit/fuzz.go:

// +build gofuzz

package cmppfuzz

import (
    "github.com/bigwhite/gocmpp"
)

func Fuzz(data []byte) int {
    p := &cmpp.Cmpp2SubmitReqPkt{}
    if err := p.Unpack(data); err != nil {
        return 0
    }
    return 1
}

这是一个“最简单”的Fuzz函数实现了,根据作者对Fuzz的规约,Fuzz的返回值是有重要含义的:

如果此次输入的数据在某种程度上是很有意义的,go-fuzz会给予这类输入更多的优先级,Fuzz应该返回1;
如果明确这些输入绝对不能放入corpus,那让Fuzz返回-1;
至于其他情况,返回0。

接下来就是go-fuzz-build和go-fuzz登场了,这与前面的介绍差不多:

$cd submit
$go-fuzz-build github.com/bigwhite/gocmpp/fuzztest/submit
$ls
cmppfuzz-fuzz.zip    corpus/            fuzz.go            gen/

在submit目录下执行go-fuzz:

$go-fuzz -bin=./cmppfuzz-fuzz.zip -workdir=./
2015/12/07 22:05:02 slaves: 4, corpus: 1 (3s ago), crashers: 0, restarts: 1/0, execs: 0 (0/sec), cover: 0, uptime: 3s
2015/12/07 22:05:05 slaves: 4, corpus: 3 (0s ago), crashers: 0, restarts: 1/0, execs: 0 (0/sec), cover: 32, uptime: 6s
2015/12/07 22:05:08 slaves: 4, corpus: 7 (1s ago), crashers: 0, restarts: 1/5424, execs: 65098 (7231/sec), cover: 131, uptime: 9s
2015/12/07 22:05:11 slaves: 4, corpus: 9 (0s ago), crashers: 0, restarts: 1/5424, execs: 65098 (5424/sec), cover: 146, uptime: 12s
... ...
2015/12/07 22:09:11 slaves: 4, corpus: 9 (4m0s ago), crashers: 0, restarts: 1/9860, execs: 4033002 (16002/sec), cover: 146, uptime: 4m12s
^C2015/12/07 22:09:13 shutting down...

这个测试非常耗cpu啊!一小会儿功夫,我的Mac Air的风扇就开始呼呼转起来了。不过我的Unpack函数并未在fuzz test中发现问题,crashers后面的数值一直是0。

go-fuzz目前似乎还不支持vendor机制,因此如果你的包像gocmpp一样使用了vendor,那需要在go-fuzz-build和go-fuzz前面加上一个GO15VENDOREXPERIMENT=”0″(如果你之前开启了GO15VENDOREXPERIMENT),就像这样:

$ GO15VENDOREXPERIMENT="0" go-fuzz-build github.com/bigwhite/gocmpp/fuzztest/submit

如果不关闭vendor,你可能会得到类似如下的错误

can't find imported package golang.org/x/text/transform

Golang技术幻灯片的查看方法

随着go 1.5的发布,golang在世界各地日益受到欢迎,golang技术鼓吹者在世界各地举办各种级别的技术会议(从GopherCon大会到小小的meetup),并在会议上分享自己的技术心得和技术想法。

Golang相关的技术幻灯片有多种格式,以.ppt, .pdf和.slide为主。ppt、pdf自然不必多说,需要直接下载查看。

.slide是随着golang诞生而出现的一种present格式,Go核心开发成员似乎十分喜欢以这种格式分享Go语言。在Golang官方,几乎所有技术会议的talk幻灯片均是以.slide形式提供的。

.slide文件通过web服务查看,目前似乎尚无本地工具可以render slide文件。

目前已知的render .slide文件的服务包括:
- talks.golang.org
- go-talks.appspot.com

talks.golang.org是golang官方的服务,用于查看Go core team发表的各次技术演讲的幻灯片资料,按年份归档。

其他Go开发者用.slide形式编写的文件可以放在自己的github.com repo中,并使用go-talks.appspot.com这个第三方服务render。

比如:Dave Cheney将自己的performance-without-the-event-loop.slide存放在 github.com/davecheney/presentations下,那我们就可以通过如下url查看该slide render后的形式:

http://go-talks.appspot.com/github.com/davecheney/presentations/performance-without-the-event-loop.slide

不过由于appspot.com是Go appengine托管服务,国内无法访问,因此前期搭建了一个go-talks的镜像go-talks.tonybai.com,国内程序员可以无需fanqiang就可以访问(由于go-talks.tonybai.com托管主机内存不大,常常出现超时甚至crash现象,望谅解)。

因此要想看到上述slide,可以访问:

http://go-talks.tonybai.com/github.com/davecheney/presentations/performance-without-the-event-loop.slide

对于talks.golang.org上的slide,比如:

http://talks.golang.org/2015/gogo.slide

如果无法fanqiang又如何访问呢?这样行么?

http://go-talks.tonybai.com/talks.golang.org/2015/gogo.slide

结果告诉我们这样是不行的。那如何访问呢?

好在talks.golang.org上的slide都放在了github.com上,repo为https://github.com/golang/talks,上述那个gogo.slide,我们可以通过:

http://go-talks.tonybai.com/github.com/github.com/golang/talks/2015/gogo.slide访问。

补充

“相濡以沫”网友在评论中给出了一种在本地查看.slide的方法:

1、go get -u golang.org/x/tools/cmd/present //需翻墙
2、go install golang.org/x/tools/cmd/present,将present可执行程序放入$GOBIN或$GOPATH/bin中
3、下载你要查看的.slide,比如go get github.com/golang/talks,cd到talks所在目录,执行./present,你会看到如下结果:

$present
2015/08/23 19:34:51 Open your web browser and visit http://127.0.0.1:3999

打开浏览器,如果要查看当前目录下的2015/tricks.slide,则在浏览器里输入:http://127.0.0.1:3999/2015/tricks.slide即可查看该.slide文件。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



Statcounter View My Stats