Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions plugin/dapp/evm/executor/attack_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,10 @@ func privKey(idx int) crypto.PrivKey { return util.TestPrivkeyList[idx] }

func fundAddr(t *testing.T, exec *EVMExecutor, addr string, amount int64) {
t.Helper()
acc := exec.mStateDB.CoinsAccount.LoadAccount(addr)
// 写到执行器的 StateDB:mStateDB 的写缓冲每笔交易重建,写进去的余额不会留到下一次 Exec
acc := exec.GetCoinsAccount().LoadAccount(addr)
acc.Balance = amount
exec.mStateDB.CoinsAccount.SaveAccount(acc)
exec.GetCoinsAccount().SaveAccount(acc)
}

func addrFromRole(cfg *ctypes.Chain33Config, roleIdx int) string {
Expand Down
9 changes: 0 additions & 9 deletions plugin/dapp/evm/executor/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ func (evm *EVMExecutor) Exec(tx *types.Transaction, index int) (*types.Receipt,
func (evm *EVMExecutor) innerExec(msg *common.Message, txHash []byte, sigType int32, index int, txFee uint64, readOnly bool) (receipt *types.Receipt, err error) {

cfg := evm.GetAPI().GetConfig()
evm.mStateDB.ResetRevertError()
// 获取当前区块的上下文信息构造EVM上下文
context := evm.NewEVMContext(msg, txHash)
execAddr := evm.getEvmExecAddress()
Expand Down Expand Up @@ -192,14 +191,6 @@ func (evm *EVMExecutor) innerExec(msg *common.Message, txHash []byte, sigType in
return receipt, vmerr
}

// A snapshot revert that failed leaves the account cache and the receipt describing
// different state, which chain33's checkKV can no longer validate. Fail the transaction
// (fee-only receipt, state rolled back by the framework) instead of committing it.
if rerr := evm.mStateDB.RevertError(); rerr != nil {
log.Error("innerExec snapshot revert failed", "error info", rerr)
return receipt, rerr
}

// 计算消耗了多少费用(实际消耗的费用)
usedFee, overflow := common.SafeMul(usedGas, uint64(msg.GasPrice()))
// 费用消耗溢出,执行失败
Expand Down
88 changes: 0 additions & 88 deletions plugin/dapp/evm/executor/snapshot_revert_failure_test.go

This file was deleted.

3 changes: 2 additions & 1 deletion plugin/dapp/evm/executor/vm/runtime/blacklist_gap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,9 @@ func (e *gapEnv) fund(addr vmcommon.Address, amount int64) {
e.coins.SaveAccount(acc)
}

// balance 经由 mdb 读取:执行期间的转账只在 mdb 的写缓冲中,直接读 StateDB 看不到
func (e *gapEnv) balance(addr vmcommon.Address) int64 {
return e.coins.LoadAccount(addr.String()).Balance
return e.mdb.CoinsAccount.LoadAccount(addr.String()).Balance
}

func (e *gapEnv) deploy(addr, creator vmcommon.Address, code []byte) {
Expand Down
88 changes: 88 additions & 0 deletions plugin/dapp/evm/executor/vm/runtime/revert_mint_poc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package runtime

// 复现安全报告:回滚子调用的转账留在 coins 缓存里,被同一交易后续转账提交,造成增发。
// 提交态 = 交易前状态 + 回执 KV(GetChangedData),与 execTxOne/ExecKVMemSet 一致。

import (
"math/big"
"testing"

ctypes "github.com/33cn/chain33/types"
vmcommon "github.com/33cn/plugin/plugin/dapp/evm/executor/vm/common"
"github.com/stretchr/testify/require"
)

func zeroArgs4() []byte { return []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00} }

// Q:把收到的 value 原样 CALL 回 caller,然后 STOP。
func buildRefundCode() []byte {
code := zeroArgs4()
return append(code, 0x34, 0x33, 0x5a, 0xf1, 0x00) // CALLVALUE CALLER GAS CALL STOP
}

// P:无 calldata 则 REVERT;有 calldata 则先把 value 转给 q,再把同额转给 payout。
func buildTrapCode(q, payout vmcommon.Address) []byte {
code := []byte{0x36, 0x60, 0x09, 0x57, 0x60, 0x00, 0x60, 0x00, 0xfd, 0x5b}
code = append(code, zeroArgs4()...)
code = append(code, 0x34, 0x73)
code = append(code, q.Bytes()...)
code = append(code, 0x5a, 0xf1, 0x50)
code = append(code, zeroArgs4()...)
code = append(code, 0x34, 0x73)
code = append(code, payout.Bytes()...)
return append(code, 0x5a, 0xf1, 0x00)
}

func TestRevertedValueTransferMints(t *testing.T) {
env := newGapEnv(t, nil)
attacker, p, q, payout := addr(0xa1), addr(0xa2), addr(0xa3), addr(0xa4)
env.deploy(q, attacker, buildRefundCode())
env.deploy(p, attacker, buildTrapCode(q, payout))
const value = 100 * 1e8
env.fund(attacker, 1000*1e8)

addrs := []vmcommon.Address{attacker, p, q, payout}
committed := map[string]int64{}
var preSum int64
for _, a := range addrs {
committed[string(env.coins.AccountKey(a.String()))] = env.balance(a)
preSum += env.balance(a)
}

_, _, _, err := env.evm.Call(AccountRef(attacker), p, []byte{0x01}, 5_000_000, value)
require.NoError(t, err)

kvs, _ := env.mdb.GetChangedData(env.mdb.GetLastSnapshot().GetID())
for _, kv := range kvs {
if _, ok := committed[string(kv.Key)]; ok {
var acc ctypes.Account
require.NoError(t, ctypes.Decode(kv.Value, &acc))
committed[string(kv.Key)] = acc.Balance
}
}
var postSum int64
for _, a := range addrs {
b := committed[string(env.coins.AccountKey(a.String()))]
t.Logf("%s committed=%d cache=%d", a, b, env.balance(a))
postSum += b
}
require.Equal(t, preSum, postSum, "coin supply changed: minted %d", postSum-preSum)
}

// 同一帧内两次 SSTORE 同一槽位后 REVERT:Snapshot.revert 正序重放 prevalue,
// 槽位停在第一次写入的值而不是原值,父帧能读到被回滚帧写过的数据。
func TestRevertedDoubleSStoreLeaks(t *testing.T) {
env := newGapEnv(t, nil)
user, s := addr(0xb1), addr(0xb2)
code := []byte{0x36, 0x60, 0x19, 0x57, // 有 calldata 跳到内层
0x60, 0x00, 0x60, 0x00, 0x60, 0x01, 0x60, 0x00, 0x60, 0x00, 0x30, 0x5a, 0xf1, 0x50, // CALL self(1 字节 calldata)
0x60, 0x00, 0x54, 0x60, 0x01, 0x55, 0x00, // slot1 = slot0
0x5b, 0x60, 0x01, 0x60, 0x00, 0x55, 0x60, 0x02, 0x60, 0x00, 0x55, 0x60, 0x00, 0x60, 0x00, 0xfd} // slot0=1; slot0=2; REVERT
env.deploy(s, user, code)
_, _, _, err := env.evm.Call(AccountRef(user), s, nil, 5_000_000, 0)
require.NoError(t, err)
slot0 := env.mdb.GetState(s.String(), vmcommon.Hash{})
slot1 := env.mdb.GetState(s.String(), vmcommon.BigToHash(new(big.Int).SetInt64(1)))
t.Logf("slot0=%x slot1=%x", slot0.Big(), slot1.Big())
require.Equal(t, vmcommon.Hash{}, slot1, "parent frame observed a value written by a reverted frame")
}
103 changes: 103 additions & 0 deletions plugin/dapp/evm/executor/vm/state/journal_kv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright Fuzamei Corp. 2018 All Rights Reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package state

import (
"sort"

"github.com/33cn/chain33/common/db"
"github.com/33cn/chain33/types"
)

// journalKV 是 MemoryStateDB 与执行器 StateDB 之间的写缓冲,作用等同 chain33 的 txcache,
// 但支持与 Snapshot 版本一一对应的嵌套回滚。
//
// EVM 执行期间所有经由 MemoryStateDB 的状态写入(coins/token 账户等)只进入本缓冲:
// 回滚某个快照时,按 undo 日志倒序恢复写之前的值,调用方无需为每种写入单独实现 revert。
// 执行成功后由 GetChangedData 把留下的写入一次性交给回执,并写回底层 StateDB。
type journalKV struct {
parent db.KV
dirty map[string][]byte // nil 值表示该 key 在本交易内不存在
undo []kvUndo
marks []int // marks[v] 为快照 v 创建时 undo 的长度
}

type kvUndo struct {
key string
prev []byte
hadPrev bool // 写之前 dirty 中是否已有该 key
}

func newJournalKV(parent db.KV) *journalKV {
return &journalKV{parent: parent, dirty: make(map[string][]byte)}
}

// Get 先读缓冲,再读底层 StateDB
func (j *journalKV) Get(key []byte) ([]byte, error) {
if v, ok := j.dirty[string(key)]; ok {
if v == nil {
return nil, types.ErrNotFound
}
return v, nil
}
return j.parent.Get(key)
}

// Set 只写缓冲,并记录 undo
func (j *journalKV) Set(key []byte, value []byte) error {
k := string(key)
prev, had := j.dirty[k]
j.undo = append(j.undo, kvUndo{key: k, prev: prev, hadPrev: had})
if value != nil {
value = append([]byte(nil), value...)
}
j.dirty[k] = value
return nil
}

// Begin/Commit/Rollback 满足 db.KV 接口;交易级的提交与回滚由执行器框架负责
func (j *journalKV) Begin() {}
func (j *journalKV) Commit() error { return nil }
func (j *journalKV) Rollback() { j.revert(0) }

// snapshot 记录快照 version 的回滚点,version 必须等于当前快照数
func (j *journalKV) snapshot(version int) {
j.marks = append(j.marks[:version], len(j.undo))
}

// revertTo 把缓冲恢复到快照 version 创建时的状态
func (j *journalKV) revertTo(version int) {
if version < 0 || version >= len(j.marks) {
return
}
j.revert(j.marks[version])
j.marks = j.marks[:version]
}

func (j *journalKV) revert(n int) {
for i := len(j.undo) - 1; i >= n; i-- {
u := j.undo[i]
if u.hadPrev {
j.dirty[u.key] = u.prev
} else {
delete(j.dirty, u.key)
}
}
j.undo = j.undo[:n]
}

// changes 返回缓冲中的全部写入,按 key 排序保证各节点一致
func (j *journalKV) changes() []*types.KeyValue {
keys := make([]string, 0, len(j.dirty))
for k := range j.dirty {
keys = append(keys, k)
}
sort.Strings(keys)
kvs := make([]*types.KeyValue, 0, len(keys))
for _, k := range keys {
kvs = append(kvs, &types.KeyValue{Key: []byte(k), Value: j.dirty[k]})
}
return kvs
}
Loading
Loading