从 Go 1.18 开始,Go 在其标准工具链中支持模糊测试。Native Go 模糊测试受 OSS-Fuzz 支持。
Go模糊测试详细教程见:进入Go模糊测试的世界 一文。
1. 概述
Fuzzing 是一种自动化测试,它不断地操纵程序的输入以查找错误。Go fuzzing 使用覆盖率指导来智能地不断重复执行模糊测试的代码,以发现并向用户报告问题。由于它可以覆盖人类经常错过的边缘情况,因此模糊测试对于发现安全漏洞特别有价值。
下面是一个 模糊测试 的例子,突出了它的主要组成部分。
上图显示整体模糊测试的示例代码,其中包含一个模糊目标( fuzz target )。 在模糊目标之前调用 f.Add
添加种子语料库,模糊目标的参数高亮显示为fuzzing参数。
2. 编写模糊测试
2.1. 要求
以下是模糊测试必须遵循的规则。
模糊测试必须是一个形如
FuzzXxx
的函数,以Fuzz
作为前缀,它只接受一个*testing.F
参数并且没有返回值。模糊测试必须在 *_test.go 文件中才能运行。
调用
(testing.F).Fuzz
方法时的匿名函数参数称之为 模糊目标,形如func(t *testing.T, xxx)
, 它必须是一个函数,接受一个*testing.T
作为第一个参数,其他后续参数称为模糊参数,且该函数没有返回值。每个模糊测试必须只有一个模糊目标。
所有 种子语料库 条目必须具有与 模糊参数 相同的类型,并且顺序相同。这适用于调用
(*testing.F).Add
添加的种子语料库和 testdata/fuzz 目录中已有的语料库文件。模糊测试参数只能是以下类型:
string
,[]byte
int
,int8
,int16
,int32
/rune
,int64
uint
,uint8
/byte
,uint16
,uint32
,uint64
float32
,float64
bool
3. 运行模糊测试
有两种运行模糊测试的模式:作为单元测试(go test
命令)或使用模糊测试(go test -fuzz=FuzzTestName
)。
默认情况下,模糊测试的运行与单元测试非常相似。每个 种子语料库条目 都将针对模糊目标进行测试,在退出之前报告任何失败。
要启用模糊测试,go test
请使用 -fuzz
标志运行,提供匹配单个模糊测试的正则表达式。默认情况下,该包中的所有其他测试将在模糊测试开始之前运行,这是为了确保模糊测试不会报告现有测试已经发现的任何问题。
请注意,模糊测试的运行时长由您决定。如果没有发现任何错误,模糊测试的执行很可能会无限期地运行。未来将支持使用 OSS-Fuzz 等工具连续运行这些模糊测试,请参阅 问题 #50192。
注意 模糊测试应该在支持覆盖检测的平台(目前是 AMD64 和 ARM64)上运行,这样语料库可以在运行时有意义地填充以便覆盖更多测试代码。 |
3.1. 命令行输出
输出将如下所示:
~ 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”对应的数字随着时间的推移逐渐减少。
3.2. 失败的输入
由于以下几个原因,在进行模糊测试时可能会发生故障:
代码或测试中发生了异常(panic)。
模糊目标(在测试失败时)调用了`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
命令将自动默认使用该语料库,当错误修复后它将用于回归测试。
您的下一步将是调试问题,修复错误,通过重新运行 go test
验证问题是否修复,并使用新的 testdata 文件提交补丁作为回归测试。
3.3. 自定义设置
默认的 go 命令设置应该适用于大多数模糊测试用例。所以通常,在命令行上执行模糊测试应该是这样的:
$ go test -fuzz={FuzzTestName}
但是,该 go 命令在运行模糊测试时确实提供了一些设置。这些都记录在 cmd/go
包 docs中。
一些重要的参数:
-fuzztime
: fuzz 目标在退出前将执行的总时间或迭代次数,默认不指定则为无限期。-fuzzminimizetime
:在每次最小化尝试期间执行模糊目标的时间或迭代次数,默认为 60 秒。您可以通过-fuzzminimizetime 0
设置模糊测试时完全禁用最小化。-parallel
: 一次运行的模糊测试进程的数量,默认值为$GOMAXPROCS
。目前,在 fuzzing 期间设置-cpu
无效。
4. 语料库文件格式
下面是一个语料库文件的例子:
go test fuzz v1 []byte("hello\\xbd\\xb2=\\xbc ⌘") int64(572293)
第一行用于通知模糊引擎文件的编码版本。虽然目前没有计划未来版本的编码格式,但设计必须支持这种可能性。
下面的每一行都是构成语料库条目的值,如果需要,可以直接复制到 Go 代码中使用。
在上面的示例中,我们有一个 []byte
切片条目,还有一个 int64
条目。这些条目类型必须与模糊测试参数类型顺序完全匹配。这些类型的模糊目标如下所示:
f.Fuzz(func(*testing.T, []byte, int64) {})
指定您自己的种子语料库值的最简单方法是使用 (*testing.F).Add
方法。在上面的示例中,它看起来像这样:
// 添加种子语料库 f.Add([]byte("hello\\xbd\\xb2=\\xbc ⌘"), int64(572293))
但是,有时候您可能需要较大的二进制文件作为种子语料库条目,您肯定不希望将其复制到测试源代码中,你可以将其作为单独的种子语料库条目保留在 testdata/fuzz/{FuzzTestName} 目录中。 file2fuzz 工具可用于将这些二进制文件转换为为 []byte
。
要使用此工具:
$ go install golang.org/x/tools/cmd/file2fuzz@latest $ file2fuzz
5. 资源
6. 词汇表
语料库条目(corpus entry): 语料库中的一个输入,可以在模糊测试时使用。这可以是特殊格式的文件(位于 testdata/fuzz 目录下),也可以通过调用 (*testing.F).Add 方法添加。
覆盖指导(coverage guidance): 一种模糊测试方法,它通过扩展代码覆盖范围来确定哪些语料库条目值得保留以备将来使用。
失败的输入(failing input): 失败的输入是一个语料库条目,当针对 模糊目标 运行时会导致错误或恐慌。
模糊目标(fuzz target): 一个函数,针对提供的语料库条目和生成的值来执行模糊测试。模糊目标是一个函数,通过入参传递给 (*testing.F).Fuzz 函数来提供给模糊测试。
模糊测试(fuzz test): 测试文件中的一个测试函数,形如
func FuzzXxx(*testing.F)
,用于执行模糊测试。模糊化(fuzzing): 一种自动化测试,它不断地操纵程序的输入,以发现代码可能存在的错误或 漏洞 等问题。
模糊参数(fuzzing arguments): 将传递给模糊目标的类型,由mutator动态生成。
模糊引擎(fuzzing engine): 一个管理 fuzzing 的工具,包括维护语料库、调用mutator、识别新的覆盖率和失败时报告。
生成的语料库: 由模糊引擎在模糊测试时随时间推移而维护的语料库,同时用于跟踪模糊测试的执行进度。它存储在
$GOCACHE/fuzz
中。这些条目仅在模糊测试时使用。package: 同一目录下编译在一起的源文件的集合。请参阅 Go 语言规范中的 包 部分。
种子语料库: 用户提供的用于模糊测试的语料库,可用于指导模糊引擎。种子语料库由两部分组成,可以通过调用
f.Add
方法来为其提供语料库条目,此外,包内的testdata/fuzz/{FuzzTestName}
目录中的文件组成(这些条目会在`go test`命令执行时默认使用,无论是否进行模糊测试参数`-fuzz`)。测试文件: 格式为
xxx_test.go
的文件,可能包含单元测试、基准测试、示例代码和模糊测试。