安全漏洞和错误会降低软件质量。为了尽早发现它们,最好是在发布之前,我们采用了模糊测试:向Solidity编译器提供随机生成的程序,并观察编译运行时和生成的代码。
自2019年第一季度以来,Solidity编译器通过Google的开源软件模糊测试 (oss-fuzz)框架进行测试。
在这篇文章中,我们简要介绍了在这方面已经完成的工作,以及目前正在进行的工作。
模糊测试概述
广义上讲,我们开发了两类模糊测试器
- 前端模糊测试器,用于测试编译器前端(解析器/分析器)。
- 后端模糊测试器,用于测试编译器后端(优化器)。
前端模糊测试器
我们开发了前端模糊测试器,以测试程序是否被正确解析和分析。前端模糊测试包括对现有测试用例(通常是单元测试)进行变异,以测试解析和程序分析如何响应极端情况下的输入。
模糊测试之所以有效,是因为Solidity编译器在输入程序解析/分析阶段以及代码生成阶段都大量使用了断言。由前端模糊测试器生成的输入可能会导致这些断言失败,从而表明编译器存在错误。
前端模糊测试器发现的示例错误
Solidity实现了NatSpec格式 用于代码文档。前端模糊测试器生成了以下程序,导致编译器断言失败(更多信息请参见注释)
contract C { ///@return modifier m22 {} } contract D is C { modifier m22 {} }
该程序可能是从以下完全有效的测试程序中变异而来。
contract C { modifier m22 { _; } } contract D is C { }
变异操作将///@return 行添加到修饰符声明中,并将修饰符声明复制到派生合约中。
一个自然的问题是:模糊测试器是如何“知道”这种特定的变异会导致错误的?答案是它不知道;模糊测试器只是对现有输入进行变异以创建新输入,碰巧这种变异发现了错误。后续问题可能是:变异是如何创建的?广义上讲,变异属于以下类别之一
- 翻转输入流中某些位置的位。
- 添加可能由手工编写的字典支持的位。
- 重新排列位。
- 删除位。
由于Solidity程序是字符串,因此将变异类别考虑为字符(例如,a-z)而不是位可能更有用。
第二个后续问题可能是:等等!随机变异很少有用?在这种情况下,它们如何能发现错误?简短的答案是,是的,如果最终目标是创建一个有效的程序,它们很少有用。例如,将if (true) {} 变异为iff (true) {}(在if关键字后添加后缀f)的可能性远大于变异为if (false) {}。这种可能的变异只会导致解析错误,这相当无趣。但请记住,这里的目标是测试编译器前端,为此,随机变异非常有用。特别是通过随机性和人工支持(Solidity关键字字典)的混合生成的程序。模糊测试器通过在其他操作中添加///@return NatSpec注释生成的测试用例证明了这一点。
如果您好奇模糊测试器是如何“合成”NatSpec注释的,其中一种方法(注意:纯粹的推测)如下
- 在某些模糊测试运行N中,添加字典标记“//”(即Solidity中的注释语法)。
- 在运行N' > N中,添加字典标记“return”。
- 最后,通过一系列添加/重新排列操作得到///@return。
Solidity模糊测试字典可以在这里找到。如果您发现有遗漏(特别是那些可能产生有趣变异的遗漏),请告诉我们。欢迎提交PR。:-)
我们要感谢外部贡献者Alex Groce和Charalambos Mitropoulos独立对Solidity编译器进行模糊测试并报告错误。
注释:
这个问题已经存在几个月了,并在0.7.6版本中修复。问题在于Solidity修饰符没有返回值,因此@return NatSpec标记没有意义。上面的程序应该导致解析错误(修复程序确保了这一点),但实际上导致断言失败。
后端模糊测试器
为了测试代码生成是否正确,需要向编译器提供一个有效的程序(输入)。如果程序包含语法(iff而不是if)或语义(使用未声明的标识符)错误,编译器将不会接受它。语义模糊测试器旨在确保程序输入不包含此类错误。
为了实现语义模糊测试器,我们依赖于Google的libprotobuf-mutator库。广义上讲,编写语义模糊测试器涉及以下步骤
- 使用protobuf接口定义语言为Solidity编写(上下文无关)语法。
- 实现一个转换器接口,该接口接受protobuf语法的生成并转换为目标语言。
我们已经实现了一个用于测试Yul优化器正确性的语义模糊测试器。该模糊测试器随机生成Yul程序,并在优化前后跟踪其行为,以标记状态差异。这有助于我们确定优化器中是否引入了错误。
后端模糊测试器发现的示例错误
冗余赋值消除器是Yul优化器的一个步骤,它(不出所料!)删除了对变量的冗余赋值。后端模糊测试器生成了以下示例,演示了此步骤中的错误
{ for {let i:= 0} lt(i,2) {i := add(i,1)} { // x is declared and implicitly // initialized to zero. let x // This assignment is not redundant // since it is used by the mstore statement // after the if statement x := 1337 if lt(i,1) { // This assignment is redundant because of the `continue` x := 42 continue } mstore(0, x) } }
冗余赋值消除器消除了对x的所有赋值,导致mstore(0, x),从而将程序优化为包含mstore(0, 0)而不是mstore(0, 1337)。这个错误已在0.6.1版本中修复。
模糊测试器是如何生成此类程序的?简而言之,答案如下。在每次模糊测试迭代中
- Libprotobuf-mutator创建Yul的protobuf语法的生成。
- Libprotobuf解析此生成以确保它是一个有效的protobuf消息。
- 如果有效,则我们编写的转换器将接受protobuf消息。
- 转换器将此protobuf消息转换为yul程序。
如果您有兴趣查看Yul的protobuf语法和转换器,请查看这里和那里。我们始终欢迎您为改进语法/转换器做出贡献。
正在进行的工作
目前,我们正在开发一个新的Solidity程序生成器,它独立于libprotobuf和libprotobuf-mutator。这样做的主要原因是protobuf IDL几乎不适合捕获编程语言语义。此外,为高级编程语言编写上下文无关语法非常费力。
新模糊测试器生成器的最终目标是能够自动生成格式良好的Solidity程序。一旦实现,我们计划编译/优化并可能在EVM虚拟机上执行生成的字节码。这将有助于我们发现编译器引入的其他难以发现的错误,这些错误只有在检查EVM状态时才会出现。
请继续关注Solidity模糊测试的更多更新!