本篇内容是根据2020年8月份Füźžįñg[1]音频录制内容的整理与翻译,
深入探讨Fuzzing并仔细研究 Go 的官方 Fuzzing 提案。
过程中为符合中文惯用表达有适当删改, 版权归原作者所有.
Mat Ryer: Hello,欢迎来到 Go Time。我是 Mat Ryer[2]。今天我们要聊的话题是 Fuzzing(模糊测试)。我们将探讨它是什么,以及如何利用它让我们的代码变得更好……我们还会仔细研究一个关于将 fuzzing 引入 Go 作为一等公民的设计草案。这非常令人兴奋,今天我们很幸运邀请到了这份草案的作者 Katie Hockman[3]。你好,Katie。
Katie Hockman: 你好,Mat。你好吗?
Mat Ryer: 很好!欢迎来到节目,感谢你的到来。
Katie Hockman: 谢谢你邀请我。
Mat Ryer: 我们还邀请了 Filippo Valsorda[4]。你好,Filippo。
Filippo Valsorda: 嗨,Mat。很高兴回来。
Mat Ryer: 很荣幸再次邀请到您,先生。
Filippo Valsorda: 同感,同感,期待今天的讨论。
Mat Ryer: 谢谢。我们还邀请了 Roberto Clapis[5]。你好,Roberto。
Roberto Clapis: 七百四十八。
Mat Ryer: [笑] 好吧……这是 fuzzing 的结果吗?
Roberto Clapis: 是的。我想看看你是否会因为整数崩溃。 [笑声]
Mat Ryer: 我并没有崩溃,也没有恐慌。我继续了……事实上,这是我之前单元测试中的一部分,所以我已经准备好了。非常感谢,欢迎来到节目。
Roberto Clapis: 谢谢。
Filippo Valsorda: 我们能花点时间表扬一下 Mat 怎么卷舌发音了两个意大利名字吗?
Roberto Clapis: 是的,发得很好。 [掌声]
Mat Ryer: 哦,这是我的荣幸。这个口音很好听,所以我总是喜欢听你们说话,也因此邀请你们上节目。如果这是你们今天唯一的贡献,那我也很满意。
Roberto Clapis: 这就是我们的初衷。
Mat Ryer: 那我们从最基础的开始吧,给那些不熟悉的人解释一下……什么是 fuzzing,它有什么用?
Katie Hockman: 是的,我可以简单介绍一下。基本上,fuzzing 是一种自动化测试形式,它通过操纵输入来发现一些你自己可能无法发现的 bug。在我看来,它是一种对现有测试的补充,比如人们常做的单元测试或集成测试……但它的不同之处在于,它可以自主运行,并且是持续进行的,所以可以说它有点“聪明”。如果它发现了有趣的输入,它可以使用某种智能来以一种有意思且有意义的方式变异这些输入,从而找到那些开发者自己可能很难发现的崩溃和恐慌。
Mat Ryer: 这很有趣,你提到的这种“智能”意味着它并不只是随机的对吗?这里面有别的东西在发生。
Katie Hockman: 是的,我认为这是一个很复杂的问题,因为在这个领域里并没有一个统一的行业标准。虽然有很多方法可以进行随机变异,但同时也有很多有趣的讨论在围绕如何优先选择哪些 corpus (语料库) 条目,稍后我会讲更多关于 corpus 的内容,简而言之就是哪些输入应该被修改,以及如何修改它们,甚至 fuzzing 引擎应该有多“聪明”。这些仍然存在争议,不同的 fuzzing 工具工作方式也不同,这在我看来其实挺酷的。
Mat Ryer: 是的,这很有趣。那么它适合在哪些场景下使用呢?比如说标准库中的 strings.Split
函数,你传入一个字符串和一个分隔符,它会在遇到分隔符的地方将字符串切分,并返回一个包含各个片段的切片。这个函数适合 fuzzing 吗?
Katie Hockman: 是的,我认为它是一个很好的测试对象……Filippo 和 Roberto 可能也有很多好的见解,关于以前使用 fuzzing 的经验,通常这些经验都带有安全上下文。而这次的提案试图将 fuzzing 引入非安全领域的开发者手中,让更多的人使用它。
在 strings.Split
的例子中,如果存在某种 off-by-one 错误,或者某种可能导致 panic 的问题,或某些输入没有满足特定的属性,那么 fuzzing 可能更容易发现这些问题……我认为这是一个很好的函数来进行 fuzzing 测试。
Mat Ryer: 是的,你通常会听到 fuzzing 应用于解析器之类的场景,因为它们通常处理的是未知的结构,可能需要在处理过程中推断这些结构……因此在这种操作中有许多可能出错的地方,比如意外的输入,或者一些你永远不会想到有人会传入的东西。这就是它与单元测试的不同之处,我猜……因为单元测试是非常有针对性的,对吧?
Katie Hockman: 是的,单元测试通常是你给定一组输入,运行某个函数,然后看它的输出;非常明确,你会说“这些是我认为重要的输入,应该能很好地测试这个函数,然后它应该产生这个输出。”Fuzzing 则不然,它可以应用于很多不同的场景,而不仅仅是解析器或复杂的加密操作等。我们有单元测试的原因是,我们并不总是知道代码中的 bug 在哪里。我们默认假设代码是正常工作的,所以我们只是“好心”地测试它,以证明它确实有效。而 fuzzing 引擎则不会有这种假设,它不会假设代码是正常工作的……它会发现那些你可能没有意识到的 bug,或者你忽略了某些依赖项导致的故障。
Roberto Clapis: 对。而且当你编写 fuzz 测试目标时,你是根据你处理的内容的属性来做预期的。相反,在单元测试中,你是根据输出做预期的。例如,在 strings.Split
的例子中,你可以说“我将调用 strings.Split
,传入两个参数,然后检查分隔符是否从返回的切片中消失了”,因为分隔符不应该出现在返回的切片中。
这通常不会在单元测试中测试到。或者你会检查返回的切片数量是否少于字符串的字符数。如果返回的字符比原始字符串还多,那一定有问题。这些通常不会进行测试……我自己写单元测试时,也不会测试这些条件。
Filippo Valsorda: 是的,另一个 fuzzing 可以很好测试的例子是,当你将分隔符重新插入到切片之间时,是否可以还原出原始的字符串?如果可以,那么它可能工作正常。这类情况 fuzzing 很擅长发现,因为它可以找到某些输入,比如分隔符在末尾,少了一个字符,或者某种情况下不能完成往返(round-trip)的操作。
Roberto Clapis: 这还进一步测试了另一个属性,也就是如果你进行了 strings.Split
和 strings.Join
,你应该得到原来的字符串……这是一个正常的期望。当我使用 strings
包时,我希望这是真的……但我不确定是否有人做了 fuzzing 来确保这是真的……特别是在处理像空切片或空字符串切片等边缘情况时,看看会发生什么会很有趣。
Mat Ryer: 是的。那么这就涉及到了一点设计问题吧。你需要考虑这种属性,然后在 fuzz 测试中对其建模,对吗?它不仅仅是你指向某个方法,然后让它随机填充一些无意义的输入。
Katie Hockman: 我认为是,也不是。我觉得这取决于你的使用目的。你可以只是向函数投放随机输入,看看它是否会 panic。这是一个可以测试的属性,你不需要了解太多。我觉得它也可以用于差异测试(differential testing)、属性测试等多种情况。它可以作为单元测试的补充,但你也可以用它来简单地找到崩溃,你可能只需要几行代码和一点点思考就能做到。
Filippo Valsorda: 差异测试确实效果非常好。它的理念是有多个相同功能的实现。例如,大数运算库---
无论你使用哪个库,如果你计算两个任意精度的小数,结果应该是相同的,这听起来很合理,对吗?不过,朋友,你无法想象 fuzzing 已经找到了多少 bug,只是通过告诉它“好吧,这里有两个函数,它们的返回值应该相同。去吧!”我会收到一些邮件,因为其中一个被测试的是 Go 的实现,当 Go 的实现和其他实现不一致时,我就会收到邮件……哦,天哪,没错,多精度运算确实很难。所以,是的,这是一个非常好的例子。
Roberto Clapis: 我做过的一次差异测试是,当 Go 中修复了一个关于 HTTP 头解析的 bug 时,我想“这个问题应该很容易用 fuzzing 找到”,所以我导入了 fasthttp
和标准的 http
库,都是 Go 的实现。我运行了 go-fuzz
25 分钟,发现了一个 bug。这个 bug 刚刚被修复,而它已经存在了 12 年。所以,是的,当你要验证某个属性时,比如“我希望头信息集合是相同的”,这很容易找到问题。还有一次,当 JSON 包进行了大量优化时,有一个差异 fuzzer 正在检查新旧版本是否以相同的方式解析 JSON……它在稳定版本发布前发现了一个 bug,这如果被发布出去可能会很糟糕。所以这又是 fuzzing 的一个成功案例,而且不是出于安全原因,仅仅是另一个测试。
Mat Ryer: 那么,在今天的 Go 语言中,你可以如何进行 fuzzing 呢?我们有哪些选择?
Katie Hockman: 有一些工具。我可以介绍一两个。我想大家最常用的就是 go-fuzz,这是最为人熟知的工具。它主要由 Dmitry Vyukov 编写,非常了不起。我曾与他讨论过这个工具,他还对现在的提案提供了很多非常好的反馈。所以,能够与他在这方面合作并获得他的意见真的很棒。如果你还没有使用过 go-fuzz,你一定要去试试。
还有另一个工具是 fzgo。我认为它算是一种概念验证工具,主要是由 [听不清 00:12:45.20] 编写的,目的是为了尝试将 fuzzing 更紧密地集成到 go 命令中,使其看起来像一个端到端的工具,而不需要像 go-fuzz 那样有许多构建步骤……我认为它还添加了一些对模块的支持……或者也许这是 go-fuzz 的一部分。总之,这两个工具尝试了不同的功能,基本上是为了建模并看看它们如何工作……而 fzgo 则是作为一种原型或实验,看看最终方案可能会是什么样子。
Mat Ryer: 你刚才提到了构建步骤……所以它不仅仅是一个运行时的工具,还有其他的过程。它是不是也进行某种 introspection(自省)或 reflection(反射)之类的操作?它是不是在某种程度上是通用的?
Katie Hockman: 嗯,当我提到构建步骤时,我不记得 go-fuzz 的所有细节,但我知道它有一个 go-fuzz build 步骤,你必须构建一个将被 fuzz 的二进制文件,然后你需要单独运行它,并自己管理 corpus(语料库)……所以有很多不同的步骤。你不能像今天的 Go 工具链那样只运行一个命令。你得学习一个不同的工作流,而这对于一些人来说是一个障碍,因为他们不想学习一个新工具。
Roberto Clapis: 我认为这也是为什么人们不愿使用它的主要原因之一,因为它是外部的,而且感觉很不一样。还有,它做的一件事是进行源码到源码的转换。它会拿你的源码并实现某种检查点机制。基本上,当你的代码运行时,它可以检查代码执行到了哪个点。基本上,当你的代码执行时,它可以检查覆盖了多少代码……有点像 cover 工具,但比 cover 工具做得更多,也更高效……这也是为什么它很难支持模块的原因之一,因为它实际上是在重写源码。
Filippo Valsorda: 这里有一些背景信息,部分让 fuzzers(模糊测试工具)显得神奇的原因是---
嗯,最近这一代的 fuzzers,从 AFL[6] 开始---
它们使用覆盖率来判断哪些变异值得进一步研究。Katie 刚才提到有不同的策略,但总体来说,它们都会查看代码的覆盖率。如果你曾经为一个文件运行 gotest-cover
,我不记得具体的标志了,但无论如何,如果你生成过覆盖率报告,你会看到绿色和红色的部分,这就是 fuzzers 做的事情;它们运行输入并检查哪些部分被触发了。如果它们改变了输入并触发了新代码,fuzzer 就会说:“啊哈!这很有用。我可以继续改变这个,或许我会发现另一条路径,或者我可以结合两条路径来测试它们。”这正是它们非常有效的原因。
有一个 AFL 的演示,它慢慢地从无到有构建了一个有效的 JPG 文件,它逐渐出现了一张照片,甚至还搞定了里面的字符和标签。这真的很厉害,甚至有点吓人。
Roberto Clapis: 有一次让我感到非常害怕,当我对 HTTP 库运行 go-fuzz 后,过了一段时间,我发现 corpus 中开始出现一些看起来像随机的东西。我当时想:“哦,太酷了。”Go 标准库开始接受一些不是 HTTP 的内容了,因为它是 HTTP/2。基本上,它开始从无到有构建有效的 HTTP/2 请求。那时候真的有点吓人,而且我当时感到很羞愧,因为我没有认出来,还不得不手动解压缩它,看看到底发生了什么。
Filippo Valsorda: Rob,如果你能裸眼读懂 HTTP/2,请一定告诉我。
Roberto Clapis: [笑]
Mat Ryer: 是啊,那是一种奇怪的超能力。我不知道你得被什么咬过才能获得这种能力。
Filippo Valsorda: 有支持小组的。我们都经历过。我的是 TLS,以前是 DNS。没关系……有帮助的。
Roberto Clapis: [笑] 谢谢,我会记住的。
Mat Ryer: 所以这真的很有趣……它不仅仅是通过外部手段改变输入。它实际上对运行中的代码有洞察,利用这些信息来影响它的行为。这有点像机器学习中的对抗训练,你有一个模型,然后另一个模型与之竞争,双方不断变得更好。它几乎让人觉得有点作弊的感觉,但通过这种技术你可以得到某种镜像的东西。所以看到它的效果真的很惊人……它真的会让人觉得它有点像有智能一样,你们几个人也提到它有点吓人……
Katie Hockman: 嗯嗯。还有另一件有点吓人的事情是,它可以反向工程你的代码,从而弄清楚输入应该是什么,然后为你生成这种输入。所以它基本上可以告诉 fuzzing 引擎:“这是让这个 if 语句通过的输入”,然后它就会这样做,帮助 fuzzing 引擎摆脱卡住的状态。我认为 go-fuzz 大概每进行一千次变异就会这样做一次,只是为了防止卡住,但不会每次都做,因为这样太耗费资源了……所以这里面有很多权衡,比如你希望这个过程有多随机?你希望多大程度上优先考虑某些输入?覆盖率作为一个指标意味着什么?这些问题都需要开发人员根据需求来设计和判断。
Mat Ryer: 是啊,听起来像是一种黑客工具,对吧?事实上,它的起源是不是来自安全领域?
Filippo Valsorda: 是的,但我喜欢 Katie 刚刚提到的,它是开发人员需要做出的权衡……我认为她指的是 fuzzing 工具的开发人员;如果我理解错了请纠正我……
Katie Hockman: 是的,是的。
Filippo Valsorda: ……因为这就是我喜欢这个提案的原因之一,它不把所有这些决定和学习这些知识的必要性交给最终用户,也就是那些只是想测试自己代码的 Go 开发人员。
Roberto Clapis: 而且,如果你看看这个提案,它试图让 fuzz 测试目标尽可能与现在的单元测试相似……所以,如果你习惯于编写单元测试---
如果你还没开始,你应该开始---
那么采用 fuzzing 的阻力会非常低,因为它几乎不会改变现有模式。
Mat Ryer: 是啊,我们应该多聊聊这个提案。但在此之前,我想先搞清楚几个概念。比如说语料库的初始化,有点像单元测试中你给定的输入和期望的输出,你也会以类似的方式为 fuzzing 工具提供一些初始数据,对吗?
Katie Hockman: 是的,我认为这个提案的一个目标就是让人们现在已有的单元测试和他们已经想出来的用例可以直接作为种子语料库使用。种子语料库满足了两个需求,至少在这个 Go 提案中是这样。首先,初始化变异引擎,给它一个良好的起点,让它从这里开始构建,然后它可以根据自己的需要管理语料库,随着它找到新的覆盖范围和有趣的东西,它可以自行扩展语料库。其次,它也可以作为一种回归测试。种子语料库要么被检查到你的测试数据目录中,要么直接检查到你的模块或包中,或者是以编程方式存在于你的测试代码中。每次运行 go test
时,都会运行这些数据,同时它也被设计为回归测试的一部分。所以你可以使用现有的东西,也可以使用新的崩溃场景,并随着你发现新的回归问题来扩展种子语料库,确保它们被测试到。
Mat Ryer: 是啊,这个功能真的很酷。如果某些测试失败了,它会自动被加入到测试中。所以下次测试时,它会明确地再次测试那个问题。这真的很不错,因为单元测试的价值在于,当你发现一个 bug 并编写测试来证明它的存在时,你就可以通过这种方式防止回归---
如果你已经修复了这个 bug,并保留了单元测试,那么你就不会再遇到同样的问题。
那么我们该如何处理语料库呢?Twitter 上的 Dominic Rouse 问:“语料库的最佳实践是什么?应该把它放到 Git 中吗?是否应该放到另一个代码库中?团队之间是否共享?还是只在本地运行?在实际情况下语料库的去向是什么?”
Katie Hockman: 我认为这取决于情况,我也认为这是一个开放性的问题,涉及到我们希望设立哪些最佳实践……但这部分也取决于开发者。如果语料库是以编程方式存在的,就像我之前提到的那样,假设你已经有现成的单元测试,你只需要把 t.run
改成 f.fuzz
;这样的事情应该是可行的。所以如果它已经是以编程方式存在的,那就继续保持这种方式。如果它失败了,那就是失败了,这也没什么。
如果你有大量的测试数据,比如说你有很多大的 HTTP 请求,或者二进制文件之类的东西,你也可以直接使用这些数据,go test
也会把测试数据作为种子语料库的一部分。所以我认为这也取决于种子语料库的具体内容---
是巨大的二进制文件?还是小东西?是最适合以编程方式构建的吗?这些问题的最佳实践我认为还未完全确定……至少对我来说是这样。
Filippo Valsorda: 我认为这与生态系统的成熟度以及这种技术的成熟度有关……当 fuzzing 只是某些安全研究人员用来攻击一个程序的工具时,他们可能只会随便保存语料库。但我觉得,正如我们为测试设置了持续集成系统,并依赖机器为我们做繁重的工作,我也预料到 fuzzing 将会走上同样的道路,一旦它融入开发者的工作流中。
所以你可能会在本地机器上保留一个小型语料库,而 Katie 的提案会自动将其放到缓存文件夹中……它会进行一次非常快速的检查,但你不会主要在笔记本上运行 fuzzer。部分让 fuzzers 有效的原因是计算机速度很快,而且你可以继续增加它的核心数。然后你上传它,一些 CI(持续集成)系统或 OSS-Fuzz(开放源代码 fuzzing 项目)会继续运行 fuzzer,它应该会保留语料库,这样当你进行更改时,语料库已经很大了,但你不会将其检查到代码库中,因为大多数人不希望在代码库中包含数兆字节的语料库。
Roberto Clapis: 没错。我也喜欢 fuzzers 的一点是,你通常可以告诉它“不要给我超过这个大小的输入”,无论是直接还是间接的方式。间接的方式是你接收到 fuzzer 提供的输入,如果它超过了一定大小,你可以返回“不要这个”。过一段时间,fuzzer 就会停止提供超出你设定大小的输入。所以如果你在测试 strings.Split
,是的,它可能会处理到一兆字节,但分割一个一千兆字节的字符串没有意义……因为你知道你在 fuzz 的代码,你不应该太过宽泛地允许输入。是的,你在做 fuzzing,但你知道你在 fuzz 什么。如果你在 fuzz 一个 JPEG 解析器,那是的,你可以提供大文件。如果你在 fuzz 一个字符串分割器,那么三千兆字节的字符串中出现 bug 的可能性很小。
Mat Ryer: 是的,这一点很重要,因为你会觉得你只需要打开 fuzzing,然后让它自己运行。这很有趣,因为这是一个持续的过程;它不像基准测试那样,你只是在笔记本上跑一下。但是在提案中确实有一个新标志来运行 fuzzing,那么预期是会在某种持续集成系统中运行它,还是其他地方?
Katie Hockman: 我认为这取决于个人希望运行 fuzzer 多长时间。如果他们愿意让它在机器上运行一段时间,或许没问题。如果他们想让它跑一个周末,这也完全可以。如果是公司---
或个人---
他们想同时 fuzz 很多不同的东西,我不确定是否能够支持同时运行多个 fuzzer。我不知道会发生什么,比如会不会有竞争条件。
有很多我不确定能否支持的情况。如果它导致某些地方崩溃,很难知道问题出在哪儿,所以在这种情况下,或许让它在某种持续集成中运行更有意义。
Mat Ryer: 我想我们会不会最终像比特币矿工那样,拥有一堆机器整天在跑 fuzzing 任务…… [笑声] 然后我们有了 Fuzzcoin。
Filippo Valsorda: OSS-Fuzz 已经存在了。谷歌有一个项目,基本上提供了我们内部称为 ClusterFuzz 的工具[7],我不确定我是否可以说出来;但好吧,我们继续吧!对于开源项目,任何开源项目都可以提交……当然,它们有一些标准,我不知道具体是什么,但它们会为你运行 fuzzing 测试。如果我们让 Go 也支持这种方式提交项目,那将会非常简单。
Mat Ryer: 这真的很令人兴奋,真的很酷。
Roberto Clapis: 我觉得 ClusterFuzz 是开源的。
Filippo Valsorda: 太好了!今天不会被解雇了。
Katie Hockman: [笑] 是的,请不要被解雇。
Mat Ryer: 但如果你真的想被解雇,请这样做---
在节目中透露一些你不该说的东西。 [笑声] 这对我们来说太酷了!多大的猛料啊!
Filippo Valsorda: 我在这方面有过历史……我们就到此为止,继续下一话题吧。
Katie Hockman: 是的,别鼓励他。 [笑]
Mat Ryer: 是啊。上次 Filippo 上节目时,他阻止了我在节目中承认犯罪行为,这真的非常棒。非常有用的服务。 [笑声]
Mat Ryer: 如果有人需要休息,我们可以稍作休息,听众们在家也可以随时休息。他们可能正通过便携设备听我们节目,所以可以随便做点什么……我也不知道为什么我要解释这个。 [笑声]
我本来想说,有些部分可能会被剪掉。如果你们有需要剪掉的内容,请告诉我们,我们会处理的。
Roberto Clapis: 哦,Mat,我听了太多这样的节目,每次你说“这段会被剪掉”,结果从来没有发生……
Mat Ryer: 我知道。他们不会帮我剪掉,但肯定会为你们三位剪掉。
Roberto Clapis: 好吧。 [笑声]
Katie Hockman: 谢谢。
Mat Ryer: 他们反而会加些我出糗的片段…… [笑声] [听不清 00:29:00.18] 加到节目里,我想“我当时并没有出糗……那是另一次我出糗的场合。”
Filippo Valsorda: 这直接在音效板上。
Mat Ryer: 对,没错。 [笑] 它只播放我出糗的片段。这就是其中之一。这就是其中一段。
Mat Ryer: 这个新提案---
我们会把链接放在节目笔记里---
它有一种非常地道的 Go 风格,设计得很优雅。就像我们过去描述单元测试时提到测试函数一样,现在有了 fuzz 函数,它们接受不同的参数,比如 testing.F
。那 testing.F
是一个接口吗?还是别的什么?
Katie Hockman: testing.F
类型和 testing.T
或 testing.B
很相似。它会实现 testing.TB
接口。
Mat Ryer: 那会有一个 testing.F
接口吗?还是它是一个强类型?
Katie Hockman: 它是一个强类型。
Mat Ryer: 它有一些方法,允许你与 fuzzing 相关的东西进行交互……但它的 API 相对简单,对吧?只有两个方法……是这样吗?
Katie Hockman: 嗯,我在提案中没有列出 testing.TB
接口中的所有方法,它会支持这些方法。比如,如果你有一些前期工作需要做,并且你想让测试失败,因为某些事情失败了,你可以这么做……诸如此类的事情。最初,一些早期设计中,f.fuzz
函数接受 testing.F
,但后来发现这样做不太清晰,而且会使事情复杂化……
我和 Filippo 讨论后,最终决定在那个函数中保留 testing.T
。所以它基本上应该和 t.run
几乎一模一样。如果你有一个 t.run
,你可以直接复制过来。
因此,在 f.fuzz
函数中,它应该看起来和感觉上都像是一个单元测试,而这个函数本身其实也像是一个单元测试……然后你需要做任何前期工作,比如设置环境、添加语料库,或者其他事情,你都可以用 testing.f
来处理这些部分。
Mat Ryer: 嗯。与 run
函数不同的是,run
函数中唯一可以传递的参数是 testing.T
,但在这些 fuzz 函数中你可以有额外的参数……它们看起来有点动态化。你能解释一下它们是如何工作的吗?
Katie Hockman: 是的,在这个 f.fuzz
函数中,首先你基本上是在告诉它,它会接受一个 testing.T
,它是绑定到这个 T
的,然后你告诉它你希望 fuzzing 引擎为你生成什么样的输入。也就是你的语料库中每个输入的结构是什么。
在提案中,例子是“它接受一个 testing.T
,还有一个字符串 A
,以及一个大整数 num
。”这就意味着“好吧,我们有一个 f.fuzz
函数,将会由 fuzzing 引擎运行。”每个输入的结构就是一个字符串和一个大整数。每次运行时,它都会使用新的字符串和大整数。
Mat Ryer: 它会动态地查看你传递的参数并改变代码吗?它会根据这些参数作出响应,还是需要你在某处定义它们?或者你需要遵循某些模式?
Katie Hockman: 我不太确定具体细节,我想要好好解释清楚……但基本上,那个字符串和大整数---
如果你往上看一点,看到提案中的 f.add
函数,它所做的事情是向语料库添加一个条目,并且它添加的是一个字符串和一个大整数,它们的顺序必须和 f.fuzz
函数中的字符串和大整数完全一致。所以,这基本上是在定义语料库的条目结构。这个语料库会手动添加,并且 fuzzing 引擎也会生成它。
Mat Ryer: 它使用一个空接口的切片工作,所以某种程度上它是通用代码,对吧?
Katie Hockman: 是的。
Mat Ryer: 如果 Go 支持泛型,这会改变或影响这个设计吗?还是你认为它大概还是会以类似的方式使用?
Katie Hockman: 我不确定这是否会影响设计……它可能会对实现产生一些影响,但我还没有仔细考虑过这个问题。不过,现在想一想,我也不太确定它会有多大变化。我认为这个函数的目的就是,它有点神奇,但它基本上就是告诉 fuzzing 引擎它需要意识到并使用的结构。
Mat Ryer: 这是一个很好的 API,能够只定义函数并让它自动识别出来,或者至少能够工作……但如果你添加了不同类型的数据,或者改变了结构,会发生什么呢?
Katie Hockman: 比如说,如果你没有用两个整数来 f.add
,而是用了其他东西?
Mat Ryer: 没错。
Katie Hockman: 我估计它可能会 panic……因为你基本上是在告诉它“这是两个整数”,而它却期待一个字符串和一个大整数。或许它可以和 staticcheck
之类的工具配合,在构建时发现这些问题。
Filippo Valsorda: 对于还没有读过提案的听众来说,f.add
是用于初始化语料库的函数;你可以用它来告诉程序“这是起点”。顺便说一下,这是我最喜欢这个提案的一个部分,因为通常你需要创建一堆文件,每个输入一个文件,然后放进一个目录里……其实我想做点别的事情。而在这里,你只需要写 f.add
,然后“这是我的 ECDSA 证书,这是我的 RSA 证书。这些是例子。开始吧。”所以 f.add
用来向语料库添加条目,而 f.fuzz
是实际运行 fuzzer 的函数,它运行一个接受相同类型参数的函数……我只是提一下,以防大家还没读过提案。
Mat Ryer: 谢谢,太棒了。我喜欢它的一点是---
它设计得很好,融入了我们已有的东西。所以它知道 go test
,并且与 go test
协作,对吧?
Katie Hockman: 是的,实际上这是我设计这一切的主要目标。我不想要一个设计得不像我们现有测试方式的方案。人们应该能够快速理解这个设计……目标是,如果你知道如何编写单元测试,你就会知道如何编写 fuzz 目标。它应该同样简单。我希望它能够与当前的 go
命令配合工作,如果人们运行 go test
,它应该能以相同的方式运行,不需要使用任何特别的东西,也不需要学习太多新东西……我希望尽量降低使用的门槛。所以如果它看起来像 Go 代码,那就是目标,我很高兴你这么说。
Roberto Clapis: 我喜欢这个设计的原因是,我做过一些研究,看到人们在实际使用中为解析器创建 fuzz 目标,通常他们所做的只是把 fuzzer 引擎传递的任何输入丢进解析器里,然后就没了。所以他们基本上只检查一个特性:是否会 panic。这有点可悲,因为你可以轻松地把某些东西输入解析器,然后也许再序列化处理一次,看看是否一致。
所以编写 fuzz 目标其实比人们想象的要容易得多,但由于 fuzzing 看起来似乎是一个陌生的概念,我看到大多数 fuzz 目标并没有断言任何东西;他们只是将输入传递给他们想要测试的函数。就像测试 strings.Split
一样,我们只是拆分字符串,然后不检查是否返回了一个字符串。这就是类型系统的作用,但你就得到这样的结果。所以我真的很期待这些成为一等公民,和原始测试目标如此接近,看看人们实际开始断言哪些属性……因为“没有 panic”作为一个属性似乎有点太弱了。
Filippo Valsorda: 是的。如果人们从这次对话中学到一件事,那应该是:Go 内置的 fuzzing 并不仅仅是为了找到 panic。它不仅仅是“提供一些输入,然后等着它崩溃”。它的目的是编写尽可能多的不可变条件和检查,然后让 fuzzer 找到那些不符合你预期的输入。
Mat Ryer: 那你觉得 fuzzing 在处理多个方法时特别有意义吗?就像 Roberto 提到的例子中,进行编码和解码,因为你可以断言这两者应该如何互操作……但如果输入是完全随机的,你怎么能对其进行断言呢?你会断言什么呢?
Roberto Clapis: 我曾经做过一件事---
我在测试我实现的一个缓存……缓存比人们通常想象的要难,所以我想确保我放进去的东西,能从缓存里拿回来。为了测试我的缓存,我用 HashMap 进行了差异化 fuzzing。HashMap 是一个完美的缓存。是的,它会无限增长,但我不在乎;这只是 fuzz 测试。所以我把数据送入缓存,当我取回数据时,如果它不在缓存里---
好吧,它被淘汰了。但如果它还在,那它应该与 HashMap 中的内容完全一致。所以你可以有一个更简单、更傻瓜的算法实现,或者可能一个更慢的实现。如果你优化了代码,你可以保留旧的、慢的代码来进行测试……通常,慢的代码更容易调试,更可靠,也更容易编写。
Mat Ryer: 因为它更慢,你可以看到发生了什么。 [笑声]
Roberto Clapis: 当然不是慢到那种程度,但对……这就是重点。
Filippo Valsorda: 另一个例子是我为 Gopher 博客写的。我有一个解析器……哦,不,实际上不是解析器,是一个序列化器。你可能会想,“如何测试一个序列化器?怎么知道它生成的东西是正确的?”我想知道的是它是否能重用缓冲区,出于性能考虑,我不想每次都分配一个新缓冲区或将缓冲区清空。我只是想给它旧的数据包,告诉它“就在这个缓冲区上重新序列化。”
所以我写了一个 fuzzer,它会解析一个数据包,但在 Go 提案中,我甚至可能不需要执行解析步骤。我只是告诉它“给我一个随机的数据包结构,然后分别在一个全是零的空缓冲区和一个全是 1 位的满缓冲区上序列化它。”如果它们输出不同,那就意味着某些字段没有被正确重置……而这种问题确实存在,这可能就是为什么某些云端的 DNS 服务器没有正常工作的原因……这就是 fuzzers 能够发现的问题。
总的来说,测试应该是关于定义预期行为的,这适用于所有类型的测试。不仅仅是定义预期的输入和输出,而是关于锁定预期。任何你能够定义的预期,不严格是“这个输入需要有这个输出”,而是“输出需要比输入长”或“输出需要比输入短”。任何类似的东西你都可以放进 fuzzer 中,作为 fuzz 目标。
Mat Ryer: 嗯……有点像元测试,或某种抽象测试。从某种程度上来说,你不处理具体的值,但你仍然处理概念、变量……
Roberto Clapis: 是的,这在某种程度上消除了你编写单元测试时面临的一个大风险。你编写单元测试时,你会有一些假设。你想要测试的是“我希望 strings.Split
确实能够拆分字符串。”然后你去测试你的东西,输入一些数据,输出一些数据,但你只是在给出例子,你并没有真正测试你想要的属性。所以我认为为 fuzz 目标编写属性断言其实更接近你通常在测试中想要做的事情。
当然,单元测试始终是必要的,但如果你在此基础上加上某些断言你原本想要测试的属性,那你就增加了很多价值。
Filippo Valsorda: 我听过一个观点,我并不支持---
转发并不等于 endorsement---
那就是“如果你已经知道你的程序会在哪些地方出错,为什么还要写单元测试呢?直接别写 bug 就好了。”我知道…… [笑声] 是的,是的,我知道。但这其中有一定的道理。你可以为你写单元测试的地方---
单元测试其实在重构和回归时更有用。但问题是---
你不太可能想到那些会让你刚写的程序出错的输入,因为你已经考虑了那些边界情况。而 fuzzing 不在乎你考虑了什么。fuzzing 会找到痛点。
Roberto Clapis: 没错……我喜欢说我为未来与代码的交互编写测试目标,因为我大多数时候都是用 TDD(测试驱动开发)的方式。所以我编写测试,然后编写实现测试内容的代码……而 fuzzer 则是为我过去编写的代码服务。它确保现在的代码确实按预期工作,而测试则确保未来的代码继续这样做。
Katie Hockman: 我很喜欢 Filippo 说的“fuzzing 引擎不在乎开发者想到了什么”。我认为这正是它的优势所在……这也是为什么需要代码审查的原因之一,因为你需要另一个更客观的人来审视代码,而我认为 fuzzing 引擎可以充当这个第三方的客观角色,它会尽一切可能去打破代码,而不关心你对代码的预期。它不在乎你怎么想的,它只关心尽可能找到更多的覆盖率和 bug。这个第三方实体的概念对我来说非常酷。
Mat Ryer: 但 Katie,你不担心 fuzzer 变得自我意识,然后到处搞破坏吗?
Katie Hockman: 其实这就是我的目标……我其实是在试图打造一个自学习的机器人,它将接管整个编程语言。
Filippo Valsorda: 你怎么知道---
Mat Ryer: 基于 fuzzing 的。
Katie Hockman: 完全是。没错。
Filippo Valsorda: 你怎么知道这不就是已经发生的事情呢?我们在这里推销fuzzing,只是为了取悦我们的fuzzer主宰?[笑声]
Katie Hockman: 其实我是一个fuzzing引擎。这一切一直都是模拟。我希望这没问题。
Mat Ryer: 嗯,模拟得很好。你说得对,它确实做得很好。不过,我还是挺喜欢这个想法……不,实际上没有。[笑声]
Roberto Clapis: 现在他知道你是个机器人,他都不知道该怎么和你互动了……[笑声]
Katie Hockman: [00:46:00.08 听不清]
Mat Ryer: 我喜欢当机器开始展现出某种自发的智能时。我觉得这真是太神奇了,特别是在一片混乱中还能这样。我学到的一点是,fuzzing其实并不是关于随机输入,而是关于对现实中你可能传入的输入进行各种变体,对吗?……这个说法不太对,因为我从屏幕上看到没有……继续吧,纠正我如果我错了。
Roberto Clapis: 不,我只是想说……我想补充一些东西,那就是……fuzzer并不关心代码在做什么,这很重要。因为如果我们有一个机器学习算法来fuzz我们的代码,试图学习代码的行为,那么某个时刻它就会像人类一样。它会理解代码应该做什么,然后接受代码确实能正常工作。相反,如果你只是使用一个算法,简单地通过随机输入进行测试,某个时刻你可能经过两年的fuzzing,找到一个新的边界情况导致崩溃……我喜欢这一点,因为人类,或者按我们定义的智能设计,是找不到这种情况的……因为为什么你会两年重复做同一件事,期待不同的结果?那不就是疯狂的定义吗?
Mat Ryer: 是的,但我们最终会有fuzzing终结者,它们到处乱跑,尝试各种不同的东西来对付你……它会去破解某些东西,砸碎它,踢一只小狗,把一个婴儿扔进海里……你懂我的意思吗?做各种事情,只是为了看看什么有效。
Roberto Clapis: 这是我们愿意接受的风险。
Mat Ryer: 这是一个风险……好吧 [00:47:40.12 听不清]
Filippo Valsorda: 这真的是我们愿意做出的牺牲。[笑声]
Mat Ryer: 那么今天有人有不受欢迎的观点要分享吗?可以和fuzzing有关,但也不一定……可以是任何话题。
Katie Hockman: 嗯,我有一个可以分享。
Mat Ryer: 那就说出来吧。
Katie Hockman: 其实这有点像……我不知道这算不算是一个观点,可能更多是个人经历,但我实际上是因为数学不够社交化才进入计算机科学领域的……所以我认为我最喜欢的部分,计算机科学中最棒的部分其实是和别人一起构建东西。我认为社交技巧可以带你走得很远,而在科技领域这个技能有点被低估了。
Roberto Clapis: 等等,你是因为社交因素才进入计算机科学的?
Katie Hockman: 没错。我不想整天一个人坐在角落里解决数学问题,但我想,“哦,我可以和别人一起构建东西。这听起来更有趣,所以我会做这个。” 但我知道这和很多人不一样。
Filippo Valsorda: 然后你进入了安全领域,因为信息安全社区是一个社交技能非常出众的典范 [00:50:33.09 听不清] [大笑]
Katie Hockman: 信息安全领域需要高度的社交技能,因为你需要能够和人沟通,理解他们……比如当他们披露报告时,你需要能够与他们沟通,理解他们,也能够把复杂的事情以非常简单的方式传达给别人,这非常困难……我认为这是一个在该领域中社交技能尤为重要的地方,因为这里的风险非常高。
Filippo Valsorda: 是的……不过,公平地说,我应该指出,Go社区非常友好。我过去合作过的人通常都很愉快。我只是对……
Roberto Clapis: 没错……
Filippo Valsorda: ……所谓传统的安全社区稍微调侃了一下。
Mat Ryer: 那他们能对你做什么呢?
Roberto Clapis: 这也是一种说法。
Mat Ryer: 你是安全的,不是吗?他们还能对你做什么呢……?
Roberto Clapis: 没错…… [笑声] Katie,你刚刚提到的,我觉得软件的人性化方面的一个重要部分是,当你设计一个API时,你必须以别人能够理解的方式来设计。我讨厌别人说“这个API的用户很愚蠢,因为他们不会正确使用它。” 当你设计某样东西时,你其实是在和用户交流……但人们却总是忽略这个问题。
Mat Ryer: 是的,这确实是对的……因为你确实会想---
起初我以为API是为机器之间的通信设计的,但其实不是。它们是为人类设计的,目的是让他们创建使机器之间得以通信的东西。是的,这很对。
但我不知道,毕达哥拉斯在聚会上可能也很风趣。他可能会玩得很开心,我不知道……[笑声] 他可能在测量各种东西,而你会说“毕达哥拉斯,放下你的尺子,休息五分钟,兄弟!吃个三明治。我切成了你喜欢的三角形。” 你知道,就是这种感觉。好了,还有其他不受欢迎的观点吗?
Filippo Valsorda: 我有一堆关于密码学的不受欢迎观点,但问题是,我觉得没人真的对这些东西有意见,只有那十个人,我们都在同一个Slack频道里,只是在彼此之间讨论这些东西……所以我不打算讨论这个。相反,我的不受欢迎观点是---
Katie,我知道你会理解的……办公室里的狗不好。真的不好。
Mat Ryer: 办公室里的狗。
Filippo Valsorda: 办公室里不该有狗。
Mat Ryer: 嗯。继续说下去。你是对狗过敏吗,Filippo?
Filippo Valsorda: 我对狗过敏,我认识很多对狗过敏的人……我还认识一些怕狗的人,他们不觉得自己能说“嘿……这狗很好看。但我怕它,所以你不能再带它来办公室了,因为我怕狗。” 没人想成为那种人。
Mat Ryer: “是的,我知道你很爱它,但对我来说,那基本上就是一个从噩梦里出来的小怪物……”
Filippo Valsorda: 是的。比如,有人可能被狗咬过,而你会想“是的,这让我非常不舒服,但我刚加入公司,我不想成为那个家伙”,所以他们不会告诉你……他们只是会走来走去,假装说“是啊,是啊……很可爱,很可爱……”,然后在办公室的边缘徘徊……
Katie Hockman: 需要说明的是,我觉得Filippo是在指我,因为我比任何人都更喜欢狗……任何和我聊过五分钟的人都知道,我几乎比任何东西都更爱狗。
Roberto Clapis: 哇……
Katie Hockman: 是的。我的确同意你的观点,这确实让事情变得复杂。对我来说,狗可能是快乐的源泉,因为我不过敏,我也喜欢它们,但如果它成为了别人冲突和不适,甚至更糟糕的来源,那就不理想了……除了服务犬的情况,Filippo也同意那完全没问题……
Filippo Valsorda: 哦,当然。
Katie Hockman: ……老实说,我觉得这是一个非常合理的观点。
Filippo Valsorda: 服务犬训练得很好,通常如果需要做一些调整,可以逐案处理。但老实说,我从没遇到过“哦不,我对服务犬过敏,而且不能避开它”的问题。但我确实有过宠物的问题,因为宠物的数量更多。这只是一个数量问题。
Mat Ryer: 但如果不允许狗进办公室,管理层该怎么展示他们的酷呢?
Filippo Valsorda: [笑] 对吧?
Mat Ryer: 接下来你还要禁止什么,Filippo?桌上足球?
Filippo Valsorda: 乒乓球桌已经过时了……[笑声]
Mat Ryer: Roberto,你怎么看办公室里的狗?
Roberto Clapis: 我有点怕大狗,所以我站在Filippo这边……不过,我也有朋友对狗过敏,所以是的,我同意……除非它们是需要的,比如服务犬,否则我不赞成带狗进办公室。伙计们,你们的不受欢迎观点---
我有很多想讨论的内容。我本来要说的我的不受欢迎观点是:我喜欢黄色,所以……[笑声] ……你们带来了重要的话题。而我的完全没用。
Katie Hockman: [笑] 这是个可怕的观点,Rob。收回它。
Mat Ryer: 你是喜欢黄色,还是喜欢Coldplay的那首歌?
Roberto Clapis: 不,颜色。就是颜色。还有它对人的影响。
Filippo Valsorda: 还有那么多更好的颜色……[笑声]
Roberto Clapis: 是的。很多漂亮的颜色……黄色就是其中之一。
Katie Hockman: 你的耳机是黄色的……
Mat Ryer: 没错。我在找黄色的线索来验证。不知道为什么,当Roberto说他喜欢黄色时,我有点怀疑,我想“嗯,这是不是个诡计?”
Roberto Clapis: 嗯,我还有别的东西可以证明……
Mat Ryer: 哦,一个黄色的gopher……
Katie Hockman: 哇哦……
Mat Ryer: 你得发张照片。这可是播客节目。
Roberto Clapis: 是啊……我今天已经发了这张照片,所以大家只需要回去看看。我会再发一遍。
Mat Ryer: 顺便说一句,我有点怕那些gopher。这是我的不受欢迎观点。那些小东西……我做梦都梦见它们。
Filippo Valsorda: 它们看起来确实有点奇怪,是吧?
Mat Ryer: 尤其是那个黄色的。好吧,这是个播客节目,所以这真是……
Filippo Valsorda: 这还是个播客节目……[笑]
Roberto Clapis: 基本上,我把gopher越靠越近摄像头,直到Mat尖叫……但他没尖叫。你通过了测试。
Mat Ryer: 我不是建议你做一个音频解说,我的意思是“我们最好根本就不要做这件事,专注于音频内容。” [笑声]
Roberto Clapis: 没错……
Mat Ryer: 很遗憾,今天的时间就到这里了……非常感谢Katie、Filippo和Roberto的加入。我们下次再见!
Füźžįñg: https://changelog.com/gotime/145
[2]Mat Ryer: https://github.com/matryer
[3]Katie Hockman: https://github.com/katiehockman
[4]Filippo Valsorda: https://github.com/FiloSottile
[5]Roberto Clapis: https://github.com/empijei
[6]AFL: https://github.com/google/AFL
[7]ClusterFuzz 的工具: https://github.com/google/clusterfuzz