PPysolate SpineREAD THE CODE · UNDERSTAND THE SYSTEM
课程与章节
CODE READING / 25

验证映射与释放

cow_linux_test.go

本章目录

本章回答:Linux COW 的关键状态能否在不运行 Python 的情况下被直接观察?测试直接操作 linuxCOW 和 linuxCOWMemory,按“创建资源 → 捕获并封存 → 两个实例 attach → 私有写入 → 固定范围增长 → release”推进;第二个测试覆盖预约失败、重复释放和地址复用。

这些断言区分三种对象:sealed memfd 是 Runner 级基线;first、second 是各自拥有 mapping 的实例;slice 只是当前 Guest memory 的借用视图。smaps 只提供当前 Linux 进程对 VMA 的观测,不推出固定的物理内存节省或所有工作负载的性能结论。

1. Linux 测试工具和第一条入口

cow_linux_test.go · L1–L17 · 打开完整源码

//go:build linux

package spine

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
	"testing"
	"unsafe"

	"golang.org/x/sys/unix"
)

func TestLinuxCOWMappingSealsAddressAndIsolation(t *testing.T) {

构建标签限制测试在 Linux。测试导入 bufio、文件/字符串/数字解析、testing、unsafe 和 unix:前者读取 /proc/self/smaps,unsafe 只取 slice 起点,unix 读取 seal、执行 Pread。第一条测试名已经列出它要同时检查的证据:mapping、seals、地址稳定和写隔离。

2. 建立空 runtime,捕获两页基线并检查 seals

cow_linux_test.go · L18–L44 · 打开完整源码

	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()

测试创建 newCOWRuntime 并确认具体类型为 *linuxCOW,用 defer 关闭 Runner 级 fd。随后预留最大 4 个 Wasm 页,只把前 2 页通过 Reallocate 暴露为当前 baseline;填充模式时按 OS page 递增,从而第一个系统页全为 17,第二个全为 18。

captureBytes 把这两页写入 memfd 并加 seals。测试通过 F_GET_SEALS 从内核回读 F_SEAL_SHRINK、F_SEAL_GROW、F_SEAL_WRITE、F_SEAL_SEAL,检查的是 fd 的实际状态。最后释放只用于生成基线的 seed mapping;后面的实例从 sealed fd attach。

状态转移:

runtime:空 fd,imageSize=0
seed:   匿名 4 页,当前可见 2 页,内容=17/18
capture:fd 写入并加四个 seal,imageSize=2 Wasm 页
seed:   释放;sealed fd 仍属于 runtime

3. 两个实例 attach 到各自的 mapping base

cow_linux_test.go · L45–L69 · 打开完整源码


	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)

测试为 first 和 second 各预留最大 4 页,并注册 cleanup。两者先把当前长度设为 2 个 Wasm 页;测试保存 first 的 slice 地址,然后 attach。attach 前后地址必须完全相同,说明 MAP_FIXED 替换的是原范围,而不是分配了一个新起点。

两个实例 attach 后都应读到封存模式:first 的第一个字节为 17,second 的第一个 OS page 为 17、第二个为 18。测试再从 first 的地址读取 smaps,要求 Shared_Clean 或 Shared_Dirty 至少一个非零。这只是“尚未私有写时内核观察到共享页”的证据,不是性能测量。

first 与 second 不要求拥有相同的 Host 地址;每个实例只要求自己的 attach 前后地址稳定。Guest memory 的相同内容来自同一个 fd,mapping base 仍是实例级状态。

4. 私有写入、封存基线和匿名尾部增长

cow_linux_test.go · L70–L94 · 打开完整源码

	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")
	}

}

first 修改两个不同 OS page 的首字节为 99 和 100,second 仍应读到 17 和 18,证明写入没有穿透到另一个 MAP_PRIVATE 映射。测试随后用 Pread 从 fd 读回两页,确认 sealed baseline 也没有改变。

接着把 first 的当前长度扩到 4 个 Wasm 页。slice 起点必须不变,原 image 之后的第一个字节必须为零;尾部来自预留的匿名范围,而不是 memfd。最后显式 release,再要求 Reallocate 返回 nil,证明释放后的实例不再可用。

这里的写隔离粒度是 OS page:写两个字节会使它们所在的系统页私有化,不表示整个 Wasm 线性内存按 64 KiB 一次复制。测试只覆盖这个具体访问。

5. helper 的资源兜底和分配失败测试

cow_linux_test.go · L96–L105 · 打开完整源码

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
}

testCOWMemory 把构造错误转成测试失败,并注册 t.Cleanup(m.Free)。cleanup 保护中途 Fatal 的路径;它不替代前一节对 release 行为的显式断言。第二条测试随后专门检查分配失败和 Free。

cow_linux_test.go · L106–L125 · 打开完整源码

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)

零 maximum 必须失败。接近 maxInt 且按 Wasm page 对齐的请求用真实 mmap 错误路径测试不可行地址空间预约,而不是注入一个假的错误。正常建立 2 页 mapping 后,测试保存地址,先 release 再 Free,要求二次清理安全,且之后 Reallocate 返回 nil。

若 smaps 仍能看到同一数字地址,测试读取 /proc/self/maps 记录当前覆盖它的 VMA;它不把地址数字再次出现当作原对象仍然存在。成功 munmap 后地址被其他分配复用是允许状态。

6. 地址读取和 /proc 解析

cow_linux_test.go · L126–L137 · 打开完整源码

	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")
	}
}

这段分支只在释放地址仍有 smaps 信息时运行。测试逐行解析 /proc/self/maps 的十六进制区间,记录是否有当前映射覆盖旧地址,并明确把复用写成日志信息。真正的资源断言是 release 结果与后续 Reallocate,不是地址永远从进程地址空间消失。

cow_linux_test.go · L138–L161 · 打开完整源码

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
		}

sliceAddress 用 unsafe.SliceData 取当前 slice 首地址,只用于比较,不长期保存这个地址。smapsForAddress 打开 /proc/self/smaps;若观测入口不可用,返回空字段,调用方只失去 smaps 证据,seal、读值和隔离断言仍独立存在。

扫描器识别 VMA 头的 start-end 区间,设置 inRange,跳过头行及目标范围外的属性。它把一个地址落在哪个 VMA 与 Guest memory 的字节内容分开处理。

7. 共享页字段和状态表

cow_linux_test.go · L162–L182 · 打开完整源码

		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
}

扫描器在目标 VMA 中读取 Shared_Clean 和 Shared_Dirty 文本;两个字段齐全就返回,否则返回已经读到的部分。smapsKB 只解析数值的 KB 部分,因为测试只比较是否为零,不把它当成通用内存统计。

阶段 memfd first second
capture 后 2 页、四个 seal seed 释放 尚未建立
attach 后、写入前 17/18 不变 17/18 17/18
first 写两个 OS page 17/18 不变 99/100 17/18
first 增长到 4 Wasm 页 仍 2 页 起点不变,尾部为零 不受影响
first release fd 仍可供其他实例使用 不可再 Reallocate cleanup 负责释放 second
runtime close fd 关闭 实例应已结束 实例应已结束

这组测试把初始化、基线、私有映射、增长和释放分别落到可观察状态;真实 Guest 对 Memory.Grow、Python 写入和 Runner 关闭的检查见 cow-guest-tests.md。