From 05c73b77065d7339199c58581261d7cd778460cb Mon Sep 17 00:00:00 2001 From: Silas Davis <silas@monax.io> Date: Fri, 2 Mar 2018 18:31:05 +0000 Subject: [PATCH] Use big.Int on Memory interface This also checks for UInt64 containment and prevents panic from negative offsets via a check on forwarding to the underlying implementation (which now uses uint64s) Signed-off-by: Silas Davis <silas@monax.io> --- execution/evm/memory.go | 53 +++++++++++++++++++++++++++--------- execution/evm/memory_test.go | 38 ++++++++++++++------------ execution/evm/vm.go | 28 +++++++++---------- 3 files changed, 74 insertions(+), 45 deletions(-) diff --git a/execution/evm/memory.go b/execution/evm/memory.go index 6eb8456c..12668614 100644 --- a/execution/evm/memory.go +++ b/execution/evm/memory.go @@ -3,6 +3,7 @@ package evm import ( "fmt" "math" + "math/big" ) const ( @@ -16,7 +17,7 @@ const ( // unlikely to make a lot of difference. var zeroBlock []byte = make([]byte, 32) -// Interface for a bounded linear memory indexed by a single int64 parameter +// Interface for a bounded linear memory indexed by a single *big.Int parameter // for each byte in the memory. type Memory interface { // Read a value from the memory store starting at offset @@ -26,20 +27,23 @@ type Memory interface { // // The value returned should be copy of any underlying memory, not a reference // to the underlying store. - Read(offset, length int64) ([]byte, error) + Read(offset, length *big.Int) ([]byte, error) // Write a value to the memory starting at offset (the index of the first byte // written will equal offset). The value is provided as bytes to be written // consecutively to the memory store. Return an error if the memory cannot be // written or allocated. - Write(offset int64, value []byte) error + Write(offset *big.Int, value []byte) error // Returns the current capacity of the memory. For dynamically allocating // memory this capacity can be used as a write offset that is guaranteed to be // unused. Solidity in particular makes this assumption when using MSIZE to // get the current allocated memory. - Capacity() int64 + Capacity() *big.Int } -func NewDynamicMemory(initialCapacity, maximumCapacity int64) Memory { +// Get a new DynamicMemory (note that although we take a maximumCapacity of uint64 we currently +// limit the maximum to int32 at runtime because we are using a single slice which we cannot guarantee +// to be indexable above int32 or all validators +func NewDynamicMemory(initialCapacity, maximumCapacity uint64) Memory { return &dynamicMemory{ slice: make([]byte, initialCapacity), maximumCapacity: maximumCapacity, @@ -54,10 +58,22 @@ func DefaultDynamicMemoryProvider() Memory { // array allocation via a backing slice type dynamicMemory struct { slice []byte - maximumCapacity int64 + maximumCapacity uint64 } -func (mem *dynamicMemory) Read(offset, length int64) ([]byte, error) { +func (mem *dynamicMemory) Read(offset, length *big.Int) ([]byte, error) { + // Ensures positive and not too wide + if !offset.IsUint64() { + return nil, fmt.Errorf("offset %v does not fit inside an unsigned 64-bit integer", offset) + } + // Ensures positive and not too wide + if !length.IsUint64() { + return nil, fmt.Errorf("length %v does not fit inside an unsigned 64-bit integer", offset) + } + return mem.read(offset.Uint64(), length.Uint64()) +} + +func (mem *dynamicMemory) read(offset, length uint64) ([]byte, error) { capacity := offset + length err := mem.ensureCapacity(capacity) if err != nil { @@ -68,8 +84,16 @@ func (mem *dynamicMemory) Read(offset, length int64) ([]byte, error) { return value, nil } -func (mem *dynamicMemory) Write(offset int64, value []byte) error { - capacity := offset + int64(len(value)) +func (mem *dynamicMemory) Write(offset *big.Int, value []byte) error { + // Ensures positive and not too wide + if !offset.IsUint64() { + return fmt.Errorf("offset %v does not fit inside an unsigned 64-bit integer", offset) + } + return mem.write(offset.Uint64(), value) +} + +func (mem *dynamicMemory) write(offset uint64, value []byte) error { + capacity := offset + uint64(len(value)) err := mem.ensureCapacity(capacity) if err != nil { return err @@ -78,18 +102,21 @@ func (mem *dynamicMemory) Write(offset int64, value []byte) error { return nil } -func (mem *dynamicMemory) Capacity() int64 { - return int64(len(mem.slice)) +func (mem *dynamicMemory) Capacity() *big.Int { + return big.NewInt(int64(len(mem.slice))) } // Ensures the current memory store can hold newCapacity. Will only grow the // memory (will not shrink). -func (mem *dynamicMemory) ensureCapacity(newCapacity int64) error { +func (mem *dynamicMemory) ensureCapacity(newCapacity uint64) error { + // Maximum length of a slice that allocates memory is the same as the native int max size + // We could rethink this limit, but we don't want different validators to disagree on + // transaction validity so we pick the lowest common denominator if newCapacity > math.MaxInt32 { // If we ever did want more than an int32 of space then we would need to // maintain multiple pages of memory return fmt.Errorf("cannot address memory beyond a maximum index "+ - "of Int32 type (%v bytes)", math.MaxInt32) + "with int32 width (%v bytes)", math.MaxInt32) } newCapacityInt := int(newCapacity) // We're already big enough so return diff --git a/execution/evm/memory_test.go b/execution/evm/memory_test.go index 923cdda4..c60a8583 100644 --- a/execution/evm/memory_test.go +++ b/execution/evm/memory_test.go @@ -3,14 +3,16 @@ package evm import ( "testing" + "math/big" + "github.com/stretchr/testify/assert" ) // Test static memory allocation with maximum == initial capacity - memory should not grow func TestDynamicMemory_StaticAllocation(t *testing.T) { mem := NewDynamicMemory(4, 4).(*dynamicMemory) - mem.Write(0, []byte{1}) - mem.Write(1, []byte{0, 0, 1}) + mem.Write(big.NewInt(0), []byte{1}) + mem.Write(big.NewInt(1), []byte{0, 0, 1}) assert.Equal(t, []byte{1, 0, 0, 1}, mem.slice) assert.Equal(t, 4, cap(mem.slice), "Slice capacity should not grow") } @@ -18,31 +20,31 @@ func TestDynamicMemory_StaticAllocation(t *testing.T) { // Test reading beyond the current capacity - memory should grow func TestDynamicMemory_ReadAhead(t *testing.T) { mem := NewDynamicMemory(4, 8).(*dynamicMemory) - value, err := mem.Read(2, 4) + value, err := mem.Read(big.NewInt(2), big.NewInt(4)) assert.NoError(t, err) // Value should be size requested assert.Equal(t, []byte{0, 0, 0, 0}, value) // Slice should have grown to that plus offset assert.Equal(t, []byte{0, 0, 0, 0, 0, 0}, mem.slice) - value, err = mem.Read(2, 6) + value, err = mem.Read(big.NewInt(2), big.NewInt(6)) assert.NoError(t, err) assert.Equal(t, []byte{0, 0, 0, 0, 0, 0}, value) assert.Equal(t, []byte{0, 0, 0, 0, 0, 0, 0, 0}, mem.slice) // Check cannot read out of bounds - _, err = mem.Read(2, 7) + _, err = mem.Read(big.NewInt(2), big.NewInt(7)) assert.Error(t, err) } // Test writing beyond the current capacity - memory should grow func TestDynamicMemory_WriteAhead(t *testing.T) { mem := NewDynamicMemory(4, 8).(*dynamicMemory) - err := mem.Write(4, []byte{1, 2, 3, 4}) + err := mem.Write(big.NewInt(4), []byte{1, 2, 3, 4}) assert.NoError(t, err) assert.Equal(t, []byte{0, 0, 0, 0, 1, 2, 3, 4}, mem.slice) - err = mem.Write(4, []byte{1, 2, 3, 4, 5}) + err = mem.Write(big.NewInt(4), []byte{1, 2, 3, 4, 5}) assert.Error(t, err) } @@ -56,21 +58,21 @@ to describe: his sensation of a few hours before on Grattan Bridge, for example. If he could get back again into that mood....`) // Write the bytes - offset := 0x1000000 - err := mem.Write(int64(offset), bytesToWrite) + offset := big.NewInt(0x1000000) + err := mem.Write(offset, bytesToWrite) assert.NoError(t, err) - assert.Equal(t, append(make([]byte, offset), bytesToWrite...), mem.slice) - assert.Equal(t, offset+len(bytesToWrite), len(mem.slice)) + assert.Equal(t, append(make([]byte, offset.Uint64()), bytesToWrite...), mem.slice) + assert.Equal(t, offset.Uint64()+uint64(len(bytesToWrite)), uint64(len(mem.slice))) // Read them back - value, err := mem.Read(int64(offset), int64(len(bytesToWrite))) + value, err := mem.Read(offset, big.NewInt(int64(len(bytesToWrite)))) assert.NoError(t, err) assert.Equal(t, bytesToWrite, value) } func TestDynamicMemory_ZeroInitialMemory(t *testing.T) { mem := NewDynamicMemory(0, 16).(*dynamicMemory) - err := mem.Write(4, []byte{1, 2, 3, 4}) + err := mem.Write(big.NewInt(4), []byte{1, 2, 3, 4}) assert.NoError(t, err) assert.Equal(t, []byte{0, 0, 0, 0, 1, 2, 3, 4}, mem.slice) } @@ -78,15 +80,15 @@ func TestDynamicMemory_ZeroInitialMemory(t *testing.T) { func TestDynamicMemory_Capacity(t *testing.T) { mem := NewDynamicMemory(1, 0x10000000).(*dynamicMemory) - assert.Equal(t, int64(1), mem.Capacity()) + assert.Equal(t, big.NewInt(1), mem.Capacity()) - capacity := int64(1234) - err := mem.ensureCapacity(capacity) + capacity := big.NewInt(1234) + err := mem.ensureCapacity(capacity.Uint64()) assert.NoError(t, err) assert.Equal(t, capacity, mem.Capacity()) - capacity = int64(123456789) - err = mem.ensureCapacity(capacity) + capacity = big.NewInt(123456789) + err = mem.ensureCapacity(capacity.Uint64()) assert.NoError(t, err) assert.Equal(t, capacity, mem.Capacity()) diff --git a/execution/evm/vm.go b/execution/evm/vm.go index 3513e5ac..fe1f51cb 100644 --- a/execution/evm/vm.go +++ b/execution/evm/vm.go @@ -425,7 +425,7 @@ func (vm *VM) call(caller, callee acm.MutableAccount, code, input []byte, value if useGasNegative(gas, GasSha3, &err) { return nil, err } - offset, size := stack.Pop64(), stack.Pop64() + offset, size := stack.PopBigInt(), stack.PopBigInt() data, memErr := memory.Read(offset, size) if memErr != nil { vm.Debugf(" => Memory err: %s", memErr) @@ -482,7 +482,7 @@ func (vm *VM) call(caller, callee acm.MutableAccount, code, input []byte, value vm.Debugf(" => %d\n", len(input)) case CALLDATACOPY: // 0x37 - memOff := stack.Pop64() + memOff := stack.PopBigInt() inputOff := stack.Pop64() length := stack.Pop64() data, ok := subslice(input, inputOff, length) @@ -502,7 +502,7 @@ func (vm *VM) call(caller, callee acm.MutableAccount, code, input []byte, value vm.Debugf(" => %d\n", l) case CODECOPY: // 0x39 - memOff := stack.Pop64() + memOff := stack.PopBigInt() codeOff := stack.Pop64() length := stack.Pop64() data, ok := subslice(code, codeOff, length) @@ -558,7 +558,7 @@ func (vm *VM) call(caller, callee acm.MutableAccount, code, input []byte, value return nil, firstErr(err, ErrUnknownAddress) } code := acc.Code() - memOff := stack.Pop64() + memOff := stack.PopBigInt() codeOff := stack.Pop64() length := stack.Pop64() data, ok := subslice(code, codeOff, length) @@ -599,8 +599,8 @@ func (vm *VM) call(caller, callee acm.MutableAccount, code, input []byte, value vm.Debugf(" => 0x%X\n", popped) case MLOAD: // 0x51 - offset := stack.Pop64() - data, memErr := memory.Read(offset, 32) + offset := stack.PopBigInt() + data, memErr := memory.Read(offset, BigWord256Length) if memErr != nil { vm.Debugf(" => Memory err: %s", memErr) return nil, firstErr(err, ErrMemoryOutOfBounds) @@ -609,7 +609,7 @@ func (vm *VM) call(caller, callee acm.MutableAccount, code, input []byte, value vm.Debugf(" => 0x%X @ 0x%X\n", data, offset) case MSTORE: // 0x52 - offset, data := stack.Pop64(), stack.Pop() + offset, data := stack.PopBigInt(), stack.Pop() memErr := memory.Write(offset, data.Bytes()) if memErr != nil { vm.Debugf(" => Memory err: %s", memErr) @@ -618,7 +618,7 @@ func (vm *VM) call(caller, callee acm.MutableAccount, code, input []byte, value vm.Debugf(" => 0x%X @ 0x%X\n", data, offset) case MSTORE8: // 0x53 - offset, val := stack.Pop64(), byte(stack.Pop64()&0xFF) + offset, val := stack.PopBigInt(), byte(stack.Pop64()&0xFF) memErr := memory.Write(offset, []byte{val}) if memErr != nil { vm.Debugf(" => Memory err: %s", memErr) @@ -670,7 +670,7 @@ func (vm *VM) call(caller, callee acm.MutableAccount, code, input []byte, value // free memory to be allocated for it if a subsequent MSTORE is made to // this offset. capacity := memory.Capacity() - stack.Push64(capacity) + stack.PushBigInt(capacity) vm.Debugf(" => 0x%X\n", capacity) case GAS: // 0x5A @@ -707,7 +707,7 @@ func (vm *VM) call(caller, callee acm.MutableAccount, code, input []byte, value case LOG0, LOG1, LOG2, LOG3, LOG4: n := int(op - LOG0) topics := make([]Word256, n) - offset, size := stack.Pop64(), stack.Pop64() + offset, size := stack.PopBigInt(), stack.PopBigInt() for i := 0; i < n; i++ { topics[i] = stack.Pop() } @@ -733,7 +733,7 @@ func (vm *VM) call(caller, callee acm.MutableAccount, code, input []byte, value return nil, ErrPermission{"create_contract"} } contractValue := stack.PopU64() - offset, size := stack.Pop64(), stack.Pop64() + offset, size := stack.PopBigInt(), stack.PopBigInt() input, memErr := memory.Read(offset, size) if memErr != nil { vm.Debugf(" => Memory err: %s", memErr) @@ -773,8 +773,8 @@ func (vm *VM) call(caller, callee acm.MutableAccount, code, input []byte, value if op != DELEGATECALL { value = stack.PopU64() } - inOffset, inSize := stack.Pop64(), stack.Pop64() // inputs - retOffset, retSize := stack.Pop64(), stack.Pop64() // outputs + inOffset, inSize := stack.PopBigInt(), stack.PopBigInt() // inputs + retOffset, retSize := stack.PopBigInt(), stack.Pop64() // outputs vm.Debugf(" => %X\n", addr) // Get the arguments from the memory @@ -868,7 +868,7 @@ func (vm *VM) call(caller, callee acm.MutableAccount, code, input []byte, value vm.Debugf("resume %s (%v)\n", callee.Address(), gas) case RETURN: // 0xF3 - offset, size := stack.Pop64(), stack.Pop64() + offset, size := stack.PopBigInt(), stack.PopBigInt() output, memErr := memory.Read(offset, size) if memErr != nil { vm.Debugf(" => Memory err: %s", memErr) -- GitLab