Skip to content

Commit 21c4335

Browse files
jwasingerholiman
authored andcommitted
eth/tracers/native: prevent panic for LOG edge-cases ethereum#26848
This PR fixes OOM panic in the callTracer as well as panicing on opcode validation errors (e.g. stack underflow) in callTracer and prestateTracer. Co-authored-by: Martin Holst Swende <[email protected]>
1 parent 7fcd9b8 commit 21c4335

File tree

7 files changed

+195
-89
lines changed

7 files changed

+195
-89
lines changed

eth/tracers/internal/tracetest/calltrace_test.go

Lines changed: 114 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import (
3232
"github.com/XinFinOrg/XDPoSChain/core/rawdb"
3333
"github.com/XinFinOrg/XDPoSChain/core/types"
3434
"github.com/XinFinOrg/XDPoSChain/core/vm"
35-
"github.com/XinFinOrg/XDPoSChain/crypto"
3635
"github.com/XinFinOrg/XDPoSChain/eth/tracers"
3736
"github.com/XinFinOrg/XDPoSChain/params"
3837
"github.com/XinFinOrg/XDPoSChain/rlp"
@@ -259,76 +258,123 @@ func benchTracer(tracerName string, test *callTracerTest, b *testing.B) {
259258
}
260259
}
261260

262-
// TestZeroValueToNotExitCall tests the calltracer(s) on the following:
263-
// Tx to A, A calls B with zero value. B does not already exist.
264-
// Expected: that enter/exit is invoked and the inner call is shown in the result
265-
func TestZeroValueToNotExitCall(t *testing.T) {
266-
var to = common.HexToAddress("0x00000000000000000000000000000000deadbeef")
267-
privkey, err := crypto.HexToECDSA("0000000000000000deadbeef00000000000000000000000000000000deadbeef")
268-
if err != nil {
269-
t.Fatalf("err %v", err)
270-
}
271-
signer := types.NewEIP155Signer(big.NewInt(1))
272-
tx, err := types.SignNewTx(privkey, signer, &types.LegacyTx{
273-
GasPrice: big.NewInt(0),
274-
Gas: 50000,
275-
To: &to,
276-
})
277-
if err != nil {
278-
t.Fatalf("err %v", err)
279-
}
280-
origin, _ := signer.Sender(tx)
281-
txContext := vm.TxContext{
282-
Origin: origin,
283-
GasPrice: big.NewInt(1),
284-
}
285-
context := vm.BlockContext{
286-
CanTransfer: core.CanTransfer,
287-
Transfer: core.Transfer,
288-
Coinbase: common.Address{},
289-
BlockNumber: new(big.Int).SetUint64(8000000),
290-
Time: new(big.Int).SetUint64(5),
291-
Difficulty: big.NewInt(0x30000),
292-
GasLimit: uint64(6000000),
293-
}
294-
var code = []byte{
295-
byte(vm.PUSH1), 0x0, byte(vm.DUP1), byte(vm.DUP1), byte(vm.DUP1), // in and outs zero
296-
byte(vm.DUP1), byte(vm.PUSH1), 0xff, byte(vm.GAS), // value=0,address=0xff, gas=GAS
297-
byte(vm.CALL),
261+
func TestInternals(t *testing.T) {
262+
var (
263+
to = common.HexToAddress("0x00000000000000000000000000000000deadbeef")
264+
origin = common.HexToAddress("0x00000000000000000000000000000000feed")
265+
txContext = vm.TxContext{
266+
Origin: origin,
267+
GasPrice: big.NewInt(1),
268+
}
269+
context = vm.BlockContext{
270+
CanTransfer: core.CanTransfer,
271+
Transfer: core.Transfer,
272+
Coinbase: common.Address{},
273+
BlockNumber: new(big.Int).SetUint64(8000000),
274+
Time: new(big.Int).SetUint64(5),
275+
Difficulty: big.NewInt(0x30000),
276+
GasLimit: uint64(6000000),
277+
}
278+
)
279+
mkTracer := func(name string, cfg json.RawMessage) tracers.Tracer {
280+
tr, err := tracers.DefaultDirectory.New(name, nil, cfg)
281+
if err != nil {
282+
t.Fatalf("failed to create call tracer: %v", err)
283+
}
284+
return tr
298285
}
299-
var alloc = types.GenesisAlloc{
300-
to: types.Account{
301-
Nonce: 1,
302-
Code: code,
286+
287+
for _, tc := range []struct {
288+
name string
289+
code []byte
290+
tracer tracers.Tracer
291+
want string
292+
}{
293+
{
294+
// TestZeroValueToNotExitCall tests the calltracer(s) on the following:
295+
// Tx to A, A calls B with zero value. B does not already exist.
296+
// Expected: that enter/exit is invoked and the inner call is shown in the result
297+
name: "ZeroValueToNotExitCall",
298+
code: []byte{
299+
byte(vm.PUSH1), 0x0, byte(vm.DUP1), byte(vm.DUP1), byte(vm.DUP1), // in and outs zero
300+
byte(vm.DUP1), byte(vm.PUSH1), 0xff, byte(vm.GAS), // value=0,address=0xff, gas=GAS
301+
byte(vm.CALL),
302+
},
303+
tracer: mkTracer("callTracer", nil),
304+
want: `{"from":"0x000000000000000000000000000000000000feed","gas":"0x7148","gasUsed":"0x54d8","to":"0x00000000000000000000000000000000deadbeef","input":"0x","calls":[{"from":"0x00000000000000000000000000000000deadbeef","gas":"0x6cbf","gasUsed":"0x0","to":"0x00000000000000000000000000000000000000ff","input":"0x","value":"0x0","type":"CALL"}],"value":"0x0","type":"CALL"}`,
303305
},
304-
origin: types.Account{
305-
Nonce: 0,
306-
Balance: big.NewInt(500000000000000),
306+
{
307+
name: "Stack depletion in LOG0",
308+
code: []byte{byte(vm.LOG3)},
309+
tracer: mkTracer("callTracer", json.RawMessage(`{ "withLog": true }`)),
310+
want: `{"from":"0x000000000000000000000000000000000000feed","gas":"0x7148","gasUsed":"0xc350","to":"0x00000000000000000000000000000000deadbeef","input":"0x","error":"stack underflow (0 \u003c=\u003e 5)","value":"0x0","type":"CALL"}`,
307311
},
308-
}
309-
statedb := tests.MakePreState(rawdb.NewMemoryDatabase(), alloc)
310-
// Create the tracer, the EVM environment and run it
311-
tracer, err := tracers.DefaultDirectory.New("callTracer", nil, nil)
312-
if err != nil {
313-
t.Fatalf("failed to create call tracer: %v", err)
314-
}
315-
evm := vm.NewEVM(context, txContext, statedb, nil, params.MainnetChainConfig, vm.Config{Tracer: tracer})
316-
msg, err := core.TransactionToMessage(tx, signer, nil, nil, nil)
317-
if err != nil {
318-
t.Fatalf("failed to prepare transaction for tracing: %v", err)
319-
}
320-
st := core.NewStateTransition(evm, msg, new(core.GasPool).AddGas(tx.Gas()))
321-
if _, err = st.TransitionDb(common.Address{}); err != nil {
322-
t.Fatalf("failed to execute transaction: %v", err)
323-
}
324-
// Retrieve the trace result and compare against the expected.
325-
res, err := tracer.GetResult()
326-
if err != nil {
327-
t.Fatalf("failed to retrieve trace result: %v", err)
328-
}
329-
wantStr := `{"from":"0x682a80a6f560eec50d54e63cbeda1c324c5f8d1b","gas":"0x7148","gasUsed":"0x54d8","to":"0x00000000000000000000000000000000deadbeef","input":"0x","calls":[{"from":"0x00000000000000000000000000000000deadbeef","gas":"0x6cbf","gasUsed":"0x0","to":"0x00000000000000000000000000000000000000ff","input":"0x","value":"0x0","type":"CALL"}],"value":"0x0","type":"CALL"}`
330-
if string(res) != wantStr {
331-
t.Fatalf("trace mismatch\n have: %v\n want: %v\n", string(res), wantStr)
312+
{
313+
name: "Mem expansion in LOG0",
314+
code: []byte{
315+
byte(vm.PUSH1), 0x1,
316+
byte(vm.PUSH1), 0x0,
317+
byte(vm.MSTORE),
318+
byte(vm.PUSH1), 0xff,
319+
byte(vm.PUSH1), 0x0,
320+
byte(vm.LOG0),
321+
},
322+
tracer: mkTracer("callTracer", json.RawMessage(`{ "withLog": true }`)),
323+
want: `{"from":"0x000000000000000000000000000000000000feed","gas":"0x7148","gasUsed":"0x5b9e","to":"0x00000000000000000000000000000000deadbeef","input":"0x","logs":[{"address":"0x00000000000000000000000000000000deadbeef","topics":[],"data":"0x000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}],"value":"0x0","type":"CALL"}`,
324+
},
325+
// TODO(daniel): make this test case pass, ref: PR #26848
326+
// {
327+
// // Leads to OOM on the prestate tracer
328+
// name: "Prestate-tracer - mem expansion in CREATE2",
329+
// code: []byte{
330+
// byte(vm.PUSH1), 0x1,
331+
// byte(vm.PUSH1), 0x0,
332+
// byte(vm.MSTORE),
333+
// byte(vm.PUSH1), 0x1,
334+
// byte(vm.PUSH5), 0xff, 0xff, 0xff, 0xff, 0xff,
335+
// byte(vm.PUSH1), 0x1,
336+
// byte(vm.PUSH1), 0x0,
337+
// byte(vm.CREATE2),
338+
// byte(vm.PUSH1), 0xff,
339+
// byte(vm.PUSH1), 0x0,
340+
// byte(vm.LOG0),
341+
// },
342+
// tracer: mkTracer("prestateTracer", json.RawMessage(`{ "withLog": true }`)),
343+
// want: `{"0x0000000000000000000000000000000000000000":{"balance":"0x0"},"0x000000000000000000000000000000000000feed":{"balance":"0x1c6bf52640350"},"0x00000000000000000000000000000000deadbeef":{"balance":"0x0","code":"0x6001600052600164ffffffffff60016000f560ff6000a0"}}`,
344+
// },
345+
} {
346+
statedb := tests.MakePreState(rawdb.NewMemoryDatabase(),
347+
types.GenesisAlloc{
348+
to: types.Account{
349+
Code: tc.code,
350+
},
351+
origin: types.Account{
352+
Balance: big.NewInt(500000000000000),
353+
},
354+
})
355+
evm := vm.NewEVM(context, txContext, statedb, nil, params.MainnetChainConfig, vm.Config{Tracer: tc.tracer})
356+
msg := &core.Message{
357+
To: &to,
358+
From: origin,
359+
Value: big.NewInt(0),
360+
GasLimit: 50000,
361+
GasPrice: big.NewInt(0),
362+
GasFeeCap: big.NewInt(0),
363+
GasTipCap: big.NewInt(0),
364+
SkipAccountChecks: false,
365+
}
366+
st := core.NewStateTransition(evm, msg, new(core.GasPool).AddGas(msg.GasLimit))
367+
if _, err := st.TransitionDb(common.Address{}); err != nil {
368+
t.Fatalf("test %v: failed to execute transaction: %v", tc.name, err)
369+
}
370+
// Retrieve the trace result and compare against the expected
371+
res, err := tc.tracer.GetResult()
372+
if err != nil {
373+
t.Fatalf("test %v: failed to retrieve trace result: %v", tc.name, err)
374+
}
375+
if string(res) != tc.want {
376+
t.Fatalf("test %v: trace mismatch\n have: %v\n want: %v\n", tc.name, string(res), tc.want)
377+
}
332378
}
333379
}
334380

eth/tracers/js/goja.go

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,6 @@ import (
3131
"github.com/dop251/goja"
3232
)
3333

34-
const (
35-
memoryPadLimit = 1024 * 1024
36-
)
37-
3834
var assetTracers = make(map[string]string)
3935

4036
// init retrieves the JavaScript transaction tracers included in go-ethereum.
@@ -570,14 +566,10 @@ func (mo *memoryObj) slice(begin, end int64) ([]byte, error) {
570566
if end < begin || begin < 0 {
571567
return nil, fmt.Errorf("tracer accessed out of bound memory: offset %d, end %d", begin, end)
572568
}
573-
mlen := mo.memory.Len()
574-
if end-int64(mlen) > memoryPadLimit {
575-
return nil, fmt.Errorf("tracer reached limit for padding memory slice: end %d, memorySize %d", end, mlen)
569+
slice, err := tracers.GetMemoryCopyPadded(mo.memory, begin, end-begin)
570+
if err != nil {
571+
return nil, err
576572
}
577-
slice := make([]byte, end-begin)
578-
end = min(end, int64(mo.memory.Len()))
579-
ptr := mo.memory.GetPtr(begin, end-begin)
580-
copy(slice[:], ptr[:])
581573
return slice, nil
582574
}
583575

@@ -958,10 +950,3 @@ func (l *steplog) setupObject() *goja.Object {
958950
o.Set("contract", l.contract.setupObject())
959951
return o
960952
}
961-
962-
func min(a, b int64) int64 {
963-
if a < b {
964-
return a
965-
}
966-
return b
967-
}

eth/tracers/js/tracer_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,12 @@ func TestTracer(t *testing.T) {
150150
}, {
151151
code: "{res: [], step: function(log) { if (log.op.toString() === 'STOP') { this.res.push(log.memory.slice(5, 1025 * 1024)) } }, fault: function() {}, result: function() { return this.res }}",
152152
want: "",
153-
fail: "tracer reached limit for padding memory slice: end 1049600, memorySize 32 at step (<eval>:1:83(20)) in server-side tracer function 'step'",
153+
fail: "reached limit for padding memory slice: 1049568 at step (<eval>:1:83(20)) in server-side tracer function 'step'",
154154
contract: []byte{byte(vm.PUSH1), byte(0xff), byte(vm.PUSH1), byte(0x00), byte(vm.MSTORE8), byte(vm.STOP)},
155155
},
156156
} {
157157
if have, err := execTracer(tt.code, tt.contract); tt.want != string(have) || tt.fail != err {
158-
t.Errorf("testcase %d: expected return value to be '%s' got '%s', error to be '%s' got '%s'\n\tcode: %v", i, tt.want, string(have), tt.fail, err, tt.code)
158+
t.Errorf("testcase %d: expected return value to be \n'%s'\n\tgot\n'%s'\nerror to be\n'%s'\n\tgot\n'%s'\n\tcode: %v", i, tt.want, string(have), tt.fail, err, tt.code)
159159
}
160160
}
161161
}

eth/tracers/native/call.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,10 @@ func (t *callTracer) CaptureEnd(output []byte, gasUsed uint64, err error) {
148148

149149
// CaptureState implements the EVMLogger interface to trace a single step of VM execution.
150150
func (t *callTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) {
151+
// skip if the previous op caused an error
152+
if err != nil {
153+
return
154+
}
151155
// Only logs need to be captured via opcode processing
152156
if !t.config.WithLog {
153157
return
@@ -176,7 +180,12 @@ func (t *callTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, sco
176180
topics[i] = common.Hash(topic.Bytes32())
177181
}
178182

179-
data := scope.Memory.GetCopy(int64(mStart.Uint64()), int64(mSize.Uint64()))
183+
data, err := tracers.GetMemoryCopyPadded(scope.Memory, int64(mStart.Uint64()), int64(mSize.Uint64()))
184+
if err != nil {
185+
// mSize was unrealistically large
186+
return
187+
}
188+
180189
log := callLog{Address: scope.Contract.Address(), Topics: topics, Data: hexutil.Bytes(data)}
181190
t.callstack[len(t.callstack)-1].Logs = append(t.callstack[len(t.callstack)-1].Logs, log)
182191
}

eth/tracers/native/prestate.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ func (t *prestateTracer) CaptureEnd(output []byte, gasUsed uint64, err error) {
133133

134134
// CaptureState implements the EVMLogger interface to trace a single step of VM execution.
135135
func (t *prestateTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) {
136+
if err != nil {
137+
return
138+
}
136139
stack := scope.Stack
137140
stackData := stack.Data()
138141
stackLen := len(stackData)

eth/tracers/tracers.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package tracers
1919

2020
import (
2121
"encoding/json"
22+
"fmt"
2223
"math/big"
2324

2425
"github.com/XinFinOrg/XDPoSChain/common"
@@ -95,3 +96,27 @@ func (d *directory) IsJS(name string) bool {
9596
// JS eval will execute JS code
9697
return true
9798
}
99+
100+
const (
101+
memoryPadLimit = 1024 * 1024
102+
)
103+
104+
// GetMemoryCopyPadded returns offset + size as a new slice.
105+
// It zero-pads the slice if it extends beyond memory bounds.
106+
func GetMemoryCopyPadded(m *vm.Memory, offset, size int64) ([]byte, error) {
107+
if offset < 0 || size < 0 {
108+
return nil, fmt.Errorf("offset or size must not be negative")
109+
}
110+
if int(offset+size) < m.Len() { // slice fully inside memory
111+
return m.GetCopy(offset, size), nil
112+
}
113+
paddingNeeded := int(offset+size) - m.Len()
114+
if paddingNeeded > memoryPadLimit {
115+
return nil, fmt.Errorf("reached limit for padding memory slice: %d", paddingNeeded)
116+
}
117+
cpy := make([]byte, size)
118+
if overlap := int64(m.Len()) - offset; overlap > 0 {
119+
copy(cpy, m.GetPtr(offset, overlap))
120+
}
121+
return cpy, nil
122+
}

eth/tracers/tracers_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,41 @@ func BenchmarkTransactionTrace(b *testing.B) {
124124
tracer.Reset()
125125
}
126126
}
127+
128+
func TestMemCopying(t *testing.T) {
129+
for i, tc := range []struct {
130+
memsize int64
131+
offset int64
132+
size int64
133+
wantErr string
134+
wantSize int
135+
}{
136+
{0, 0, 100, "", 100}, // Should pad up to 100
137+
{0, 100, 0, "", 0}, // No need to pad (0 size)
138+
{100, 50, 100, "", 100}, // Should pad 100-150
139+
{100, 50, 5, "", 5}, // Wanted range fully within memory
140+
{100, -50, 0, "offset or size must not be negative", 0}, // Errror
141+
{0, 1, 1024*1024 + 1, "reached limit for padding memory slice: 1048578", 0}, // Errror
142+
{10, 0, 1024*1024 + 100, "reached limit for padding memory slice: 1048666", 0}, // Errror
143+
144+
} {
145+
mem := vm.NewMemory()
146+
mem.Resize(uint64(tc.memsize))
147+
cpy, err := GetMemoryCopyPadded(mem, tc.offset, tc.size)
148+
if want := tc.wantErr; want != "" {
149+
if err == nil {
150+
t.Fatalf("test %d: want '%v' have no error", i, want)
151+
}
152+
if have := err.Error(); want != have {
153+
t.Fatalf("test %d: want '%v' have '%v'", i, want, have)
154+
}
155+
continue
156+
}
157+
if err != nil {
158+
t.Fatalf("test %d: unexpected error: %v", i, err)
159+
}
160+
if want, have := tc.wantSize, len(cpy); have != want {
161+
t.Fatalf("test %d: want %v have %v", i, want, have)
162+
}
163+
}
164+
}

0 commit comments

Comments
 (0)