模糊测试即将成为标准库的一部分。我们采访了 Katie Hockman 和 Jay Conrod,他们是负责设计和实施模糊测试的Go团队成员。我们深入研究了细节,听取了一些最佳实践,了解了模糊测试可以在哪些方面帮助您编写代码,并进一步了解了模糊测试的工作原理。
本篇内容是根据2021年6月份#187 Fuzzing in the standard library音频录制内容的整理与翻译
过程中为符合中文惯用表达有适当删改, 版权归原作者所有.
Mat Ryer :大家好,欢迎收听 Go Time!我是 Mat Ryer,今天我们要回顾一下 Go 中的模糊测试(fuzzing),因为......Beta 版已经发布了!希望在剪辑时能让这个部分听起来更酷一点,加个音效什么的......也许加点蓬松感的效果,会很不错。
今天我们请到了两位让模糊测试成为现实的嘉宾。先介绍他们之前,我要快速做个 shout-out---
给聚会做个 shout-out。这可能会成为节目的常规环节......因为我们可能很快又能线下见面了,见见你所在地区的 Gopher 们应该会很不错。听起来像个约会应用,但不是。如果真是那样,你肯定会右滑"Golang North-East"。这是英国东北部的一群很棒的人。如果你不清楚英格兰和英国的区别......你该读读书了。威斯康星之外还有一个世界呢。
英国东北部---
这是我最喜欢的部分......Golang North-East 之所以叫这个名字,是因为"Go North-East"这个名字已经被当地一家公交公司占用了。所以我要为那里的精彩聚会 shout-out。如果你想让你的聚会被 shout-out,或者被 shout,或者 shout-outed---
我不确定 shout out 的过去时是什么......
Johnny Boursiquot:或者直接被喊......
Mat Ryer:对,被喊出来,哈哈......我们会对你的聚会大声喊叫,收取一点小费。如果你想这样的话,请在 Twitter 上联系我们,@GoTimeFM,我们会为你 shout-out。
好了,让我们认识一下今天的嘉宾。欢迎 Katie Hockman 回到节目。Katie 是 Go 安全团队的软件工程师,专注于模糊测试,之前是模块镜像和校验和数据库的技术负责人。欢迎回来,Katie。 (译者注:Hockman曾于2018 年 8 月 - 2022 年 2 月供职Go Team的安全团队,目前在东京一家公司工作)
Katie Hockman:谢谢再次邀请我。
Mat Ryer:总是很高兴见到你。我们还邀请了 Jay Conrod。虽然 Jay 的名字听起来像个 JavaScript 项目,但实际上他是 Go 命令行工具团队的一名软件工程师,主要负责模糊测试和模块支持。Jay,欢迎来到 Go Time。
Jay Conrod:嗨,谢谢邀请我。
Mat Ryer :绝对的。很高兴你能来。我的联合主持人兼好朋友 Johnny Boursiquot 也加入了我们。你好,Johnny。
Johnny Boursiquot:你好......
Mat Ryer :[模仿英音] 你好,我喜欢这个。欢迎回来。Johnny,前几天我想确保拼对你的名字,所以我在谷歌上搜索了一下,结果你的脸跳了出来......我想"这是怎么回事?"于是我用无痕模式搜索了一下---
这是我第一次用那个功能---
结果还是一样的,当你搜 Boursiquot 时,仍然是你的脸出来了。
Johnny Boursiquot:是的,我的 SEO 优化做得很好。
Mat Ryer: 是的,确实如此。太棒了。好了,让我们进入正题。我们上一次是在 2020 年 8 月的第 145 期节目中讨论模糊测试的,如果你想去听的话......但现在 Beta 版已经发布了,对吧?很令人兴奋。
Johnny Boursiquot:对于那些不懂 Mat 语言的人来说,他说的[模仿英音] beta,其实是指 beta 版。
Mat Ryer:哦,非常感谢。
Johnny Boursiquot:听起来像"beater"。谁在打谁呢?为什么要打人?哈哈......
Mat Ryer:好的,谢谢你的翻译。如果你想把我的正确英语翻译成不正确的英语,请随时联系我。
Johnny Boursiquot:是的,请随时找我。
Mat Ryer:对,太棒了。
Johnny Boursiquot:哈哈哈......
Mat Ryer:是的。不过,显然,如果你现在听莎士比亚的口音,他听起来会更像美国人。我不知道你有没有听过这个说法。其实并没有那么简单。他可能会说[听不清 00:06:07.22],类似这样的东西。
Katie Hockman:那是你的美国口音吗?
Mat Ryer:是的,这是布鲁克林的莎士比亚。"嘿,罗密欧!"
Jay Conrod:那会很有趣......
Mat Ryer:"嘿,罗密欧,你在哪儿?!"他不知道罗密欧在哪儿。也情有可原。好了,冒着让这一期节目完全变成模糊测试的风险,我们是不是可以快速回顾一下模糊测试是什么,它的用途是什么?
Katie Hockman :我可以先讲讲基本概念,然后 Jay 可以补充。简单来说,模糊测试是一种自动化测试形式;与其你告诉它测试什么,它会为你生成输入,能够发现你可能漏掉的安全漏洞、崩溃或边界情况,而这些通常不会被你常规的单元测试覆盖。
Mat Ryer:是的,非常酷。因为你在安全团队工作,所以模糊测试的主要重点是确保程序的安全性吗?
Katie Hockman :是的。我们团队有很多项目和不同的重点,但模糊测试的确是其中一个主要的好处......单元测试非常重要,但它们依赖于开发人员编写的测试,而开发人员通常会对自己的代码做一些假设......所以我们往往会在测试中也引入这些假设。而模糊测试则是一个独立的第三方观察者,它可以检查所有的东西,发现安全问题或其他你可能没有考虑到的 bug,因为你假设你的代码是正确的。
Mat Ryer:是的,这也是可以理解的。那么 Jay,模糊测试的目标是什么?它是试图让你的 Go 代码崩溃吗,还是有关于输出的断言?它具体在做什么?
Jay Conrod : 是的,如果发现崩溃,那肯定是个很好的问题指示。不过总体来说,它更像是单元测试,如果失败了,那就说明有问题,如果通过了---
嗯,它其实并不会真正通过,而是会不断生成随机输入,直到发现问题,或者直到你感到厌烦并放弃。
但它擅长发现你意料之外的东西,比如你不会为其编写测试的情况。比如,你写了一个解析器......我们今天还在讨论这个,你写了一个只接受有效的 Unicode 的解析器。你根本没想到要为网络上传来的随机垃圾数据写一个测试用例。所以它非常擅长发现你意想不到的问题。
Johnny Boursiquot :这是不是超出了所谓的"合同"范围?比如你刚才的例子---
如果我在文档中明确指出我的代码只接受有效的 Unicode,那这就是我的预期,对吗?但我们现在是在超出合同范围,超出软件预期使用方式的情况下测试,基本上是在说:"以防万一,让我们增加一些鲁棒性,以防我们没有得到预期的输入",这是我们现在所处的领域吗?
Jay Conrod:是的,我觉得你说得很对。我觉得你有一定的余地可以说"我对某些输入不感兴趣",比如如果你有一个完全不关心的输入,你的模糊测试函数可以直接返回,不做任何处理,模糊测试器就会认为"好吧,这个不有趣,我会尝试其他的输入"。
Katie Hockman:补充一点,问题不一定是无效的输入。假设你有一个接受括号的解析器,左括号和右括号的匹配是有效的。但是你没想到如果嵌套 500 个左括号再跟 500 个右括号,这样组合在一起会发生什么。所以有时候输入是完全有效的,但它不是你想过的,也不是你测试所覆盖的。
Johnny Boursiquot:那么你认为测试失败后应该怎么处理?如果我在功能测试中发现某个行为不对,我会去修复代码,让它按预期工作。那么在模糊测试中,如果发现"给定某种输入,我的代码崩溃了",接下来该干什么?是要给输入添加更多的验证,限制接受的输入种类吗?测试失败后预期的行为是什么?
Katie Hockman:我觉得完全取决于具体的失败和你的代码在做什么;你的代码是客户端的还是服务端的?是文档中明确的行为,还是需要添加的验证?是你应该检查的东西吗?还是你应该在文档中写明"如果你这样做,这就是会发生的情况"?有时候是 bug,有时候不是......Jay,你怎么看?
Jay Conrod:Johnny,你刚才提到"合同",我觉得这是一个很好的类比。如果模糊测试器生成的输入超出了函数的合同范围......比如说,假设你确实希望函数在接收到无效的 Unicode 字符时崩溃。你可以在模糊测试函数中对其进行包裹,你可以说"这是无效的输入,我不想测试它。"或者你甚至可以验证"当我传递无效字符时,它确实会崩溃",然后你可以从那个崩溃中恢复。
Johnny Boursiquot:这很有道理。
Mat Ryer:是的,这很有趣。我喜欢编写尽量避免崩溃的代码,而是依赖错误处理......我想在这种情况下,错误是模糊测试可以接受的结果,对吧?
Katie Hockman:是的,你可以检查错误。
Jay Conrod:是的。崩溃只是表示问题的一种方式。你同样可以说"这是一个错误。"就像你在测试包中一样,你可以说"t.error, t.fatal",类似的东西。
Katie Hockman : 是的,这个设计有一些独特之处,这是其他 Go 或其他语言中的模糊测试器不一定具备的---
我们不仅仅是在寻找崩溃或[听不清 00:11:59.15]。基本上,你可以几乎直接复制现有单元测试中的代码,包括所有的 t.errors
和 t.fatals
,然后将它们放入 f.fuzz
函数中,这个函数在后台循环运行......它可以像一个测试一样工作。它不仅仅是寻找崩溃,从某种意义上来说它也是一种基于属性的测试;你可以设置一些属性,看看"这是否按照预期工作?",如果不是,那么比如可以调用 t.error
。
Mat Ryer :当我第一次看到设计提案时,令我感到有趣的是,得到一个新的 testing.F
类型,它只有两个方法,add
和 fuzz
。add
方法让你可以添加一些看起来真实的数据,然后你将一个方法传递给 fuzz
,它会进行变异,创建输入......它不仅仅是随机数据,对吗?某种意义上,它是有一定真实性的数据。你提到了变异器(mutator)---
它是如何工作的?它如何决定接下来选择哪种数据?
Katie Hockman :它有几个组成部分。关于变异的底层代码,比如它将字节切片的一部分移到另一部分,或者翻转一个比特,或者添加一个有趣的值,等等......很多这些来自 Go Fuzz 项目,这是 Go 社区多年来合作的成果,由 Dmitry Vyukov 领导。所以一部分是随机的,但它也很聪明,知道自己在做什么。它根据类型进行不同的变异,但也使用覆盖率引导。因此当它运行时,它会检查是否找到了一些尚未触及的新路径?这个值是否有趣?如果是,它就会成为语料库的一部分,并且随着运行,它会不断学习,变异器会使用这些新发现的东西。
所以,不一定是变异器本身在做聪明的事情,而是模糊测试引擎在判断什么是有趣的,什么不是有趣的,决定哪些数据应该继续变异,哪些不应该。
Jay Conrod :是的。深入一步,我们使用了编译器插桩 ,在基本块级别添加了计数器。所以每当你的程序调用一个函数,或者返回,或者在 if
语句中走向一个分支时,它都会递增一个计数器。变异器可以读取所有这些计数器并说:"哦,我刚刚传递了一个触发了新路径的输入。我们走了一条以前没有见过的路径。这很有趣,我们将基于此派生出一堆新输入。"
Johnny Boursiquot :关于限制条件---
我在阅读设计文档时看到提到了一些资源利用方面的限制,比如 CPU 和内存。首先,为什么计算资源的利用率会增加?其次,是否有一个可调整的参数,可以控制你想要进行多少模糊测试?比如你可以说"我觉得跑这么多次测试就够了"或者"我想在 CI 中让它尽情运行,哪怕每次测试花费 1000 美元"?对此有什么控制吗?
Jay Conrod :我觉得这是我们还需要进一步完善的一个领域。如果大家在 Beta 期间对此有反馈,那对我们来说会非常有帮助。目前我们有一个 fuzztime
的标志......我记得类似 Benchtime
。Benchtime
的功能是一样的。它是一个超时时间,比如它会运行 30 秒,或者你设置的其他时间,或者你可以指定一个固定的迭代次数,比如运行 1000 次函数调用,或 100,000 次等。
至于 CPU 和内存,目前我们还没有控制。默认情况下,它会运行与 GOMAXPROCS
相同数量的工作进程,这可能太多了,也可能太少,具体取决于你在做什么。不过这也可以通过 go test
已经支持的 -parallel
标志进行配置。
Mat Ryer:那么你推荐人们如何使用这个功能呢?你之前提到可以一直运行,直到你厌烦。它是每次保存文件时就运行一点模糊测试,还是你期望会有模糊测试服务器在代码上不断运行,尝试不同的输入?
Katie Hockman :我们的回答可能会基于 Go 社区和其他地方已经在做的事情。我认为一个最终目标是将其集成到像 OSS-Fuzz 这样的平台中,它可以持续运行模糊测试器,并在发现问题时报告。目前已经有使用 go-fuzz 的模糊测试目标在做这件事,我们也希望为原生支持提供类似的功能。所以这是一个方式。也许你写完代码后将其提交,然后你希望它持续运行,特别是对于那些变化不大的代码。
你可能想测试那些已经存在一段时间的代码,并希望让它长时间运行......但也许你也可以在一小时内运行它,或者......这取决于代码的测试情况,以及你在运行时希望找到什么问题......但是目前这还是比较昂贵的。如果你默认运行 8 个进程,比如说你运行 8 个,那么你整台电脑都会为这个任务而忙碌,可能导致其他事情无法运作。所以你可能只想将 -parallel=1
或 2,不过这样你一次就只能运行一个测试。在未来,我们希望可以让你在一个循环中运行多个目标,但现在一次只能运行一个。
所以目前还是有一些限制,我们也希望听到反馈,比如"大家希望如何运行它?什么对大家有用?"这些反馈对我们非常重要。
Mat Ryer :你提到它使用了编译器插桩,运行这些模糊测试时会干扰某些东西。这个功能是否只有 Go 团队,或者通过贡献和修改 Go 工具链,才能实现这样的实现?因为我知道 Damian Gryski 也有一个项目,其他人也有贡献,工作方式稍有不同。这个设计特别好,它非常自然地融入了现有的 Go 测试框架。如果你习惯写 Go 的单元测试,感觉非常熟悉;你知道你有一个以 fuzz-
开头的函数,而不是 test-
,它接收一个 testing.F
而不是 testing.T
,这非常熟悉。这种工具修改的方式是设计中的关键吗?
Jay Conrod :我认为编译器插桩---
我们其实很幸运,因为我们可以重用为 LibFuzzer 设计的插桩功能。LibFuzzer 是另一个完全不同的模糊测试器,它使用了同样的插桩功能,这非常棒。我们只需要对运行时做一些调整,以便使用这些数据。我认为我们的创新不在这里,但特别有意思的是我们通过 go test
和 testing
包暴露了它。这使得它对更多人来说更加容易上手,不需要安装额外的工具。它就在那儿,看起来就像单元测试或基准测试,大家已经知道如何调用 t.fail
、t.error
、t.log
等。这让它非常容易上手。
Mat Ryer:是的,我觉得这是一个相当不同寻常的测试技术,而这种熟悉感会帮助人们开始使用它,这非常令人兴奋。如果你们不介意的话,我想问一下,你们是怎么参与到模糊测试中的?
Katie Hockman:我可以先说,Jay 可以补充......我是在 2019 年加入 Go 安全团队的,大概是在 2020 年那些不堪回首的日子之前的六个月......然后我们基本上想找一些新的项目来做,现在我们有了新的人员配置可以专注于这些项目......我们也看到了 Go 社区早已希望模糊测试功能上线。已经有很多相关的工作完成了,很多工作都是为了向 Go 社区展示这一功能的有用性,以及它的益处。
这个提案已经存在很多年了,收到了很多支持票,Go 团队也看到了这一点。所以我们一直以来都希望实现这个功能,而 Go 社区也一直期待它的到来。我们现在正好有时间来做这件事。安全团队正试图专注于代码的端到端安全性,从你写代码到它上线运行,这只是其中的一部分。这对我们团队来说非常重要。所以我基本上从一开始就参与了这个项目,当时我被指派开始思考这个问题。
Mat Ryer:Jay,你呢?
Jay Conrod:我得说,是 Katie 把我拉进这个项目的。
Mat Ryer:哦,真的吗?
Katie Hockman:确实是我,哈哈。
Jay Conrod :她确实提出了提案和很多 API 设计。我过去几年一直在做 Go 命令行工具的工作,主要是模块支持。在之前的工作中,我做过更多编译器和运行时相关的工作,所以回到这种低层次的插桩和管理一堆高速通信的进程---
从技术角度来说,这对我来说非常有趣......但我也对这个项目的安全性方面感到兴奋。正如 Katie 所说,我们正在做 Go 的端到端安全性工作。我们真的希望 Go 成为一个安全的编程语言,并将其作为 Go 的一个真正优势。这里面有很多方面,但我认为模糊测试确实是我们在这个领域做的一个令人兴奋的新尝试。现在 Go 团队几乎所有的工作都或多或少与安全性相关。对于我们所有人来说,安全性是头等大事。
Mat Ryer:从 Go 模块工作中抽出身来做点不同的事是不是很不错?
Jay Conrod:哈哈哈,是的,做点不一样的事情感觉不错......
Johnny Boursiquot:Mat,这个问题有点"钓鱼"啊......哈哈哈。
Katie Hockman:确实挺有趣的,因为我知道 Jay 是个非常忙碌的人,他当时就说:"哦,我就......" 我当时想:"我想和 Jay 一起做这个,但不知道他有没有时间。" 有人对我说:"你可以直接问他啊。" 我想:"好吧。" 所以我就打电话给他,跟他说:"嘿,我想让你和我一起做这个。我认为你有我没有的技能,我觉得你非常适合这个项目。"
Johnny Boursiquot:Mat 就说:"对对,不管是什么,随便。"[笑]
Katie Hockman:于是我就[听不清 00:22:13.21],把他拉进来了,感觉很棒。哈哈哈......
Mat Ryer:是啊,感觉像是"我放下那些混乱的事情,来做模糊测试吧。"
Katie Hockman:基本上是这样。
Mat Ryer:不错。
Mat Ryer:是的,设计过程很有趣......这个过程是怎样的?它是一直像现在这样吗,还是经历了很多变化?
Katie Hockman:肯定花了很长时间。我可能大概有六个月都处于设计阶段。这是一个漫长的过程,并不是说那段时间我百分之百都在专注于设计,但最开始是从"好吧,什么是模糊测试?其他人做了什么?Go Fuzz 是什么?它是怎样的?人们喜欢它的哪些方面?我们关心什么?"这些问题开始的。我们花了很多时间来梳理哪些对这个功能来说是重要的,哪些是不重要的,然后做出决定,比如"我们是要自己做一个模糊测试引擎,还是使用 LibFuzzer 或现有的模糊测试引擎?"这是一个很大的决定。而这些决定往往需要很多时间,因为一旦做出选择,就很难再更改了。所以你真的需要非常深入地思考这些问题。
因此,设计确实随着时间发生了一些变化。起初,大家对 f.fuzz
的布局有些抵触......因为它和单元测试中的 testing.t
不一样。于是人们会说:"如果我们做一个模糊测试,它运行起来更像 go-fuzz 会怎样?"如果你看那个设计,它有些不同,但那是大家已经习惯的东西。所以有一些抵触情绪,比如"这真的是我们想要的吗?这符合大家的习惯吗?它看起来对吗?它能为我们未来提供足够的灵活性吗?"
因此,我们通过不断的小调整,逐渐让设计趋于完善。最初可能只是我和 Filippo Valsorda 以及其他几个人讨论,后来又和 Dmitry Vyukov 以及更多的人交谈,之后又逐步扩大到更广泛的 Go 社区。所以整个过程耗时较长,但每一次小的迭代都非常有用。
Mat Ryer:是的。
Johnny Boursiquot :很有意思,你们经历了这么长的过程,最终达到了一个非常优雅、简单且熟悉的 API 设计......你看到它时,会觉得"当然,testing.f
,它非常合适。" 但我相信这花了不少时间来完善和调整。我必须说,我对它的简洁性感到非常满意。它对于 Go 开发者来说非常熟悉,大家可以很容易地将它整合到现有的测试套件中。你提到过这一点,但我觉得值得深入探讨,因为让东西变得简单并不简单,我觉得你们在设计一个开发者容易接受的东西方面做得非常好。首先,将它作为标准库的一部分是关键,接下来是让人们使用它,提供反馈,这又是另一个步骤。所以我认为这是一个很好的开始。我很期待更多地使用它。
Katie Hockman:是的,谢谢。我想再次强调,这个项目是一个集体的努力。很多人参与进来,提供了非常有用的反馈,所有提交问题、评论提案的人,无论是在 Google 内部还是外部,大家都给予了很大的帮助。这绝对不是一个人的工作,甚至不是五个人或十个人的工作。
Mat Ryer:是的,每当你看到好的用户体验时,它看起来都很显然,这让人感觉它很简单。但实际上并不是......我同意,我觉得这种熟悉感会对人们开始使用它至关重要......因为如果你已经在写单元测试,你可以很轻松地上手。我觉得这一点经常被低估。你有没有需要拒绝很多想法和功能的情况?
Katie Hockman:是的,这很难......我们仍然不得不拒绝一些提议,因为最终我们想做所有事情,但我们总是要决定哪些是"现在不行",哪些是"以后再说",还有哪些是"现在可以做的"。比如,字典支持(dictionary support);有些人确实在用这个功能。我自己不是特别熟悉,但我知道很多其他的模糊测试引擎会用这个功能来改进它们的变异器和模糊测试引擎。我们只能说:"这要留到以后。"
提高模糊测试引擎的性能---
这也要留到以后。我们首先要确保用户体验工作正常。甚至如何支持结构化模糊测试---
比如支持结构体或其他复杂类型---
这也可以放到以后,但我们必须做出决定,哪些是现在需要发布的关键功能,以获得反馈,然后哪些是以后可以慢慢改进的。
Johnny Boursiquot:所以你可以完全预见到未来的 Go 版本中,我的模糊测试代码不会被破坏,只会变得更好、更快、更高效。
Katie Hockman:是的。
Jay Conrod:我们在 Beta 阶段还需要做很多工作。我们非常希望能从大家那里得到反馈,看看哪些地方可以改进......因为目前我们所有的代码都还在一个分支上,还没合并到主分支。我们还处在可以进行改动的阶段。我们还没有强制的兼容性保证,但在接下来的几个月里,我们有时间来收集反馈。如果有人觉得"这个地方应该完全不同,这就是原因......"我希望我们不会发现设计有重大失误,但我们确实还有时间,所以请告诉我们什么地方有效,什么地方无效。
Mat Ryer:是的,我觉得这是个很好的提醒。如果你从这一集播客中带走什么东西的话,那就是请在你的代码中试用它,看看你能发现什么......如果你发现了一些有趣的崩溃情况,请告诉我们......因为这正是模糊测试的意义所在。实际上,我知道标准库中已经通过模糊测试发现了很多问题......
Katie Hockman:是的。
Mat Ryer:这很令人鼓舞......
Katie Hockman:是的,请一定要告诉我们。我现在内部有一个文档在跟踪这些问题,但我最终会分享一些更公开的东西......不过在此期间,你可以在模糊测试的 Slack 频道或 Gophers 频道发布这些问题,或者直接私信我,或者通过 Twitter 和我联系,或者联系 Jay。
Mat Ryer :我很好奇,想深入了解一下......我相信我们的听众/观众也会对它的实际工作原理感兴趣。我知道有 BinaryMarshaler
和 BinaryUnmarshaler
类型的接口,还有 TextMarshaler
和 TextUnmarshaler
接口。这些是模糊测试中的新类型吗?
Katie Hockman:这里可能有一点误解。那部分还没有实现。我们在设计草案中讨论这些接口时,主要是在探讨如何支持结构体,如何支持复杂类型。
Mat Ryer:哦,我明白了。
Katie Hockman:一种实现方式是:如果你实现了标准库中的文本编组接口,或者实现了二进制编组接口,我们可以将结构体解组成字节数据,对这些字节进行变异,然后再重新编组回来,运行它们的代码。但没有其他简单的方法可以做到这一点。这可能是我们最开始支持结构体的一种方式,即我们说"我们只支持实现了这些接口的结构体的模糊测试。"然后未来我们可能会进一步改进。
Mat Ryer:明白了。那么,如果我们想在 Beta 版中试用模糊测试,今天它能支持哪些类型呢?
Jay Conrod:目前它只支持基本类型,比如整数,还有布尔值,尽管模糊测试对布尔值的效果不太好......是的,整数、字节切片、字符串之类的;你可以为一个函数传递多个参数,但将来我们会支持结构体。我们也讨论过接口支持,比如不同接口的实现......我不是说在 Beta 阶段,但最初,你能做的最有用的事情就是对字符串和字节切片进行模糊测试。
Mat Ryer:我猜如果你有结构体,你仍然可以选择在模糊测试中使用字符串,然后在那个小的模糊函数中创建结构体,对吗?
Katie Hockman:绝对可以。是的,你可以每次都动态生成它们。这实际上可能是一个好的做法。即便将来我们支持结构体,如果你能通过某种方式保证创建的结构体能通过验证并执行一些有用的操作,那可能仍然是最好的方式。
Mat Ryer: 是的,这个设计中好的地方之一就是你在编写 Go 代码。所以它很熟悉,你可以写 Go 代码,做你需要做的任何事情来支持模糊测试过程。我觉得这也是一个非常聪明的设计原则。我想我们最终可能会在会议上看到关于如何使用模糊测试功能的有趣演讲。
Katie Hockman:我对此非常期待。
Mat Ryer:我敢肯定,很多人会在他们自己的领域和问题中创新出解决方案。我觉得这是对任何设计 API 的人来说很好的一课。有时候你可能会想写一个 DSL,或者做一些非常针对你场景的东西,但有时候函数是让人们控制和参与某个包或某个过程的好方法。我发现,匿名函数作为设计模式非常有用,因为它为用户解锁了很多可能性,它不局限于你设计时想到的那一种方式。是的,我真的很喜欢这一点,尤其是它是测试代码,这真的很好。
Katie Hockman :我希望大家开始使用差异模糊测试(differential fuzzing),这是我的目标。因为它不仅仅是为了找崩溃,就像我们之前提到的。你可以用它来看看"两个不同的函数是否行为一致?它们是否有相同的输入和输出?"等等。你可以用它在非常独特的场景中,这只是其中之一。我有一个梦想,就是希望能够利用这个基础设施测试你本地开发分支上的代码是否与主分支或其他分支上的代码行为一致。这种差异模糊测试将非常棒。
我目前还没有具体的实现思路,但有很多我们可以做的事情,基础设施已经支持这些功能。比如,它已经支持对两个不同的结构体进行差异模糊测试,只要它们有相同的值;或者对两个不同的函数进行差异模糊测试,只要它们有相同的输入值,等等。
Mat Ryer:是啊,如果你要发布一个向后兼容的新 API,这会非常有用......
Katie Hockman:没错。
Mat Ryer:......或者你要彻底重写某些东西。而当前模糊测试的一个有趣的特点是它会在运行过程中记住一些东西,但它是把这些记忆存储在哪里呢?
Jay Conrod:我们有一个"有趣值"的概念,尽管这可能不是最好的术语......
Mat Ryer:听起来其实还不错......
Jay Conrod :......所谓的"有趣值"是指能够扩展代码覆盖率的值,或者是模糊测试器希望从中派生更多输入的值。所以我们现在将这些值存储在构建缓存中的一个子目录中......如果你运行 go env GOCACHE
,你会看到一个存放所有构建工件的目录。这些工件包括所有的 .a
文件,很多年前它们还曾经存放在 GOPATH 包中,那是在 Go 1.10 之前的事。所以我们现在把它们存放在那个目录中。目前这是一个非常简单的实现,但它基本上能工作;这是我们计划在 Beta 版中改进的众多事情之一......目录中会有很多小文件。你可以通过 go clean -fuzzcache
命令清除这些缓存,因为它们可能会变得非常大。
Mat Ryer:是的,那么如果它真的变得很大,你再次运行模糊测试操作时,它会如何使用这些信息?它是仅仅记录操作历史,还是会用这些信息来决定新的测试值?
Jay Conrod :基本上,它会把缓存中的所有值分发给每个工作进程。我们有一个协调者/工人模型,测试二进制文件---
实际上是通过 go test
运行的二进制文件---
将任务发送给所有正在运行相同二进制文件的工作进程。我们会将每个缓存值分发给每个工作进程,工作进程运行变异器以从这些值中派生出新的测试值。这些值就像是模糊测试的起点。
如果我们找到新的崩溃点或是扩展覆盖率的值,我们会尝试最小化它,这意味着我们会寻找一个较小的值来实现相同的效果......最小化完成后,我们会将新值写入缓存。
Katie Hockman : 我想补充说明一下---
我们有两种方式存储测试集条目。我们有 Jay 提到的缓存目录,还可以将数据存储在 testdata
目录中......这个目录位于你的包内,类似于崩溃错误等内容会存储在这里。如果有 panic
,或者触发了 t.error
或 t.fatal
,这些信息都会写入 testdata
。模糊测试引擎如何运行、它的作用以及如何解释这两个目录的方式有所不同,因为它们服务于不同的目的。
Mat Ryer:这很有意思。
Jay Conrod :testdata
中的崩溃实际上作为回归测试使用。一旦你发现了崩溃,并修复了它,go test
即便在不执行模糊测试时,也会运行所有这些输入,确保你的程序仍然成功运行。因此,即便不再进行模糊测试,你的模糊测试目标也会继续运行,确保之前的崩溃点仍然被修复。
Mat Ryer :这真是个好主意。实际上,我现在就想去试试。这就是官方模糊测试实现的优势所在---
这种集成让它更闪亮,因为这完全合乎逻辑。在单元测试中,如果你实践过测试驱动开发(TDD),最终你会得到一个不错的测试套件,当你进行代码重构或修改时可以依赖它们。这就是你提到的回归保护。
我认为拥有良好测试覆盖率的一个好处是,它让你有信心进行大胆的重构和更改......作为开发者,这非常有价值。因为你在项目进行过程中会学到很多东西,你不可能一开始就知道所有事情,所以拥有这样的测试对这个过程至关重要,这样你就不会不断地破坏现有功能或者重新引入之前的问题。真的很酷。
Jay Conrod:确实,拥有良好的回归测试集能大大减少软件开发中的恐惧和焦虑。在那些拥有良好测试集的项目中,我会更加放心地工作。
Mat Ryer :没错,这确实能给你信心。另外,我在代码中努力做到的一件事是,如果我的测试通过了,我就可以将代码推送到生产环境。在某种意义上,这就是我对软件的唯一承诺---
测试。而有些人当我解释这个想法时,会有点犹豫,他们并不完全理解它的价值。但事实上,能够自信地说"是的,这就是它做的一切,它履行了所有承诺,这可以上线"---
虽然这并不完美,但如果你能做到,这是一个非常好的属性......所以我完全同意。
模糊测试是否与现有的单元测试接口交互?还是说这是完全新的,你需要从头开始编写?
Katie Hockman :嗯,如果你有现有的测试,关于 f.fuzz
函数的一大好处是它不需要传递 testing.f
,而是使用 testing.t
。因此,你的模糊测试目标函数(fuzz foo),类似于 test foo
的著名测试,你的顶级函数接收 testing.f
,然后你可以调用 f.add
来添加新的测试集条目,或者调用 f.cleanup
以便在结束时执行一些清理工作。
但是,当你运行 f.fuzz
函数时,它接收一个 testing.t
以及所需的输入,类似于 t.run
,它接受 t
和一些输入值等。
基本上,如果你有现有的单元测试,你可以将它们复制到 f.fuzz
函数中,稍微包裹一下模糊测试目标---
我们有一个模糊测试目标。我的梦想是将来人们在编写单元测试时,更多地使用这种方式。只要有写模糊测试的价值,你就可以将单元测试写成一个模糊测试目标,只需添加一行,即 f.fuzz
,然后你就拥有了一切......与其编写表驱动测试(table-driven tests),在测试开始时定义结构体、测试用例,然后使用 t.run
,你可以使用 f.add
,并将这些放入种子测试集中---
即运行的初始测试集,然后你原本放在 t.run
中的所有代码只需放入 f.fuzz
。
Mat Ryer:这也行得通,因为你添加的内容总是会被测试。
Katie Hockman :是的,通过 go test
。就像其他单元测试一样......以及所有与该模糊测试目标相关的 testdata
,这些都是默认行为的一部分。
Mat Ryer:你认为这会让测试变慢吗?比如,当我保存文件时,我会运行该包的测试。Go 构建和运行速度非常快,所以几乎不会注意到有什么动作......但这可能会引入一些延迟。你觉得会这样吗?
Katie Hockman :嗯,默认情况下它不会运行模糊测试,这是关键。它默认只作为单元测试运行。所以它的表现不应有太大不同,不应做任何不同的事情。当你运行 go test
时,无论是否包裹在模糊测试目标中,它的行为都应该差不多。所以它不应该变慢。唯一可能变慢的情况是你选择使用 -fuzz
运行它。
Mat Ryer:明白了。
Jay Conrod:是的,这一切都在测试包中,所以它基本上就像编写另一个单元测试。使用的是相同的基础设施。而且当你不进行模糊测试时,我们不会使用编译器插桩来构建包,所以也不会有任何开销。因此,如果你不使用模糊测试,它基本上是免费的。
Mat Ryer :这真的很好,不是吗?事实上,真的是很好。这类特性,我能预见到这会成为你编写测试的新方式---
虽然可能并非在所有情况下,但在合适的情况下,我能看到它成为默认的测试编写方式。非常有趣。
Mat Ryer:在设计过程中,你记得的困难是什么?有没有什么特别棘手或充满争议的事情,或者什么东西真的花了时间去解决?
Johnny Boursiquot:你不必提名字......
Mat Ryer:[笑]
Katie Hockman :我觉得 f.fuzz
函数花了很多心思......仅仅是它的基本结构,决定是否......因为最初的设计,当我第一次写下这个功能并与 Filippo 以及其他一些关注模糊测试很久的人讨论时,它看起来非常像 go-fuzz
。我不认为 go-fuzz
有什么问题。但当时它看起来就是那样---
有个 fuzz foo
,testing.app
,然后它会运行你写在其中的内容......这基本上是对他们工作的延续。
接下来我们开始思考"我们想做的所有其他事情呢?这样会行得通吗?"于是我想"好吧,也许我们可以做些不同的事情。"也许我们可以更清楚地表明这是运行模糊测试的操作,然后让人们在需要时在上面做一些预处理......于是我们就想到了"也许我们需要 f.fuzz
",而不是让 f.fuzz
接收 testing.f
,我们让它接收 testing.t
。这是一个较晚做出的决定。最开始,f.fuzz
接收的是 testing.f
。然后我们想"既然它做的都是单元测试做的事情,为什么不能让它接收 testing.t
呢?"于是我们决定"哦,这样更好。"然后它开始以这种方式慢慢成型。但这确实花了很多时间,需要我们仔细思考如何处理。
Mat Ryer :是的,这很有趣,也很有意思......模糊方法---
不对,应该是函数,对吧?不,它是方法,是不是?在 testing.f
上......
Katie Hockman :对,它是 testing.f
上的方法。
Mat Ryer:明白了,我只是想为我们挑剔的听众确认一下;我们有一些挑剔的听众会发邮件......
Katie Hockman:[笑] 我常常说错。
Mat Ryer :对。所以这是一个函数---
不,我又说错了吧?这是个方法......
Katie Hockman :我称它为 f.fuzz
函数,因为我觉得这样说有用。我不知道,Go 开发者通常不使用"方法"这个词,所以......
Mat Ryer :是的。它们在某种程度上也都是函数,对吧?差别只是它们有接收者......所以从某种意义上说,那些挑剔的听众......[听不清]。不过那个 fuzz
......它接收的是空接口,但实际上它期待的是一个函数。这个特别有意思,它有点不太寻常......为什么会这样?为什么它接收的是接口而不是类型化函数?
Jay Conrod :因为如果它接收接口,它可以让你传递任何类型的函数......我们现在有一个限制,就是你传递给 f.add
的内容必须与模糊函数的类型匹配。所以如果你 f.add
一堆字符串,那么你的模糊函数必须接收字符串。我们会在运行时通过反射验证它,比如"如果它接收的是字符串和整数,那么它必须是字符串和整数",等等。
我们也不能完全静态地完成这项工作。我们曾讨论过---
它可能会与泛型一起出现,所以我们能不能做一些泛型相关的事情?但我们需要验证 testdata
目录中的所有内容,以及缓存中的所有内容......所以我们 90% 的工作都是在运行时处理的,因此我们觉得需要这么做。不过......是的,它接收接口,这样它就可以接受任何类型的函数。你的函数可以匹配你想要测试的任何数据类型。
Mat Ryer :这很酷。那么 add
方法是可变参数的,对吗?所以它可以接收任意数量的参数,然后你的函数必须匹配这些参数,而且它还会在开头加上 t
......这其实是一个很简单的模式。而且很合理,因为你有种子数据,这就是你在编写的契约。
我注意到在网站上的示例中,你有一个解析查询的 URL 解析函数正在被测试......如果查询字符串出错,你会调用 t.skip
。这很有趣......用户在测试代码中可能有责任去验证输入是否合理,对吗?
Katie Hockman : 是的。所以在这种情况下,如果它没有通过基本检查......比如你想做一些事情,比如解码、编码、再解码,但第一次解码失败了。你可能会觉得"好吧,给出的输入不合法,所以我会跳过它。"没有合适的错误可以处理这种情况。但如果它第一次通过了---
比如它成功解码了,我重新编码,再次尝试解码,但第二次解码失败了,这就是个问题。某个地方发生了错误。所以你需要在那个时刻做出这些决定。
Mat Ryer:是的,这很有道理,因为从这个角度来说,你不仅需要关心你在测试什么,还需要知道你在做什么,但我认为这也是 Go 代码的一个优势,因为你只是在编写 Go 代码,而我们知道如何编写 Go 代码。所以我们可以把所有这些知识直接应用过来。
Katie Hockman :是的。我还刚刚添加了一些稍微不同的东西......我们会看看效果如何。以前,如果你想调用 f.Add(0)
,默认情况下它是 int64
类型,但假设你的 f.fuzz
输入是 int
类型,它会失败,因为它认为"int64
不是 int
。"而你可能会想"但我明明写的是 f.Add(0)
!"或者你可能传递了字符串而不是字节切片,类似的情况。
所以我们刚刚添加的功能是基本上在可以转换的情况下(加上引号"可以"),我们会进行转换。我想看看这是否会让用户更加困惑,还是会提供更好的用户体验。所以如果你在写代码时注意到某些情况,并且感到疑惑,请告诉我们。我想知道这种方式是否对大家有意义。
Johnny Boursiquot:我已经......如果可以的话,我要给你一些实时反馈......[笑]
Mat Ryer :我们节目的新环节---
Johnny 的实时反馈。[笑声]
Johnny Boursiquot:Johnny 的实时、建设性、希望不要太烦人的反馈......即使这种转换是可能的,我仍然希望看到那个错误,因为我已经习惯了代码中的某种显式性。如果幕后发生了一些转换,从某种意义上说这改善了开发者体验......但我还是希望如果我传递了错误的类型,能得到某种程度的提醒。
Mat Ryer :但从某种意义上说你并没有传递错误的类型。当你添加它时,你确实传递了一个 int
......但这很有趣;你认为人们会明确使用类型来指定 add
中的类型吗?你可以通过 f.Add(int(0))
Katie Hockman: 嗯,我不确定。这是个问题。有些情况下,可能某个函数需要传递一个 int
,如果你传递 0,它是可以通过的。你今天不需要去包裹它。尽管如果你传递 0,通常它会默认使用 int64
,我想是这样的。所以有些情况下它实际上会在底层进行转换,你可能不会注意到,或者直到你真的仔细观察时才会意识到"哦,原来它确实做了转换"。所以我不知道,也许有,也许没有。也许这并没有意义。我想看看人们在实际使用中的反馈 [未听清 00:51:05.21]。肯定有些情况我们不应该这样做......比如,如果你调用 f.Add(-1)
,然后它被转换为 uint
,这会让它变成 uint
的最大值。我们肯定不想这么做,但我们正在研究如何做出这个决定,或者是否应该直接定个硬性规定"你必须自己处理",因为我们收到了反馈,认为另一种方式很烦人。
Jay Conrod: 我很好奇这个问题会随着时间如何发展......比如说你将函数的签名从 int
改成 int64
。你想让它更明确。因此你有很多缓存中的值,这些值是通过大量的 CPU 计算得出的,它们全是 int
。我们希望继续使用这些值,并在合适的情况下继续将它们转换为 int64
。这可能有助于保持模糊测试的覆盖率。
Mat Ryer: 是啊,这很有趣。
Katie Hockman: 这很棘手。关于这个问题有一个讨论。我可以找个方法---
可能我会在 Slack 上发,或者在 Twitter 上发......但确实有一个关于这个问题的讨论。如果你有反馈意见,请在那个讨论中发表。
Mat Ryer: 在 go-fuzz
包的 repo 中,有一个"奖杯"部分,列出了它帮助发现并修复的许多问题。你觉得这个功能也会有类似的东西吗?
Katie Hockman: 是的。这正是我 [未听清 00:52:18.17] 的地方,有些人通过 Twitter 私信我,或者发邮件告诉我"我发现了一个 bug!"我会说:"太好了!"然后我会把它记下来。某个时候我也想把这个公开出来。目前我还在收集这些信息。所以现在的情况是,如果你发现了什么问题,告诉我们,我们会记下来。不过将来,我希望找到一种方法,让人们能自己添加到这个奖杯列表中,或者至少能以更正式的方式报告这些问题。但我们确实想知道这些问题,所以如果你发现了什么,请告诉我们,如果你愿意分享的话。
Johnny Boursiquot: 有些发现会涉及安全问题吗?
Katie Hockman: 对,确实需要做出决定。如果你发现了一个崩溃问题,但它没有报告任何信息,我们是不会知道的。显然,它是在本地运行的,我们不会知道。当本地模糊测试崩溃时,我不会收到任何通知......不过这其实是件好事。[笑声] 但如果你发现了安全漏洞,除非你想让公众知道,否则不要告诉我。但如果它已经是公开提交的 bug,那就太好了。
Mat Ryer: 所以,你基本上不能保守秘密。好吧,既然说到不太受欢迎的事......现在是"不受欢迎的意见"时间!
Mat Ryer: 好,今天谁有一个"美味的"不受欢迎的意见要分享?
Johnny Boursiquot: 或者"苦涩的"......
Mat Ryer: 或者"苦涩的"......[笑] 对。
Jay Conrod: 我有一个。Mat,我不确定你会不会觉得这是对你个人的攻击......
Mat Ryer: 哦,不会是关于发际线的吧?
Johnny Boursiquot: 说吧!我想听听![笑]
Jay Conrod: 我的不受欢迎的意见是:Ctrl+V 或 Cmd+V(对于 Mac 用户来说)应该默认粘贴带有格式的内容。
Mat Ryer: 这太荒唐了。这真的是 [未听清 00:54:04.26]。
Jay Conrod: 我知道,对吧?我的理由是,如果你在同一个文档中粘贴,比如你要移动一个段落,你肯定希望保留格式。如果你从同一个应用程序的另一个文档中复制,可能你也想保留格式。我知道从浏览器粘贴时带有你不想要的格式很奇怪,但我认为 Ctrl+V 应该始终在任何地方都做同样的事情。我喜欢那些简单、不太"魔法化"的软件,或者至少是易于理解和解释的,即使它做的是复杂的事情。
Johnny Boursiquot: 所以你才会从事模糊测试的工作。[笑声]
Jay Conrod: 没错,这也是我在做模块工作的原因。[笑声] Go 是一门语言---
虽然其中有些部分对我来说有点太"魔法化"了,但 Go 是一门重视简洁和明确性的语言,这就是为什么我对粘贴有这样的"糟糕"意见。
Mat Ryer: 是啊。哇,这个意见真的......我从来没有在这个节目里生气过......[笑声] 你提出了一个非常有说服力的理由,我们会在 Twitter 上测试一下这个意见,但我之前提到过---
我不应该需要使用某种"复制粘贴爪子"来仅仅复制文本。我不记得我什么时候真的想要保留格式。我确实在 Word 文档或 Google Docs 里使用格式,我会正确使用样式,就像使用样式表一样;如果你这么做了,我不明白你为什么还会想要保留那些格式。
我觉得---
好吧,保留这些信息,但如果你想让背景颜色在某个小部分稍微不同一点 [未听清 00:55:54.01],那应该是你自己去操作,而不是别人这样。
Katie Hockman: 我有一个没那么让人生气的"不受欢迎的意见"。[笑声]
Mat Ryer: 好的。
Katie Hockman: 这个意见可能会让你感觉好一点。我不知道这是不是一个不太受欢迎的意见,但我觉得这个世界需要更多的技术写作者。我认为软件工程师在撰写文档方面平均水平并不高。当然,有些人做得很好,但我觉得世界上有太多的文档和服务,人们不去使用它们,因为文档写得很难读,或者是由那些太过专注于细节的人写的。如果我们有更多的技术写作者,世界会变得更加愉快、明亮,而且更容易理解。
Mat Ryer: 这很有意思。
Johnny Boursiquot: 看来你最近在读 OpenTelemetry 的文档......[笑声]
Mat Ryer: 他们的名字,我以为你在说他们的名字......
Johnny Boursiquot: 哦,对不起。
Katie Hockman: 我的意思是,我觉得技术写作者真的被低估了。
Mat Ryer: 是的,我完全同意。我喜欢和别人交换角色,去为其他项目写文档,因为在那个时候不理解某个东西反而是一种美德。所以这个意见确实很有意思,我也非常同意。好的文档真的能带来巨大的不同......所以我完全同意。如果你为文档做贡献,那么你绝对是一名真正的工程师。
好了,我们的时间到了。这真是一期很棒的节目。非常感谢你们的参与,也感谢你们带我们深入了解模糊测试这个疯狂的世界。请大家在家里试试,无论你在哪里,可能你现在还在家里......所以请试试,然后告诉我们你的感受。
非常感谢我们的嘉宾,Katie Hockman、Jay Conrod,当然还有 Johnny Boursiquot......Johnny,你也在这里,对吧?
Johnny Boursiquot: 是的,我在这里。你看到了我,对吧?
Mat Ryer: 对,没错。
Johnny Boursiquot: 好的。[笑声]
Mat Ryer: 你说那句话时听起来像在撒谎。
Katie Hockman: 非常感谢。
Mat Ryer: 谢谢!
Jay Conrod: 谢谢邀请我们来,这真是一次愉快的体验。
Mat Ryer: 太棒了。你们下次一定要再来。
Katie Hockman: 非常感谢。
Mat Ryer: 好的,下次再见,在 Go Time。
Mat Ryer: 我还是没法接受那个"复制粘贴"的事情。[笑声] 我真的......你是我见过的第一个有这种观点的人。我简直惊呆了。
Jay Conrod: 我就是想找一个真正不受欢迎的意见。
Mat Ryer: 我觉得你找到了。
Johnny Boursiquot: 是的,我觉得你击中了要害。
Jay Conrod: 在我过去的工作中,我曾在 Google Docs 工作,想象一个"复制粘贴爪子"让我感到莫名的快乐。
Johnny Boursiquot: 尤其是在 Mac 上,这真的是糟透了......
Mat Ryer: 是啊。这很奇怪。你自己找麻烦。有时它会锁定格式,然后你不得不向你的朋友和家人解释为什么 [未听清 00:59:54.23]。但你知道,你确实为这个观点提供了一个很好的理由,而我们在 Twitter 上发布这些意见时,大家听了这些理由,确实会被说服。这就是为什么大多数情况下,不受欢迎的意见反而会被投为"受欢迎"。所以我们拭目以待吧。
Johnny Boursiquot: 但这次不会。[笑]
Mat Ryer: 我希望不会,但我们拭目以待。
Jay Conrod: 我可能会收到 Twitter 上的通知,说"这个账号传播了糟糕的意见,被暂停 72 小时。"[笑声] [未听清 01:00:25.22]
Mat Ryer: 是啊,这会很有趣。而且通知的信息格式可能也有点错。[未听清 01:00:34.00] [笑声]
Johnny Boursiquot: 是不是复制粘贴的 [未听清 01:00:38.07]
Jay Conrod: 它可能会太过粗体,背景是黄色之类的......
Mat Ryer: 是啊,背景颜色。肯定吧。你怎么可能还想要背景颜色呢?