概述
Fuzzing 是一种自动化测试,它不断地操纵程序的输入以查找错误。Go fuzzing 使用覆盖率指导来智能地遍历被模糊测试的代码,以发现并向用户报告故障。由于它可以达到人类经常错过的边缘情况,因此模糊测试对于发现安全漏洞和漏洞特别有价值。
下面是一个模糊测试的例子,突出了它的主要组成部分。
编写模糊测试
要求
以下是模糊测试必须遵循的规则。
- 模糊测试必须是一个名为的函数FuzzXxx,它只接受 a *testing.F并且没有返回值。
- 模糊测试必须在 *_test.go 文件中才能运行。
- 模糊目标必须是一个方法调用, 它(*testing.F).Fuzz接受 a*testing.T作为第一个参数,然后是模糊参数。没有返回值。
- 每个模糊测试必须恰好有一个模糊目标。
- 所有种子语料库条目必须具有与模糊测试参数相同的类型,并且顺序相同。这适用于 (*testing.F).Add对模糊测试的 testdata/fuzz 目录中的任何语料库文件的调用。
- 模糊测试参数只能是以下类型:
(1)string,[]byte;(2)int, int8, int16, int32/rune,int64;(3)uint, uint8/ byte, uint16, uint32,uint64;(4)float32,float64;(5)bool
建议
以下建议将帮助您充分利用模糊测试。
- 模糊目标应该是快速和确定的,这样模糊引擎才能有效地工作,新的故障和代码覆盖率可以很容易地重现。
- 由于模糊目标是在多个工作人员之间以不确定的顺序并行调用的,因此模糊目标的状态不应持续到每次调用结束后,并且模糊目标的行为不应依赖于全局状态。
运行模糊测试
有两种运行模糊测试的模式:作为单元测试(默认go test)或使用模糊测试(go test -fuzz=FuzzTestName)。
默认情况下,模糊测试的运行与单元测试非常相似。每个种子语料库条目都将针对模糊目标进行测试,在退出之前报告任何失败。
要启用模糊测试,go test请使用-fuzz标志运行,提供匹配单个模糊测试的正则表达式。默认情况下,该包中的所有其他测试将在模糊测试开始之前运行。这是为了确保模糊测试不会报告现有测试已经发现的任何问题。
请注意,由您决定运行模糊测试多长时间。如果没有发现任何错误,fuzzing 的执行很可能会无限期地运行。
注意:模糊测试应该在支持覆盖检测的平台(目前是 AMD64 和 ARM64)上运行,这样语料库可以在运行时有意义地增长,并且可以在模糊测试时覆盖更多代码。
命令行输出
在进行模糊测试时,模糊测试引擎会 生成新的输入并针对提供的模糊目标运行它们。默认情况下,它会继续运行,直到找到失败的输入,或者用户取消进程(例如使用 Ctrl^C)。
输出将如下所示:
~ go test -fuzz FuzzFoo
fuzz: elapsed: 0s, gathering baseline coverage: 0/192 completed
fuzz: elapsed: 0s, gathering baseline coverage: 192/192 completed, now fuzzing with 8 workers
fuzz: elapsed: 3s, execs: 325017 (108336/sec), new interesting: 11 (total: 202)
fuzz: elapsed: 6s, execs: 680218 (118402/sec), new interesting: 12 (total: 203)
fuzz: elapsed: 9s, execs: 1039901 (119895/sec), new interesting: 19 (total: 210)
fuzz: elapsed: 12s, execs: 1386684 (115594/sec), new interesting: 21 (total: 212)
PASS
ok foo 12.692s
第一行表示在开始模糊测试之前收集了“基线覆盖率”。
为了收集基线覆盖率,模糊引擎同时执行种子语料库和生成的语料库,以确保没有发生错误并了解现有语料库已经提供的代码覆盖率。
以下几行提供了对主动模糊测试执行的深入了解:
- elapsed:自进程开始以来经过的时间量
- execs:针对模糊目标运行的输入总数(自最后一个日志行以来的平均 execs/sec)
- new interesting:在这个模糊执行期间添加到生成的语料库中的需要关注的输入的总数(与整个语料库的总大小)
为了使输入有用,它必须将代码覆盖范围扩大到现有生成的语料库可以达到的范围之外。新的需要关注的输入的数量在开始时快速增长并最终放缓是典型的,随着新分支的发现偶尔会爆发。
随着语料库中的输入开始覆盖更多代码行,如果模糊引擎找到新的代码路径,您应该会看到new interesting数字随着时间的推移逐渐减少。
输入失败
由于以下几个原因,在进行模糊测试时可能会发生故障:
- 代码或测试中发生了恐慌。
- t.Fail直接或通过诸如 t.Error或等方法调用的模糊目标t.Fatal。
- 发生了不可恢复的错误,例如os.Exit堆栈溢出。
- 模糊目标完成时间太长。目前,执行模糊目标的超时时间为 1 秒。这可能由于死锁或无限循环或代码中的预期行为而失败。这就是为什么建议您的 fuzz 目标要快的原因之一。
如果发生错误,fuzzing 引擎将尝试将输入最小化到最小的可能和最人类可读的值,这仍然会产生错误。
最小化完成后,将记录错误消息,输出将以如下内容结束:
Failing input written to testdata/fuzz/FuzzFoo/a878c3134fe0404d44eb1e662e5d8d4a24beb05c3d68354903670ff65513ff49
To re-run:
go test -run=FuzzFoo/a878c3134fe0404d44eb1e662e5d8d4a24beb05c3d68354903670ff65513ff49
FAIL
exit status 1
FAIL foo 0.839s
模糊引擎将此失败的输入写入该模糊测试的种子语料库,现在它将默认运行。一旦错误得到修复,它将作为回归测试。
您的下一步将是诊断问题,修复错误,通过重新运行go test验证修复,并使用新的 testdata 文件提交补丁作为回归测试。
自定义设置
默认的 go 命令设置应该适用于大多数模糊测试用例。所以通常,在命令行上执行模糊测试应该是这样的:
$ go test -fuzz={FuzzTestName}
但是,该go命令在运行模糊测试时确实提供了一些设置。这些都记录在cmd/go包 docs中。
强调一些标志:
- -fuzztime: fuzz 目标在退出前将执行的总时间或迭代次数,默认为无限期。
- -fuzzminimizetime:在每次最小化尝试期间执行模糊目标的时间或迭代次数,默认为 60 秒。您可以通过设置-fuzzminimizetime 0从而在模糊测试时完全禁用最小化。
- -parallel: 一次运行的模糊测试进程的数量,默认值 $GOMAXPROCS。