3 月 28 日,Solidity 代码生成器中发现了一个错误,该错误通过以太坊基金会赏金计划,由 Certora 的 John Toman 报告。此错误已在 2020 年 4 月 6 日发布的 0.6.5 版本中修复。此错误存在于所有之前的 Solidity 版本中。
我们将其严重程度级别指定为“低”,因为我们发现此错误并不常见,同时也很难利用。
谁应该关注
如果您部署了一个合约,该合约分配了用户提供的长度的内存数组,但没有复制或遍历它,则可能导致内存损坏。特别是,在分配内存数组时,如果长度足够大,会导致后续的内存分配与数组的内存区域重叠。但是,数组的长度被正确存储,因此在这些情况下,复制或遍历数组(我们筛选的所有合约在分配后立即执行的操作)将终止并以超出 gas 限制的方式回滚交易。
如何检查合约是否易受攻击
如果您使用new T[](length) 分配动态内存数组,其中长度取决于用户输入,您可能容易受到此错误的影响。如果可以提供足够大的长度,则后续的内存分配将具有重叠的内存区域,并且使用内存暂存空间的操作可能会使数组的内容失效。但是,如果您在创建数组后遍历或复制数组,则交易将以超出 gas 限制的方式回滚,因为数组长度已正确存储。
安全示例
contract C { function f(uint length) public { uint[] memory x = new uint[](length); // This is safe because the loop will run out of gas. for (uint i = 0; i < length; ++i) { x[i] = i**2; } ... } }
另一个安全示例
contract C { uint[] x; function f(uint length) public { // This is safe because the copy from memory to storage will // iterate over the whole array and go out-of-gas. x = new uint[](length); ... } }
可能存在漏洞的示例
contract C { function f(uint length, AnotherContract c, uint index) public { uint[] memory x = new uint[](length); uint[] memory y = new uint[](4); y[0] = c.g(x[index]); y[1] = c.g(x[index + 1]); y[2] = c.g(x[index + 2]); y[3] = c.g(x[index + 3]); c.h(y); } }
技术细节
Solidity 在运行时没有对动态数组分配施加上限长度。虽然数组的长度直接存储,但要分配的内存量(即“空闲内存指针”的增量)被计算为数组长度的 32 倍。由于此乘法没有防溢出保护,因此足够大的长度会导致溢出,导致实际分配的内存量很小(即空闲内存指针只会根据溢出大小增加)。因此,后续的内存分配将使用与最初分配的数组重叠的内存区域。从 Solidity 版本 0.6.5 开始,动态内存数组的最大分配大小为 2**64-1。尝试分配更大的数组现在将直接回滚。这有效地防止了此类溢出发生。