Go 1.18即将发布,希望在几周后发布。它';这是一个巨大的发布,有很多期待,但原生模糊在我心中有一个特殊的位置。(我当然非常有偏见:在我离开谷歌之前,我曾与凯蒂·霍克曼和罗兰·鞋匠合作构建模糊系统)。泛型也很酷,我想,但是将模糊集成到测试包和go测试中会让每个人都更容易访问模糊测试,从而更容易在go中编写安全、正确的代码。
关于how Go#39;s的模糊系统实际上是有效的,所以我';我在这里谈一谈。如果你';我想尝试一下,开始使用模糊是一个很好的教程。
模糊化是一种测试技术,测试基础设施使用随机生成的输入调用代码,以检查它是否产生正确的结果或合理的错误。模糊测试是对单元测试的补充,在单元测试中,在给定一组静态输入的情况下,测试代码是否生成正确的输出。单元测试的局限性在于,您只使用预期的输入进行真正的测试;模糊测试擅长于发现暴露奇怪行为的意外输入。一个好的模糊系统也会对被测试的代码进行检测,这样它就能有效地生成扩展代码覆盖范围的输入。
模糊测试通常用于检查解析器和验证器,尤其是在安全上下文中使用的任何东西。Fuzzing非常擅长发现导致安全问题的bug,比如二进制编码中的无效长度、截断输入、整数溢出、无效unicode等等。
还有其他使用模糊的方法。例如,差分模糊通过向两个实现输入相同的随机输入并检查输出是否匹配,来验证同一事物的两个实现是否具有相同的行为。你也可以在用户界面上使用模糊";猴子";测试:模糊引擎可以产生随机点击、击键和点击,测试验证应用程序没有';不要崩溃。
起毛并不是新鲜事。go fuzz可能是当今使用最广泛的工具,在开发原生fuzzing时,我们当然借鉴了它的设计。Go 1.18中的新功能是将模糊直接集成到Go测试和测试包中。这个接口非常类似于测试接口,测试。T
例如,如果您有一个名为ParseSomething的函数,您可以编写一个如下所示的模糊测试。这将检查对于任何随机输入,ParseSomething是否成功或返回ParseError。
包解析器导入(";错误";";测试";)var seeds=[]字节{nil,[]字节(";123";),[]字节(";(12)";),]func FuzzParseSomething(f*testing.f){for_u,seed:=range seeds{f.Add(seed)}f.Fuzz(t*testing.t,input[]字节){err:=ParseSomething(input)if err==nil{return}if parseErr:=(*ParseError)(nil)!错误。As(err,&;parseErr){t.致命的(err)}
当go测试正常运行(没有-fuzz标志)时,FuzzParseSomething被视为单元测试。提供给F.fuzz的fuzz函数是通过来自种子语料库的输入来调用的:在F.Add中注册的输入,以及从testdata/corpus/FuzzParseSomething中的文件读取的输入。如果fuzz函数崩溃或调用T.Fail,则测试失败,go test以非零状态退出。
在这种模式下,模糊系统将使用随机生成的输入调用模糊函数,使用来自种子语料库和缓存语料库的输入作为起点。生成的扩展覆盖范围的输入被最小化并添加到缓存的语料库中。生成的导致错误的输入被最小化并添加到种子语料库中,有效地成为新的回归测试用例。以后的go测试运行将失败,直到问题得到解决,即使没有启用模糊功能。
同样,还有';与其他系统相比,这里没有什么新奇之处。其优势在于对界面的熟悉和易用性。编写第一个fuzz测试很容易,因为fuzzing遵循测试包的约定。那里';It’团队中的每个人都不需要安装和学习新工具。
您可能已经知道,go test为每个被测试的包构建一个测试可执行文件,然后运行这些可执行文件以获得测试和基准测试结果。模糊化遵循这种模式,尽管有一些不同。
当使用-fuzz标志调用go test时,go test使用附加的覆盖率工具编译测试可执行文件。Go编译器已经为libFuzzer提供了插装支持,所以我们重新使用了它。编译器向每个基本块添加一个8位计数器。计数器速度快且近似:它包裹在溢出上,在那里';没有跨线程的同步。(我们不得不告诉种族检测器不要抱怨对这些计数器的写入)。计数器数据在运行时由内部/fuzz包使用,其中大部分模糊逻辑都在运行。
go test构建一个插入指令的可执行文件后,会像往常一样运行它。这被称为协调过程。这个过程从大多数通过go测试的标志开始,包括-fuzz=pattern,它使用它来识别要模糊的目标;目前,每次go测试调用只能模糊一个目标(#46312)。当目标调用F.Fuzz时,控制权被传递给Fuzz。CoordinateFuzzing,它初始化模糊系统并开始协调器事件循环。
协调器启动几个工作进程,这些进程运行相同的测试可执行文件并执行实际的模糊化。工人从一个未记录的命令行标志开始,该标志告诉他们是工人。模糊处理必须在单独的进程中完成,这样,如果工作进程完全崩溃,协调器仍然可以找到并记录导致崩溃的输入。
协调员通过一对管道,使用基于JSON的临时RPC协议与每个工作人员进行通信。协议非常基本,因为我们没有';我们不需要像gRPC这样复杂的东西,我们也不需要';我不想在标准库中引入任何新内容。每个工作进程还将一些状态保存在内存映射的临时文件中,与协调器共享。大多数情况下,这只是一个迭代计数和随机数生成器状态。如果工作者完全崩溃,协调器可以从共享内存恢复其状态,而无需工作者首先通过管道礼貌地发送消息。
在协调器启动worker之后,它通过从种子语料库和模糊缓存语料库(在$GOCACHE的子目录中)发送worker输入来收集基线覆盖率。每个worker运行其给定的输入,然后返回其覆盖率计数器的快照。协调器将这些计数器粗化并合并为一个组合覆盖阵列。
接下来,协调器从种子语料库和缓存的语料库中发送输入以进行模糊化:每个工作者都得到一个输入和一个基线覆盖数组的副本。然后,每个工作者随机改变其输入(翻转位、删除或插入字节等),并调用fuzz函数。为了减少通信开销,每个工作人员可以在不需要协调员进一步输入的情况下持续变异和呼叫100毫秒。每次通话后,工作人员都会检查是否报告了错误(T.Fail)或与基线覆盖率阵列相比是否发现了新的覆盖率。如果是,工人报告";有趣";立即将信息反馈给协调员。
当协调人收到产生新覆盖范围的输入时,它会比较工作人员';s对当前组合覆盖阵列的覆盖:it';有可能是另一个工作人员已经发现了提供相同覆盖范围的输入。如果是,则丢弃新输入。如果新的输入确实提供了新的覆盖范围,协调器会将其发送回工作人员(可能是另一个工作人员)以最小化。最小化就像模糊化一样,但工作人员会执行随机突变,以创建一个较小的输入,该输入仍至少提供一些新的覆盖范围。较小的输入往往更快,因此它';值得花时间提前最小化,以便以后更快地进行模糊处理。worker进程在其';即使它找不到更小的东西,它也做了最小化。协调器将最小化的输入添加到缓存的语料库中并继续。稍后,协调器可能会将最小化的输入发送给工作人员,以进行进一步的模糊处理。这就是模糊系统如何适应新的覆盖范围。
当协调器接收到导致错误的输入时,它会再次将输入发送回工作人员以最小化。在这种情况下,工作人员试图找到仍然会导致错误的较小输入,尽管不一定是相同的错误。最小化输入后,协调器将其保存到testdata/corpus/$FuzzTarget中,优雅地关闭工作进程,然后以非零状态退出。
如果工作进程在模糊化时崩溃,协调器可以使用发送给工作进程的输入恢复导致崩溃的输入,而工作进程';s RNG状态和迭代计数(都留在共享内存中)。崩溃输入通常不会最小化,因为最小化是一个高度有状态的过程,每次崩溃都会破坏该状态。这在理论上是可能的,但没有';还没做完。
模糊化通常会一直持续,直到发现错误,或者用户按Ctrl-C中断过程,或者通过使用-fuzztime标志设置的截止日期。模糊引擎优雅地处理中断,无论它们是传递给协调器还是工作进程。例如,如果工作进程在最小化导致错误的输入时被中断,协调器将保存未最小化的输入。
我';我对这次发布感到非常兴奋,尽管我不得不承认,去吧';s的新模糊引擎距离与其他模糊系统在功能和性能上达到同等水平还有很长的路要走。许多改进是可能的,但它';它已经处于有用的状态,并且API是稳定的。我';我很高兴';现在开始发货了。
您可以在带有模糊标签的问题跟踪器上找到未解决问题的列表。那些有Go1的人。19里程碑被视为最高优先级,但问题可能会根据用户反馈和开发人员带宽重新确定优先级。
不管怎样,去试试,报告错误,并请求功能!如果您在自己的代码(或其他人的代码!)中发现任何好的bug,将它们添加到Go wiki上的模糊奖杯案例中。