本篇内容是根据2020年8月份Füźžįñg音频录制内容的整理与翻译,
深入探讨Fuzzing并仔细研究 Go 的官方 Fuzzing 提案。
过程中为符合中文惯用表达有适当删改, 版权归原作者所有.
Mat Ryer: Hello,欢迎来到 Go Time。我是 Mat Ryer。今天我们要聊的话题是 Fuzzing(模糊测试)。我们将探讨它是什么,以及如何利用它让我们的代码变得更好……我们还会仔细研究一个关于将 fuzzing 引入 Go 作为一等公民的设计草案。这非常令人兴奋,今天我们很幸运邀请到了这份草案的作者 Katie Hockman。你好,Katie。
Katie Hockman: 你好,Mat。你好吗?
Mat Ryer: 很好!欢迎来到节目,感谢你的到来。
Katie Hockman: 谢谢你邀请我。
Mat Ryer: 我们还邀请了 Filippo Valsorda。你好,Filippo。
Filippo Valsorda: 嗨,Mat。很高兴回来。
Mat Ryer: 很荣幸再次邀请到您,先生。
Filippo Valsorda: 同感,同感,期待今天的讨论。
Mat Ryer: 谢谢。我们还邀请了 Roberto Clapis。你好,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 年。所以,是的,当你要验证某个属性时,比如“我希望头信息集合是相同的”,这很容易找到问