Go Fuzzing

Go 从 1.18 版本开始在其标准工具链中支持模糊测试。原生 Go 模糊测试 受 OSS-Fuzz 支持

尝试一下 Go 模糊测试教程

概览

模糊测试是一种自动化测试,它不断地操纵程序的输入以查找 bug。Go 模糊测试使用覆盖率引导智能地遍历被模糊测试的代码,从而向用户发现并报告故障。由于它可以触及人类通常会忽略的边缘情况,因此模糊测试对于发现安全漏洞和弱点尤其有价值。

下面是一个 模糊测试 的示例,重点介绍了其主要组成部分。

Example code showing the overall fuzz test, with a fuzz target within
it. Before the fuzz target is a corpus addition with f.Add, and the parameters
of the fuzz target are highlighted as the fuzzing arguments. Example code showing the overall fuzz test, with a fuzz target within
it. Before the fuzz target is a corpus addition with f.Add, and the parameters
of the fuzz target are highlighted as the fuzzing arguments.

编写模糊测试

要求

以下是模糊测试必须遵守的规则。

  • 模糊测试必须是一个命名为 FuzzXxx 的函数,它只接受一个 *testing.F 参数,并且没有返回值。
  • 模糊测试必须位于 *_test.go 文件中才能运行。
  • 一个 模糊目标 必须是对 (*testing.F).Fuzz 的方法调用,它接受一个 *testing.T 作为第一个参数,后跟模糊参数。没有返回值。
  • 每个模糊测试必须有且只有一个模糊目标。
  • 所有 种子语料库 条目必须具有与 模糊参数 相同的类型,并按相同顺序排列。这对于调用 (*testing.F).Add 和模糊测试的 testdata/fuzz 目录中的任何语料库文件都适用。
  • 模糊参数只能是以下类型
    • string, []byte
    • int, int8, int16, int32/rune, int64
    • uint, uint8/byte, uint16, uint32, uint64
    • float32, float64
    • bool

建议

以下是一些建议,可以帮助您充分利用模糊测试。

  • 模糊目标应快速且确定性,以便模糊引擎能够高效工作,并轻松重现新的故障和代码覆盖率。
  • 由于模糊目标在多个工作进程中以非确定性顺序并行调用,因此模糊目标的状1态不应在每次调用结束后持续存在,并且模糊目标的行为不应依赖于全局状态。

运行模糊测试

运行模糊测试有两种模式:作为单元测试(默认 go test),或使用模糊测试(go test -fuzz=FuzzTestName)。

默认情况下,模糊测试的运行方式与单元测试类似。每个 种子语料库条目 都将针对模糊目标进行测试,并在退出前报告任何故障。

要启用模糊测试,请运行 go test 并使用 -fuzz 标志,提供一个匹配单个模糊测试的正则表达式。默认情况下,该包中的所有其他测试将在模糊测试开始之前运行。这是为了确保模糊测试不会报告任何已被现有测试捕获的问题。

请注意,您需要自行决定模糊测试的运行时间。如果模糊测试未发现任何错误,其执行可能会无限期地运行。将来将支持使用 OSS-Fuzz 等工具持续运行这些模糊测试,请参阅 Issue #50192

注意: 应该在支持覆盖率插桩的平台(目前是 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:已针对模糊目标运行的输入总数(并显示自上次日志行以来的平均每秒执行数)
  • new interesting:在此次模糊执行期间添加到生成语料库中的“有趣”输入的总数(以及整个语料库的总大小)

为了使输入被认为是“有趣”,它必须扩展现有生成语料库无法达到的代码覆盖范围。通常,“新有趣”输入的数量在开始时会快速增长,然后逐渐放缓,并可能随着新分支的发现而出现偶尔的爆发。

随着语料库中的输入开始覆盖更多代码行,您应该会看到“新有趣”的数量随时间推移而逐渐减少,如果模糊引擎找到新的代码路径,则会出现偶尔的爆发。

失败的输入

模糊测试期间可能由于多种原因发生故障

  • 代码或测试中发生了 panic。
  • 模糊目标调用了 t.Fail,无论是直接调用还是通过 t.Errort.Fatal 等方法。
  • 发生了不可恢复的错误,例如 os.Exit 或堆栈溢出。
  • 模糊目标花费了太长时间才能完成。目前,模糊目标的执行超时时间为 1 秒。这可能是由于死锁或无限循环,或者由于代码中的预期行为。这也是 建议使模糊目标快速 的原因之一。

如果发生错误,模糊引擎将尝试将输入最小化为最小化且最易读的、仍能产生错误的值。要配置此设置,请参阅 自定义设置 部分。

最小化完成后,将记录错误消息,输出将以类似如下的内容结束

    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 运行,并在 bug 修复后作为回归测试。

下一步您将需要诊断问题,修复 bug,通过重新运行 go test 验证修复,并提交补丁,同时将新的 testdata 文件作为回归测试。

自定义设置

默认的 go 命令设置应适用于大多数模糊测试用例。因此,通常在命令行上执行模糊测试应该如下所示

$ go test -fuzz={FuzzTestName}

然而,go 命令在运行模糊测试时提供了一些设置。这些设置在 cmd/go 包文档 中有说明。

举例说明

  • -fuzztime:模糊目标在退出前执行的总时间或迭代次数,默认不限制。
  • -fuzzminimizetime:在每次最小化尝试期间执行模糊目标的次数或时间,默认 60 秒。您可以通过在模糊测试时设置 -fuzzminimizetime 0 来完全禁用最小化。
  • -parallel:同时运行的模糊进程数,默认为 $GOMAXPROCS。目前,在模糊测试时设置 -cpu 无效。

语料库文件格式

语料库文件采用特殊格式编码。这对于 种子语料库生成的语料库 都是相同的格式。

下面是一个语料库文件的示例

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} 目录中的独立语料库条目。在 golang.org/x/tools/cmd/file2fuzz file2fuzz 工具可用于将这些二进制文件转换为为 []byte 编码的语料库文件。

使用此工具

$ go install golang.org/x/tools/cmd/file2fuzz@latest
$ file2fuzz -h

资源

  • 教程:
  • 文档:
    • testing 包文档描述了编写模糊测试时使用的 testing.F 类型。
    • cmd/go 包文档描述了与模糊测试相关的标志。
  • 技术细节:

词汇表

语料库条目: 语料库中的一个输入,可在模糊测试时使用。这可以是一个特殊格式的文件,也可以是对 (*testing.F).Add 的调用。

覆盖率引导: 一种模糊测试方法,它使用代码覆盖率的扩展来确定哪些语料库条目值得保留供将来使用。

失败的输入: 失败的输入是语料库中的一个条目,当针对 模糊目标 运行时,该条目会导致错误或 panic。

模糊目标: 模糊测试中的函数,在模糊测试期间针对语料库条目和生成的 L 进行执行。它通过将函数传递给 (*testing.F).Fuzz 来提供给模糊测试。

模糊测试: 测试文件中的一个函数,形式为 func FuzzXxx(*testing.F),可用于模糊测试。

模糊测试: 一种自动化测试,它不断地操纵程序的输入以查找 bug 或 漏洞 等问题,代码可能容易受到这些问题的影响。

模糊参数: 将传递给模糊目标并由 变异器 变异的 L。

模糊引擎: 管理模糊测试的工具,包括维护语料库、调用变异器、识别新覆盖率以及报告故障。

生成的语料库: 模糊引擎在模糊测试期间随着时间推移而维护的语料库,用于跟踪进度。它存储在 $GOCACHE/fuzz 中。这些条目仅在模糊测试时使用。

变异器: 模糊测试期间使用的工具,在将语料库条目传递给模糊目标之前,随机操纵它们。

包: 同一目录中一起编译的源文件集合。请参阅 Go 语言规范中的 Packages 部分。

种子语料库: 用户为模糊测试提供的语料库,可用于指导模糊引擎。它由模糊测试中 f.Add 调用提供的语料库条目以及包内的 testdata/fuzz/{FuzzTestName} 目录中的文件组成。这些条目默认使用 go test 运行,无论是否进行模糊测试。

测试文件: 格式为 xxx_test.go 的文件,可能包含测试、基准测试、示例和模糊测试。

漏洞: 代码中对安全敏感的弱点,可能被攻击者利用。

反馈

如果您遇到任何问题或有功能想法,请 提交 issue

有关该功能的讨论和一般反馈,您还可以参与 Gophers Slack 中的#fuzzing 频道