Go Fuzzing
Go 从 Go 1.18 开始在其标准工具链中支持 fuzzing。原生 Go fuzz 测试受到 OSS-Fuzz 支持。
概述
Fuzzing 是一种自动测试,它持续操纵程序输入以查找错误。Go fuzzing 使用覆盖率引导来智能地遍历被 fuzz 的代码,以查找并向用户报告故障。由于它可以触及人类通常会遗漏的边缘情况,因此 fuzz 测试对于发现安全漏洞和漏洞特别有价值。
以下是一个 fuzz 测试 的示例,突出了它的主要组成部分。
编写 fuzz 测试
要求
以下是 fuzz 测试必须遵循的规则。
- fuzz 测试必须是一个名为
FuzzXxx
的函数,它只接受一个*testing.F
,并且没有返回值。 - fuzz 测试必须位于 *_test.go 文件中才能运行。
- 一个 fuzz 目标 必须是对
(*testing.F).Fuzz
的方法调用,该方法将*testing.T
作为第一个参数,后面是 fuzzing 参数。没有返回值。 - 每个 fuzz 测试必须恰好有一个 fuzz 目标。
- 所有 种子语料库 条目必须具有与 fuzzing 参数 相同的类型,并且顺序相同。这适用于对
(*testing.F).Add
的调用以及 fuzz 测试的 testdata/fuzz 目录中的任何语料库文件。 - fuzzing 参数只能是以下类型
string
、[]byte
int
、int8
、int16
、int32
/rune
、int64
uint
、uint8
/byte
、uint16
、uint32
、uint64
float32
、float64
bool
建议
以下建议将帮助您充分利用 fuzzing。
- fuzz 目标应该快速且确定性,以便 fuzzing 引擎能够高效地工作,并且可以轻松地重现新的故障和代码覆盖率。
- 由于 fuzz 目标在多个工作程序之间并行调用,并且顺序不确定,因此 fuzz 目标的状态不应该在每次调用结束时保留,并且 fuzz 目标的行为不应该依赖于全局状态。
运行 fuzz 测试
运行 fuzz 测试有两种模式:作为单元测试(默认 go test
),或使用 fuzzing(go test -fuzz=FuzzTestName
)。
默认情况下,fuzz 测试就像单元测试一样运行。每个 种子语料库条目 都会针对 fuzz 目标进行测试,在退出之前报告任何错误。
要启用 fuzzing,请使用 -fuzz
标志运行 go test
,提供与单个 fuzz 测试匹配的正则表达式。默认情况下,该包中的所有其他测试将在 fuzzing 开始之前运行。这是为了确保 fuzzing 不会报告现有测试已经捕获的任何问题。
请注意,您需要决定运行 fuzzing 的时间。如果 fuzzing 没有找到任何错误,它可能会无限期地运行。将来会支持使用 OSS-Fuzz 等工具连续运行这些 fuzz 测试,请参见 问题 #50192。
注意:fuzzing 应该在支持覆盖率检测的平台上运行(目前是 AMD64 和 ARM64),以便语料库在运行时可以有意义地增长,并且可以在 fuzzing 时覆盖更多代码。
命令行输出
在 fuzzing 过程中,fuzzing 引擎 生成新的输入并将它们运行到提供的 fuzz 目标。默认情况下,它会持续运行,直到找到 错误输入 或用户取消该进程(例如,使用 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
前几行表明在 fuzzing 开始之前收集了“基线覆盖率”。
为了收集基线覆盖率,fuzzing 引擎会执行 种子语料库 和 生成的语料库,以确保没有发生错误,并了解现有语料库已经提供的代码覆盖率。
接下来的几行提供了对正在进行的 fuzzing 执行的洞察
- elapsed:自进程开始以来经过的时间
- execs:已针对 fuzz 目标运行的输入总数(自上次日志行以来,平均 execs/sec)
- new interesting:在此 fuzzing 执行期间添加到生成的语料库的“有趣”输入的总数(以及整个语料库的总大小)
要成为“有趣”的输入,它必须扩展现有生成的语料库无法触及的代码覆盖率。通常情况下,新的有趣输入的数量在开始时会迅速增长,并最终逐渐减慢,偶尔会随着发现新的分支而突然增加。
您应该预计随着语料库中的输入开始覆盖更多代码行,“new interesting” 的数量会随着时间的推移而逐渐减少,如果 fuzzing 引擎发现了新的代码路径,偶尔也会突然增加。
错误输入
fuzzing 过程中可能会出现错误,原因如下
- 代码中发生了恐慌或测试发生了恐慌。
- fuzz 目标调用了
t.Fail
,无论是直接调用还是通过t.Error
或t.Fatal
等方法。 - 发生了不可恢复的错误,例如
os.Exit
或堆栈溢出。 - fuzz 目标花费了太长时间才能完成。目前,fuzz 目标执行的超时时间为 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
fuzzing 引擎将此 错误输入 写入该 fuzz 测试的种子语料库中,现在它将默认情况下使用 go test
运行,作为回归测试,一旦错误被修复。
下一步,您需要诊断问题,修复错误,通过重新运行 go test
验证修复,并提交包含新 testdata 文件的补丁作为回归测试。
自定义设置
默认的 go 命令设置应该适用于大多数 fuzzing 用例。因此,通常情况下,在命令行上执行 fuzzing 应该看起来像这样
$ go test -fuzz={FuzzTestName}
但是,go
命令在运行 fuzzing 时提供了一些设置。这些在 cmd/go
包文档 中有记录。
重点介绍几个
-fuzztime
:fuzz 目标在退出之前执行的总时间或迭代次数,默认值为无限期。-fuzzminimizetime
:每次最小化尝试中 fuzz 目标执行的时间或迭代次数,默认值为 60 秒。您可以在 fuzzing 时通过设置-fuzzminimizetime 0
来完全禁用最小化。-parallel
:同时运行的 fuzzing 进程数量,默认值为$GOMAXPROCS
。目前,在 fuzzing 期间设置 -cpu 没有任何效果。
语料库文件格式
语料库文件以特殊格式编码。这对于 种子语料库 和 生成的语料库 来说是相同的格式。
以下是一个语料库文件的示例
go test fuzz v1
[]byte("hello\\xbd\\xb2=\\xbc ⌘")
int64(572293)
第一行用于告知 fuzzing 引擎文件的编码版本。虽然目前没有计划未来版本的编码格式,但设计必须支持这种可能性。
以下的每一行都是构成语料库条目的值,如果需要,可以将其直接复制到 Go 代码中。
在上面的示例中,我们有一个 []byte
后面跟着一个 int64
。这些类型必须与 fuzzing 参数完全匹配,并且顺序相同。针对这些类型的 fuzz 目标将如下所示
f.Fuzz(func(*testing.T, []byte, int64) {})
指定您自己的种子语料库值的 easiest 方法是使用 (*testing.F).Add
方法。在上面的示例中,这将如下所示
f.Add([]byte("hello\\xbd\\xb2=\\xbc ⌘"), int64(572293))
但是,你可能拥有大型二进制文件,你可能更倾向于不将它们作为代码复制到你的测试中,而是将它们保留为测试数据/fuzz/{FuzzTestName} 目录中的单个种子语料库条目。 file2fuzz
工具位于 golang.org/x/tools/cmd/file2fuzz,可用于将这些二进制文件转换为针对 []byte
编码的语料库文件。
要使用此工具
$ go install golang.org/x/tools/cmd/file2fuzz@latest
$ file2fuzz -h
资源
- 教程:
- 尝试一下 使用 Go 进行模糊测试的教程,深入了解新概念。
- 对于更短的 Go 模糊测试入门教程,请参阅 博客文章。
- 文档:
- 技术细节:
术语表
语料库条目: 语料库中的一个输入,可在模糊测试时使用。 这可以是格式特殊的 文件,或者是对 (*testing.F).Add
的调用。
覆盖率引导: 一种模糊测试方法,它使用代码覆盖率的扩展来确定哪些语料库条目值得保留以供将来使用。
失败输入: 失败输入是当针对 模糊测试目标 运行时会导致错误或恐慌的语料库条目。
模糊测试目标: 模糊测试的函数,该函数在模糊测试期间针对语料库条目和生成的 值执行。 它通过将函数传递给 (*testing.F).Fuzz
提供给模糊测试。
模糊测试: 测试文件中形式为 func FuzzXxx(*testing.F)
的函数,可用于模糊测试。
模糊测试: 一种自动测试类型,它持续地对程序输入进行操作,以查找诸如错误或 漏洞 等问题,代码可能容易受到这些问题的攻击。
模糊测试参数: 将传递给模糊测试目标的类型,以及由 变异器 进行修改的类型。
模糊测试引擎: 一种管理模糊测试的工具,包括维护语料库、调用变异器、识别新的覆盖率以及报告失败。
生成语料库: 模糊测试引擎在模糊测试期间随着时间的推移维护的语料库,用于跟踪进度。 它存储在 $GOCACHE
/fuzz 中。 这些条目仅在模糊测试期间使用。
变异器: 模糊测试期间使用的工具,在将语料库条目传递给模糊测试目标之前对其进行随机操作。
包: 同一目录中的一组源文件,它们一起编译。 请参阅 Go 语言规范中的 包部分。
种子语料库: 模糊测试的用户提供的语料库,可用于指导模糊测试引擎。 它由模糊测试中 f.Add 调用提供的语料库条目和包中测试数据/fuzz/{FuzzTestName} 目录中的文件组成。 这些条目默认情况下通过 go test
运行,无论是否进行模糊测试。
测试文件: 格式为 xxx_test.go 的文件,其中可能包含测试、基准测试、示例和模糊测试。
反馈
如果您遇到任何问题或有功能想法,请 提交问题。
有关功能的讨论和一般反馈,您也可以参加 Gophers Slack 中的 #fuzzing 频道。