EVM是运行Solidity编译出来的Bytecode,如果编译器出问题,造成的结果是毁灭性的(比如之前的Vyper编译器安全事件)。Solidity编译器也在不断更新迭代,我们这里来回顾几个以往编译器版本存在的问题。
ABIEncoderV2 Array
Ethereum Release tag v0.5.10
在0.4.7
~0.5.9
之间,使用ABI encoder会造成一些未知错误。比如下面的代码:
pragma solidity 0.5.1;
pragma experimental ABIEncoderV2;
contract A {
uint[2][3] bad_arr = [[1, 2], [3, 4], [5, 6]];
/* Array of arrays passed to abi.encode is vulnerable */
function bad() public view returns(bytes memory){
bytes memory b = abi.encode(bad_arr);
return b;
}
}
看似bad_arr的结果是[[1, 2], [3, 4], [5, 6]]
,实际上ABI encoder解析出来的结果是如下:[[1, 2], [2, 3], [3, 4]]
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000004
为了避免这种潜在的问题,推荐使用^0.5.10
的编译器版本。
consturctor
Ethereum Release tag v0.4.23
在0.4.22
版本,一个合约可以包含两种构造器:constructor()
和function <Contract Name>()
,编译器选择的方式:谁先写,就用谁的。
举个例子:A合约constructor()
会被使用,x的结果是0。B合约function B()
会被使用,x的结果是1
pragma solidity 0.4.22;
contract A {
uint256 public x; // 0
constructor() public {
x = 0;
}
function A() public {
x = 1;
}
}
contract B {
uint256 public x; // 1
function B() public {
x = 1;
}
constructor() public {
x = 0;
}
}
建议统一采用constructor()
和高版本的编译器。
nested structs
Ethereum issues 5520
在0.5.0
版本之前,如果mapping中使用了嵌套struct的结构体,那么会返回错误的值。
下面的例子中,hello[any]
的值都是 128:
pragma solidity 0.4.26;
contract test {
struct A {
uint x;
}
struct B {
A y;
}
mapping(uint256 => B) public hello;
constructor() public { // 试试获取hello[0], [1], [2]的值
A memory a;
a.x = 10;
B memory b;
b.y = a;
hello[0] = b;
hello[1] = b;
hello[2] = b;
}
}
建议使用更高版本的编译器就没有问题了:
pragma solidity 0.8.17;
contract test {
struct A {
uint x;
uint256 get;
}
struct B {
uint256 num;
A y;
}
mapping(uint256 => B) public hello;
constructor() public {
A memory a;
a.x = 10;
a.get = 2;
B memory b;
b.y = a;
b.num = 11;
hello[1] = b;
}
// this works: hello[1]:
// 0:uint256: num 11
// 1:tuple(uint256,uint256): y 10,2
}
immutable&view bug
Ethereum issues 14049
此bug存在于目前任何版本的编译器(0~0.8.22),immutable的变量会被内联汇编隐式地修改。
来看这个例子:假设我们用0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
作为msg.sender来调用这个合约
pragma solidity 0.8.21;
contract C {
address public immutable a;
constructor() public {
a = 0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db;
F(msg.sender);
}
function F(address witnessAddress) public pure returns(address){
assembly{
mstore(0x80,witnessAddress)
}
}
}
运行结果是:a的值为0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
。很奇怪吧,一个pure的函数,居然会对合约产生影响。
原因:immutable类型的变量会从Memory 0x80位置开始依次存储(每bytes32一个变量),然后编译器会用SLOAD从这个位置取值到Stack中。这就意味着,如果在此期间,Memory的0x80位置之后的某些位置内容被修改了,就会影响这个immutable变量,即使是pure, view修饰的。这些操作是在内存中的,虽然不写到链上,但是如果有些操作需要用到Memory的数据,那么这些操作也会被影响。可能造成这种情况是使用内联汇编相关的操作,比如mstore,codecopy等。
其实严格意义上来说不算bug,这是EVM取immutable类型的值的规则,任何操作都是在堆栈上进行的,在使用内联汇编时需要格外注意。
总结
本文举了4个例子,其中三个是以往编译器存在的问题,一个是长期存在的,那么对于开发者来说,使用最新版本的编译器是最好的选择,在使用内联汇编的时候需要格外的小心。对于从事安全的审计师来说,熟悉编译器的潜在问题和EVM的运行规则更是至关重要的。
目前由于编译器造成的重大安全问题并不多,如果想探索更多,可以关注Ethereum issues和Ethereum Release发行页,有条件的可以研究其编译器。
作者:陈钦
编辑:舒婷
原文始发于微信公众号(ChainSecLabs):Compiler Bugs