From 2d09e18eaf04818e7a0b6625c4d9ff83546c5a70 Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Thu, 9 Mar 2023 12:25:39 -0500 Subject: [PATCH 1/2] core/vm,eth/tracers/native: prevent tracer method handler crash when logs are enabled and a LOG opcode would underflow the stack or access memory that hasn't been expanded yet --- .../internal/tracetest/calltrace_test.go | 68 +++++++++++++++++++ eth/tracers/js/goja.go | 17 ++--- eth/tracers/native/call.go | 11 ++- eth/tracers/tracers.go | 34 ++++++++++ 4 files changed, 118 insertions(+), 12 deletions(-) diff --git a/eth/tracers/internal/tracetest/calltrace_test.go b/eth/tracers/internal/tracetest/calltrace_test.go index 62182e3a82b4..ee629a4ff3a1 100644 --- a/eth/tracers/internal/tracetest/calltrace_test.go +++ b/eth/tracers/internal/tracetest/calltrace_test.go @@ -332,3 +332,71 @@ func TestZeroValueToNotExitCall(t *testing.T) { t.Fatalf("trace mismatch\n have: %v\n want: %v\n", string(res), wantStr) } } + +func TestMemExpansion(t *testing.T) { + var ( + to = common.HexToAddress("0x00000000000000000000000000000000deadbeef") + origin = common.HexToAddress("0x00000000000000000000000000000000feed") + txContext = vm.TxContext{ + Origin: origin, + GasPrice: big.NewInt(1), + } + context = vm.BlockContext{ + CanTransfer: core.CanTransfer, + Transfer: core.Transfer, + Coinbase: common.Address{}, + BlockNumber: new(big.Int).SetUint64(8000000), + Time: 5, + Difficulty: big.NewInt(0x30000), + GasLimit: uint64(6000000), + } + alloc = core.GenesisAlloc{ + to: core.GenesisAccount{ + Code: []byte{ + byte(vm.PUSH1), 0x1, + byte(vm.PUSH1), 0x0, + byte(vm.MSTORE), + byte(vm.PUSH1), 0xff, + byte(vm.PUSH1), 0x0, + byte(vm.LOG0), + }, + }, + origin: core.GenesisAccount{ + Nonce: 0, + Balance: big.NewInt(500000000000000), + }, + } + ) + _, statedb := tests.MakePreState(rawdb.NewMemoryDatabase(), alloc, false) + // Create the tracer, the EVM environment and run it + tracer, err := tracers.DefaultDirectory.New("callTracer", nil, json.RawMessage(`{ "withLog": true }`)) + if err != nil { + t.Fatalf("failed to create call tracer: %v", err) + } + evm := vm.NewEVM(context, txContext, statedb, params.MainnetChainConfig, vm.Config{Debug: true, Tracer: tracer}) + + var nonce uint64 = 0 + var gasLimit uint64 = 50000 + value := big.NewInt(0) + gasPrice := big.NewInt(1) + gasFeeCap := big.NewInt(0) + gasTipCap := big.NewInt(0) + data := make([]byte, 0) + accessList := types.AccessList{} + isFake := false + + msg := types.NewMessage(origin, &to, nonce, value, gasLimit, gasPrice, gasFeeCap, gasTipCap, data, accessList, isFake) + st := core.NewStateTransition(evm, msg, new(core.GasPool).AddGas(gasLimit)) + if _, err = st.TransitionDb(); err != nil { + t.Fatalf("failed to execute transaction: %v", err) + } + // Retrieve the trace result and compare against the etalon + res, err := tracer.GetResult() + if err != nil { + t.Fatalf("failed to retrieve trace result: %v", err) + } + wantStr := `TODO fix this` + if string(res) != wantStr { + t.Fatalf("trace mismatch\n have: %v\n want: %v\n", string(res), wantStr) + } +} diff --git a/eth/tracers/js/goja.go b/eth/tracers/js/goja.go index 8e52f5b21077..bceb848aa257 100644 --- a/eth/tracers/js/goja.go +++ b/eth/tracers/js/goja.go @@ -32,10 +32,6 @@ import ( jsassets "github.com/ethereum/go-ethereum/eth/tracers/js/internal/tracers" ) -const ( - memoryPadLimit = 1024 * 1024 -) - var assetTracers = make(map[string]string) // init retrieves the JavaScript transaction tracers included in go-ethereum. @@ -258,7 +254,7 @@ func (t *jsTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope if !t.traceStep { return } - if t.err != nil { + if t.err != nil || err != nil { return } @@ -572,13 +568,12 @@ func (mo *memoryObj) slice(begin, end int64) ([]byte, error) { return nil, fmt.Errorf("tracer accessed out of bound memory: offset %d, end %d", begin, end) } mlen := mo.memory.Len() - if end-int64(mlen) > memoryPadLimit { - return nil, fmt.Errorf("tracer reached limit for padding memory slice: end %d, memorySize %d", end, mlen) - } slice := make([]byte, end-begin) - end = min(end, int64(mo.memory.Len())) - ptr := mo.memory.GetPtr(begin, end-begin) - copy(slice[:], ptr[:]) + end = min(end, int64(mlen)) + slice, err := tracers.GetMemoryCopyPadded(mo.memory, begin, end-begin) + if err != nil { + return nil, err + } return slice, nil } diff --git a/eth/tracers/native/call.go b/eth/tracers/native/call.go index 02ee152a5aa1..88fc2be40c25 100644 --- a/eth/tracers/native/call.go +++ b/eth/tracers/native/call.go @@ -148,6 +148,10 @@ func (t *callTracer) CaptureEnd(output []byte, gasUsed uint64, err error) { // CaptureState implements the EVMLogger interface to trace a single step of VM execution. func (t *callTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { + // skip if the previous op caused an error + if err != nil { + return + } // Only logs need to be captured via opcode processing if !t.config.WithLog { return @@ -176,7 +180,12 @@ func (t *callTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, sco topics[i] = common.Hash(topic.Bytes32()) } - data := scope.Memory.GetCopy(int64(mStart.Uint64()), int64(mSize.Uint64())) + data, err := tracers.GetMemoryCopyPadded(scope.Memory, int64(mStart.Uint64()), int64(mSize.Uint64())) + if err != nil { + // mSize was unrealistically large + return + } + log := callLog{Address: scope.Contract.Address(), Topics: topics, Data: hexutil.Bytes(data)} t.callstack[len(t.callstack)-1].Logs = append(t.callstack[len(t.callstack)-1].Logs, log) } diff --git a/eth/tracers/tracers.go b/eth/tracers/tracers.go index 856f52a10de3..78cab68b23a8 100644 --- a/eth/tracers/tracers.go +++ b/eth/tracers/tracers.go @@ -19,6 +19,7 @@ package tracers import ( "encoding/json" + "fmt" "math/big" "github.com/ethereum/go-ethereum/common" @@ -95,3 +96,36 @@ func (d *directory) IsJS(name string) bool { // JS eval will execute JS code return true } + +const ( + memoryPadLimit = 1024 * 1024 +) + +// GetMemoryCopyPadded returns offset + size as a new slice. +// It zero-pads the slice if it extends beyond memory bounds. +func GetMemoryCopyPadded(m *vm.Memory, offset, size int64) ([]byte, error) { + if offset < 0 { + return nil, fmt.Errorf("offset cannot be smaller than zero") + } + + if int(offset) >= m.Len() { + // case 1: the slice falls entirely outside memory bounds + if size > memoryPadLimit { + return nil, fmt.Errorf("reached limit for padding memory slice: offset %d, size %d", offset, size) + } + padded := make([]byte, size) + return padded, nil + } else if int(offset+size) >= m.Len() { + // case 2: the slice overlaps memory bounds + if int(offset+size)-m.Len() > memoryPadLimit { + return nil, fmt.Errorf("reached limit for padding memory slice: offset %d, size %d", offset, size) + } + padded := make([]byte, size) + memSlice := m.GetCopy(offset, int64(m.Len()) - offset) + copy(padded[0:len(memSlice)], memSlice) + return padded, nil + } + // case 3: the slice falls inside memory bounds + memSlice := m.GetCopy(offset, size) + return memSlice, nil +} From 7aa085ea0d559742ed130ed5be47b2292a209efb Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Mon, 20 Mar 2023 13:03:44 +0100 Subject: [PATCH 2/2] eth/tracers/internal: tests for log memexpansion and stack depletion eth/tracers: refactor mem-copy and add test eth/tracers/internal: add test producing OOM on prestate-tracer eth/tracers: fix OOM on prestate-tracer eth/tracers/js: make memory.slice use padding eth/tracers/js: rm unused 'min'-method --- .../internal/tracetest/calltrace_test.go | 217 ++++++++---------- eth/tracers/js/goja.go | 12 +- eth/tracers/js/tracer_test.go | 4 +- eth/tracers/native/prestate.go | 3 + eth/tracers/tracers.go | 37 ++- eth/tracers/tracers_test.go | 38 +++ 6 files changed, 155 insertions(+), 156 deletions(-) diff --git a/eth/tracers/internal/tracetest/calltrace_test.go b/eth/tracers/internal/tracetest/calltrace_test.go index ee629a4ff3a1..c5405e245179 100644 --- a/eth/tracers/internal/tracetest/calltrace_test.go +++ b/eth/tracers/internal/tracetest/calltrace_test.go @@ -31,7 +31,6 @@ import ( "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" - "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" @@ -260,80 +259,7 @@ func benchTracer(tracerName string, test *callTracerTest, b *testing.B) { } } -// TestZeroValueToNotExitCall tests the calltracer(s) on the following: -// Tx to A, A calls B with zero value. B does not already exist. -// Expected: that enter/exit is invoked and the inner call is shown in the result -func TestZeroValueToNotExitCall(t *testing.T) { - var to = common.HexToAddress("0x00000000000000000000000000000000deadbeef") - privkey, err := crypto.HexToECDSA("0000000000000000deadbeef00000000000000000000000000000000deadbeef") - if err != nil { - t.Fatalf("err %v", err) - } - signer := types.NewEIP155Signer(big.NewInt(1)) - tx, err := types.SignNewTx(privkey, signer, &types.LegacyTx{ - GasPrice: big.NewInt(0), - Gas: 50000, - To: &to, - }) - if err != nil { - t.Fatalf("err %v", err) - } - origin, _ := signer.Sender(tx) - txContext := vm.TxContext{ - Origin: origin, - GasPrice: big.NewInt(1), - } - context := vm.BlockContext{ - CanTransfer: core.CanTransfer, - Transfer: core.Transfer, - Coinbase: common.Address{}, - BlockNumber: new(big.Int).SetUint64(8000000), - Time: 5, - Difficulty: big.NewInt(0x30000), - GasLimit: uint64(6000000), - } - var code = []byte{ - byte(vm.PUSH1), 0x0, byte(vm.DUP1), byte(vm.DUP1), byte(vm.DUP1), // in and outs zero - byte(vm.DUP1), byte(vm.PUSH1), 0xff, byte(vm.GAS), // value=0,address=0xff, gas=GAS - byte(vm.CALL), - } - var alloc = core.GenesisAlloc{ - to: core.GenesisAccount{ - Nonce: 1, - Code: code, - }, - origin: core.GenesisAccount{ - Nonce: 0, - Balance: big.NewInt(500000000000000), - }, - } - _, statedb := tests.MakePreState(rawdb.NewMemoryDatabase(), alloc, false) - // Create the tracer, the EVM environment and run it - tracer, err := tracers.DefaultDirectory.New("callTracer", nil, nil) - if err != nil { - t.Fatalf("failed to create call tracer: %v", err) - } - evm := vm.NewEVM(context, txContext, statedb, params.MainnetChainConfig, vm.Config{Debug: true, Tracer: tracer}) - msg, err := core.TransactionToMessage(tx, signer, nil) - if err != nil { - t.Fatalf("failed to prepare transaction for tracing: %v", err) - } - st := core.NewStateTransition(evm, msg, new(core.GasPool).AddGas(tx.Gas())) - if _, err = st.TransitionDb(); err != nil { - t.Fatalf("failed to execute transaction: %v", err) - } - // Retrieve the trace result and compare against the etalon - res, err := tracer.GetResult() - if err != nil { - t.Fatalf("failed to retrieve trace result: %v", err) - } - 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"}` - if string(res) != wantStr { - t.Fatalf("trace mismatch\n have: %v\n want: %v\n", string(res), wantStr) - } -} - -func TestMemExpansion(t *testing.T) { +func TestInternals(t *testing.T) { var ( to = common.HexToAddress("0x00000000000000000000000000000000deadbeef") origin = common.HexToAddress("0x00000000000000000000000000000000feed") @@ -350,53 +276,104 @@ func TestMemExpansion(t *testing.T) { Difficulty: big.NewInt(0x30000), GasLimit: uint64(6000000), } - alloc = core.GenesisAlloc{ - to: core.GenesisAccount{ - Code: []byte{ - byte(vm.PUSH1), 0x1, - byte(vm.PUSH1), 0x0, - byte(vm.MSTORE), - byte(vm.PUSH1), 0xff, - byte(vm.PUSH1), 0x0, - byte(vm.LOG0), - }, - }, - origin: core.GenesisAccount{ - Nonce: 0, - Balance: big.NewInt(500000000000000), - }, - } ) - _, statedb := tests.MakePreState(rawdb.NewMemoryDatabase(), alloc, false) - // Create the tracer, the EVM environment and run it - tracer, err := tracers.DefaultDirectory.New("callTracer", nil, json.RawMessage(`{ "withLog": true }`)) - if err != nil { - t.Fatalf("failed to create call tracer: %v", err) + mkTracer := func(name string, cfg json.RawMessage) tracers.Tracer { + tr, err := tracers.DefaultDirectory.New(name, nil, cfg) + if err != nil { + t.Fatalf("failed to create call tracer: %v", err) + } + return tr } - evm := vm.NewEVM(context, txContext, statedb, params.MainnetChainConfig, vm.Config{Debug: true, Tracer: tracer}) - - var nonce uint64 = 0 - var gasLimit uint64 = 50000 - value := big.NewInt(0) - gasPrice := big.NewInt(1) - gasFeeCap := big.NewInt(0) - gasTipCap := big.NewInt(0) - data := make([]byte, 0) - accessList := types.AccessList{} - isFake := false - msg := types.NewMessage(origin, &to, nonce, value, gasLimit, gasPrice, gasFeeCap, gasTipCap, data, accessList, isFake) - st := core.NewStateTransition(evm, msg, new(core.GasPool).AddGas(gasLimit)) - if _, err = st.TransitionDb(); err != nil { - t.Fatalf("failed to execute transaction: %v", err) - } - // Retrieve the trace result and compare against the etalon - res, err := tracer.GetResult() - if err != nil { - t.Fatalf("failed to retrieve trace result: %v", err) - } - wantStr := `TODO fix this` - if string(res) != wantStr { - t.Fatalf("trace mismatch\n have: %v\n want: %v\n", string(res), wantStr) + for _, tc := range []struct { + name string + code []byte + tracer tracers.Tracer + want string + }{ + { + // TestZeroValueToNotExitCall tests the calltracer(s) on the following: + // Tx to A, A calls B with zero value. B does not already exist. + // Expected: that enter/exit is invoked and the inner call is shown in the result + name: "ZeroValueToNotExitCall", + code: []byte{ + byte(vm.PUSH1), 0x0, byte(vm.DUP1), byte(vm.DUP1), byte(vm.DUP1), // in and outs zero + byte(vm.DUP1), byte(vm.PUSH1), 0xff, byte(vm.GAS), // value=0,address=0xff, gas=GAS + byte(vm.CALL), + }, + tracer: mkTracer("callTracer", nil), + 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"}`, + }, + { + name: "Stack depletion in LOG0", + code: []byte{byte(vm.LOG3)}, + tracer: mkTracer("callTracer", json.RawMessage(`{ "withLog": true }`)), + want: `{"from":"0x000000000000000000000000000000000000feed","gas":"0x7148","gasUsed":"0xc350","to":"0x00000000000000000000000000000000deadbeef","input":"0x","error":"stack underflow (0 \u003c=\u003e 5)","value":"0x0","type":"CALL"}`, + }, + { + name: "Mem expansion in LOG0", + code: []byte{ + byte(vm.PUSH1), 0x1, + byte(vm.PUSH1), 0x0, + byte(vm.MSTORE), + byte(vm.PUSH1), 0xff, + byte(vm.PUSH1), 0x0, + byte(vm.LOG0), + }, + tracer: mkTracer("callTracer", json.RawMessage(`{ "withLog": true }`)), + want: `{"from":"0x000000000000000000000000000000000000feed","gas":"0x7148","gasUsed":"0x5b9e","to":"0x00000000000000000000000000000000deadbeef","input":"0x","logs":[{"address":"0x00000000000000000000000000000000deadbeef","topics":[],"data":"0x000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}],"value":"0x0","type":"CALL"}`, + }, + { + // Leads to OOM on the prestate tracer + name: "Prestate-tracer - mem expansion in CREATE2", + code: []byte{ + byte(vm.PUSH1), 0x1, + byte(vm.PUSH1), 0x0, + byte(vm.MSTORE), + byte(vm.PUSH1), 0x1, + byte(vm.PUSH5), 0xff, 0xff, 0xff, 0xff, 0xff, + byte(vm.PUSH1), 0x1, + byte(vm.PUSH1), 0x0, + byte(vm.CREATE2), + byte(vm.PUSH1), 0xff, + byte(vm.PUSH1), 0x0, + byte(vm.LOG0), + }, + tracer: mkTracer("prestateTracer", json.RawMessage(`{ "withLog": true }`)), + want: `{"0x0000000000000000000000000000000000000000":{"balance":"0x0"},"0x000000000000000000000000000000000000feed":{"balance":"0x1c6bf52640350"},"0x00000000000000000000000000000000deadbeef":{"balance":"0x0","code":"0x6001600052600164ffffffffff60016000f560ff6000a0"}}`, + }, + } { + _, statedb := tests.MakePreState(rawdb.NewMemoryDatabase(), + core.GenesisAlloc{ + to: core.GenesisAccount{ + Code: tc.code, + }, + origin: core.GenesisAccount{ + Balance: big.NewInt(500000000000000), + }, + }, false) + evm := vm.NewEVM(context, txContext, statedb, params.MainnetChainConfig, vm.Config{Debug: true, Tracer: tc.tracer}) + msg := &core.Message{ + To: &to, + From: origin, + Value: big.NewInt(0), + GasLimit: 50000, + GasPrice: big.NewInt(0), + GasFeeCap: big.NewInt(0), + GasTipCap: big.NewInt(0), + SkipAccountChecks: false, + } + st := core.NewStateTransition(evm, msg, new(core.GasPool).AddGas(msg.GasLimit)) + if _, err := st.TransitionDb(); err != nil { + t.Fatalf("test %v: failed to execute transaction: %v", tc.name, err) + } + // Retrieve the trace result and compare against the expected + res, err := tc.tracer.GetResult() + if err != nil { + t.Fatalf("test %v: failed to retrieve trace result: %v", tc.name, err) + } + if string(res) != tc.want { + t.Fatalf("test %v: trace mismatch\n have: %v\n want: %v\n", tc.name, string(res), tc.want) + } } } diff --git a/eth/tracers/js/goja.go b/eth/tracers/js/goja.go index bceb848aa257..33298f3097fa 100644 --- a/eth/tracers/js/goja.go +++ b/eth/tracers/js/goja.go @@ -254,7 +254,7 @@ func (t *jsTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope if !t.traceStep { return } - if t.err != nil || err != nil { + if t.err != nil { return } @@ -567,9 +567,6 @@ func (mo *memoryObj) slice(begin, end int64) ([]byte, error) { if end < begin || begin < 0 { return nil, fmt.Errorf("tracer accessed out of bound memory: offset %d, end %d", begin, end) } - mlen := mo.memory.Len() - slice := make([]byte, end-begin) - end = min(end, int64(mlen)) slice, err := tracers.GetMemoryCopyPadded(mo.memory, begin, end-begin) if err != nil { return nil, err @@ -954,10 +951,3 @@ func (l *steplog) setupObject() *goja.Object { o.Set("contract", l.contract.setupObject()) return o } - -func min(a, b int64) int64 { - if a < b { - return a - } - return b -} diff --git a/eth/tracers/js/tracer_test.go b/eth/tracers/js/tracer_test.go index 524d17474930..1f3753721116 100644 --- a/eth/tracers/js/tracer_test.go +++ b/eth/tracers/js/tracer_test.go @@ -150,12 +150,12 @@ func TestTracer(t *testing.T) { }, { 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 }}", want: "", - fail: "tracer reached limit for padding memory slice: end 1049600, memorySize 32 at step (:1:83(20)) in server-side tracer function 'step'", + fail: "reached limit for padding memory slice: 1049568 at step (:1:83(20)) in server-side tracer function 'step'", contract: []byte{byte(vm.PUSH1), byte(0xff), byte(vm.PUSH1), byte(0x00), byte(vm.MSTORE8), byte(vm.STOP)}, }, } { if have, err := execTracer(tt.code, tt.contract); tt.want != string(have) || tt.fail != err { - 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) + 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) } } } diff --git a/eth/tracers/native/prestate.go b/eth/tracers/native/prestate.go index 948d09ef767c..42ec4fe7828b 100644 --- a/eth/tracers/native/prestate.go +++ b/eth/tracers/native/prestate.go @@ -133,6 +133,9 @@ func (t *prestateTracer) CaptureEnd(output []byte, gasUsed uint64, err error) { // CaptureState implements the EVMLogger interface to trace a single step of VM execution. func (t *prestateTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { + if err != nil { + return + } stack := scope.Stack stackData := stack.Data() stackLen := len(stackData) diff --git a/eth/tracers/tracers.go b/eth/tracers/tracers.go index 78cab68b23a8..023f73ef3708 100644 --- a/eth/tracers/tracers.go +++ b/eth/tracers/tracers.go @@ -104,28 +104,19 @@ const ( // GetMemoryCopyPadded returns offset + size as a new slice. // It zero-pads the slice if it extends beyond memory bounds. func GetMemoryCopyPadded(m *vm.Memory, offset, size int64) ([]byte, error) { - if offset < 0 { - return nil, fmt.Errorf("offset cannot be smaller than zero") + if offset < 0 || size < 0 { + return nil, fmt.Errorf("offset or size must not be negative") } - - if int(offset) >= m.Len() { - // case 1: the slice falls entirely outside memory bounds - if size > memoryPadLimit { - return nil, fmt.Errorf("reached limit for padding memory slice: offset %d, size %d", offset, size) - } - padded := make([]byte, size) - return padded, nil - } else if int(offset+size) >= m.Len() { - // case 2: the slice overlaps memory bounds - if int(offset+size)-m.Len() > memoryPadLimit { - return nil, fmt.Errorf("reached limit for padding memory slice: offset %d, size %d", offset, size) - } - padded := make([]byte, size) - memSlice := m.GetCopy(offset, int64(m.Len()) - offset) - copy(padded[0:len(memSlice)], memSlice) - return padded, nil - } - // case 3: the slice falls inside memory bounds - memSlice := m.GetCopy(offset, size) - return memSlice, nil + if int(offset+size) < m.Len() { // slice fully inside memory + return m.GetCopy(offset, size), nil + } + paddingNeeded := int(offset+size) - m.Len() + if paddingNeeded > memoryPadLimit { + return nil, fmt.Errorf("reached limit for padding memory slice: %d", paddingNeeded) + } + cpy := make([]byte, size) + if overlap := int64(m.Len()) - offset; overlap > 0 { + copy(cpy, m.GetPtr(offset, overlap)) + } + return cpy, nil } diff --git a/eth/tracers/tracers_test.go b/eth/tracers/tracers_test.go index 7c5ec65650ee..46182db48b98 100644 --- a/eth/tracers/tracers_test.go +++ b/eth/tracers/tracers_test.go @@ -109,3 +109,41 @@ func BenchmarkTransactionTrace(b *testing.B) { tracer.Reset() } } + +func TestMemCopying(t *testing.T) { + for i, tc := range []struct { + memsize int64 + offset int64 + size int64 + wantErr string + wantSize int + }{ + {0, 0, 100, "", 100}, // Should pad up to 100 + {0, 100, 0, "", 0}, // No need to pad (0 size) + {100, 50, 100, "", 100}, // Should pad 100-150 + {100, 50, 5, "", 5}, // Wanted range fully within memory + {100, -50, 0, "offset or size must not be negative", 0}, // Errror + {0, 1, 1024*1024 + 1, "reached limit for padding memory slice: 1048578", 0}, // Errror + {10, 0, 1024*1024 + 100, "reached limit for padding memory slice: 1048666", 0}, // Errror + + } { + mem := vm.NewMemory() + mem.Resize(uint64(tc.memsize)) + cpy, err := GetMemoryCopyPadded(mem, tc.offset, tc.size) + if want := tc.wantErr; want != "" { + if err == nil { + t.Fatalf("test %d: want '%v' have no error", i, want) + } + if have := err.Error(); want != have { + t.Fatalf("test %d: want '%v' have '%v'", i, want, have) + } + continue + } + if err != nil { + t.Fatalf("test %d: unexpected error: %v", i, err) + } + if want, have := tc.wantSize, len(cpy); have != want { + t.Fatalf("test %d: want %v have %v", i, want, have) + } + } +}