Files
wasm2go/internal/codegen/helpers/helpers_mematomic_test.go

226 lines
8.5 KiB
Go

package helpers
import (
"encoding/binary"
"sync"
"testing"
"unsafe"
)
// Coverage for the memory-access, passive-segment, atomic, and thread helpers
// that codegen emits into generated output via //go:embed. They are selected by
// name at generation time, so nothing in this package references them directly;
// without a test they are both unlinted (staticcheck U1000) and, worse,
// unguarded against regression. These tests exercise each one against a
// hand-built Module, exactly as generated New() sets one up.
// newMemModuleM builds a Module whose M (the unsafe base pointer the
// mem-access and atomic helpers dereference) points at its memory, the way
// generated New() initialises it. newTestModule alone leaves M nil, which
// these helpers dereference through, so tests that touch memory must use this.
func newMemModuleM(size int) *Module {
m := newTestModule(make([]byte, size))
m.M = unsafe.Pointer(unsafe.SliceData(m.memory))
return m
}
func TestMemoryInitAndDataDrop(t *testing.T) {
m := newMemModuleM(64)
m.dataSegs = [][]byte{[]byte("hello world")}
// Copy "hello" (src 0, len 5) to offset 4.
memoryInit(m, 0, 4, 0, 5)
if got := string(m.memory[4:9]); got != "hello" {
t.Errorf("memoryInit copied %q, want %q", got, "hello")
}
// dataEnd tracks the end of the installed segment (dst 4 + n 5).
if m.dataEnd != 9 {
t.Errorf("dataEnd = %d, want 9 (dst+n)", m.dataEnd)
}
// n == 0 is a no-op and must not touch memory or dataEnd.
memoryInit(m, 0, 60, 0, 0)
if m.dataEnd != 9 {
t.Errorf("dataEnd moved on a zero-length init: %d", m.dataEnd)
}
// Re-initialising with identical bytes is the compare-then-write path: it
// must leave the memory correct (and, though we cannot observe the skipped
// write directly, must not corrupt it).
memoryInit(m, 0, 4, 0, 5)
if got := string(m.memory[4:9]); got != "hello" {
t.Errorf("second memoryInit corrupted memory: %q", got)
}
// Out of bounds on the source and on the destination both trap.
mustPanic(t, "wasm: memory.init out of bounds", func() { memoryInit(m, 0, 0, 0, 100) })
mustPanic(t, "wasm: memory.init out of bounds", func() { memoryInit(m, 0, 60, 0, 10) })
// After data.drop the segment is nil; naming it in memory.init traps.
dataDrop(m, 0)
if m.dataSegs[0] != nil {
t.Error("dataDrop did not nil the segment")
}
mustPanic(t, "wasm: memory.init out of bounds", func() { memoryInit(m, 0, 0, 0, 1) })
}
// TestMemoryInit64DataEnd pins the memory64 variant's shared-image
// bookkeeping: a passive-segment install below 4GiB must RAISE dataEnd
// to its real extent, not saturate it. Threads builds make every
// segment passive (memory.init at start-up is the only installer), so
// a saturating memoryInit64 silently disabled copy-on-write image
// sharing for every wasm64-threads engine.
func TestMemoryInit64DataEnd(t *testing.T) {
m := newMemModuleM(64)
m.dataSegs = [][]byte{[]byte("hello world")}
memoryInit64(m, 0, 4, 0, 5)
if got := string(m.memory[4:9]); got != "hello" {
t.Errorf("memoryInit64 copied %q, want %q", got, "hello")
}
if m.dataEnd != 9 {
t.Errorf("dataEnd = %d, want 9 (dst+n)", m.dataEnd)
}
// Zero-length init is a no-op.
memoryInit64(m, 0, 60, 0, 0)
if m.dataEnd != 9 {
t.Errorf("dataEnd moved on a zero-length init: %d", m.dataEnd)
}
// Identical re-init takes the compare-then-write path (an
// image-backed instance re-running the start section must not
// fault shared pages private) and leaves the bytes correct.
memoryInit64(m, 0, 4, 0, 5)
if got := string(m.memory[4:9]); got != "hello" {
t.Errorf("second memoryInit64 corrupted memory: %q", got)
}
// A later, higher install raises dataEnd monotonically.
memoryInit64(m, 0, 20, 6, 5)
if got := string(m.memory[20:25]); got != "world" {
t.Errorf("memoryInit64 copied %q, want %q", got, "world")
}
if m.dataEnd != 25 {
t.Errorf("dataEnd = %d, want 25", m.dataEnd)
}
// Bounds checks trap like the 32-bit variant.
mustPanic(t, "wasm: memory.init out of bounds", func() { memoryInit64(m, 0, 0, 0, 100) })
mustPanic(t, "wasm: memory.init out of bounds", func() { memoryInit64(m, 0, 60, 0, 10) })
dataDrop(m, 0)
mustPanic(t, "wasm: memory.init out of bounds", func() { memoryInit64(m, 0, 0, 0, 1) })
}
func TestAtomicMemoryOps(t *testing.T) {
m := newMemModuleM(64)
// store / load, 32 and 64 bit.
atomicStore32(m, 0, 0, 0x11223344)
if got := atomicLoad32(m, 0, 0); got != 0x11223344 {
t.Errorf("atomicLoad32 = %#x, want 0x11223344", got)
}
atomicStore64(m, 8, 0, 0x1122334455667788)
if got := atomicLoad64(m, 8, 0); got != 0x1122334455667788 {
t.Errorf("atomicLoad64 = %#x", got)
}
// RMW helpers return the OLD value and leave the new one behind.
atomicStore32(m, 0, 0, 100)
if old := atomicRmwAdd32(m, 0, 0, 5); old != 100 {
t.Errorf("atomicRmwAdd32 old = %d, want 100", old)
}
if got := atomicLoad32(m, 0, 0); got != 105 {
t.Errorf("after add: %d, want 105", got)
}
if old := atomicRmwSub32(m, 0, 0, 5); old != 105 {
t.Errorf("atomicRmwSub32 old = %d, want 105", old)
}
atomicStore32(m, 0, 0, 0xF0)
if old := atomicRmwAnd32(m, 0, 0, 0x3C); old != 0xF0 || atomicLoad32(m, 0, 0) != 0x30 {
t.Errorf("atomicRmwAnd32: old %#x new %#x, want 0xF0 / 0x30", old, atomicLoad32(m, 0, 0))
}
atomicStore32(m, 0, 0, 0xF0)
if old := atomicRmwOr32(m, 0, 0, 0x0F); old != 0xF0 || atomicLoad32(m, 0, 0) != 0xFF {
t.Errorf("atomicRmwOr32: old %#x new %#x, want 0xF0 / 0xFF", old, atomicLoad32(m, 0, 0))
}
atomicStore32(m, 0, 0, 0xFF)
if old := atomicRmwXor32(m, 0, 0, 0x0F); old != 0xFF || atomicLoad32(m, 0, 0) != 0xF0 {
t.Errorf("atomicRmwXor32: old %#x new %#x, want 0xFF / 0xF0", old, atomicLoad32(m, 0, 0))
}
// The generic op-taking RMW cores (atomicRmw32At / atomicRmw64At).
atomicStore32(m, 0, 0, 7)
if old := atomicRmw32At(m, atomicEA(m, 0, 0, 4), func(o uint32) uint32 { return o * 3 }); old != 7 || atomicLoad32(m, 0, 0) != 21 {
t.Errorf("atomicRmw32At: old %d new %d, want 7 / 21", old, atomicLoad32(m, 0, 0))
}
atomicStore64(m, 8, 0, 4)
if old := atomicRmw64At(m, atomicEA(m, 8, 0, 8), func(o uint64) uint64 { return o + 100 }); old != 4 || atomicLoad64(m, 8, 0) != 104 {
t.Errorf("atomicRmw64At: old %d new %d, want 4 / 104", old, atomicLoad64(m, 8, 0))
}
// Sub-word atomics: store and zero-extended load, 8- and 16-bit lanes.
atomicStore32_8(m, 16, 0, 0xAB)
if got := atomicLoad32_8u(m, 16, 0); got != 0xAB {
t.Errorf("atomicLoad32_8u = %#x, want 0xAB", got)
}
atomicStore32_16(m, 16, 0, 0xBEEF)
if got := atomicLoad32_16u(m, 16, 0); got != 0xBEEF {
t.Errorf("atomicLoad32_16u = %#x, want 0xBEEF", got)
}
atomicStore64_8(m, 24, 0, 0xCD)
if got := atomicLoad64_8u(m, 24, 0); got != 0xCD {
t.Errorf("atomicLoad64_8u = %#x, want 0xCD", got)
}
atomicStore64_16(m, 24, 0, 0xFACE)
if got := atomicLoad64_16u(m, 24, 0); got != 0xFACE {
t.Errorf("atomicLoad64_16u = %#x, want 0xFACE", got)
}
atomicStore64_32(m, 24, 0, 0x0BADF00D)
if got := atomicLoad64_32u(m, 24, 0); got != 0x0BADF00D {
t.Errorf("atomicLoad64_32u = %#x, want 0x0BADF00D", got)
}
// atomicEA's bounds and alignment checks (reached through the ops above).
mustPanic(t, "wasm: unaligned atomic access", func() { atomicLoad32(m, 1, 0) })
mustPanic(t, "wasm: atomic access out of bounds", func() { atomicLoad32(m, 60, 8) })
}
func TestThreadSpawnRunsThreadStart(t *testing.T) {
m := newMemModuleM(64)
var mu sync.Mutex
var gotTID, gotArg int32
m.threadStart = func(child *Module, tid, arg int32) {
mu.Lock()
gotTID, gotArg = tid, arg
mu.Unlock()
// The agent runs on a struct COPY that shares the memory: a write here
// must be visible through the parent Module.
binary.LittleEndian.PutUint32(child.memory[0:4], uint32(arg*2))
}
tid := threadSpawn(m, 21)
if tid != 1 {
t.Errorf("first threadSpawn tid = %d, want 1", tid)
}
ThreadsWait(m)
mu.Lock()
defer mu.Unlock()
if gotTID != 1 || gotArg != 21 {
t.Errorf("threadStart saw tid=%d arg=%d, want 1/21", gotTID, gotArg)
}
if got := int32(binary.LittleEndian.Uint32(m.memory[0:4])); got != 42 {
t.Errorf("agent's write not visible through shared memory: %d, want 42", got)
}
// A module that exports no wasi_thread_start returns -1.
if got := threadSpawn(newMemModuleM(8), 0); got != -1 {
t.Errorf("threadSpawn with no threadStart = %d, want -1", got)
}
}
func TestWasmTrapHelpersPanic(t *testing.T) {
mustPanic(t, "wasm: memory.init out of bounds", wasm_trap_meminit_oob)
mustPanic(t, "wasm: atomic access out of bounds", wasm_trap_atomic_oob)
mustPanic(t, "wasm: unaligned atomic access", wasm_trap_atomic_unaligned)
mustPanic(t, "wasm: blocking atomic wait with no other agents (wasi-threads not enabled)",
wasm_trap_atomic_wait_forever)
}