From 975cbea2f42b8f503e0437b361df68dca6b13aff Mon Sep 17 00:00:00 2001 From: jiashuoz Date: Tue, 29 Sep 2026 17:51:13 +0800 Subject: [PATCH 1/2] experiment: seal and verify private full-VM artifact bundles --- experiments/memorybundle/main.go | 137 ++++++++++++++++++++++++ experiments/memorybundle/main_test.go | 147 ++++++++++++++++++++++++++ 2 files changed, 284 insertions(+) create mode 100644 experiments/memorybundle/main.go create mode 100644 experiments/memorybundle/main_test.go diff --git a/experiments/memorybundle/main.go b/experiments/memorybundle/main.go new file mode 100644 index 0000000..b703a23 --- /dev/null +++ b/experiments/memorybundle/main.go @@ -0,0 +1,137 @@ +// memorybundle is an operator-only experiment; it is not a production resume API. +// All paths must be on a private, operator-owned filesystem with no other writers. +package main + +import ( + "context" + "errors" + "flag" + "fmt" + "os" + "path/filepath" + + "github.com/tokencanopy/rainier/checkpoint" + "github.com/tokencanopy/rainier/internal/driver" +) + +var required = map[string]bool{"memory": true, "vmstate": true, "rootfs": true, "workspace": true, "home": true, "metadata.json": true} + +func validate(dir string) error { + info, err := os.Lstat(dir) + if err != nil { + return err + } + if !info.IsDir() || info.Mode().Perm()&0077 != 0 { + return errors.New("artifact directory must be private") + } + entries, err := os.ReadDir(dir) + if err != nil { + return err + } + if len(entries) != len(required) { + return errors.New("incorrect artifact count") + } + for _, e := range entries { + info, err := e.Info() + if err != nil { + return err + } + if !required[e.Name()] || !info.Mode().IsRegular() || info.Size() == 0 { + return errors.New("unexpected or empty artifact") + } + } + return nil +} +func bundle(action, storePath, keyPath, dir string, generation uint64) error { + if action != "seal" && action != "verify" && action != "open" { + return errors.New("unknown action") + } + c := checkpoint.Context{Workspace: "memory-experiment", Session: "synthetic-session", Generation: generation} + if err := c.Validate(); err != nil { + return err + } + key, err := driver.LoadCheckpointKey(keyPath) + if err != nil { + return err + } + store, err := driver.NewDirBlobStore(storePath) + if err != nil { + return err + } + keys, err := checkpoint.NewStaticKeyWrapper("memory-experiment-key", key) + if err != nil { + return err + } + ctx := context.Background() + // The local operator owns this experimental store and key. This is NOT a + // control-plane authorization policy and must never be wired into a service. + reader, err := checkpoint.NewReader(store, keys, checkpoint.ReaderOptions{Authorize: func(context.Context, checkpoint.Context, checkpoint.Manifest) error { return nil }}) + if err != nil { + return err + } + switch action { + case "seal": + if err := validate(dir); err != nil { + return err + } + writer, err := checkpoint.NewWriter(store, keys, checkpoint.WriterOptions{KeyRef: "memory-experiment-key"}) + if err != nil { + return err + } + if _, err = writer.Write(ctx, c, checkpoint.DirSource(dir)); err != nil { + return err + } + _, err = reader.Verify(ctx, c) + return err + case "verify": + _, err = reader.Verify(ctx, c) + return err + case "open": + if dir == "" { + return errors.New("destination required") + } + absolute, err := filepath.Abs(dir) + if err != nil { + return err + } + lock, err := os.OpenFile(absolute+".lock", os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600) + if err != nil { + return err + } + lock.Close() + defer os.Remove(absolute + ".lock") + if _, err = os.Lstat(absolute); !os.IsNotExist(err) { + return errors.New("destination already exists or cannot be checked") + } + stage, err := os.MkdirTemp(filepath.Dir(absolute), ".memory-restore-") + if err != nil { + return err + } + defer os.RemoveAll(stage) + if _, err = reader.Restore(ctx, c, stage); err != nil { + return err + } + if err = validate(stage); err != nil { + return err + } + return os.Rename(stage, absolute) + } + panic("unreachable") +} +func main() { + action := flag.String("action", "", "seal, verify, or open") + store := flag.String("store", "", "private experimental blob directory") + key := flag.String("key", "", "private 32-byte key file, outside artifact directory") + dir := flag.String("dir", "", "source or new destination directory") + generation := flag.Uint64("generation", 0, "explicit snapshot generation") + flag.Parse() + if *store == "" || *key == "" || flag.NArg() != 0 { + fmt.Fprintln(os.Stderr, "store and key required; no positional arguments") + os.Exit(2) + } + if err := bundle(*action, *store, *key, *dir, *generation); err != nil { + fmt.Fprintln(os.Stderr, "memory bundle operation failed:", err) + os.Exit(1) + } + fmt.Println("memory bundle operation verified") +} diff --git a/experiments/memorybundle/main_test.go b/experiments/memorybundle/main_test.go new file mode 100644 index 0000000..d67ae67 --- /dev/null +++ b/experiments/memorybundle/main_test.go @@ -0,0 +1,147 @@ +package main + +import ( + "bytes" + "context" + "github.com/tokencanopy/rainier/checkpoint" + "github.com/tokencanopy/rainier/internal/driver" + "os" + "path/filepath" + "testing" +) + +var artifacts = []string{"memory", "vmstate", "rootfs", "workspace", "home", "metadata.json"} + +func fixture(t *testing.T) (string, string, string) { + t.Helper() + root := t.TempDir() + source := filepath.Join(root, "source") + os.Mkdir(source, 0700) + for _, n := range artifacts { + if err := os.WriteFile(filepath.Join(source, n), []byte("synthetic "+n), 0600); err != nil { + t.Fatal(err) + } + } + key := filepath.Join(root, "key") + os.WriteFile(key, bytes.Repeat([]byte{42}, 32), 0600) + return source, filepath.Join(root, "store"), key +} +func TestRoundTrip(t *testing.T) { + source, store, key := fixture(t) + if err := bundle("seal", store, key, source, 1); err != nil { + t.Fatal(err) + } + if err := bundle("verify", store, key, "", 1); err != nil { + t.Fatal(err) + } + target := filepath.Join(t.TempDir(), "restore") + if err := bundle("open", store, key, target, 1); err != nil { + t.Fatal(err) + } + for _, n := range artifacts { + b, e := os.ReadFile(filepath.Join(target, n)) + if e != nil || string(b) != "synthetic "+n { + t.Fatalf("artifact %s mismatch", n) + } + } + if err := bundle("open", store, key, target, 1); err == nil { + t.Fatal("overwrote existing destination") + } +} +func TestRejectInvalidSource(t *testing.T) { + for _, kind := range []string{"missing", "extra", "symlink", "directory"} { + t.Run(kind, func(t *testing.T) { + source, store, key := fixture(t) + switch kind { + case "missing": + os.Remove(filepath.Join(source, "home")) + case "extra": + os.WriteFile(filepath.Join(source, "extra"), []byte("x"), 0600) + case "symlink": + os.Remove(filepath.Join(source, "home")) + os.Symlink(key, filepath.Join(source, "home")) + case "directory": + os.Remove(filepath.Join(source, "home")) + os.Mkdir(filepath.Join(source, "home"), 0700) + } + if err := bundle("seal", store, key, source, 1); err == nil { + t.Fatal("accepted invalid source") + } + }) + } +} +func TestRejectDamageWithoutPublishing(t *testing.T) { + for _, kind := range []string{"wrong-key", "tamper", "truncate", "wrong-generation"} { + t.Run(kind, func(t *testing.T) { + source, store, key := fixture(t) + if err := bundle("seal", store, key, source, 1); err != nil { + t.Fatal(err) + } + generation := uint64(1) + if kind == "wrong-key" { + os.WriteFile(key, bytes.Repeat([]byte{43}, 32), 0600) + } else if kind == "wrong-generation" { + generation = 2 + } else { + var largest string + var size int64 + filepath.Walk(store, func(p string, i os.FileInfo, e error) error { + if e == nil && i.Mode().IsRegular() && i.Size() > size { + largest = p + size = i.Size() + } + return e + }) + b, e := os.ReadFile(largest) + if e != nil { + t.Fatal(e) + } + if kind == "tamper" { + b[len(b)/2] ^= 1 + } else { + b = b[:len(b)/2] + } + os.WriteFile(largest, b, 0600) + } + target := filepath.Join(t.TempDir(), "restore") + if err := bundle("open", store, key, target, generation); err == nil { + t.Fatal("accepted damaged bundle") + } + if _, err := os.Lstat(target); !os.IsNotExist(err) { + t.Fatal("published failed restore") + } + }) + } +} + +func TestRestoreRejectsAuthenticatedIncompleteBundle(t *testing.T) { + source, storePath, keyPath := fixture(t) + os.Remove(filepath.Join(source, "home")) + key, err := driver.LoadCheckpointKey(keyPath) + if err != nil { + t.Fatal(err) + } + store, err := driver.NewDirBlobStore(storePath) + if err != nil { + t.Fatal(err) + } + keys, err := checkpoint.NewStaticKeyWrapper("memory-experiment-key", key) + if err != nil { + t.Fatal(err) + } + writer, err := checkpoint.NewWriter(store, keys, checkpoint.WriterOptions{KeyRef: "memory-experiment-key"}) + if err != nil { + t.Fatal(err) + } + _, err = writer.Write(context.Background(), checkpoint.Context{Workspace: "memory-experiment", Session: "synthetic-session", Generation: 1}, checkpoint.DirSource(source)) + if err != nil { + t.Fatal(err) + } + target := filepath.Join(t.TempDir(), "restore") + if err := bundle("open", storePath, keyPath, target, 1); err == nil { + t.Fatal("published incomplete but authenticated bundle") + } + if _, err := os.Lstat(target); !os.IsNotExist(err) { + t.Fatal("failed restore was published") + } +} From 673f28343cdb46f7d1a5965e8cddadd587960776 Mon Sep 17 00:00:00 2001 From: jiashuoz Date: Tue, 29 Sep 2026 18:04:57 +0800 Subject: [PATCH 2/2] docs: bound the private memory bundle experiment --- experiments/memorybundle/README.md | 44 ++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 experiments/memorybundle/README.md diff --git a/experiments/memorybundle/README.md b/experiments/memorybundle/README.md new file mode 100644 index 0000000..26a1715 --- /dev/null +++ b/experiments/memorybundle/README.md @@ -0,0 +1,44 @@ +# Experimental full-VM artifact bundle + +This operator-only tool reuses `checkpoint` streaming authenticated encryption +for a **disposable same-host memory-resume experiment**. It changes no driver, +resume API, checkpoint format, or production policy. Authenticated guest RAM can +contain credentials; production's existing prohibition remains in force. + +Build with `GOOS=linux GOARCH=amd64 go build -o memorybundle ./experiments/memorybundle`. +The source is a private (`0700`) directory containing exactly six nonempty regular +files: `memory`, `vmstate`, `rootfs`, `workspace`, `home`, and `metadata.json`. +The operator must pause the guest and capture matching disk contents before +sealing; this tool cannot establish that consistency itself. + +```sh +memorybundle -action seal -store /private/experiment/bundle -key /private/experiment/key -dir /private/experiment/capture -generation 1 +memorybundle -action verify -store /private/experiment/bundle -key /private/experiment/key -generation 1 +memorybundle -action open -store /private/experiment/bundle -key /private/experiment/key -dir /private/experiment/restored -generation 1 +``` + +Generate a random 32-byte key in a separate `0600` file. Keep the source, store, +key, and destination parent operator-owned and inaccessible to other writers. +Use a dedicated store, never a production checkpoint store: the context is +intentionally fixed to `memory-experiment` / `synthetic-session`. Specify a new +positive generation for each capture. There is no control-plane authorization +or freshness ledger; possession of the local key authorizes this experiment. + +`seal` verifies the encrypted stream after writing it. `verify` authenticates +content but does not check the six-file layout. `open` authenticates into a private +staging directory, validates the layout, then publishes a new destination. It +refuses existing destinations and serializes cooperating opens with an exclusive +lock file. An interrupted open may leave private staging/lock files; inspect and +remove them only after confirming no operation remains active. + +Before starting any restored VMM, the separate operator harness must validate +software/CPU compatibility, pin every disk path, retain the network slot, confirm +the original VMM is gone, and prevent replay of an already consumed generation. +The tool does not start or stop VMs, interpret metadata, refresh credentials, +compress sparse files, or preserve sparse allocation on extraction. Full logical +memory and disk sizes therefore determine ciphertext and restore storage costs. + +Delete **all** encrypted and plaintext copies, staging files, keys, and disposable +host disks after the experiment. Removing a credential from the live guest does +not remove its earlier copy from a snapshot. Never upload bundles or guest logs +to a PR, CI artifact store, or public issue.