注册
登录
新闻动态
其他科技
返回
Go 的新 fuzzing 系统的内部结构
作者:
糖果
发布时间:
2024-05-11 08:14:36 (3天前)
来源:
https://jayconrod.com
Go 1.18 即将发布,希望在几周内发布。这是一个巨大的发布,有很多值得期待的地方,但原生模糊测试在我心中有着特殊的地位。(我当然是有偏见的:在我离开 Google之前,我曾与 Katie Hockman 和 Roland Shoemaker 一起构建了 fuzzing 系统)。我猜泛型也很酷,但是将模糊测试集成到testing包中,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被视为单元测试。提供的 fuzz 函数使用来自种子语料库F.Fuzz的输入调用:注册的输入和从. 如果 fuzz 函数发生恐慌或调用,则测试失败,并以非零状态退出。F.Addtestdata/corpus/FuzzParseSomethingT.Failgo test 可以通过运行标志来启用模糊测试go test,-fuzz如下所示: go test -fuzz=FuzzParseSomething 在这种模式下,模糊测试系统将使用来自种子语料库和缓存语料库的输入作为起点,使用随机生成的输入调用模糊函数。扩展覆盖范围的生成输入被最小化并添加到缓存的语料库中。生成的导致错误的输入被最小化并添加到种子语料库中,有效地成为新的回归测试用例。go test即使未启用模糊测试,以后的运行也会失败,直到问题得到解决。 同样,与其他系统相比,这里没有什么真正新颖的东西。优势在于界面的熟悉度和易用性。编写你的第一个模糊测试很容易,因为模糊测试遵循testing包的约定。团队中的每个人都无需安装和学习新工具。 ##### 模糊系统如何工作? 您可能已经知道go test为每个被测试的包构建一个测试可执行文件,然后运行这些可执行文件以获得测试和基准测试结果。Fuzzing 遵循这种模式,尽管存在一些差异。 当使用标志go test调用时,使用额外的覆盖检测编译测试可执行文件。Go 编译器已经对libFuzzer提供了检测支持,因此我们重用了它。编译器为每个基本块添加一个 8 位计数器。计数器快速且近似:它在溢出时包装,并且没有跨线程同步。(我们必须告诉种族检测器不要抱怨对这些计数器的写入)。计数器数据在运行时由internal/fuzz包使用,大多数模糊测试逻辑都在其中。-fuzzgo test 在go test构建了一个检测的可执行文件后,它像往常一样运行它。这称为协调器进程。这个过程从传递给 的大多数标志开始go test,包括-fuzz=pattern,它用于识别要模糊测试的目标;go test目前,每次调用只能对一个目标进行模糊测试( #46312)。当该目标调用F.Fuzz时,控制权被传递给fuzz.CoordinateFuzzing,它初始化模糊测试系统并开始协调器事件循环。 协调器启动几个工作进程,它们运行相同的测试可执行文件并执行实际的模糊测试。工人以一个未记录的命令行标志开始,告诉他们是工人。Fuzzing 必须在单独的进程中完成,这样如果工作进程完全崩溃,协调器仍然可以找到并记录导致崩溃的输入。 ![](/user/files/xs3Ba-Q5J9FwC7j_uQjbUnQAOnPqC8jOoX6ivjNk4ec.png) 协调器通过一对管道使用临时的基于 JSON 的 RPC 协议与每个工作人员进行通信。该协议非常基础,因为我们不需要像 gRPC 这样复杂的东西,而且我们不想在标准库中引入任何新的东西。每个工作人员还在内存映射的临时文件中保存一些状态,与协调器共享。大多数情况下,这只是一个迭代计数和随机数生成器状态。如果工作人员完全崩溃,协调器可以从共享内存中恢复其状态,而无需工作人员首先通过管道礼貌地发送消息。 $GOCACHE在协调器启动工作人员后,它通过从种子语料库和模糊缓存语料库(在 的子目录中)发送工作人员输入来收集基线覆盖率。每个工作人员运行其给定的输入,然后报告其覆盖计数器的快照。协调器将这些计数器粗化并合并为一个组合覆盖数组。 接下来,协调器从种子语料库和缓存语料库发送输入以进行模糊测试:每个工作人员都获得一个输入和基线覆盖数组的副本。然后每个工作人员随机改变其输入(翻转位、删除或插入字节等)并调用模糊函数。为了减少通信开销,每个工作人员可以在 100 毫秒内保持变异和调用,而无需协调器的进一步输入。每次调用后,工作人员检查是否报告了错误(带有T.Fail)或与基线覆盖率数组相比是否发现了新的覆盖率。如果是这样,worker 立即将“有趣”的输入报告给协调器。 当协调器接收到产生新覆盖的输入时,它会将工作人员的覆盖范围与当前组合的覆盖范围数组进行比较:另一个工作人员可能已经发现了提供相同覆盖范围的输入。如果是这样,则丢弃新输入。如果新输入确实提供了新的覆盖,协调器将其发送回一个工作人员(可能是不同的工作人员)以进行最小化。最小化就像 fuzzing 一样,但是 worker 执行随机突变来创建一个更小的输入,它仍然提供至少一些新的覆盖。较小的输入往往更快,因此值得花时间预先最小化,以使以后的模糊测试过程更快。工作进程在完成最小化时报告回来,即使它没有找到更小的东西。协调器将最小化的输入添加到缓存的语料库中并继续。稍后,协调器可能会将最小化的输入发送给工作人员以进行进一步的模糊测试。这就是模糊测试系统如何适应寻找新的覆盖范围。 当协调器收到导致错误的输入时,它会再次将输入发送回工作人员以进行最小化。在这种情况下,工作人员尝试找到仍然会导致错误的较小输入,尽管不一定是相同的错误。输入最小化后,协调器将其保存到testdata/corpus/$FuzzTarget,优雅地关闭工作进程,然后以非零状态退出。 ![](/user/files/f4ntZqbV-egaF-hBis0tC_47UPmkSCv8GQz-KTwjnWY.png) 如果一个工作进程在模糊测试时崩溃,协调器可以使用发送给工作人员的输入以及工作人员的 RNG 状态和迭代计数(都留在共享内存中)来恢复导致崩溃的输入。崩溃输入通常不会最小化,因为最小化是一个高度有状态的过程,每次崩溃都会消除该状态。理论上是可行的,但还没有实现。 模糊测试通常会一直持续到发现错误或用户通过按 Ctrl-C 或通过-fuzztime标志设置的最后期限来中断该过程。模糊引擎优雅地处理中断,无论它们是传递给协调器还是工作进程。例如,如果工作人员在最小化导致错误的输入时被打断,协调器将保存未最小化的输入。 ##### fuzzing 的未来 我对这个版本感到非常兴奋,尽管我不得不承认,Go 的新 fuzzing 引擎仍然是在功能和性能上与其他 fuzzing 系统相媲美的方法。许多改进是可能的,但它已经处于有用状态,并且 API 是稳定的。我很高兴它现在正在发货。 您可以在带有标签的问题跟踪器上找到未解决问题的列表。fuzz具有Go1.19里程碑的那些被认为是最高优先级,尽管问题可能会根据用户反馈和开发人员带宽重新排序。 无论如何,去尝试一下,报告错误,并请求功能!如果您在自己的代码(或其他人的代码!)中发现任何好的错误,请将它们添加到Go wiki 上的Fuzzing 奖杯案例中。
收藏
举报
1 条回复
动动手指,沙发就是你的了!
登录
后才能参与评论