//go:build linux package host import ( "bytes" "errors" "os" "path/filepath" "reflect" "strings" "testing" "github.com/carlosframework/platform/internal/hostbox" "golang.org/x/sys/unix" ) const servingOtherBoot = "01234567-89ab-4cde-8fab-0123456789ab" func TestServingParkBootIdentity(t *testing.T) { f := newServingParkFS("/data", nil) id, err := f.currentBoot() if err != nil || id == "" || f.checkBoot(id) != nil { t.Fatal("kernel boot identity", id, err) } for _, id := range []string{"", "not-a-boot", strings.Repeat("0", 36), "00000000-0000-0000-0000-000000000000", strings.ToUpper(servingOtherBoot), servingOtherBoot + "\n", "01234567-89ab-4cde-8fab-0123456789ag"} { t.Run(id, func(t *testing.T) { f.boot = func() (string, error) { return id, nil } if _, err := f.currentBoot(); !errors.Is(err, hostbox.ErrChanged) { t.Fatal("malformed boot accepted", err) } }) } f.boot = func() (string, error) { return "", unix.EIO } if _, err := f.currentBoot(); !errors.Is(err, unix.EIO) { t.Fatal("boot read failure hidden", err) } f.boot = nil if _, err := f.currentBoot(); !errors.Is(err, hostbox.ErrUnsupported) { t.Fatal("missing boot dependency accepted", err) } } // Change only the boot observation. Device, inode and mount number stay equal, // reproducing the equality that a recycled mount ID can produce after reboot. func TestServingParkBootReuseRefusesEffects(t *testing.T) { for _, fault := range []string{"reboot", "missing-boot", "read-error", "malformed"} { t.Run(fault, func(t *testing.T) { f := newServingParkDiskFixture(t) a, receipts := f.evidence(1, true, "missing") f.put("", "last writer") r, err := f.park(a, receipts) if err != nil { t.Fatal(err) } f.complete(a) before, err := f.disk.Read("namespace.json") if err != nil { t.Fatal(err) } var n servingParkNamespace if servingParkDecode(before, &n) != nil || n.BootID == "" || n.BootID == servingOtherBoot { t.Fatal("fixture did not capture the current boot") } parent, id, err := f.fs.parent() if err != nil { t.Fatal(err) } parent.Close() if id != r.Source.Parent { t.Fatal("test changed more than the boot") } switch fault { case "reboot": f.fs.boot = func() (string, error) { return servingOtherBoot, nil } case "missing-boot": n.BootID = "" if err := f.ledger.write("namespace.json", n); err != nil { t.Fatal(err) } case "read-error": f.fs.boot = func() (string, error) { return "", unix.EIO } case "malformed": f.fs.boot = func() (string, error) { return "invalid", nil } } before, err = f.disk.Read("namespace.json") if err != nil { t.Fatal(err) } writes := 0 save := func() error { writes++; return nil } for name, call := range map[string]func(*servingParkRecord) error{ "inspect": f.fs.CheckParked, "prepare": func(r *servingParkRecord) error { return f.fs.Prepare(r, save) }, "move": func(r *servingParkRecord) error { return f.fs.Move(r, save) }, "delete": func(r *servingParkRecord) error { return f.fs.Delete(r, save) }, } { copy := r if err := call(©); err == nil || !reflect.DeepEqual(r, copy) || writes != 0 { t.Fatal(name, "reused boot authority", err, writes) } } after, err := f.disk.Read("namespace.json") if err != nil || !bytes.Equal(before, after) { t.Fatal("refusal rewrote namespace", err) } body, err := os.ReadFile(f.path(a, "")) if err != nil || string(body) != "last writer" { t.Fatal("refusal changed database", err) } }) } } func TestServingNamespaceBootRecoveryRefusesAdoption(t *testing.T) { for _, boundary := range []string{"parent", "namespace", "quarantine", "old-parent", "old-namespace", "old-quarantine"} { t.Run(boundary, func(t *testing.T) { f := newServingRootFixture(t, true) fs := f.p.ledger.fs.(*servingParkFS) path := filepath.Join(fs.data, "instances", ".serving-parked") if strings.Contains(boundary, "quarantine") { if err := os.Mkdir(path, 0700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(path, "unknown"), []byte("retain"), 0600); err != nil { t.Fatal(err) } } a, err := f.park() if err != nil { t.Fatal(err) } anchor, err := fs.namespaceParent() if err != nil || anchor.BootID == "" { t.Fatal("missing boot anchor", err) } name := "namespace-parent.json" switch boundary { case "namespace", "old-namespace": name = "namespace.json" case "quarantine", "old-quarantine": name = "namespace-quarantine.json" } if strings.HasPrefix(boundary, "old-") { body, err := f.disk.Read(name) if err != nil { t.Fatal(err) } body = bytes.ReplaceAll(body, []byte(`"BootID":"`+anchor.BootID+`",`), nil) if err := f.disk.Write(name, body); err != nil { t.Fatal(err) } } else { fs.boot = func() (string, error) { return servingOtherBoot, nil } } before := servingBootSnapshot(t, f.disk.dir) claim := f.f.read() if err := fs.recoverRecordedNamespace(t.Context()); err == nil { t.Fatal("startup accepted missing or changed boot") } if _, err := fs.recoverNamespace(t.Context(), anchor.Parent); err == nil { t.Fatal("quarantine accepted missing or changed boot") } if err := fs.recordNamespaceParent(t.Context(), a.Successor, a.Directory); err == nil { // A valid existing anchor need not examine the namespace, but it // must never overwrite the earlier boot or infer a missing one. if boundary != "old-namespace" && boundary != "old-quarantine" { t.Fatal("authenticated parent adopted old boot") } } if after := servingBootSnapshot(t, f.disk.dir); !reflect.DeepEqual(before, after) || !reflect.DeepEqual(claim, f.f.read()) { t.Fatal("recovery refusal changed private evidence or claim") } }) } } func servingBootSnapshot(t *testing.T, root string) map[string]string { t.Helper() names, err := os.ReadDir(root) if err != nil { t.Fatal(err) } out := make(map[string]string) for _, name := range names { if name.IsDir() { continue } body, err := os.ReadFile(filepath.Join(root, name.Name())) if err != nil { t.Fatal(err) } out[name.Name()] = string(body) } return out }