前序博客:
主要见:
Polygon zkEVM为提供(opcode层面兼容的)EVM等价zk-rollup,具有良好的用户体验并兼容现有以太坊生态和工具。
主要关注:
Spearbit研究人员分为2组:
经过10天(共12人周)的培训,Spearbit在(2022年12月5日至2022年12月16日)审计周期中,共发现了32个问题,其中致命漏洞7个,高风险漏洞1个,中等风险漏洞1个,不兼容漏洞1个,信息提示类22个,当前已解决了其中的27个问题。
在本审计报告中,风险分级为:
风险级别 | 影响:高 | 影响:中等 | 影响:低 |
---|---|---|---|
可能性:高 | 致命 | 高 | 中等 |
可能性:中等 | 高 | 中等 | 低 |
可能性:低 | 中等 | 低 | 低 |
其中:
本轮review重点关注:
除外的测试用例列表长度约为2200个entries,可分为如下类别:
已研究了一些预编译测试,因为有些支持的预编译(如ecrecover)失败,原因在于其结合了多个预编译(通常是sha256)。实际并不对这些测试做调整。
当前专注于研究因资源限制,且与EVM指定相关限制导致失败的测试用例。其中包括MemoryTests、VMTests、StackTests,但因时间有限,未覆盖每个测试用例。
本轮review重点关注:
该ROM中包含如下内容:
因时间有限,重点关注:
本轮review重点关注:
以PIL编写的所有zkEVM状态机有:
为辅助审计,开发了PIL状态机的形式化分析工具:
该PoC工具可做:
arith.pil状态机,形式化为,所有多项式以整数值表示。只要所有值和所有算术运算均在所用域内,则该形式化表示是正确的。
arith.pil状态机,展开为32行,初始行号为0,证明了其如下属性:
mem.pil状态机内,read,表示为 m O p ′ = 1 ∧ m W r ′ = 0 mOp'=1\land mWr'=0 mOp′=1∧mWr′=0,输出值 v a l i ′ , i ∈ [ 0 , 7 ] val_i',i\in[0,7] vali′,i∈[0,7]取决于 l a s t A c c e s s lastAccess lastAccess是否为 1 1 1。 l a s t A c c e s s lastAccess lastAccess为请求read之前的address access:
mem.pil状态机内,writes,可将任意值写入 v a l i ′ val_i' vali′内。这些值未在Mem状态机内约束,因此当有write时,其query是非确定性的:
writes和内存操作之间关系的另一简单但有用的定理为:
mem.pil中,内存访问表,先按地址排序,然后按step排序。从而要求下一行的地址要么是增加的,要么地址值不变,但step是增加的。除第一行和最后一行之外,其它相邻行都需满足该要求:
addr和lastAccess之间感兴趣的另一关系为,若当前行的lastAccess为0,则下一行的地址保持不变:
具体上下文见:
这些均会影响memory expansion。
以opCALLDATACOPY为例,其未对用于计算wordsize cost的free变量进行约束:
GAS - 3 => GAS :JMPN(outOfGas)
GAS - ${3*((C+31)/32)} => GAS :JMPN(outOfGas) ; Arith
在某些地方,如opRETURNDATACOPY中使用arith状态机来计算:
;${3*((C+31)/32)}
C+31 => A
;(C+31)/32
A :MSTORE(arithA)
32 :MSTORE(arithB)
:CALL(divARITH)
$ => A :MLOAD(arithRes1)
; Mul operation with Arith
; 3*((C+31)/32)
3 :MSTORE(arithA)
A :MSTORE(arithB)
:CALL(mulARITH)
$ => A :MLOAD(arithRes1)
GAS - A => GAS :JMPN(outOfGas)
具体修复PR见:
上下文见:
常量多项式BYTE_C4096,未对255之后的值做wrap around,支持inV值大于一个字节,从而可能carry over,并引起MSTORE8在写入指定byte到内存之前,写入一个 1 1 1。
背景知识为:
推荐修复策略:
@@ -11,7 +11,7 @@ const CONST_F = {
BYTE2B: (i) => (i & 0xFF), // [0..255]
// 0 (x4096), 1 (x4096), ..., 255 (x4096), 0 (x4096), ...
- BYTE_C4096: (i) => (i >> 12), // [0:4096..255:4096]
+ BYTE_C4096: (i) => (i >> 12) & 0xFF, // [0:4096..255:4096]
// 0 - 1023 WR256 = 0 WR8 = 0 i & 0xC00 = 0x000
// 1024 - 2047 WR256 = 0 WR8 = 0 i & 0xC00 = 0x400
修复PR见:
上下文见:
在opBYTE中,范围检查 31 - B => D :JMPN(opBYTE0)
是不正确的,因为其仅检查了B0,若任意的B1-B7为非零值,则opBYTE应为0值时,可能为其它值。
如:
类似用例未在Ethereum State Tests中覆盖,但多种不同版本的实现通过单元测试(如evmone)进行了覆盖。
推荐修复措施为:
:SUB
修复PR见:
上下文:
VMTests/vmPerformance/loopExp.json中的某些以太坊测试用例有如下错误:
Input: /0xPolygonHermez/zkevm-testvectors/tools/ethereum-tests/eth-inputs/GeneralStateTests/VMTests/loo c
pExp_1.json↪→
Start executor JS...
Error
Error: Program terminated with registers A, D, E, SR, CTX, PC, MAXMEM, zkPC not set to zero
失败的测试用例列表为:
loopExp_1
loopExp_2
loopExp_3
具体测试用例见:
相比于Ethereum tests,其修改为具有更低交易gas limit,并降低了loop迭代次数,以满足3000万 gas limit的限制。
很难诊断具体原因,因测试框架并不保存类似错误情况下的trace log文件,具体见后面的“Trace log is not saved in case of “register not set to zero” error”信息类漏洞。
可能是EXP实现中存在bug。
建议:
修复PR见:
上下文见:
该opSAR实现是不正确的。当A为“negative”(符号位为1)时,SHRarithBit错误为(0-A)。
如:sar(0x80…01, 1),结果为0xc0…01,而不是c0…00:
sign(A) == 1
abs(A) == 0x7f..ff
abs(A) >> 1 == 0x3f..ff
0 - (abs(A) >> 1) == 0xc0..01
如下测试用例应可发现该bug,但其被禁用了:
[ FAILED ] stShift.shiftCombinations
[ FAILED ] stShift.shiftSignedCombinations
建议:
NOT(SHR(NOT(A), D))
,其中A为shift数,D为要移动的位数,当前实现中,NOT为bit wise negation,而不是算术negation。MASK = SUB(0, E)
RESULT = XOR(SHR(XOR(A, MASK), D), MASK)
修复PR见:
上下文见:
divARITH子程序中,使用arith状态机来做除法。使用的为A * B + C = D * 2**256 +E
方程式,所有变量均为256位无符号数字,但该方程式是整数运算方程式。
该子程序会同时计算 E/A 和 E%A,E和A为该子程序的输入,并按状态机变量的方式赋值。
在调用arith状态机之前,该子程序会做如下2个checks:
然后当其调用arith状态机时,会设置:D=0, B=$(E/A)(free input), C=$(E%A)(free input)。选择这些free inputs,会让该状态机成功。
问题在于:
在第469行,其注释声称在调用完arith状态机后,会检查“remainder < divisor”,但实际代码做的是不同的检查:
这就意味着,恶意prover可伪造无效的除法和模运算。
注意,由于在调用arith状态机之前检查了E
推荐: 修复PR见: 上下文见: 当RESET为1时,prover可任意设置cIn中的值。这样,prover可设置cIn为1,来证明0+0==1。 建议: 修复PR见: 上下文见: 其仅检查目标是0x5b(JUMPDEST)byte,但不检查0x5b是否为之前PUSH指令的一部分。 虽然JUMPDEST opcode初始需求是为了让EVM代码更容易转换到机器代码(通过启用基本执行块探测),但后来开发人员和工具都期望使用此功能,以避免(恶意)错误隐藏在代码的数据部分。 如果zkEVM允许此类代码有效,则分析部署的代码将需要改进的工具,安全研究人员社区必须意识到这一差异。 如下字节码0x6001600055600A56615B5B5B应fail: 建议: 当前Polygon-Hermez认为: 上下文见: 在EIP-2: Homestead Hard-fork Changes中,限制了高S值的签名。 当前,s值大于secp256k1n/2的所有交易签名,都认为是无效的。该ECDSA recover预编译合约保持不变,且将仍保持接受高s值,这样做的好处有——在合约内恢复old Bitcoin签名。 processTx中的交易处理代码,会将raw签名细节传递给ecrecover,其仅验证r值在[1, secp256k1n-1]范围内,且s值在[1, secp256k1n-1]范围内。由于ecrecover代码用于预编译,其是合理的。但用于交易处理时,有效的范围应为[1, secp256k1n/2]。 详情也可参看以太坊黄皮书附录F。 建议: 修复PR见: 上下文: Ethereum Test Suite设计为支持多个协议版本(称为Network),可自genesis(称为Frontier)递增至最新release。 某些测试用例启用于: Polygon zkEVM仅与Berlin release紧密关联,可能会错过某些测试用例。https://github.com/spearbit-audits/ethereum-state-tests/tree/ecrecover-all-forks中更新了一些Berlin中启用的测试用例,但需要更深入地review该test suite。 建议: 修复的PR有:【Polygon zkEVM团队将review整个Ethereum test suite来识别与Berlin版本想过的missing test,以提升测试覆盖率】 在源码中,仍有不少TODO和question注释。这些可能导致行为是否是故意的不确定性。再加上许多以太坊状态测试案例被禁用,它们很可能掩盖了真正的问题。 建议: 相关修复PR: 当Ethereum测试执行结束有错误: 建议: 修复PR见: 上下文见: identity内部循环——名为IDENTITYreturnLoop,会做STEPS、BINARY、POSEIDON_G counters减法,但这些并未在该loop内使用,因为没有发生任何arithmetic计算或哈希计算。 这些减法是浪费的,同时会导致区块内包含更少的交易数。 建议: 修复PR见:【将在更底层的函数做zk-counters检查】 上下文见: 在opPUSH实现中,检查了push data是否在code boundaries: 无需做256位精度的比较,因为这些值均在32位范围内。因此,可将LT替换为常规减法: 此外,对于push data仅部分位于code boundaries内的病态情况,将D调整为B-PC。特别是,D可以为0,因此请检查readPush是否能正确处理。 最后,可以注意到,在病态情况下,推送到stack的值永远不会被访问(因为PUSH是最后一条指令)。因此,在这种情况下,可始终设置0=>D。 相关修复PR: 上下文见: eth-tests-folder.sh脚本总是重新生成输入,甚至没有update选项。这与eth-tests.sh脚本逻辑不一致,eth-tests.sh脚本支持不重新生成,仅运行测试用例。 建议: 同时,建议有可选项,来运行指定folder内的单个测试。 相关修复PR: 上下文: README中提及了一些额外的选项,但如何激活这些选项并不清晰: 如,可能想要对gen-inputs.js运行添加–evm-debug选项。 建议: 相关修复PR: 上下文: 左移操作分为2种情况: 这2种情况,都以完全相同的值来调用ARITH状态机,唯一的区别仅在于其中一种情况使用D=0(ARITH状态机内D为overflow),而另一种情况,D为包含该overflow的free input。 根据该overflow是否为0,会多次调用BINARY和exponentiation机制。最后,2种情况均可将D用作free input。 建议: 相关PR见: 上下文: 当单个Ethereum test文件内有多个测试时,gen-inputs.js 生成不同的具有 建议: Polygon-Hermez回复: 上下文: PaddingKK.crValid和PaddingKK.r8Valid具有相同的值。这不同于“Binary/MemAlign中的重复constant多项式”,因为这不是2个不相关状态机的偶然情况,而是在同一状态机内。2个多项式均在哈希相关状态机paper内被引用。 r8Valid: 建议: 相关修复PR: 上下文: 当运行evm_benchmarks中某些计算量重的测试用例时,观察到的结果输出是state root不匹配: 但是启用tracer output显示实际的 execution error 为 OOCS (out of counters): 建议: 相关修复PR:【更好的终端和详细输出】 上下文: ethereum/tests测试套件的设置很容易出错,尤其是考虑到不清楚要使用哪些分支,以及最近对其中一些分支的更改会中断运行,否则直到最近才能正常运行。 updated readme 有帮助,至少在验证所运行的测试套件是否正确时,是有帮助的,虽然偶尔有错误的分支。 当前的setup总结为: 主要障碍在于,zkevm-proverjs在其root目录下生成文件cache-main-pil.json。当切换该repo分支时,需手工删除该文件,或其有与新分支不兼容的风险。 修复PR: 上下文: opSDIV可实现为对binary状态机更少的调用次数。 其使用 建议: 修复PR: 上下文: opNOT中注释说2226,实际应为2256 建议: 修复PR: 上下文: 当测试执行结束出现如下错误: 看起来似乎是某个input generatiosn内存不足。如下日志指出: 最后,stQuadraticComplexity文件夹没有info.txt总结文件,且eth-tests-get-info.sh无法找到该test。 建议: Polygon-Hermez回复: 上下文: 以eth-tests.sh脚本来运行ethereum tests,要求clone zkevm-rom库,并构建rom.json文件,当首次设置tests时,这些并不明确。同时,还需要zkevm-proverjs repo。 建议: 修复PR: 上下文: 当gas limit超过30M时,则不做尝试,甚至其实际可能花费更少的gas。测试中的交易gas limit可为人一直,且并不代表其实际要花费的gas量。 建议: Polygon-Hermez回复: 上下文: Binary.P_A和MemAlign.BYTE2A是相同的,Binary.P_B和MemAlign.BYTE2B是相同的。 建议: 修复: 上下文: 该框架循环从以太坊测试套件中读取各个目录树下的每个JSON tests,以生成eth-inputs。但是,其遍历代码默认最大深度为3,而新的stEOF/stEIP3540文件深度超过了3。 在提取test suit时,由于eth-test.sh不强化指定的commit hash,当首次尝试setup tests时,可创建随机的新的失败。 建议: 相关PR: 上下文: EVM中的很多指令不会引起stack overflow。zkEVM中的某些指令实现,具有不必要的stack overflow检查,如opNOT和opDUPx中的%CALLDATA_OFFSET - SP :JMPN(stackOverflow)。 建议: 修复PR: 上下文: 调用FirstByte的测试用例 被禁用,具体见develop分支中的execption列表和main分支中的execption列表。这些会检查新合约的创建code不能以0xEF字节开头。 缺少EIP-3541的支持可以理解,因zkEVM支持Berlin,且该修改是在London upgrade中引入。但是,这是一个future兼容性修改,可回滚多个网络,实际支持是有用的。 建议: 修复PR: 上下文: 无需手工干预的情况下,当前无法复制和安装zkevm-testvectors。如,checkout v0.4.0.0,和运行npm install 或 yarn install,有: 在zkevm-commonjs/package.json中引用了无效的package版本: 该问题的原因在于,最新的npm/yarn期待version版本是Semver兼容的,相关问题见:Private github repository returns invalid package version #6195。 当前的解决方案是,创建所有依赖的赋值版本,并替换其版本号为仅包含3个点(如0.4.0、0.4.1、0.4.0-patch1)。此外,需同时在testvectors和修改后的commonjs中运行install命令。 注意: 建议: 修复: 上下文: 以pilcom编译该文件返回错误,显然该约束degree太高(3)。 建议:
- ; check divisor > reminder
- C => A ; reminder
- E => B ; divisor
+ ; check divisor > reminder
+ C => A ; reminder
+ E => B ; divisor
2.7 致命漏洞7:binary状态机内未约束的carry值
cIn' * ( 1 - RESET' ) = cOut * ( 1 - RESET' );
如对于binary状态机的收割运算,该漏洞可被利用为:// sm_binary.js:337
+ pols.cIn[0] = 1n;
// binary.pil:82
- cIn' * (1 - RESET) = cOut * ( 1 - RESET' )
+ cIn' = cOut * ( 1 - RESET' )
3. 不兼容性漏洞
3.1 不兼容性漏洞1:可跳转进push-data,与以太坊主网背离
; check if is a jumpDest (0x5B)
0x5B => B
$ :EQ, JMPC(readCode, invalidJump)
以太坊jumpdest-analysis规定,不应跳转到push-data,这是执行过程中代码和数据的有力分离。# Bytes Opcode
# Location
# -------- ----------
# 00-01 PUSH1 0x01
# 02-03 PUSH1 0x00
# 04 SSTORE
# 05-06 PUSH1 0x0A (End of PUSH data location)
# 07 JUMP
# 08-0A PUSH2 0x5B5B
# 0B JUMPDEST
4. 高风险漏洞
4.1 高风险漏洞1:交易处理未拒绝可延展签名
5. 中等风险漏洞
5.1 中等风险漏洞1:Ethereum Test Suite设计运行多个版本而不是单个版本
6. 信息类风险
6.1 信息类风险1:源码中仍有TODO和question注释
6.2 信息类风险2:“register not set to zero”错误时,未保存trace log
Error: Program terminated with registers A, D, E, SR, CTX, PC, MAXMEM, zkPC not set to zero
时,在zkevm-proverjs/src/sm/sm_main/logs-full-trace 目录下并未创建trace文件,使得难于deug该错误。类似loopExp错误也存在没有trace文件,难于debug的问题。
6.3 信息类风险3:identity预编译内部循环检查了未使用变量
6.4 信息类风险4:可能的opPUSH优化
$ => B :MLOAD(bytecodeLength)
PC + D => A
$ :LT,JMPC(opAuxPUSHA2)
PC => A
B - A => D
$ => B :MLOAD(bytecodeLength)
B - PC - D :JMPN(opAuxPUSHA2)
B - PC => D
6.5 信息类风险5:应有选项来运行独立测试
if [ -f "eth-inputs/$group/$folder/info.txt" ] && [ "$1" != "update" ]
then
echo "Exist"
else
npx mocha --max-old-space-size=12000 gen-inputs.js --group $group --folder $folder --output
eth-inputs↪→
fi
6.6 信息类风险6:test运行时不易于使用debug选项
同时,生成的traces置于未说明的难以发现的路径,如:
6.7 信息类风险7:可能的SHL简化
6.8 信息类风险8:同一文件内的测试索引号是随机的
6.9 信息类风险9:PaddingKK中重复constant多项式
引入crValid:
6.10 信息类风险10:当state root不匹配时,测试框架报OOC execution错误
Input: 0xPolygonHermez2/zkevm-testvectors/tools/ethereum-tests/eth-inputs/GeneralStateTests/evm_benchma c
rks/JUMPDEST_n0.json↪→
Start executor JS...
Error
Error: Assert Error: newStateRoot does not match
{
tx_hash: '0xdb9be29bc49dd4615aae90d141869d51b320fb938e896bd3be9c240bd1fa18be',
rlp_tx: '0xf8638001843000000094be7c43a58000000000000000000000000000000180808207f3a0c77643128f488ea15b c
0d6ed338b8f99ad155a0f5a2eb8d5f3efc4c0ac1782a39a043f032bde2bd2b5fbdc154d32886bae6fd6664f↪→
f5f307d9adbf843b60dcd451f',
type: 0,
return_value: '',
gas_left: '29453541',
gas_used: '546459',
gas_refunded: '0',
error: 'OOCS',
create_address: '',
state_root: '0x786576b88c34f7812afdb7eb7a9a188948843e8361dd53deb376b895f21927aa',
logs: [],
call_trace: {
context: {
to: '0xbe7c43a580000000000000000000000000000001',
type: 'CALL',
data: '0x',
gas: '30000000',
value: '0',
batch: '0x00',
output: '',
gas_used: '546459',
execution_time: '',
old_state_root: '0x786576b88c34f7812afdb7eb7a9a188948843e8361dd53deb376b895f21927aa',
nonce: 0,
gasPrice: '1',
chainId: 1000,
from: '0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b',
return_value: ''
},
steps: [
...
6.11 信息类风险11:测试框架onboarding指南不清晰
但首次clone代码库时,不容易注意到该行为,但在现有setup中切换分支时,应说明该问题。
6.12 信息类风险12:opSDIV仅需调用binary状态机内的XOR一次而不是2次
opSDIV调用abs来分别获取绝对值和符号位。若符号位相同,则使用绝对值的unsigned除法。若符号位不同,则取绝对值的unsigned除法的负值。XOR(XOR(sgnA, sgnB), 1)
这种复杂的方式来计算“2个符号位是否不同”。实际上可使用EQ(sgnA, sgnB)
来实现相同的效果。
或者有其它更简单的方式来计算,从而压根不需要调用binary状态机。
6.13 信息类风险13:opNOT的无效注释
6.14 信息类风险14:若某测试文件的input生产内存不足,则跳过该测试文件夹
./eth-tests-get-info.sh: line 36: let: err_gen_perc=(100*0+0/2)/0: division by 0 (error token is "0")
./eth-tests-get-info.sh: line 37: let: err_exec_perc=(100*0+0/2)/0: division by 0 (error token is "0")
./eth-tests-get-info.sh: line 38: let: ok_perc=(100*0+0/2)/0: division by 0 (error token is "0")
Test name: Return50000_2_0.json
WRITE: /0xPolygonHermez/zkevm-testvectors/tools/ethereum-tests/eth-inputs/GeneralStateTests/stQuadratic c
ComplexityTest/Return50000_2_0.json↪→
Test name: Return50000_2_1.json
Killed
这可能会影响passed/failed/generation错误的统计分析。OOM错误掩埋在log中,当其实际被跳过时,很难注意到这些测试。
6.15 信息类风险15:未说明测试框架要求
6.16 信息类风险16:若交易 gaslimit > 30M,则跳过或忽略该测试
6.17 信息类风险17:Binary/MemAlign中的重复常量多项式
6.18 信息类风险18:test生成器解析stEOF tests(以及其他过于嵌套路径)失败
6.19 信息类风险19:很多opcodes中不必要的stack overflow检查
6.20 信息类风险20:应支持EIP-3541
6.21 信息类风险21:package.json库中npm/yarn使用不兼容Semver版本
% yarn install
yarn install v1.22.11
warning package.json: License should be a valid SPDX license expression
warning package.json: "dependencies" has dependency "chai" with range "^4.3.6" that collides with a
dependency in "devDependencies" of the same name with version "^4.3.4"↪→
info No lockfile found.
warning @0xpolygonhermez/[email protected]: License should be a valid SPDX license expression
warning @0xpolygonhermez/[email protected]: "dependencies" has dependency "chai" with range "^4.3.6"
that collides with a dependency in "devDependencies" of the same name with version "^4.3.4"↪→
[1/4] Resolving packages...
error Can't add "@0xpolygonhermez/zkevm-commonjs": invalid package version "0.4.0.1".
info Visit https://yarnpkg.com/en/docs/cli/install for documentation about this command.
"version": "0.4.0.1",
6.22 信息类风险22:PIL hello world multiplier不可编译
namespace Multiplier(%N);
pol constant SET;
pol commit freeIn;
pol commit out;
out' = SET*freeIn + (1 - SET)*(out * freeIn);
node src/pil.js multiplier.pil
Error: ERROR multiplier.pil:undefined: Degre too high
namespace Adder(%N);
pol constant SET;
pol commit freeIn;
pol commit out;
out' = SET*freeIn + (1 - SET)*(out + freeIn);
附录:Polygon Hermez 2.0 zkEVM系列博客