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

COW 上的真实 Python

cow_test.go

本章目录

本章回答:Linux COW 的 allocator、attach 和释放是否仍支持真实 Guest 的正常执行?测试在 Linux 上加载仓库已有的 dist/spine.wasm,先验证真实 Memory.Grow 和匿名尾部,再检查工具、Python 写入、PLM、prefix、错误、取消、实例 mapping 回收和 Runner fd 回收。

测试区分两层状态:Guest memory 包括 Wasm 可见字节、当前长度和私有写入;Runner state 包括编译模块、Manifest 工具和 sealed COW image。r.image == nil 只说明 COW Runner 没有同时保留 full-copy Go 字节,不表示它没有基线;基线在 r.cow 管理的 memfd 中。测试只针对这个 artifact 的干净 _initialize 边界,不把它泛化为任意 Wasm 状态格式。

1. 平台、artifact 和运行上下文

cow_test.go · L1–L23 · 打开完整源码

package spine

import (
	"context"
	"encoding/json"
	"errors"
	"os"
	"runtime"
	"strings"
	"testing"
	"time"
)

func TestPreparedCOWRealGuestLifecycle(t *testing.T) {
	if runtime.GOOS != "linux" {
		t.Skip("Linux COW is intentionally unsupported on this platform")
	}
	wasm, err := os.ReadFile("dist/spine.wasm")
	if err != nil {
		t.Fatal(err)
	}
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
	defer cancel()

测试使用 spine 包,导入上下文、JSON、错误、文件、runtime、字符串、testing 和 time。入口先检查 runtime.GOOS;非 Linux 明确 Skip,因为本实现没有承诺其他平台的 COW。随后读取真实 dist/spine.wasm,缺失就失败,建立两分钟上下文并在退出时取消。

因此“跳过”与“通过”分开:macOS 上可以使用 full-copy,但不能用它替代 Linux COW 的测试证据。artifact 也不能用简化模块替代,因为 memory maximum、初始化内容和导出函数都影响 allocator 路径。

2. Manifest 工具是测试输入,不是基线内容

cow_test.go · L24–L41 · 打开完整源码

	manifest := Manifest{"lookup": {Call: func(ctx context.Context, args json.RawMessage) (any, error) {
		var request struct {
			Key string `json:"key"`
		}
		if err := json.Unmarshal(args, &request); err != nil {
			return nil, err
		}
		switch request.Key {
		case "price":
			return 21, nil
		case "first":
			return 20, nil
		case "second":
			return 6, nil
		default:
			return nil, errors.New("missing key: " + request.Key)
		}
	}, AllowEarlyRead: true}}

测试注册具名 lookup。回调把 json.RawMessage 解码成带 key 的请求,price 返回 21,first 返回 20,second 返回 6,未知 key 返回 missing key: ...。AllowEarlyRead: true 只影响 PLM 能否提前准备;它不让工具在构造参考 Guest 时自动执行。

工具函数、闭包和错误对象属于 Host/Runner 侧。COW memfd 只承载参考 Guest 的线性内存;测试后面用工具调用证明 bridge 仍能工作,用跨轮 Python 属性证明 Guest 写入没有回到基线。

3. 构造 COW Runner 并测真实增长

cow_test.go · L42–L69 · 打开完整源码

	r, err := NewPreparedCOW(ctx, wasm, manifest)
	if err != nil {
		t.Fatal(err)
	}
	defer r.Close(context.Background())
	// Exercise wazero's actual Memory.Grow, not just the allocator hook.
	m, err := r.newGuest(ctx, &boundedText{}, &boundedText{})
	if err != nil {
		t.Fatal(err)
	}
	before, _ := m.Memory().Read(0, 1)
	oldSize := m.Memory().Size()
	if _, ok := m.Memory().Grow(1); !ok {
		t.Fatal("COW memory growth failed")
	}
	after, _ := m.Memory().Read(0, 1)
	tail, ok := m.Memory().Read(oldSize, 65536)
	if !ok || &before[0] != &after[0] {
		t.Fatal("growth changed address or hid tail")
	}
	for _, b := range tail {
		if b != 0 {
			t.Fatal("new memory was not zero")
		}
	}
	if _, ok := m.Memory().Grow(65536); ok {
		t.Fatal("growth exceeded declared maximum")
	}

NewPreparedCOW 成功后延迟关闭 Runner。测试直接调用 r.newGuest,确保使用 wazero 的真实 Memory.Grow 而不是只调用 allocator hook。它保存偏移 0 的借用 view 和增长前的 oldSize,增长一个 Wasm page 后再次读取偏移 0,并从 oldSize 读取新页。

断言有三个层次:偏移 0 的地址没有改变,说明 mapping base 稳定;新页可读,说明 Guest 可见长度确实增长;新页每个字节为零,说明匿名尾部提供了 Wasm 需要的零值。随后尝试增长 65536 个 Wasm page,要求失败,说明模块声明的 maximum 仍是硬边界,预留虚拟地址范围不绕过 Wasm 上限。

这里 65536 是 Wasm page 数量/大小语义,不能拿 Linux OS page 来解释。内核层的 OS page 私有化由 cow-kernel-tests.md 直接观察。

4. 关闭探针 Guest 后检查 Runner 级所有权

cow_test.go · L70–L79 · 打开完整源码

	if err := m.Close(context.Background()); err != nil {
		t.Fatal(err)
	}
	if r.image != nil {
		t.Fatal("COW runner retained a Go-owned full image copy")
	}
	if r.cow == nil || !r.cow.ready() {
		t.Fatal("COW runner did not retain a sealed prepared image")
	}

测试关闭刚才的 module,再检查 r.image == nil 与 r.cow.ready() == true。第一个断言排除同时保存 full-copy Go image;第二个断言说明关闭一个实例只结束实例级 mapping,Runner 级 sealed image 仍可供后续 Guest attach。

这两个字段属于不同层次:m.Close 影响当前 Guest 和它的 cowMemory;r.Close 才负责 Runner 的 runtime、编译代码和 COW fd。测试在所有行为结束前不关闭 Runner。

5. 普通 Run、私有 Python 状态和 PLM

cow_test.go · L80–L92 · 打开完整源码

	if out, err := r.Run(ctx, `result=lookup(key="price")`, nil); err != nil || string(out.Value) != "21" {
		t.Fatalf("ordinary run: out=%+v err=%v", out, err)
	}
	if out, err := r.Run(ctx, `import builtins; builtins.cow_secret=123; result=builtins.cow_secret`, nil); err != nil || string(out.Value) != "123" {
		t.Fatalf("private-state write: out=%+v err=%v", out, err)
	}
	if out, err := r.Run(ctx, `import builtins; result=hasattr(builtins,"cow_secret")`, nil); err != nil || string(out.Value) != "false" {
		t.Fatalf("private-state leak: out=%+v err=%v", out, err)
	}
	out, err := r.RunPLM(ctx, "a=lookup(key='first')\nb=lookup(key='second')\nresult=a+b", nil)
	if err != nil || string(out.Value) != "26" || !strings.Contains(out.Transformed, "_spine_prepare") {
		t.Fatalf("PLM run: out=%+v err=%v", out, err)
	}

普通 Run 调用 lookup(price),应得到 21,确认 COW 恢复后 Host bridge 仍交付结果。下一轮在 builtins 写入 cow_secret=123 并同轮读回 123,证明当前 Guest mapping 可写;再开一轮检查属性为 false,证明该写入没有改 sealed 基线或下一个实例。

随后运行两个相邻工具赋值,期望 20+6=26,并检查 transformed 文本含 _spine_prepare。这条断言把 PLM 的执行语义与 COW 的内存恢复分开:COW 替换实例创建和基线接入,不移除 PLM 的准备队列与结果交付。

6. Prefix、工具错误和错误后的新实例

cow_test.go · L93–L105 · 打开完整源码

	chunks := make(chan string, 2)
	chunks <- "result=lookup(key='price')\n"
	chunks <- ""
	close(chunks)
	if out, err := r.RunPrefix(ctx, chunks, nil); err != nil || string(out.Value) != "21" {
		t.Fatalf("prefix run: out=%+v err=%v", out, err)
	}
	if _, err := r.Run(ctx, `result=lookup(key="absent")`, nil); err == nil || !strings.Contains(err.Error(), "missing key: absent") {
		t.Fatalf("guest/tool error: %v", err)
	}
	if out, err := r.Run(ctx, `result=7`, nil); err != nil || string(out.Value) != "7" {
		t.Fatalf("after error: out=%+v err=%v", out, err)
	}

prefix 测试创建容量为 2 的 channel,发送一段 lookup(price) 源码和空字符串,再关闭 channel;RunPrefix 应在真实 Guest 中完成这次输入并返回 21。这里测试的是 prefix 的输入交接,不是把 chunks 当作 COW image。

接着调用不存在的 key,要求返回包含 missing key: absent 的错误。错误来自 Guest/Host bridge,不能被 COW 代码吞成成功。下一轮 result=7 必须成功,说明错误轮的 module 已结束,Runner 的 sealed image 和工具注册仍可继续使用。

7. 取消后的继续运行

cow_test.go · L106–L113 · 打开完整源码

	deadline, stop := context.WithTimeout(ctx, 2*time.Second)
	if _, err := r.Run(deadline, `while True: pass`, nil); err == nil || deadline.Err() == nil {
		t.Fatalf("timeout: err=%v deadline=%v", err, deadline.Err())
	}
	stop()
	if out, err := r.Run(ctx, `result=8`, nil); err != nil || string(out.Value) != "8" {
		t.Fatalf("after timeout: out=%+v err=%v", out, err)
	}

测试给 while True: pass 建立两秒 deadline,要求 Run 返回错误且 deadline.Err() 有原因;二者一起检查 Guest 返回和 context 状态没有被混淆。随后调用 stop(),再执行 result=8,验证取消只结束这一轮。

这里不能把返回错误直接等同于“worker 已释放”。真正的 Run 收尾由运行时的取消协作和 module 关闭完成;测试只在下一轮成功后继续检查最终 mapping 状态。

8. Run 完成后的 mapping 和 Runner 关闭后的 fd

cow_test.go · L114–L134 · 打开完整源码

	maps, err := os.ReadFile("/proc/self/maps")
	if err != nil {
		t.Fatal(err)
	}
	if strings.Contains(string(maps), "memfd:pysolate-spine-cow") {
		t.Fatal("COW mapping remained after runs completed")
	}
	if err := r.Close(context.Background()); err != nil {
		t.Fatal(err)
	}
	entries, err := os.ReadDir("/proc/self/fd")
	if err != nil {
		t.Fatal(err)
	}
	for _, entry := range entries {
		target, _ := os.Readlink("/proc/self/fd/" + entry.Name())
		if strings.Contains(target, "memfd:pysolate-spine-cow") {
			t.Fatal("COW fd leaked")
		}
	}
}

所有 Run 完成后,测试读取 /proc/self/maps,要求没有 memfd:pysolate-spine-cow。这是实例 mapping 已经从进程映射表消失的观察。随后显式关闭 Runner;这一步释放 Runner 级 runtime、编译代码和 COW fd。

测试再读取 /proc/self/fd,逐个跟随符号链接,拒绝仍指向 memfd:pysolate-spine-cow 的条目。两次检查不能合并:maps 证明实例映射没有残留,fd 目录证明 Runner 级文件描述符没有残留。测试先结束所有 Run 再 r.Close,遵守 ownership 顺序。

9. 从构造到释放的观察表

阶段 Guest memory 观察 Runner state 观察
NewPreparedCOW 返回 参考 Guest 已关闭 cow.ready() 为真,image 为空
Grow(1) 起点不变,新 Wasm 页全零 sealed image 不变
超过 maximum Grow 返回失败 Runner 仍可用
写 cow_secret 当前轮可读 123 下一轮读不到,memfd 未被写入
PLM / prefix 结果为 26 / 21 Manifest 与 COW 状态未改
工具错误 / deadline 当前轮失败 下一轮仍能创建 Guest
Run 完成 当前 mapping 不在 maps fd 仍由 Runner 持有
r.Close 后 实例应已结束 /proc/self/fd 无目标 memfd

这些观察证明本仓库的 COW 生命周期:参考 Guest 提交 Guest memory 基线,每轮在自己的 mapping 上运行,增长受 Wasm maximum 约束,实例和 Runner 分层释放。它们不覆盖 Host globals、WASI 资源或任意执行时刻的 Go 状态恢复;那些状态不由 COW memfd 代替。