Files
wazero/internal/wasm/memory_test.go
Crypt Keeper 45ff2fe12f Propagates context to all api interface methods that aren't constant (#502)
This prepares for exposing operations like Memory.Grow while keeping the
ability to trace what did that, by adding a `context.Context` initial
parameter. This adds this to all API methods that mutate or return
mutated data.

Before, we made a change to trace functions and general lifecycle
commands, but we missed this part. Ex. We track functions, but can't
track what closed the module, changed memory or a mutable constant.
Changing to do this now is not only more consistent, but helps us
optimize at least the interpreter to help users identify otherwise
opaque code that can cause harm. This is critical before we add more
functions that can cause harm, such as Memory.Grow.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
2022-04-25 08:13:18 +08:00

725 lines
20 KiB
Go

package wasm
import (
"context"
"math"
"testing"
"github.com/tetratelabs/wazero/internal/testing/require"
)
func TestMemoryPageConsts(t *testing.T) {
require.Equal(t, MemoryPageSize, uint32(1)<<MemoryPageSizeInBits)
require.Equal(t, MemoryPageSize, uint32(1<<16))
require.Equal(t, MemoryMaxPages, uint32(1<<16))
}
func Test_MemoryPagesToBytesNum(t *testing.T) {
for _, numPage := range []uint32{0, 1, 5, 10} {
require.Equal(t, uint64(numPage*MemoryPageSize), MemoryPagesToBytesNum(numPage))
}
}
func Test_MemoryBytesNumToPages(t *testing.T) {
for _, numbytes := range []uint32{0, MemoryPageSize * 1, MemoryPageSize * 10} {
require.Equal(t, numbytes/MemoryPageSize, memoryBytesNumToPages(uint64(numbytes)))
}
}
func TestMemoryInstance_Grow_Size(t *testing.T) {
for _, ctx := range []context.Context{nil, testCtx} { // Ensure it doesn't crash on nil!
max := uint32(10)
m := &MemoryInstance{Max: max, Buffer: make([]byte, 0)}
require.Equal(t, uint32(0), m.Grow(ctx, 5))
require.Equal(t, uint32(5), m.PageSize(ctx))
// Zero page grow is well-defined, should return the current page correctly.
require.Equal(t, uint32(5), m.Grow(ctx, 0))
require.Equal(t, uint32(5), m.PageSize(ctx))
require.Equal(t, uint32(5), m.Grow(ctx, 4))
require.Equal(t, uint32(9), m.PageSize(ctx))
// At this point, the page size equal 9,
// so trying to grow two pages should result in failure.
require.Equal(t, int32(-1), int32(m.Grow(ctx, 2)))
require.Equal(t, uint32(9), m.PageSize(ctx))
// But growing one page is still permitted.
require.Equal(t, uint32(9), m.Grow(ctx, 1))
// Ensure that the current page size equals the max.
require.Equal(t, max, m.PageSize(ctx))
}
}
func TestIndexByte(t *testing.T) {
for _, ctx := range []context.Context{nil, testCtx} { // Ensure it doesn't crash on nil!
var mem = &MemoryInstance{Buffer: []byte{0, 0, 0, 0, 16, 0, 0, 0}, Min: 1}
v, ok := mem.IndexByte(ctx, 4, 16)
require.True(t, ok)
require.Equal(t, uint32(4), v)
_, ok = mem.IndexByte(ctx, 5, 16)
require.False(t, ok)
_, ok = mem.IndexByte(ctx, 9, 16)
require.False(t, ok)
}
}
func TestReadByte(t *testing.T) {
for _, ctx := range []context.Context{nil, testCtx} { // Ensure it doesn't crash on nil!
var mem = &MemoryInstance{Buffer: []byte{0, 0, 0, 0, 0, 0, 0, 16}, Min: 1}
v, ok := mem.ReadByte(ctx, 7)
require.True(t, ok)
require.Equal(t, byte(16), v)
_, ok = mem.ReadByte(ctx, 8)
require.False(t, ok)
_, ok = mem.ReadByte(ctx, 9)
require.False(t, ok)
}
}
func TestReadUint32Le(t *testing.T) {
for _, ctx := range []context.Context{nil, testCtx} { // Ensure it doesn't crash on nil!
var mem = &MemoryInstance{Buffer: []byte{0, 0, 0, 0, 16, 0, 0, 0}, Min: 1}
v, ok := mem.ReadUint32Le(ctx, 4)
require.True(t, ok)
require.Equal(t, uint32(16), v)
_, ok = mem.ReadUint32Le(ctx, 5)
require.False(t, ok)
_, ok = mem.ReadUint32Le(ctx, 9)
require.False(t, ok)
}
}
func TestWriteUint32Le(t *testing.T) {
for _, ctx := range []context.Context{nil, testCtx} { // Ensure it doesn't crash on nil!
var mem = &MemoryInstance{Buffer: make([]byte, 8), Min: 1}
require.True(t, mem.WriteUint32Le(ctx, 4, 16))
require.Equal(t, []byte{0, 0, 0, 0, 16, 0, 0, 0}, mem.Buffer)
require.False(t, mem.WriteUint32Le(ctx, 5, 16))
require.False(t, mem.WriteUint32Le(ctx, 9, 16))
}
}
func TestPagesToUnitOfBytes(t *testing.T) {
tests := []struct {
name string
pages uint32
expected string
}{
{
name: "zero",
pages: 0,
expected: "0 Ki",
},
{
name: "one",
pages: 1,
expected: "64 Ki",
},
{
name: "megs",
pages: 100,
expected: "6 Mi",
},
{
name: "max memory",
pages: MemoryMaxPages,
expected: "4 Gi",
},
{
name: "max uint32",
pages: math.MaxUint32,
expected: "3 Ti",
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.expected, PagesToUnitOfBytes(tc.pages))
})
}
}
func TestMemoryInstance_HasSize(t *testing.T) {
memory := &MemoryInstance{Buffer: make([]byte, MemoryPageSize)}
tests := []struct {
name string
offset uint32
sizeInBytes uint64
expected bool
}{
{
name: "simple valid arguments",
offset: 0, // arbitrary valid offset
sizeInBytes: 8, // arbitrary valid size
expected: true,
},
{
name: "maximum valid sizeInBytes",
offset: memory.Size(testCtx) - 8,
sizeInBytes: 8,
expected: true,
},
{
name: "sizeInBytes exceeds the valid size by 1",
offset: 100, // arbitrary valid offset
sizeInBytes: uint64(memory.Size(testCtx) - 99),
expected: false,
},
{
name: "offset exceeds the memory size",
offset: memory.Size(testCtx),
sizeInBytes: 1, // arbitrary size
expected: false,
},
{
name: "offset + sizeInBytes overflows in uint32",
offset: math.MaxUint32 - 1, // invalid too large offset
sizeInBytes: 4, // if there's overflow, offset + sizeInBytes is 3, and it may pass the check
expected: false,
},
{
name: "address.wast:200",
offset: 4294967295,
sizeInBytes: 1,
expected: false,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.expected, memory.hasSize(tc.offset, uint32(tc.sizeInBytes)))
})
}
}
func TestMemoryInstance_ReadUint16Le(t *testing.T) {
tests := []struct {
name string
memory []byte
offset uint32
expected uint16
expectedOk bool
}{
{
name: "valid offset with an endian-insensitive v",
memory: []byte{0xff, 0xff},
offset: 0, // arbitrary valid offset.
expected: math.MaxUint16,
expectedOk: true,
},
{
name: "valid offset with an endian-sensitive v",
memory: []byte{0xfe, 0xff},
offset: 0, // arbitrary valid offset.
expected: math.MaxUint16 - 1,
expectedOk: true,
},
{
name: "maximum boundary valid offset",
offset: 1,
memory: []byte{0x00, 0x1, 0x00},
expected: 1, // arbitrary valid v
expectedOk: true,
},
{
name: "offset exceeds the maximum valid offset by 1",
memory: []byte{0xff, 0xff},
offset: 1,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
for _, ctx := range []context.Context{nil, testCtx} { // Ensure it doesn't crash on nil!
memory := &MemoryInstance{Buffer: tc.memory}
v, ok := memory.ReadUint16Le(ctx, tc.offset)
require.Equal(t, tc.expectedOk, ok)
require.Equal(t, tc.expected, v)
}
})
}
}
func TestMemoryInstance_ReadUint32Le(t *testing.T) {
tests := []struct {
name string
memory []byte
offset uint32
expected uint32
expectedOk bool
}{
{
name: "valid offset with an endian-insensitive v",
memory: []byte{0xff, 0xff, 0xff, 0xff},
offset: 0, // arbitrary valid offset.
expected: math.MaxUint32,
expectedOk: true,
},
{
name: "valid offset with an endian-sensitive v",
memory: []byte{0xfe, 0xff, 0xff, 0xff},
offset: 0, // arbitrary valid offset.
expected: math.MaxUint32 - 1,
expectedOk: true,
},
{
name: "maximum boundary valid offset",
offset: 1,
memory: []byte{0x00, 0x1, 0x00, 0x00, 0x00},
expected: 1, // arbitrary valid v
expectedOk: true,
},
{
name: "offset exceeds the maximum valid offset by 1",
memory: []byte{0xff, 0xff, 0xff, 0xff},
offset: 1,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
for _, ctx := range []context.Context{nil, testCtx} { // Ensure it doesn't crash on nil!
memory := &MemoryInstance{Buffer: tc.memory}
v, ok := memory.ReadUint32Le(ctx, tc.offset)
require.Equal(t, tc.expectedOk, ok)
require.Equal(t, tc.expected, v)
}
})
}
}
func TestMemoryInstance_ReadUint64Le(t *testing.T) {
tests := []struct {
name string
memory []byte
offset uint32
expected uint64
expectedOk bool
}{
{
name: "valid offset with an endian-insensitive v",
memory: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
offset: 0, // arbitrary valid offset.
expected: math.MaxUint64,
expectedOk: true,
},
{
name: "valid offset with an endian-sensitive v",
memory: []byte{0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
offset: 0, // arbitrary valid offset.
expected: math.MaxUint64 - 1,
expectedOk: true,
},
{
name: "maximum boundary valid offset",
offset: 1,
memory: []byte{0x00, 0x1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
expected: 1, // arbitrary valid v
expectedOk: true,
},
{
name: "offset exceeds the maximum valid offset by 1",
memory: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
offset: 1,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
for _, ctx := range []context.Context{nil, testCtx} { // Ensure it doesn't crash on nil!
memory := &MemoryInstance{Buffer: tc.memory}
v, ok := memory.ReadUint64Le(ctx, tc.offset)
require.Equal(t, tc.expectedOk, ok)
require.Equal(t, tc.expected, v)
}
})
}
}
func TestMemoryInstance_ReadFloat32Le(t *testing.T) {
tests := []struct {
name string
memory []byte
offset uint32
expected float32
expectedOk bool
}{
{
name: "valid offset with an endian-insensitive v",
memory: []byte{0xff, 0x00, 0x00, 0xff},
offset: 0, // arbitrary valid offset.
expected: math.Float32frombits(uint32(0xff0000ff)),
expectedOk: true,
},
{
name: "valid offset with an endian-sensitive v",
memory: []byte{0xfe, 0x00, 0x00, 0xff},
offset: 0, // arbitrary valid offset.
expected: math.Float32frombits(uint32(0xff0000fe)),
expectedOk: true,
},
{
name: "maximum boundary valid offset",
offset: 1,
memory: []byte{0x00, 0xcd, 0xcc, 0xcc, 0x3d},
expected: 0.1, // arbitrary valid v
expectedOk: true,
},
{
name: "offset exceeds the maximum valid offset by 1",
memory: []byte{0xff, 0xff, 0xff, 0xff},
offset: 1,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
for _, ctx := range []context.Context{nil, testCtx} { // Ensure it doesn't crash on nil!
memory := &MemoryInstance{Buffer: tc.memory}
v, ok := memory.ReadFloat32Le(ctx, tc.offset)
require.Equal(t, tc.expectedOk, ok)
require.Equal(t, tc.expected, v)
}
})
}
}
func TestMemoryInstance_ReadFloat64Le(t *testing.T) {
tests := []struct {
name string
memory []byte
offset uint32
expected float64
expectedOk bool
}{
{
name: "valid offset with an endian-insensitive v",
memory: []byte{0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff},
offset: 0, // arbitrary valid offset.
expected: math.Float64frombits(uint64(0xff000000000000ff)),
expectedOk: true,
},
{
name: "valid offset with an endian-sensitive v",
memory: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0x7f},
offset: 0, // arbitrary valid offset.
expected: math.MaxFloat64, // arbitrary valid v
expectedOk: true,
},
{
name: "maximum boundary valid offset",
offset: 1,
memory: []byte{0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0x7f},
expected: math.MaxFloat64, // arbitrary valid v
expectedOk: true,
},
{
name: "offset exceeds the maximum valid offset by 1",
memory: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
offset: 1,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
for _, ctx := range []context.Context{nil, testCtx} { // Ensure it doesn't crash on nil!
memory := &MemoryInstance{Buffer: tc.memory}
v, ok := memory.ReadFloat64Le(ctx, tc.offset)
require.Equal(t, tc.expectedOk, ok)
require.Equal(t, tc.expected, v)
}
})
}
}
func TestMemoryInstance_WriteUint16Le(t *testing.T) {
memory := &MemoryInstance{Buffer: make([]byte, 100)}
tests := []struct {
name string
offset uint32
v uint16
expectedOk bool
expectedBytes []byte
}{
{
name: "valid offset with an endian-insensitive v",
offset: 0, // arbitrary valid offset.
v: math.MaxUint16,
expectedOk: true,
expectedBytes: []byte{0xff, 0xff},
},
{
name: "valid offset with an endian-sensitive v",
offset: 0, // arbitrary valid offset.
v: math.MaxUint16 - 1,
expectedOk: true,
expectedBytes: []byte{0xfe, 0xff},
},
{
name: "maximum boundary valid offset",
offset: memory.Size(testCtx) - 2, // 2 is the size of uint16
v: 1, // arbitrary valid v
expectedOk: true,
expectedBytes: []byte{0x1, 0x00},
},
{
name: "offset exceeds the maximum valid offset by 1",
offset: memory.Size(testCtx) - 2 + 1, // 2 is the size of uint16
v: 1, // arbitrary valid v
expectedBytes: []byte{0xff, 0xff},
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
for _, ctx := range []context.Context{nil, testCtx} { // Ensure it doesn't crash on nil!
require.Equal(t, tc.expectedOk, memory.WriteUint16Le(ctx, tc.offset, tc.v))
if tc.expectedOk {
require.Equal(t, tc.expectedBytes, memory.Buffer[tc.offset:tc.offset+2]) // 2 is the size of uint16
}
}
})
}
}
func TestMemoryInstance_WriteUint32Le(t *testing.T) {
memory := &MemoryInstance{Buffer: make([]byte, 100)}
tests := []struct {
name string
offset uint32
v uint32
expectedOk bool
expectedBytes []byte
}{
{
name: "valid offset with an endian-insensitive v",
offset: 0, // arbitrary valid offset.
v: math.MaxUint32,
expectedOk: true,
expectedBytes: []byte{0xff, 0xff, 0xff, 0xff},
},
{
name: "valid offset with an endian-sensitive v",
offset: 0, // arbitrary valid offset.
v: math.MaxUint32 - 1,
expectedOk: true,
expectedBytes: []byte{0xfe, 0xff, 0xff, 0xff},
},
{
name: "maximum boundary valid offset",
offset: memory.Size(testCtx) - 4, // 4 is the size of uint32
v: 1, // arbitrary valid v
expectedOk: true,
expectedBytes: []byte{0x1, 0x00, 0x00, 0x00},
},
{
name: "offset exceeds the maximum valid offset by 1",
offset: memory.Size(testCtx) - 4 + 1, // 4 is the size of uint32
v: 1, // arbitrary valid v
expectedBytes: []byte{0xff, 0xff, 0xff, 0xff},
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
for _, ctx := range []context.Context{nil, testCtx} { // Ensure it doesn't crash on nil!
require.Equal(t, tc.expectedOk, memory.WriteUint32Le(ctx, tc.offset, tc.v))
if tc.expectedOk {
require.Equal(t, tc.expectedBytes, memory.Buffer[tc.offset:tc.offset+4]) // 4 is the size of uint32
}
}
})
}
}
func TestMemoryInstance_WriteUint64Le(t *testing.T) {
memory := &MemoryInstance{Buffer: make([]byte, 100)}
tests := []struct {
name string
offset uint32
v uint64
expectedOk bool
expectedBytes []byte
}{
{
name: "valid offset with an endian-insensitive v",
offset: 0, // arbitrary valid offset.
v: math.MaxUint64,
expectedOk: true,
expectedBytes: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
},
{
name: "valid offset with an endian-sensitive v",
offset: 0, // arbitrary valid offset.
v: math.MaxUint64 - 1,
expectedOk: true,
expectedBytes: []byte{0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
},
{
name: "maximum boundary valid offset",
offset: memory.Size(testCtx) - 8, // 8 is the size of uint64
v: 1, // arbitrary valid v
expectedOk: true,
expectedBytes: []byte{0x1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
},
{
name: "offset exceeds the maximum valid offset by 1",
offset: memory.Size(testCtx) - 8 + 1, // 8 is the size of uint64
v: 1, // arbitrary valid v
expectedOk: false,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
for _, ctx := range []context.Context{nil, testCtx} { // Ensure it doesn't crash on nil!
require.Equal(t, tc.expectedOk, memory.WriteUint64Le(ctx, tc.offset, tc.v))
if tc.expectedOk {
require.Equal(t, tc.expectedBytes, memory.Buffer[tc.offset:tc.offset+8]) // 8 is the size of uint64
}
}
})
}
}
func TestMemoryInstance_WriteFloat32Le(t *testing.T) {
memory := &MemoryInstance{Buffer: make([]byte, 100)}
tests := []struct {
name string
offset uint32
v float32
expectedOk bool
expectedBytes []byte
}{
{
name: "valid offset with an endian-insensitive v",
offset: 0, // arbitrary valid offset.
v: math.Float32frombits(uint32(0xff0000ff)),
expectedOk: true,
expectedBytes: []byte{0xff, 0x00, 0x00, 0xff},
},
{
name: "valid offset with an endian-sensitive v",
offset: 0, // arbitrary valid offset.
v: math.Float32frombits(uint32(0xff0000fe)), // arbitrary valid v
expectedOk: true,
expectedBytes: []byte{0xfe, 0x00, 0x00, 0xff},
},
{
name: "maximum boundary valid offset",
offset: memory.Size(testCtx) - 4, // 4 is the size of float32
v: 0.1, // arbitrary valid v
expectedOk: true,
expectedBytes: []byte{0xcd, 0xcc, 0xcc, 0x3d},
},
{
name: "offset exceeds the maximum valid offset by 1",
offset: memory.Size(testCtx) - 4 + 1, // 4 is the size of float32
v: math.MaxFloat32, // arbitrary valid v
expectedBytes: []byte{0xff, 0xff, 0xff, 0xff},
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
for _, ctx := range []context.Context{nil, testCtx} { // Ensure it doesn't crash on nil!
require.Equal(t, tc.expectedOk, memory.WriteFloat32Le(ctx, tc.offset, tc.v))
if tc.expectedOk {
require.Equal(t, tc.expectedBytes, memory.Buffer[tc.offset:tc.offset+4]) // 4 is the size of float32
}
}
})
}
}
func TestMemoryInstance_WriteFloat64Le(t *testing.T) {
memory := &MemoryInstance{Buffer: make([]byte, 100)}
tests := []struct {
name string
offset uint32
v float64
expectedOk bool
expectedBytes []byte
}{
{
name: "valid offset with an endian-insensitive v",
offset: 0, // arbitrary valid offset.
v: math.Float64frombits(uint64(0xff000000000000ff)),
expectedOk: true,
expectedBytes: []byte{0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff},
},
{
name: "valid offset with an endian-sensitive v",
offset: 0, // arbitrary valid offset.
v: math.MaxFloat64, // arbitrary valid v
expectedOk: true,
expectedBytes: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0x7f},
},
{
name: "maximum boundary valid offset",
offset: memory.Size(testCtx) - 8, // 8 is the size of float64
v: math.MaxFloat64, // arbitrary valid v
expectedOk: true,
expectedBytes: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0x7f},
},
{
name: "offset exceeds the maximum valid offset by 1",
offset: memory.Size(testCtx) - 8 + 1, // 8 is the size of float64
v: math.MaxFloat64, // arbitrary valid v
expectedOk: false,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
for _, ctx := range []context.Context{nil, testCtx} { // Ensure it doesn't crash on nil!
require.Equal(t, tc.expectedOk, memory.WriteFloat64Le(ctx, tc.offset, tc.v))
if tc.expectedOk {
require.Equal(t, tc.expectedBytes, memory.Buffer[tc.offset:tc.offset+8]) // 8 is the size of float64
}
}
})
}
}