516 lines
17 KiB
Go
516 lines
17 KiB
Go
// Copyright 2020 The gVisor Authors.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// Package marshal_test contains manual tests for the marshal interface. These
|
|
// are intended to test behaviour not covered by the automatically generated
|
|
// tests.
|
|
package marshal_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"reflect"
|
|
"runtime"
|
|
"testing"
|
|
"unsafe"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"gvisor.dev/gvisor/pkg/syserror"
|
|
"gvisor.dev/gvisor/pkg/usermem"
|
|
"gvisor.dev/gvisor/tools/go_marshal/analysis"
|
|
"gvisor.dev/gvisor/tools/go_marshal/marshal"
|
|
"gvisor.dev/gvisor/tools/go_marshal/test"
|
|
)
|
|
|
|
var simulatedErr error = syserror.EFAULT
|
|
|
|
// mockTask implements marshal.Task.
|
|
type mockTask struct {
|
|
taskMem usermem.BytesIO
|
|
}
|
|
|
|
// populate fills the task memory with the contents of val.
|
|
func (t *mockTask) populate(val interface{}) {
|
|
var buf bytes.Buffer
|
|
// Use binary.Write so we aren't testing go-marshal against its own
|
|
// potentially buggy implementation.
|
|
if err := binary.Write(&buf, usermem.ByteOrder, val); err != nil {
|
|
panic(err)
|
|
}
|
|
t.taskMem.Bytes = buf.Bytes()
|
|
}
|
|
|
|
func (t *mockTask) setLimit(n int) {
|
|
if len(t.taskMem.Bytes) < n {
|
|
grown := make([]byte, n)
|
|
copy(grown, t.taskMem.Bytes)
|
|
t.taskMem.Bytes = grown
|
|
return
|
|
}
|
|
t.taskMem.Bytes = t.taskMem.Bytes[:n]
|
|
}
|
|
|
|
// CopyScratchBuffer implements marshal.Task.CopyScratchBuffer.
|
|
func (t *mockTask) CopyScratchBuffer(size int) []byte {
|
|
return make([]byte, size)
|
|
}
|
|
|
|
// CopyOutBytes implements marshal.Task.CopyOutBytes. The implementation
|
|
// completely ignores the target address and stores a copy of b in its
|
|
// internally buffer, overriding any previous contents.
|
|
func (t *mockTask) CopyOutBytes(_ usermem.Addr, b []byte) (int, error) {
|
|
return t.taskMem.CopyOut(nil, 0, b, usermem.IOOpts{})
|
|
}
|
|
|
|
// CopyInBytes implements marshal.Task.CopyInBytes. The implementation
|
|
// completely ignores the source address and always fills b from the begining of
|
|
// its internal buffer.
|
|
func (t *mockTask) CopyInBytes(_ usermem.Addr, b []byte) (int, error) {
|
|
return t.taskMem.CopyIn(nil, 0, b, usermem.IOOpts{})
|
|
}
|
|
|
|
// unsafeMemory returns the underlying memory for m. The returned slice is only
|
|
// valid for the lifetime for m. The garbage collector isn't aware that the
|
|
// returned slice is related to m, the caller must ensure m lives long enough.
|
|
func unsafeMemory(m marshal.Marshallable) []byte {
|
|
if !m.Packed() {
|
|
// We can't return a slice pointing to the underlying memory
|
|
// since the layout isn't packed. Allocate a temporary buffer
|
|
// and marshal instead.
|
|
var buf bytes.Buffer
|
|
if err := binary.Write(&buf, usermem.ByteOrder, m); err != nil {
|
|
panic(err)
|
|
}
|
|
return buf.Bytes()
|
|
}
|
|
|
|
// reflect.ValueOf(m)
|
|
// .Elem() // Unwrap interface to inner concrete object
|
|
// .Addr() // Pointer value to object
|
|
// .Pointer() // Actual address from the pointer value
|
|
ptr := reflect.ValueOf(m).Elem().Addr().Pointer()
|
|
|
|
size := m.SizeBytes()
|
|
|
|
var mem []byte
|
|
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&mem))
|
|
hdr.Data = ptr
|
|
hdr.Len = size
|
|
hdr.Cap = size
|
|
|
|
return mem
|
|
}
|
|
|
|
// unsafeMemorySlice returns the underlying memory for m. The returned slice is
|
|
// only valid for the lifetime for m. The garbage collector isn't aware that the
|
|
// returned slice is related to m, the caller must ensure m lives long enough.
|
|
//
|
|
// Precondition: m must be a slice.
|
|
func unsafeMemorySlice(m interface{}, elt marshal.Marshallable) []byte {
|
|
kind := reflect.TypeOf(m).Kind()
|
|
if kind != reflect.Slice {
|
|
panic("unsafeMemorySlice called on non-slice")
|
|
}
|
|
|
|
if !elt.Packed() {
|
|
// We can't return a slice pointing to the underlying memory
|
|
// since the layout isn't packed. Allocate a temporary buffer
|
|
// and marshal instead.
|
|
var buf bytes.Buffer
|
|
if err := binary.Write(&buf, usermem.ByteOrder, m); err != nil {
|
|
panic(err)
|
|
}
|
|
return buf.Bytes()
|
|
}
|
|
|
|
v := reflect.ValueOf(m)
|
|
length := v.Len() * elt.SizeBytes()
|
|
|
|
var mem []byte
|
|
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&mem))
|
|
hdr.Data = v.Pointer() // This is a pointer to the first elem for slices.
|
|
hdr.Len = length
|
|
hdr.Cap = length
|
|
|
|
return mem
|
|
}
|
|
|
|
func isZeroes(buf []byte) bool {
|
|
for _, b := range buf {
|
|
if b != 0 {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// compareMemory compares the first n bytes of two chuncks of memory represented
|
|
// by expected and actual.
|
|
func compareMemory(t *testing.T, expected, actual []byte, n int) {
|
|
t.Logf("Expected (%d): %v (%d) + (%d) %v\n", len(expected), expected[:n], n, len(expected)-n, expected[n:])
|
|
t.Logf("Actual (%d): %v (%d) + (%d) %v\n", len(actual), actual[:n], n, len(actual)-n, actual[n:])
|
|
|
|
if diff := cmp.Diff(expected[:n], actual[:n]); diff != "" {
|
|
t.Errorf("Memory buffers don't match:\n--- expected only\n+++ actual only\n%v", diff)
|
|
}
|
|
}
|
|
|
|
// limitedCopyIn populates task memory with src, then unmarshals task memory to
|
|
// dst. The task signals an error at limit bytes during copy-in, which should
|
|
// result in a truncated unmarshalling.
|
|
func limitedCopyIn(t *testing.T, src, dst marshal.Marshallable, limit int) {
|
|
var task mockTask
|
|
task.populate(src)
|
|
task.setLimit(limit)
|
|
|
|
n, err := dst.CopyIn(&task, usermem.Addr(0))
|
|
if n != limit {
|
|
t.Errorf("CopyIn copied unexpected number of bytes, expected %d, got %d", limit, n)
|
|
}
|
|
if err != simulatedErr {
|
|
t.Errorf("CopyIn returned unexpected error, expected %v, got %v", simulatedErr, err)
|
|
}
|
|
|
|
expectedMem := unsafeMemory(src)
|
|
defer runtime.KeepAlive(src)
|
|
actualMem := unsafeMemory(dst)
|
|
defer runtime.KeepAlive(dst)
|
|
|
|
compareMemory(t, expectedMem, actualMem, n)
|
|
|
|
// The last n bytes should be zero for actual, since actual was
|
|
// zero-initialized, and CopyIn shouldn't have touched those bytes. However
|
|
// we can only guarantee we didn't touch anything in the last n bytes if the
|
|
// layout is packed.
|
|
if dst.Packed() && !isZeroes(actualMem[n:]) {
|
|
t.Errorf("Expected the last %d bytes of copied in object to be zeroes, got %v\n", dst.SizeBytes()-n, actualMem)
|
|
}
|
|
}
|
|
|
|
// limitedCopyOut marshals src to task memory. The task signals an error at
|
|
// limit bytes during copy-out, which should result in a truncated marshalling.
|
|
func limitedCopyOut(t *testing.T, src marshal.Marshallable, limit int) {
|
|
var task mockTask
|
|
task.setLimit(limit)
|
|
|
|
n, err := src.CopyOut(&task, usermem.Addr(0))
|
|
if n != limit {
|
|
t.Errorf("CopyOut copied unexpected number of bytes, expected %d, got %d", limit, n)
|
|
}
|
|
if err != simulatedErr {
|
|
t.Errorf("CopyOut returned unexpected error, expected %v, got %v", simulatedErr, err)
|
|
}
|
|
|
|
expectedMem := unsafeMemory(src)
|
|
defer runtime.KeepAlive(src)
|
|
actualMem := task.taskMem.Bytes
|
|
|
|
compareMemory(t, expectedMem, actualMem, n)
|
|
}
|
|
|
|
// copyOutN marshals src to task memory, requesting the marshalling to be
|
|
// limited to limit bytes.
|
|
func copyOutN(t *testing.T, src marshal.Marshallable, limit int) {
|
|
var task mockTask
|
|
task.setLimit(limit)
|
|
|
|
n, err := src.CopyOutN(&task, usermem.Addr(0), limit)
|
|
if err != nil {
|
|
t.Errorf("CopyOut returned unexpected error: %v", err)
|
|
}
|
|
if n != limit {
|
|
t.Errorf("CopyOut copied unexpected number of bytes, expected %d, got %d", limit, n)
|
|
}
|
|
|
|
expectedMem := unsafeMemory(src)
|
|
defer runtime.KeepAlive(src)
|
|
actualMem := task.taskMem.Bytes
|
|
|
|
t.Logf("Expected: %v + %v\n", expectedMem[:n], expectedMem[n:])
|
|
t.Logf("Actual : %v + %v\n", actualMem[:n], actualMem[n:])
|
|
|
|
compareMemory(t, expectedMem, actualMem, n)
|
|
}
|
|
|
|
// TestLimitedMarshalling verifies marshalling/unmarshalling succeeds when the
|
|
// underyling copy in/out operations partially succeed.
|
|
func TestLimitedMarshalling(t *testing.T) {
|
|
types := []reflect.Type{
|
|
// Packed types.
|
|
reflect.TypeOf((*test.Type2)(nil)),
|
|
reflect.TypeOf((*test.Type3)(nil)),
|
|
reflect.TypeOf((*test.Timespec)(nil)),
|
|
reflect.TypeOf((*test.Stat)(nil)),
|
|
reflect.TypeOf((*test.InetAddr)(nil)),
|
|
reflect.TypeOf((*test.SignalSet)(nil)),
|
|
reflect.TypeOf((*test.SignalSetAlias)(nil)),
|
|
// Non-packed types.
|
|
reflect.TypeOf((*test.Type1)(nil)),
|
|
reflect.TypeOf((*test.Type4)(nil)),
|
|
reflect.TypeOf((*test.Type5)(nil)),
|
|
reflect.TypeOf((*test.Type6)(nil)),
|
|
reflect.TypeOf((*test.Type7)(nil)),
|
|
reflect.TypeOf((*test.Type8)(nil)),
|
|
}
|
|
|
|
for _, tyPtr := range types {
|
|
// Remove one level of pointer-indirection from the type. We get this
|
|
// back when we pass the type to reflect.New.
|
|
ty := tyPtr.Elem()
|
|
|
|
// Partial copy-in.
|
|
t.Run(fmt.Sprintf("PartialCopyIn_%v", ty), func(t *testing.T) {
|
|
expected := reflect.New(ty).Interface().(marshal.Marshallable)
|
|
actual := reflect.New(ty).Interface().(marshal.Marshallable)
|
|
analysis.RandomizeValue(expected)
|
|
|
|
limitedCopyIn(t, expected, actual, expected.SizeBytes()/2)
|
|
})
|
|
|
|
// Partial copy-out.
|
|
t.Run(fmt.Sprintf("PartialCopyOut_%v", ty), func(t *testing.T) {
|
|
expected := reflect.New(ty).Interface().(marshal.Marshallable)
|
|
analysis.RandomizeValue(expected)
|
|
|
|
limitedCopyOut(t, expected, expected.SizeBytes()/2)
|
|
})
|
|
|
|
// Explicitly request partial copy-out.
|
|
t.Run(fmt.Sprintf("PartialCopyOutN_%v", ty), func(t *testing.T) {
|
|
expected := reflect.New(ty).Interface().(marshal.Marshallable)
|
|
analysis.RandomizeValue(expected)
|
|
|
|
copyOutN(t, expected, expected.SizeBytes()/2)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestLimitedMarshalling verifies marshalling/unmarshalling of slices of
|
|
// marshallable types succeed when the underyling copy in/out operations
|
|
// partially succeed.
|
|
func TestLimitedSliceMarshalling(t *testing.T) {
|
|
types := []struct {
|
|
arrayPtrType reflect.Type
|
|
copySliceIn func(task marshal.Task, addr usermem.Addr, dstSlice interface{}) (int, error)
|
|
copySliceOut func(task marshal.Task, addr usermem.Addr, srcSlice interface{}) (int, error)
|
|
unsafeMemory func(arrPtr interface{}) []byte
|
|
}{
|
|
// Packed types.
|
|
{
|
|
reflect.TypeOf((*[20]test.Stat)(nil)),
|
|
func(task marshal.Task, addr usermem.Addr, dst interface{}) (int, error) {
|
|
slice := dst.(*[20]test.Stat)[:]
|
|
return test.CopyStatSliceIn(task, addr, slice)
|
|
},
|
|
func(task marshal.Task, addr usermem.Addr, src interface{}) (int, error) {
|
|
slice := src.(*[20]test.Stat)[:]
|
|
return test.CopyStatSliceOut(task, addr, slice)
|
|
},
|
|
func(a interface{}) []byte {
|
|
slice := a.(*[20]test.Stat)[:]
|
|
return unsafeMemorySlice(slice, &slice[0])
|
|
},
|
|
},
|
|
{
|
|
reflect.TypeOf((*[1]test.Stat)(nil)),
|
|
func(task marshal.Task, addr usermem.Addr, dst interface{}) (int, error) {
|
|
slice := dst.(*[1]test.Stat)[:]
|
|
return test.CopyStatSliceIn(task, addr, slice)
|
|
},
|
|
func(task marshal.Task, addr usermem.Addr, src interface{}) (int, error) {
|
|
slice := src.(*[1]test.Stat)[:]
|
|
return test.CopyStatSliceOut(task, addr, slice)
|
|
},
|
|
func(a interface{}) []byte {
|
|
slice := a.(*[1]test.Stat)[:]
|
|
return unsafeMemorySlice(slice, &slice[0])
|
|
},
|
|
},
|
|
{
|
|
reflect.TypeOf((*[5]test.SignalSetAlias)(nil)),
|
|
func(task marshal.Task, addr usermem.Addr, dst interface{}) (int, error) {
|
|
slice := dst.(*[5]test.SignalSetAlias)[:]
|
|
return test.CopySignalSetAliasSliceIn(task, addr, slice)
|
|
},
|
|
func(task marshal.Task, addr usermem.Addr, src interface{}) (int, error) {
|
|
slice := src.(*[5]test.SignalSetAlias)[:]
|
|
return test.CopySignalSetAliasSliceOut(task, addr, slice)
|
|
},
|
|
func(a interface{}) []byte {
|
|
slice := a.(*[5]test.SignalSetAlias)[:]
|
|
return unsafeMemorySlice(slice, &slice[0])
|
|
},
|
|
},
|
|
// Non-packed types.
|
|
{
|
|
reflect.TypeOf((*[20]test.Type1)(nil)),
|
|
func(task marshal.Task, addr usermem.Addr, dst interface{}) (int, error) {
|
|
slice := dst.(*[20]test.Type1)[:]
|
|
return test.CopyType1SliceIn(task, addr, slice)
|
|
},
|
|
func(task marshal.Task, addr usermem.Addr, src interface{}) (int, error) {
|
|
slice := src.(*[20]test.Type1)[:]
|
|
return test.CopyType1SliceOut(task, addr, slice)
|
|
},
|
|
func(a interface{}) []byte {
|
|
slice := a.(*[20]test.Type1)[:]
|
|
return unsafeMemorySlice(slice, &slice[0])
|
|
},
|
|
},
|
|
{
|
|
reflect.TypeOf((*[1]test.Type1)(nil)),
|
|
func(task marshal.Task, addr usermem.Addr, dst interface{}) (int, error) {
|
|
slice := dst.(*[1]test.Type1)[:]
|
|
return test.CopyType1SliceIn(task, addr, slice)
|
|
},
|
|
func(task marshal.Task, addr usermem.Addr, src interface{}) (int, error) {
|
|
slice := src.(*[1]test.Type1)[:]
|
|
return test.CopyType1SliceOut(task, addr, slice)
|
|
},
|
|
func(a interface{}) []byte {
|
|
slice := a.(*[1]test.Type1)[:]
|
|
return unsafeMemorySlice(slice, &slice[0])
|
|
},
|
|
},
|
|
{
|
|
reflect.TypeOf((*[7]test.Type8)(nil)),
|
|
func(task marshal.Task, addr usermem.Addr, dst interface{}) (int, error) {
|
|
slice := dst.(*[7]test.Type8)[:]
|
|
return test.CopyType8SliceIn(task, addr, slice)
|
|
},
|
|
func(task marshal.Task, addr usermem.Addr, src interface{}) (int, error) {
|
|
slice := src.(*[7]test.Type8)[:]
|
|
return test.CopyType8SliceOut(task, addr, slice)
|
|
},
|
|
func(a interface{}) []byte {
|
|
slice := a.(*[7]test.Type8)[:]
|
|
return unsafeMemorySlice(slice, &slice[0])
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range types {
|
|
// The body of this loop is generic over the type tt.arrayPtrType, with
|
|
// the help of reflection. To aid in readability, comments below show
|
|
// the equivalent go code assuming
|
|
// tt.arrayPtrType = typeof(*[20]test.Stat).
|
|
|
|
// Equivalent:
|
|
// var x *[20]test.Stat
|
|
// arrayTy := reflect.TypeOf(*x)
|
|
arrayTy := tt.arrayPtrType.Elem()
|
|
|
|
// Partial copy-in of slices.
|
|
t.Run(fmt.Sprintf("PartialCopySliceIn_%v", arrayTy), func(t *testing.T) {
|
|
// Equivalent:
|
|
// var x [20]test.Stat
|
|
// length := len(x)
|
|
length := arrayTy.Len()
|
|
if length < 1 {
|
|
panic("Test type can't be zero-length array")
|
|
}
|
|
// Equivalent:
|
|
// elem := new(test.Stat).(marshal.Marshallable)
|
|
elem := reflect.New(arrayTy.Elem()).Interface().(marshal.Marshallable)
|
|
|
|
// Equivalent:
|
|
// var expected, actual interface{}
|
|
// expected = new([20]test.Stat)
|
|
// actual = new([20]test.Stat)
|
|
expected := reflect.New(arrayTy).Interface()
|
|
actual := reflect.New(arrayTy).Interface()
|
|
|
|
analysis.RandomizeValue(expected)
|
|
|
|
limit := (length * elem.SizeBytes()) / 2
|
|
// Also make sure the limit is partially inside one of the elements.
|
|
limit += elem.SizeBytes() / 2
|
|
analysis.RandomizeValue(expected)
|
|
|
|
var task mockTask
|
|
task.populate(expected)
|
|
task.setLimit(limit)
|
|
|
|
n, err := tt.copySliceIn(&task, usermem.Addr(0), actual)
|
|
if n != limit {
|
|
t.Errorf("CopyIn copied unexpected number of bytes, expected %d, got %d", limit, n)
|
|
}
|
|
if n < length*elem.SizeBytes() && err != simulatedErr {
|
|
t.Errorf("CopyIn returned unexpected error, expected %v, got %v", simulatedErr, err)
|
|
}
|
|
|
|
expectedMem := tt.unsafeMemory(expected)
|
|
defer runtime.KeepAlive(expected)
|
|
actualMem := tt.unsafeMemory(actual)
|
|
defer runtime.KeepAlive(actual)
|
|
|
|
compareMemory(t, expectedMem, actualMem, n)
|
|
|
|
// The last n bytes should be zero for actual, since actual was
|
|
// zero-initialized, and CopyIn shouldn't have touched those bytes. However
|
|
// we can only guarantee we didn't touch anything in the last n bytes if the
|
|
// layout is packed.
|
|
if elem.Packed() && !isZeroes(actualMem[n:]) {
|
|
t.Errorf("Expected the last %d bytes of copied in object to be zeroes, got %v\n", (elem.SizeBytes()*length)-n, actualMem)
|
|
}
|
|
})
|
|
|
|
// Partial copy-out of slices.
|
|
t.Run(fmt.Sprintf("PartialCopySliceOut_%v", arrayTy), func(t *testing.T) {
|
|
// Equivalent:
|
|
// var x [20]test.Stat
|
|
// length := len(x)
|
|
length := arrayTy.Len()
|
|
if length < 1 {
|
|
panic("Test type can't be zero-length array")
|
|
}
|
|
// Equivalent:
|
|
// elem := new(test.Stat).(marshal.Marshallable)
|
|
elem := reflect.New(arrayTy.Elem()).Interface().(marshal.Marshallable)
|
|
|
|
// Equivalent:
|
|
// var expected, actual interface{}
|
|
// expected = new([20]test.Stat)
|
|
// actual = new([20]test.Stat)
|
|
expected := reflect.New(arrayTy).Interface()
|
|
|
|
analysis.RandomizeValue(expected)
|
|
|
|
limit := (length * elem.SizeBytes()) / 2
|
|
// Also make sure the limit is partially inside one of the elements.
|
|
limit += elem.SizeBytes() / 2
|
|
analysis.RandomizeValue(expected)
|
|
|
|
var task mockTask
|
|
task.populate(expected)
|
|
task.setLimit(limit)
|
|
|
|
n, err := tt.copySliceOut(&task, usermem.Addr(0), expected)
|
|
if n != limit {
|
|
t.Errorf("CopyIn copied unexpected number of bytes, expected %d, got %d", limit, n)
|
|
}
|
|
if n < length*elem.SizeBytes() && err != simulatedErr {
|
|
t.Errorf("CopyIn returned unexpected error, expected %v, got %v", simulatedErr, err)
|
|
}
|
|
|
|
expectedMem := tt.unsafeMemory(expected)
|
|
defer runtime.KeepAlive(expected)
|
|
actualMem := task.taskMem.Bytes
|
|
|
|
compareMemory(t, expectedMem, actualMem, n)
|
|
})
|
|
}
|
|
}
|