This repository has been archived by the owner on May 13, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 346
Implement dynamic memory and fix out of bounds issues #607
Merged
benjaminbollen
merged 1 commit into
hyperledger-archives:develop
from
silasdavis:evm-arrays
May 15, 2017
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
package vm | ||
|
||
import ( | ||
"fmt" | ||
"math" | ||
) | ||
|
||
const ( | ||
defaultInitialMemoryCapacity = 0x100000 // 1 MiB | ||
defaultMaximumMemoryCapacity = 0x1000000 // 16 MiB | ||
) | ||
|
||
// Change the length of this zero array to tweak the size of the block of zeros | ||
// written to the backing slice at a time when it is grown. A larger number may | ||
// lead to less calls to append to achieve the desired capacity although it is | ||
// 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 | ||
// for each byte in the memory. | ||
type Memory interface { | ||
// Read a value from the memory store starting at offset | ||
// (index of first byte will equal offset). The value will be returned as a | ||
// length-bytes byte slice. Returns an error if the memory cannot be read or | ||
// is not allocated. | ||
// | ||
// The value returned should be copy of any underlying memory, not a reference | ||
// to the underlying store. | ||
Read(offset, length int64) ([]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 | ||
// 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 | ||
} | ||
|
||
func NewDynamicMemory(initialCapacity, maximumCapacity int64) Memory { | ||
return &dynamicMemory{ | ||
slice: make([]byte, initialCapacity), | ||
maximumCapacity: maximumCapacity, | ||
} | ||
} | ||
|
||
func DefaultDynamicMemoryProvider() Memory { | ||
return NewDynamicMemory(defaultInitialMemoryCapacity, defaultMaximumMemoryCapacity) | ||
} | ||
|
||
// Implements a bounded dynamic memory that relies on Go's (pretty good) dynamic | ||
// array allocation via a backing slice | ||
type dynamicMemory struct { | ||
slice []byte | ||
maximumCapacity int64 | ||
} | ||
|
||
func (mem *dynamicMemory) Read(offset, length int64) ([]byte, error) { | ||
capacity := offset + length | ||
err := mem.ensureCapacity(capacity) | ||
if err != nil { | ||
return nil, err | ||
} | ||
value := make([]byte, length) | ||
copy(value, mem.slice[offset:capacity]) | ||
return value, nil | ||
} | ||
|
||
func (mem *dynamicMemory) Write(offset int64, value []byte) error { | ||
capacity := offset + int64(len(value)) | ||
err := mem.ensureCapacity(capacity) | ||
if err != nil { | ||
return err | ||
} | ||
copy(mem.slice[offset:capacity], value) | ||
return nil | ||
} | ||
|
||
func (mem *dynamicMemory) Capacity() int64 { | ||
return 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 { | ||
if newCapacity > math.MaxInt32 { | ||
// If we ever did want to 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) | ||
} | ||
newCapacityInt := int(newCapacity) | ||
// We're already big enough so return | ||
if newCapacityInt <= len(mem.slice) { | ||
return nil | ||
} | ||
if newCapacity > mem.maximumCapacity { | ||
return fmt.Errorf("Cannot grow memory because it would exceed the "+ | ||
"current maximum limit of %v bytes", mem.maximumCapacity) | ||
} | ||
// Ensure the backing array of slice is big enough | ||
// Grow the memory one word at time using the pre-allocated zeroBlock to avoid | ||
// unnecessary allocations. Use append to make use of any spare capacity in | ||
// the slice's backing array. | ||
for newCapacityInt > cap(mem.slice) { | ||
// We'll trust Go exponentially grow our arrays (at first). | ||
mem.slice = append(mem.slice, zeroBlock...) | ||
} | ||
// Now we've ensured the backing array of the slice is big enough we can | ||
// just re-slice (even if len(mem.slice) < newCapacity) | ||
mem.slice = mem.slice[:newCapacity] | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
package vm | ||
|
||
import ( | ||
"testing" | ||
|
||
"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}) | ||
assert.Equal(t, []byte{1, 0, 0, 1}, mem.slice) | ||
assert.Equal(t, 4, cap(mem.slice), "Slice capacity should not grow") | ||
} | ||
|
||
// 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) | ||
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) | ||
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) | ||
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}) | ||
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}) | ||
assert.Error(t, err) | ||
} | ||
|
||
func TestDynamicMemory_WriteRead(t *testing.T) { | ||
mem := NewDynamicMemory(1, 0x10000000).(*dynamicMemory) | ||
// Text is out of copyright | ||
bytesToWrite := []byte(`He paused. He felt the rhythm of the verse about him in the room. | ||
How melancholy it was! Could he, too, write like that, express the | ||
melancholy of his soul in verse? There were so many things he wanted | ||
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) | ||
assert.NoError(t, err) | ||
assert.Equal(t, append(make([]byte, offset), bytesToWrite...), mem.slice) | ||
assert.Equal(t, offset+len(bytesToWrite), len(mem.slice)) | ||
|
||
// Read them back | ||
value, err := mem.Read(int64(offset), 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}) | ||
assert.NoError(t, err) | ||
assert.Equal(t, []byte{0, 0, 0, 0, 1, 2, 3, 4}, mem.slice) | ||
} | ||
|
||
func TestDynamicMemory_Capacity(t *testing.T) { | ||
mem := NewDynamicMemory(1, 0x10000000).(*dynamicMemory) | ||
|
||
assert.Equal(t, int64(1), mem.Capacity()) | ||
|
||
capacity := int64(1234) | ||
err := mem.ensureCapacity(capacity) | ||
assert.NoError(t, err) | ||
assert.Equal(t, capacity, mem.Capacity()) | ||
|
||
capacity = int64(123456789) | ||
err = mem.ensureCapacity(capacity) | ||
assert.NoError(t, err) | ||
assert.Equal(t, capacity, mem.Capacity()) | ||
|
||
// Check doesn't shrink or err | ||
err = mem.ensureCapacity(12) | ||
assert.NoError(t, err) | ||
assert.Equal(t, capacity, mem.Capacity()) | ||
} | ||
|
||
func TestDynamicMemory_ensureCapacity(t *testing.T) { | ||
mem := NewDynamicMemory(4, 16).(*dynamicMemory) | ||
// Check we can grow within bounds | ||
err := mem.ensureCapacity(8) | ||
assert.NoError(t, err) | ||
expected := make([]byte, 8) | ||
assert.Equal(t, expected, mem.slice) | ||
|
||
// Check we can grow to bounds | ||
err = mem.ensureCapacity(16) | ||
assert.NoError(t, err) | ||
expected = make([]byte, 16) | ||
assert.Equal(t, expected, mem.slice) | ||
|
||
err = mem.ensureCapacity(1) | ||
assert.NoError(t, err) | ||
assert.Equal(t, 16, len(mem.slice)) | ||
|
||
err = mem.ensureCapacity(17) | ||
assert.Error(t, err, "Should not be possible to grow over capacity") | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -87,10 +87,10 @@ func SNativeContracts() map[string]*SNativeContractDescription { | |
`, | ||
"addRole", | ||
[]abi.Arg{ | ||
arg("_account", abi.AddressTypeName), | ||
arg("_role", roleTypeName), | ||
abiArg("_account", abi.AddressTypeName), | ||
abiArg("_role", roleTypeName), | ||
}, | ||
ret("result", abi.BoolTypeName), | ||
abiReturn("result", abi.BoolTypeName), | ||
ptypes.AddRole, | ||
addRole}, | ||
|
||
|
@@ -102,10 +102,10 @@ func SNativeContracts() map[string]*SNativeContractDescription { | |
`, | ||
"removeRole", | ||
[]abi.Arg{ | ||
arg("_account", abi.AddressTypeName), | ||
arg("_role", roleTypeName), | ||
abiArg("_account", abi.AddressTypeName), | ||
abiArg("_role", roleTypeName), | ||
}, | ||
ret("result", abi.BoolTypeName), | ||
abiReturn("result", abi.BoolTypeName), | ||
ptypes.RmRole, | ||
removeRole}, | ||
|
||
|
@@ -117,10 +117,10 @@ func SNativeContracts() map[string]*SNativeContractDescription { | |
`, | ||
"hasRole", | ||
[]abi.Arg{ | ||
arg("_account", abi.AddressTypeName), | ||
arg("_role", roleTypeName), | ||
abiArg("_account", abi.AddressTypeName), | ||
abiArg("_role", roleTypeName), | ||
}, | ||
ret("result", abi.BoolTypeName), | ||
abiReturn("result", abi.BoolTypeName), | ||
ptypes.HasRole, | ||
hasRole}, | ||
|
||
|
@@ -133,11 +133,11 @@ func SNativeContracts() map[string]*SNativeContractDescription { | |
`, | ||
"setBase", | ||
[]abi.Arg{ | ||
arg("_account", abi.AddressTypeName), | ||
arg("_permission", permFlagTypeName), | ||
arg("_set", abi.BoolTypeName), | ||
abiArg("_account", abi.AddressTypeName), | ||
abiArg("_permission", permFlagTypeName), | ||
abiArg("_set", abi.BoolTypeName), | ||
}, | ||
ret("result", permFlagTypeName), | ||
abiReturn("result", permFlagTypeName), | ||
ptypes.SetBase, | ||
setBase}, | ||
|
||
|
@@ -149,9 +149,9 @@ func SNativeContracts() map[string]*SNativeContractDescription { | |
`, | ||
"unsetBase", | ||
[]abi.Arg{ | ||
arg("_account", abi.AddressTypeName), | ||
arg("_permission", permFlagTypeName)}, | ||
ret("result", permFlagTypeName), | ||
abiArg("_account", abi.AddressTypeName), | ||
abiArg("_permission", permFlagTypeName)}, | ||
abiReturn("result", permFlagTypeName), | ||
ptypes.UnsetBase, | ||
unsetBase}, | ||
|
||
|
@@ -163,9 +163,9 @@ func SNativeContracts() map[string]*SNativeContractDescription { | |
`, | ||
"hasBase", | ||
[]abi.Arg{ | ||
arg("_account", abi.AddressTypeName), | ||
arg("_permission", permFlagTypeName)}, | ||
ret("result", abi.BoolTypeName), | ||
abiArg("_account", abi.AddressTypeName), | ||
abiArg("_permission", permFlagTypeName)}, | ||
abiReturn("result", abi.BoolTypeName), | ||
ptypes.HasBase, | ||
hasBase}, | ||
|
||
|
@@ -177,9 +177,9 @@ func SNativeContracts() map[string]*SNativeContractDescription { | |
`, | ||
"setGlobal", | ||
[]abi.Arg{ | ||
arg("_permission", permFlagTypeName), | ||
arg("_set", abi.BoolTypeName)}, | ||
ret("result", permFlagTypeName), | ||
abiArg("_permission", permFlagTypeName), | ||
abiArg("_set", abi.BoolTypeName)}, | ||
abiReturn("result", permFlagTypeName), | ||
ptypes.SetGlobal, | ||
setGlobal}, | ||
), | ||
|
@@ -324,14 +324,14 @@ func (function *SNativeFunctionDescription) NArgs() int { | |
return len(function.Args) | ||
} | ||
|
||
func arg(name string, abiTypeName abi.TypeName) abi.Arg { | ||
func abiArg(name string, abiTypeName abi.TypeName) abi.Arg { | ||
return abi.Arg{ | ||
Name: name, | ||
TypeName: abiTypeName, | ||
} | ||
} | ||
|
||
func ret(name string, abiTypeName abi.TypeName) abi.Return { | ||
func abiReturn(name string, abiTypeName abi.TypeName) abi.Return { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was renamed so as to not clash with the local variable |
||
return abi.Return{ | ||
Name: name, | ||
TypeName: abiTypeName, | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was renamed to be consistent with ret below