//go:build linux && rootrunner package host import ( "bytes" "context" "os" "os/exec" "path/filepath" "reflect" "strings" "testing" "time" "github.com/carlosframework/platform/internal/instancefs" "golang.org/x/sys/unix" ) func servingRunnerScratchFilesystem(t *testing.T, dir string, size int64) string { t.Helper() image := filepath.Join(dir, "scratch.ext4") f, err := os.OpenFile(image, os.O_CREATE|os.O_EXCL|os.O_RDWR, 0600) if err != nil { t.Fatal(err) } err = f.Truncate(size) closeErr := f.Close() if err != nil || closeErr != nil { t.Fatal(err, closeErr) } if out, err := exec.CommandContext(t.Context(), "mkfs.ext4", "-q", "-F", image).CombinedOutput(); err != nil { t.Fatal(err, string(out)) } out, err := exec.CommandContext(t.Context(), "losetup", "--find", "--show", image).CombinedOutput() if err != nil { t.Fatal(err, string(out)) } device := strings.TrimSpace(string(out)) t.Cleanup(func() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if out, err := exec.CommandContext(ctx, "losetup", "--detach", device).CombinedOutput(); err != nil { t.Error(err, string(out)) } }) mount := filepath.Join(dir, "scratch") if err := os.Mkdir(mount, 0700); err != nil { t.Fatal(err) } if err := unix.Mount(device, mount, "ext4", unix.MS_NOSUID|unix.MS_NODEV, ""); err != nil { t.Fatal(err) } t.Cleanup(func() { if err := unix.Unmount(mount, 0); err != nil { t.Error(err) } }) t.Logf("private fixture filesystem: %d MiB on %s", size>>20, device) return mount } func TestRootServingParkSnapshotClone(t *testing.T) { servingRootRunner(t) // This runner really is EBS/NVMe; exercise the production sysfs resolver. rootDir, err := os.Open("/") if err != nil { t.Fatal(err) } id, err := servingParkDirID(rootDir) rootDir.Close() if err != nil || servingParkVolume(id.Device) == "" { t.Fatal("runner has no readable EBS volume identity", err) } t.Log("kernel backing EBS volume", servingParkVolume(id.Device)) base := t.TempDir() mount := servingRunnerScratchFilesystem(t, base, 64<<20) t.Setenv("TMPDIR", mount) t.Run("clone", func(t *testing.T) { f, r := servingRebootRecordFixture(t, "complete") if err := f.ledger.journal.Close(); err != nil { t.Fatal(err) } servingRunnerMount(t, f.data) f.fs.mount = instancefs.MountNoSymfollow before := r.Source.Files // Tear down the nested bind for the block-level snapshot, then recreate it. instances := filepath.Join(f.data, "instances") if err := unix.Unmount(instances, 0); err != nil { t.Fatal(err) } if err := unix.Unmount(mount, 0); err != nil { t.Fatal(err) } clone := filepath.Join(base, "clone.ext4") if out, err := exec.CommandContext(t.Context(), "cp", "--sparse=always", filepath.Join(base, "scratch.ext4"), clone).CombinedOutput(); err != nil { t.Fatal(err, string(out)) } out, err := exec.CommandContext(t.Context(), "losetup", "--find", "--show", clone).CombinedOutput() if err != nil { t.Fatal(err, string(out)) } device := strings.TrimSpace(string(out)) // The original helper owns unmount; detach this device only after that. defer func() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if out, err := exec.CommandContext(ctx, "losetup", "--detach", device).CombinedOutput(); err != nil { t.Error(err, string(out)) } }() if err := unix.Mount(device, mount, "ext4", unix.MS_NOSUID|unix.MS_NODEV, ""); err != nil { t.Fatal(err) } if err := unix.Mount(instances, instances, "", unix.MS_BIND, ""); err != nil { t.Fatal(err) } if err := unix.Mount("", instances, "", unix.MS_BIND|unix.MS_REMOUNT|unix.MS_NOSYMFOLLOW|unix.MS_NOSUID|unix.MS_NODEV|unix.MS_NOEXEC, ""); err != nil { t.Fatal(err) } // Loop devices have no EBS serial. Model only this hardware boundary; // filesystem identity and cloning below are real kernel observations. f.fs.volume = func(uint64) string { return "vol11111111111111111" } path := f.path(r.Authorization, "") original, err := os.ReadFile(path) if err != nil { t.Fatal(err) } changed := bytes.Repeat([]byte{'X'}, len(original)) if err := os.WriteFile(path, changed, 0600); err != nil { t.Fatal(err) } observation, err := f.fs.inspectRebootRecord(t.Context(), r.Authorization.ID) if err != nil || !reflect.DeepEqual(observation.Record.Source.Files, before) { t.Fatal("clone did not preserve filesystem proof", err) } if observation.Topology.CurrentParent.Device == r.Source.Parent.Device { t.Fatal("clone did not change device") } root := &servingParkRoot{ledger: f.ledger} if err := root.commitServingReboot(t.Context(), r.Authorization.ID); err != nil { t.Fatal("retention recovery", err) } f.now = r.ReapAfter if err := f.ledger.reapLocked(t.Context(), r.Authorization.ID); err == nil { t.Fatal("snapshot clone reaped") } for _, item := range r.Source.Files { if item.Present { if _, err := os.Stat(f.path(r.Authorization, strings.TrimPrefix(item.Name, r.Authorization.Successor.Host+".db"))); err != nil { t.Fatal("clone artifact removed", err) } } } got, err := os.ReadFile(path) if err != nil || !bytes.Equal(got, changed) { t.Fatal("clone bytes changed", err) } t.Log("snapshot clone preserves handles/inodes, changes content and volume serial; recovery succeeds and deletion refuses") // Explicit test-only root re-enrollment is the positive control: with the // original serial now asserted, the very same cloned artifacts can be reaped. // This makes the serial refusal decisive, rather than an unrelated mismatch. binding, err := servingParkReadBinding(f.disk, "retirement-"+r.Authorization.ID) if err != nil || binding == nil || binding.Volume != "" { t.Fatal("clone acquired destructive binding", binding, err) } binding.Volume = r.Source.Volume if err := f.ledger.write("current-retirement-"+r.Authorization.ID+".json", binding); err != nil { t.Fatal(err) } f.fs.volume = func(uint64) string { return r.Source.Volume } if err := f.ledger.reapLocked(t.Context(), r.Authorization.ID); err != nil { t.Fatal("same-volume control has another refusal", err) } if _, err := os.Stat(path); !os.IsNotExist(err) { t.Fatal("same-volume control did not delete", err) } t.Log("same snapshot with explicit fixture root re-enrollment reaped successfully; volume guard was decisive") }) }