//go:build linux package host import ( "context" "errors" "net" "os" "path/filepath" "reflect" "testing" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/provision" "golang.org/x/sys/unix" ) func TestServingNamespaceAuthenticatedParentAndRecovery(t *testing.T) { f := newServingRootFixture(t, true) fs := f.p.ledger.fs.(*servingParkFS) path := filepath.Join(fs.data, "instances", ".serving-parked") if err := os.Mkdir(path, 0700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(path, "unknown"), []byte("keep"), 0600); err != nil { t.Fatal(err) } before := f.f.read() a, err := f.park() if err != nil { t.Fatal(err) } anchor, err := fs.namespaceParent() if err != nil { t.Fatal(err) } r, err := f.p.ledger.read(a.ID) if err != nil || anchor.Parent != r.Source.Parent { t.Fatal("parent was not captured from the root installation", err) } var q servingParkQuarantine body, err := f.disk.Read("namespace-quarantine.json") if err != nil || servingParkDecode(body, &q) != nil || !q.Complete { t.Fatal("authenticated recovery", err) } body, err = os.ReadFile(filepath.Join(filepath.Dir(path), q.name(), "unknown")) if err != nil || string(body) != "keep" { t.Fatal("unknown bytes changed", err) } if !reflect.DeepEqual(f.f.read(), before) { t.Fatal("namespace recovery changed the claim") } } func TestServingNamespaceParentRequiresIndependentInstallation(t *testing.T) { for _, fault := range []string{"peer", "census", "installation", "inventory", "receipt", "source", "mode"} { t.Run(fault, func(t *testing.T) { f := newServingRootFixture(t, true) fs := f.p.ledger.fs.(*servingParkFS) path := filepath.Join(fs.data, "instances", ".serving-parked") if err := os.Mkdir(path, 0700); err != nil { t.Fatal(err) } switch fault { case "peer": f.p.root.Stop.Authenticate = func(context.Context, net.Conn) (StopPeer, error) { return StopPeer{}, hostbox.ErrChanged } case "census": f.p.root.RequireNewWork = func(context.Context) error { return hostbox.ErrChanged } case "installation": f.actor.Enrollment = "changed" case "inventory": f.actor.Binding = "8" case "receipt": f.mutateRoot(func(m map[string]any) { m["State"].(map[string]any)["fence_journal"] = []any{} }) case "source": if err := os.Rename(filepath.Dir(f.db), filepath.Dir(f.db)+"-saved"); err != nil { t.Fatal(err) } if err := os.Mkdir(filepath.Dir(f.db), 0700); err != nil { t.Fatal(err) } case "mode": if err := os.Chmod(filepath.Dir(path), 0777); err != nil { t.Fatal(err) } } if _, err := f.park(); err == nil { t.Fatal("unverified recovery accepted") } for _, name := range []string{"namespace-parent.json", "namespace.json", "namespace-quarantine.json"} { if _, err := f.disk.Read(name); !errors.Is(err, os.ErrNotExist) { t.Fatal("unverified recovery wrote", name, err) } } if _, err := os.Stat(path); err != nil { t.Fatal("obstruction moved without authority", err) } }) } } func TestServingNamespaceStartupUsesRecordedParent(t *testing.T) { for _, boundary := range []string{"parent", "quarantine-rename", "fresh-before", "fresh", "complete"} { 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 err := os.Mkdir(path, 0700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(path, "unknown"), []byte("retained"), 0600); err != nil { t.Fatal(err) } fs.state = quarantineWriteFault{Store: f.disk, before: func(name string, body []byte) error { var q servingParkQuarantine if boundary == "fresh-before" && name == "namespace-quarantine.json" && servingParkDecode(body, &q) == nil && q.Fresh.Inode != 0 { return unix.EIO } return nil }, after: func(name string, body []byte) error { var q servingParkQuarantine if boundary == "parent" && name == "namespace-parent.json" { return unix.EIO } if name == "namespace-quarantine.json" && servingParkDecode(body, &q) == nil && (boundary == "fresh" && q.Fresh.Inode != 0 || boundary == "complete" && q.Complete) { return unix.EIO } return nil }} if boundary == "quarantine-rename" { fs.rename = func(a int, b string, c int, d string, flags uint) error { if err := unix.Renameat2(a, b, c, d, flags); err != nil { return err } return unix.EIO } } if _, err := f.park(); !errors.Is(err, unix.EIO) { t.Fatal("missing interrupted boundary", err) } before := f.f.read() restarted := *f.p ledger := *f.p.ledger restarted.ledger = &ledger freshFS := newServingParkFS(fs.data, f.disk) freshFS.owner, freshFS.mount = fs.owner, fs.mount ledger.fs = freshFS restarted.root.Stop.Authenticate = func(context.Context, net.Conn) (StopPeer, error) { panic("startup used a peer") } // Namespace recovery needs only the independently recorded parent, // not a surviving source or a freshly selected host installation. if err := os.Rename(filepath.Dir(f.db), filepath.Dir(f.db)+"-saved"); err != nil { t.Fatal(err) } page, err := restarted.recoverServingParks(t.Context(), "", servingParkRecoveryLimit) if err != nil || len(page.Results) != 0 || page.More { t.Fatalf("startup recovery: %+v %v", page, err) } var q servingParkQuarantine body, err := f.disk.Read("namespace-quarantine.json") if err != nil || servingParkDecode(body, &q) != nil || !q.Complete { t.Fatal("startup did not finish quarantine", err) } body, err = os.ReadFile(filepath.Join(filepath.Dir(path), q.name(), "unknown")) if err != nil || string(body) != "retained" || !reflect.DeepEqual(before, f.f.read()) { t.Fatal("startup changed retained bytes or claim", err) } beforeRecord, err := f.disk.Read("namespace-quarantine.json") if err != nil { t.Fatal(err) } if page, err = restarted.recoverServingParks(t.Context(), "", servingParkRecoveryLimit); err != nil || len(page.Results) != 0 { t.Fatal("startup replay", page, err) } afterRecord, err := f.disk.Read("namespace-quarantine.json") if err != nil || string(beforeRecord) != string(afterRecord) { t.Fatal("completed startup rewrote quarantine", err) } }) } } func TestServingNamespaceStartupCannotCaptureParent(t *testing.T) { f, _, path := quarantineFixture(t) if err := f.ledger.withNamespace(t.Context(), func() error { return f.fs.recoverRecordedNamespace(t.Context()) }); err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(path, "unknown")); err != nil { t.Fatal("startup moved unrecorded obstruction", err) } for _, name := range []string{"namespace-parent.json", "namespace.json", "namespace-quarantine.json"} { if _, err := f.disk.Read(name); !errors.Is(err, os.ErrNotExist) { t.Fatal("startup created authority", name, err) } } } func TestServingNamespaceParentCannotBeReplaced(t *testing.T) { for _, fault := range []string{"parent", "corrupt-anchor", "corrupt-namespace", "mount", "mode"} { t.Run(fault, func(t *testing.T) { f := newServingRootFixture(t, true) a, err := f.park() if err != nil { t.Fatal(err) } fs := f.p.ledger.fs.(*servingParkFS) before, err := f.disk.Read("namespace-parent.json") if err != nil { t.Fatal(err) } switch fault { case "parent": path := filepath.Join(fs.data, "instances") if err := os.Rename(path, path+"-saved"); err != nil { t.Fatal(err) } if err := os.Mkdir(path, 0700); err != nil { t.Fatal(err) } // Even moving the genuine inventoried child into the replacement // parent does not authorize refreshing the recorded parent. if err := os.Rename(filepath.Join(path+"-saved", provision.InstanceID(f.current.Host)), filepath.Dir(f.db)); err != nil { t.Fatal(err) } case "corrupt-anchor": if err := f.disk.Write("namespace-parent.json", []byte("{}")); err != nil { t.Fatal(err) } before = []byte("{}") case "corrupt-namespace": if err := f.disk.Write("namespace.json", []byte("{}")); err != nil { t.Fatal(err) } case "mount": fs.mount = func(string) (bool, error) { return false, nil } case "mode": if err := os.Chmod(filepath.Join(fs.data, "instances"), 0777); err != nil { t.Fatal(err) } } page, err := f.p.recoverServingParks(t.Context(), "", servingParkRecoveryLimit) if err != nil || len(page.Results) == 0 || page.Results[0].ID != "namespace" || page.Results[0].Err == nil { t.Fatal("startup accepted unsafe parent", page, err) } if fault == "parent" { if err := f.p.ledger.withNamespace(t.Context(), func() error { return fs.recordNamespaceParent(t.Context(), f.current, a.Directory) }); err == nil { t.Fatal("parent recorder accepted a moved genuine source") } } if _, err := f.park(); err == nil { t.Fatal("authenticated request refreshed unsafe parent") } after, err := f.disk.Read("namespace-parent.json") if err != nil || string(before) != string(after) { t.Fatal("parent authority changed", err) } if _, err := f.disk.Read("namespace-quarantine.json"); !errors.Is(err, os.ErrNotExist) { t.Fatal("unsafe parent authorized quarantine", err) } }) } } func TestServingNamespaceParentUpgradePreservesEarlierEvidence(t *testing.T) { for _, fault := range []string{"", "parent", "corrupt-namespace", "corrupt-quarantine"} { t.Run(fault, func(t *testing.T) { f, parent, _ := quarantineFixture(t) f.put("", "current database") a, _ := f.evidence(10, true, "missing") if _, err := quarantineRecover(t, f, parent); err != nil { t.Fatal(err) } // These are checkpoint-14 namespace records: no parent anchor yet. switch fault { case "parent": body, err := f.disk.Read("namespace.json") if err != nil { t.Fatal(err) } var n servingParkNamespace if err := servingParkDecode(body, &n); err != nil { t.Fatal(err) } n.Parent.Inode++ if err := f.ledger.write("namespace.json", n); err != nil { t.Fatal(err) } case "corrupt-namespace": if err := f.disk.Write("namespace.json", []byte("{}")); err != nil { t.Fatal(err) } case "corrupt-quarantine": if err := f.disk.Write("namespace-quarantine.json", []byte("{}")); err != nil { t.Fatal(err) } } err := f.ledger.withNamespace(t.Context(), func() error { return f.fs.recordNamespaceParent(t.Context(), a.Successor, a.Directory) }) if (err != nil) != (fault != "") { t.Fatal("parent upgrade", err) } if fault != "" { if _, err := f.disk.Read("namespace-parent.json"); !errors.Is(err, os.ErrNotExist) { t.Fatal("upgrade blessed changed private evidence", err) } return } before, err := f.disk.Read("namespace-parent.json") if err != nil { t.Fatal(err) } // A failed write seam proves an established anchor is read-only. f.fs.state = quarantineWriteFault{Store: f.disk, before: func(string, []byte) error { return unix.EIO }} if err := f.ledger.withNamespace(t.Context(), func() error { if err := f.fs.recordNamespaceParent(t.Context(), a.Successor, a.Directory); err != nil { return err } return f.fs.recoverRecordedNamespace(t.Context()) }); err != nil { t.Fatal("completed recovery wrote state", err) } after, err := f.disk.Read("namespace-parent.json") if err != nil || string(before) != string(after) { t.Fatal("parent replay changed record", err) } }) } }