Go 的新 fuzzing 系统的内部结构

举报
宇宙之一粟 发表于 2022/07/31 22:45:20 2022/07/31
【摘要】 Go 1.18 中将模糊测试集成到测试包和 go test 将使每个人都可以更容易地进行模糊测试,从而更容易在 Go 中编写安全、正确的代码。关于 Go 的 fuzzing 系统的实际工作原理还没有写太多,所以我将在这里讨论一下。如果您想尝试一下,开始使用 fuzzing 是一个很棒的教程。什么是模糊测试?Fuzzing 是一种测试技术,测试基础设施使用随机生成的输入调用您的代码,以检查它是...

Go 1.18 中将模糊测试集成到测试包和 go test 将使每个人都可以更容易地进行模糊测试,从而更容易在 Go 中编写安全、正确的代码。关于 Go 的 fuzzing 系统的实际工作原理还没有写太多,所以我将在这里讨论一下。如果您想尝试一下,开始使用 fuzzing 是一个很棒的教程。


什么是模糊测试?

Fuzzing 是一种测试技术,测试基础设施使用随机生成的输入调用您的代码,以检查它是否产生正确的结果或合理的错误。模糊测试补充了单元测试,在给定一组静态输入的情况下,您可以测试您的代码是否产生正确的输出。单元测试的局限性在于您只能使用预期的输入进行真正的测试;模糊测试非常适合发现暴露奇怪行为的意外输入。一个好的模糊测试系统还可以检测正在测试的代码,以便它可以有效地生成扩展代码覆盖率的输入。模糊测试通常用于检查解析器和验证器,尤其是在安全上下文中使用的任何东西。 Fuzzing 非常适合发现导致安全问题的错误,例如二进制编码中的无效长度、截断的输入、整数溢出、无效的 unicode 等等。

还有其他方法可以使用模糊测试。例如,差分模糊测试通过向两个实现提供相同的随机输入并检查输出是否匹配来验证同一事物的两个实现是否具有相同的行为。您还可以将模糊测试用于用户界面“猴子”测试:模糊测试引擎可以产生随机敲击、击键和点击,并且测试验证应用程序不会崩溃。


Go 中的 fuzzing 发生了什么

Fuzzing 对 Go 来说并不新鲜。 go-fuzz 可能是当今使用最广泛的工具,我们在开发原生模糊测试时当然借鉴了它的设计。 Go 1.18 中的新功能是模糊测试直接集成到 go test 和 testing 包中。该接口与测试接口 testing.T 非常相似。

例如,如果您有一个名为 ParseSomething 的函数,您可以编写如下所示的模糊测试。这会检查任何随机输入,ParseSomething 要么成功要么返回 ParseError。

package parser

import (
	"errors"
	"testing"
)

var seeds = [][]byte{
	nil,
	[]byte("123"),
	[]byte("(12)"),
}

func FuzzParseSomething(f *testing.F) {
	for _, seed := range seeds {
		f.Add(seed)
	}
	f.Fuzz(func(t *testing.T, input []byte) {
		err := ParseSomething(input)
		if err == nil {
			return
		}
		if parseErr := (*ParseError)(nil); !errors.As(err, &parseErr) {
			t.Fatal(err)
		}
	})
}

当 go test 正常运行时(没有 -fuzz 标志),FuzzParseSomething 被视为单元测试。提供给 F.Fuzz 的 fuzz 函数使用来自种子语料库的输入调用:使用 F.Add 注册的输入和从 testdata/corpus/FuzzParseSomething 中的文件读取的输入。如果 fuzz 函数发生恐慌或调用 T.Fail,则测试失败,并且 go test 以非零状态退出。可以通过运行带有 -fuzz 标志的 go test 来启用模糊测试,如下所示:

go test -fuzz=FuzzParseSomething

在这种模式下,模糊测试系统将使用来自种子语料库和缓存语料库的输入作为起点,使用随机生成的输入调用模糊函数。扩展覆盖范围的生成输入被最小化并添加到缓存的语料库中。生成的导致错误的输入被最小化并添加到种子语料库中,有效地成为新的回归测试用例。稍后的 go 测试运行将失败,直到问题得到解决,即使未启用模糊测试也是如此。


同样,与其他系统相比,这里没有什么真正新颖的东西。优势在于界面的熟悉度和易用性。编写你的第一个模糊测试很容易,因为模糊测试遵循测试包的约定。团队中的每个人都无需安装和学习新工具。


模糊系统如何工作?

您可能已经知道 go test 为每个被测试的包构建一个测试可执行文件,然后运行这些可执行文件以获得测试和基准测试结果。 Fuzzing 遵循这种模式,尽管存在一些差异。


当使用 -fuzz 标志调用 go test 时,go test 使用额外的覆盖检测编译测试可执行文件。 Go 编译器已经对 libFuzzer 提供了检测支持,因此我们重用了它。编译器为每个基本块添加一个 8 位计数器。计数器快速且近似:它在溢出时包装,并且没有跨线程同步。 (我们必须告诉种族检测器不要抱怨对这些计数器的写入)。计数器数据在运行时由 internal/fuzz 包使用,大多数模糊测试逻辑都在其中。


在 go test 构建了一个检测的可执行文件之后,它像往常一样运行它。这称为协调器进程。此过程从通过 go test 的大多数标志开始,包括 -fuzz=pattern,它用于识别要模糊测试的目标;目前,每次 go test 调用只能对一个目标进行模糊测试 (#46312)。当该目标调用 F.Fuzz 时,控制权被传递给 fuzz.CoordinateFuzzing,它初始化模糊测试系统并开始协调器事件循环。协调器启动几个工作进程,它们运行相同的测试可执行文件并执行实际的模糊测试。工人以一个未记录的命令行标志开始,告诉他们是工人。 Fuzzing 必须在单独的进程中完成,这样如果工作进程完全崩溃,协调器仍然可以找到并记录导致崩溃的输入。



协调器通过一对管道使用临时的基于 JSON 的 RPC 协议与每个工作人员进行通信。该协议非常基础,因为我们不需要像 gRPC 这样复杂的东西,而且我们不想在标准库中引入任何新的东西。每个工作人员还在内存映射的临时文件中保存一些状态,与协调器共享。大多数情况下,这只是一个迭代计数和随机数生成器状态。如果工作人员完全崩溃,协调器可以从共享内存中恢复其状态,而无需工作人员首先通过管道礼貌地发送消息。


在协调器启动工作人员后,它通过从种子语料库和模糊缓存语料库(在 $GOCACHE 的子目录中)发送工作人员输入来收集基线覆盖率。每个工作人员运行其给定的输入,然后报告其覆盖计数器的快照。协调器将这些计数器粗化并合并为一个组合覆盖数组。


接下来,协调器从种子语料库和缓存语料库发送输入以进行模糊测试:每个工作人员都获得一个输入和基线覆盖数组的副本。然后每个工作人员随机改变其输入(翻转位、删除或插入字节等)并调用模糊函数。为了减少通信开销,每个工作人员可以在 100 毫秒内保持变异和调用,而无需协调器的进一步输入。每次调用后,工作人员检查是否报告了错误(使用 T.Fail)或与基线覆盖率数组相比是否发现了新的覆盖率。如果是这样,worker 立即将“有趣”的输入报告给协调器。


当协调器接收到产生新覆盖范围的输入时,它将工作人员的覆盖范围与当前组合的覆盖范围数组进行比较:另一个工作人员可能已经发现了提供相同覆盖范围的输入。如果是这样,则丢弃新输入。如果新输入确实提供了新的覆盖,协调器将其发送回一个工作人员(可能是不同的工作人员)以进行最小化。最小化就像模糊测试,但工作人员执行随机突变以创建更小的输入,但仍提供至少一些新的覆盖范围。较小的输入往往更快,因此值得花时间预先最小化,以使以后的模糊测试过程更快。工作进程在完成最小化时报告回来,即使它没有找到更小的东西。协调器将最小化的输入添加到缓存的语料库中并继续。稍后,协调器可能会将最小化的输入发送给工作人员以进行进一步的模糊测试。这就是模糊系统如何适应寻找新的覆盖面。


当协调器收到导致错误的输入时,它会再次将输入发送回工作人员以进行最小化。在这种情况下,工作人员尝试找到仍然会导致错误的较小输入,尽管不一定是相同的错误。输入最小化后,协调器将其保存到 testdata/corpus/$FuzzTarget,优雅地关闭工作进程,然后以非零状态退出。



如果一个工作进程在模糊测试时崩溃,协调器可以使用发送给工作人员的输入以及工作人员的 RNG 状态和迭代计数(都留在共享内存中)来恢复导致崩溃的输入。崩溃输入通常不会最小化,因为最小化是一个高度有状态的过程,每次崩溃都会消除该状态。理论上是可行的,但还没有实现。


模糊测试通常会一直持续到发现错误或用户通过按 Ctrl-C 或通过 -fuzztime 标志设置的截止日期来中断进程。模糊引擎优雅地处理中断,无论它们是传递给协调器还是工作进程。例如,如果工作人员在最小化导致错误的输入时被打断,协调器将保存未最小化的输入。


fuzzing 的未来

我对这个版本感到非常兴奋,尽管我不得不承认,Go 的新 fuzzing 引擎仍然是在功能和性能上与其他 fuzzing 系统相媲美的方法。许多改进是可能的,但它已经处于有用状态,并且 API 是稳定的。我很高兴它现在正在发展。


您可以在带有 fuzz 标签的问题跟踪器上找到未解决问题的列表。具有 Go1.19 里程碑的那些被认为是最高优先级,尽管问题可能会根据用户反馈和开发人员带宽重新排序。无论如何,去尝试一下,报告错误,并请求功能!如果您在自己的代码(或其他人的代码!)中发现任何好的错误,请将它们添加到 Go wiki 上的 Fuzzing 奖杯案例中。


参考链接:Internals of Go's new fuzzing system — jayconrod.com

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。