源码索引 / CURRENT IMPLEMENTATION
cow_linux_test.go
182 行 · 点击行号可定位;函数链接随声明位置更新,不手填行号。
跳到函数 / 声明
//go:build linux
package spine
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
"testing"
"unsafe"
"golang.org/x/sys/unix"
)
func TestLinuxCOWMappingSealsAddressAndIsolation(t *testing.T) {
resource, err := newCOWRuntime()
if err != nil {
t.Fatal(err)
}
cow := resource.(*linuxCOW)
defer cow.close()
seed := testCOWMemory(t, 4*wasmPageSize)
baseline := seed.Reallocate(2 * wasmPageSize)
if baseline == nil {
t.Fatal("seed allocation failed")
}
for i := range baseline {
baseline[i] = byte((i / cowPageSize) + 17)
}
if err := cow.captureBytes(baseline); err != nil {
t.Fatal(err)
}
seals, err := unix.FcntlInt(uintptr(cow.fd), unix.F_GET_SEALS, 0)
if err != nil {
t.Fatal(err)
}
wantSeals := unix.F_SEAL_SHRINK | unix.F_SEAL_GROW | unix.F_SEAL_WRITE | unix.F_SEAL_SEAL
if seals&wantSeals != wantSeals {
t.Fatalf("seals=%#x want all %#x", seals, wantSeals)
}
seed.Free()
first := testCOWMemory(t, 4*wasmPageSize)
second := testCOWMemory(t, 4*wasmPageSize)
defer first.Free()
defer second.Free()
firstBytes := first.Reallocate(2 * wasmPageSize)
secondBytes := second.Reallocate(2 * wasmPageSize)
firstAddr := sliceAddress(firstBytes)
if err := cow.attachBytes(firstBytes); err != nil {
t.Fatal(err)
}
if got := sliceAddress(firstBytes); got != firstAddr {
t.Fatalf("mapping address changed: before=%#x after=%#x", firstAddr, got)
}
if err := cow.attachBytes(secondBytes); err != nil {
t.Fatal(err)
}
if firstBytes[0] != 17 || secondBytes[0] != 17 || secondBytes[cowPageSize] != 18 {
t.Fatalf("attached mapping did not expose sealed baseline")
}
sharedClean, sharedDirty := smapsForAddress(firstAddr)
if smapsKB(sharedClean)+smapsKB(sharedDirty) == 0 {
t.Fatalf("smaps showed no shared clean/dirty pages before private writes")
}
t.Logf("smaps shared baseline before writes: Shared_Clean=%s Shared_Dirty=%s", sharedClean, sharedDirty)
firstBytes[0] = 99
firstBytes[cowPageSize] = 100
if secondBytes[0] != 17 || secondBytes[cowPageSize] != 18 {
t.Fatalf("private write leaked across mappings: second=%d,%d", secondBytes[0], secondBytes[cowPageSize])
}
sealed := make([]byte, 2*wasmPageSize)
n, err := unix.Pread(cow.fd, sealed, 0)
if err != nil || n != len(sealed) {
t.Fatalf("read sealed baseline: n=%d err=%v", n, err)
}
if sealed[0] != 17 || sealed[cowPageSize] != 18 {
t.Fatalf("private writes changed sealed baseline")
}
grown := first.Reallocate(4 * wasmPageSize)
if sliceAddress(grown) != firstAddr || grown[2*wasmPageSize] != 0 {
t.Fatalf("fixed-range growth moved or dirtied the anonymous tail")
}
if err := first.release(); err != nil {
t.Fatalf("unmap COW prefix and anonymous tail: %v", err)
}
if first.Reallocate(wasmPageSize) != nil {
t.Fatal("released mapping remains usable")
}
}
func testCOWMemory(t *testing.T, maximum uint64) *linuxCOWMemory {
t.Helper()
m, err := newLinuxCOWMemory(maximum)
if err != nil {
t.Fatal(err)
}
t.Cleanup(m.Free)
return m
}
func TestLinuxCOWAllocationFailureAndFree(t *testing.T) {
if _, err := newLinuxCOWMemory(0); err == nil {
t.Fatal("empty reservation must fail")
}
// An impossible address-space request exercises the real mmap error path.
if m, err := newLinuxCOWMemory(uint64(maxInt()) & ^uint64(wasmPageSize-1)); err == nil {
m.Free()
t.Fatal("impossible reservation unexpectedly succeeded")
}
m := testCOWMemory(t, 2*wasmPageSize)
data := m.Reallocate(2 * wasmPageSize)
address := sliceAddress(data)
if err := m.release(); err != nil {
t.Fatalf("munmap failed: %v", err)
}
m.Free()
if m.Reallocate(wasmPageSize) != nil {
t.Fatal("freed memory remains available")
}
clean, dirty := smapsForAddress(address)
if clean != "" || dirty != "" {
maps, _ := os.ReadFile("/proc/self/maps")
for _, line := range strings.Split(string(maps), "\n") {
var start, end uintptr
if _, err := fmt.Sscanf(line, "%x-%x", &start, &end); err == nil && address >= start && address < end {
t.Logf("freed address %#x currently in %s", address, line)
}
}
t.Log("address was reused after successful munmap; this is permitted")
}
}
func sliceAddress(data []byte) uintptr {
return uintptr(unsafe.Pointer(unsafe.SliceData(data)))
}
func smapsForAddress(address uintptr) (sharedClean, sharedDirty string) {
file, err := os.Open("/proc/self/smaps")
if err != nil {
return "", ""
}
defer file.Close()
var inRange bool
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if fields := strings.Fields(line); len(fields) > 0 && strings.Contains(fields[0], "-") {
var start, end uintptr
if _, err := fmt.Sscanf(fields[0], "%x-%x", &start, &end); err == nil {
inRange = address >= start && address < end
}
continue
}
if !inRange {
continue
}
if strings.HasPrefix(line, "Shared_Clean:") {
sharedClean = strings.TrimSpace(strings.TrimPrefix(line, "Shared_Clean:"))
}
if strings.HasPrefix(line, "Shared_Dirty:") {
sharedDirty = strings.TrimSpace(strings.TrimPrefix(line, "Shared_Dirty:"))
}
if sharedClean != "" && sharedDirty != "" {
return sharedClean, sharedDirty
}
}
return sharedClean, sharedDirty
}
func smapsKB(value string) int {
fields := strings.Fields(value)
if len(fields) == 0 {
return 0
}
kb, _ := strconv.Atoi(fields[0])
return kb
}