2021年9月28日,Solidity 团队发现,对于短于 256 位的带符号整数类型的不可变变量,其值的符号扩展(清理)并不总是正确执行。
据我们所知,仅在使用内联汇编时才能以未清理状态访问该值。此错误从 Solidity 0.6.5 引入immutable 功能起就存在,并在 0.8.9 中修复。
我们为该错误分配了“极低”的严重性。
技术细节
在构造阶段,当 Solidity 中分配不可变变量时,该值会存储在内存中,直到创建完成。对于短于 32 字节的类型,这些值会在 32 字节字中左对齐存储,并且当再次读取这些值时,它们会相应地向右移位,但不会对值进行清理。特别是,不会对带符号值执行 signextend 操作。
例如,如果您将 -2 存储在 int8 中,则它将以二进制补码编码存储为 0xfe 的单字节值。由于 EVM 中的字使用 32 字节,因此在执行诸如带符号除法之类的操作之前,需要正确地对该值进行符号扩展,否则该值将被解释为正值。
符号扩展是指将高位设置为与符号位相同的值,符号位是实际类型的最高有效位。
以 32 位二进制补码表示的 -2(int8)为 0xfe,但是,在 EVM 中,它被符号扩展为 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe,当解释为 int256 时,它也表示值 -2。请注意,当解释为 int256 时,0xfe 为 +254。
之前没有发现缺少的符号扩展,因为 Solidity 始终在需要具有干净的高位(例如复制到内存、比较或除值)的操作之前执行额外的清理(如果执行两次,则优化器可以将其删除)。
因此,获取不可变变量的“未清理”值的唯一方法(据我们所知)是首先将其分配给局部变量,然后在内联汇编中从该局部变量读取。
受影响的示例
在下面的示例中,对 f() 的调用返回 0x000000...0000fe 而不是 0xffffff...fffffe。
contract C { int8 immutable x = -2; function f() public view returns (bytes32 r) { int8 y = x; assembly { r := y } } }
如果您使用类似 r = bytes32(int(y)); 的方法在没有内联汇编的情况下返回值,它将返回正确的值。使用 getter 时也是如此 - int8 public immutable x = -2。
需要内联汇编的理由
我们认为,此错误的出现需要内联汇编,这不仅是因为我们相信 Solidity 编译器在需要时始终正确地清理值,还因为另一个原因
此错误仅存在于我们称为“传统”代码生成器中。在新的基于 Yul-IR 的代码生成器(仍被视为实验性)中,已正确实现了读取不可变值时的清理。
为了在新实现中查找错误,我们使用生成式(基于语言语法生成的 Solidity 程序)和变异式(现有 Solidity 程序的变异变体)测试创建引擎定期对两种实现进行模糊测试。
当然,这种机制不仅可以查找新实现中的错误,还可以查找旧实现中的错误。
生成式引擎目前未使用内联汇编,因为我们知道两种实现的汇编级别存在差异,这会导致过多的误报。
由于模糊测试器在它成为编译器的一部分的近 18 个月内没有发现该错误,因此我们非常确定除了内联汇编之外,没有其他方法可以访问未清理的值。