从 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

2.2. 建议

以下建议将帮助您充分利用模糊测试。

  • 模糊目标应该是快速和确定的,这样模糊引擎才能有效地工作,新的失败用例和已覆盖用例可以很容易地重用。

  • 由于模糊目标是在多个 worker 之间以不确定的顺序并行调用的,因此模糊目标的状态不应持续到每次调用结束后,并且模糊目标的行为不应依赖于全局状态。

3. 运行模糊测试

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

默认情况下,模糊测试的运行与单元测试非常相似。每个 种子语料库条目 都将针对模糊目标进行测试,在退出之前报告任何失败。

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

请注意,模糊测试的运行时长由您决定。如果没有发现任何错误,模糊测试的执行很可能会无限期地运行。未来将支持使用 OSS-Fuzz 等工具连续运行这些模糊测试,请参阅 问题 #50192

注意

模糊测试应该在支持覆盖检测的平台(目前是 AMD64 和 ARM64)上运行,这样语料库可以在运行时有意义地填充以便覆盖更多测试代码。

3.1. 命令行输出

在进行模糊测试时,模糊测试引擎 会生成新的输入并运行它们。默认情况下,它会持续运行,直到找到 失败的输入,或者用户取消进程(例如使用 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”对应的数字随着时间的推移逐渐减少。

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. 资源

教程

  • 尝试 [[Go模糊测试入门教程(译)]] 以深入了解新概念。

  • 有关 Go 模糊测试的简短介绍性教程,请参阅 博客文章

文档

  • testing 包文档描述了编写模糊测试时使用的 testing.F 类型。

  • cmd/go 包文档描述了与模糊测试相关的标志。

技术细节

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 中。这些条目仅在模糊测试时使用。

  • mutator: 一种在模糊测试时使用的工具,它在将语料库条目传递给模糊目标之前随机操作它们。

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

  • 种子语料库: 用户提供的用于模糊测试的语料库,可用于指导模糊引擎。种子语料库由两部分组成,可以通过调用 f.Add 方法来为其提供语料库条目,此外,包内的 testdata/fuzz/{FuzzTestName} 目录中的文件组成(这些条目会在`go test`命令执行时默认使用,无论是否进行模糊测试参数`-fuzz`)。

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

  • 漏洞: 代码中的安全敏感漏洞,可以被攻击者利用。

7. 反馈

如果您遇到任何问题或对某个功能有想法,请 提出问题

您还可以参与Gophers Slack中的 fuzzing频道 参与更多模糊测试新特性的讨论和反馈。


相关阅读