//go:build linux && rootrunner package host import ( "bytes" "context" "crypto/sha256" "encoding/hex" "encoding/json" "os" "path/filepath" "strings" "testing" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/rootstate" "golang.org/x/sys/unix" ) const persistedRebootDir = "/var/lib/carlos-rootrunner-p3-persisted" type persistedReboot struct { Entry hostbox.StopInventoryEntry Artifact hostbox.Artifact Unit proofUnitFile Generation release.GenerationBinding File hostbox.StopFileIdentity } func TestRootRebootPersistedFamiliesPhase1(t *testing.T) { if os.Geteuid() != 0 { t.Fatal("root required") } if err := os.Mkdir(persistedRebootDir, 0755); err != nil { t.Fatal(err) } for _, name := range []string{"data", "opt", "units", "data/state"} { if err := os.Mkdir(filepath.Join(persistedRebootDir, name), 0755); err != nil { t.Fatal(err) } } path := filepath.Join(persistedRebootDir, "data/state") f, err := hostbox.OpenInventoryFile(path) if err != nil { t.Fatal(err) } stable, err := rootstate.ObjectIdentity(f) f.Close() if err != nil { t.Fatal(err) } database := filepath.Join(path, "db.sqlite") if err := os.WriteFile(database, []byte("db"), 0600); err != nil { t.Fatal(err) } fileID, err := stopIdentity(database) if err != nil { t.Fatal(err) } e := hostbox.StopInventoryEntry{Kind: "path", ArtifactID: "state", ManagedRoot: "data", RelativePath: "state", Ownership: "owner", Materialization: "materialized", Device: stable.Device, Inode: stable.Inode, Type: stable.Mode & 0170000, Identity: stable} unitPath := filepath.Join(persistedRebootDir, "units/app.service") if err := os.WriteFile(unitPath, []byte("[Service]\nExecStart=/bin/true\n"), 0644); err != nil { t.Fatal(err) } source := proofBindingSource{openFile: rootstate.OpenTrustedFile, persistent: rootstate.PersistentIdentity} _, unit, err := source.trustedBytes(unitPath) if err != nil { t.Fatal(err) } versionPath := release.VersionPath(filepath.Join(persistedRebootDir, "opt"), "bab", "v1", "km") if err := os.MkdirAll(filepath.Dir(versionPath), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(versionPath, []byte("executable"), 0555); err != nil { t.Fatal(err) } g := release.GenerationBinding{Binding: release.HistoryBinding{Host: "h.example", Account: "bab", App: "km"}, Target: release.GenerationOwner{Host: "h.example", Incarnation: "inc", Stamp: 1, Session: 1, Box: "box", Enrollment: "en", BootID: stable.Boot, ControllerID: "controller"}, Installation: "install", Attempt: "main", Inputs: "inputs", Invocation: "invocation", Record: "record", RecordETag: "etag", ControllerPID: os.Getpid(), ControllerStart: "start", Version: "v1", Digest: strings.Repeat("a", 64), Path: versionPath, Tree: filepath.Dir(versionPath)} for path, dst := range map[string]*rootstate.FileIdentity{g.Path: &g.Executable, g.Tree: &g.TreeIdentity} { opened, err := hostbox.OpenInventoryFile(path) if err != nil { t.Fatal(err) } *dst, err = rootstate.Identity(opened) opened.Close() if err != nil { t.Fatal(err) } } for _, name := range []string{"old-pins", "stable-pins"} { state, err := rootstate.Open(filepath.Join(persistedRebootDir, name), true, false) if err != nil { t.Fatal(err) } ledger := &release.RootGenerations{Store: state} binding := g if name == "stable-pins" { binding.ExecutableProof = testPersistentInventory(t, g.Path) binding.TreeProof = testPersistentInventory(t, g.Tree) } if _, err = ledger.Reserve(binding); err != nil { t.Fatal(err) } pins := release.RootGenerationPins{Ledger: ledger, Verify: func(context.Context, []release.GenerationReservation) error { return nil }} if _, err = pins.StagedPins(t.Context(), filepath.Join(persistedRebootDir, "opt")); err != nil { t.Fatal("initial pins", err) } state.Close() } parker := hostbox.TypedParker{CapturePersistent: true, Roots: map[string]string{"data": filepath.Join(persistedRebootDir, "data")}, ParkedRoot: filepath.Join(persistedRebootDir, "data"), Incarnation: "inc", Host: "h.example", Retirement: "retired"} a, err := parker.CaptureArtifact("data", "state", "owner", hostbox.ArtifactPath) if err != nil { t.Fatal(err) } s, err := rootstate.Open(filepath.Join(persistedRebootDir, "authority"), true, false) if err != nil { t.Fatal(err) } defer s.Close() raw, _ := json.Marshal(persistedReboot{File: fileID, Entry: e, Artifact: a, Unit: unit, Generation: g}) if err = s.Write("fixture.json", raw); err != nil { t.Fatal(err) } t.Logf("boot=%s device=%d mount=%d", stable.Boot, stable.Device, stable.Mount) } func TestRootRebootPersistedFamiliesPhase2(t *testing.T) { defer os.RemoveAll(persistedRebootDir) s, err := rootstate.Open(filepath.Join(persistedRebootDir, "authority"), false, false) if err != nil { t.Fatal(err) } defer s.Close() raw, err := s.Read("fixture.json") if err != nil { t.Fatal(err) } var f persistedReboot if err = json.Unmarshal(raw, &f); err != nil { t.Fatal(err) } boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id") if err != nil || strings.TrimSpace(string(boot)) == f.Entry.Identity.Boot { t.Fatal("actual reboot required", err) } roots := map[string]string{"data": filepath.Join(persistedRebootDir, "data"), "opt": filepath.Join(persistedRebootDir, "opt"), "units": filepath.Join(persistedRebootDir, "units")} in := hostbox.StopInventory{Complete: true, Host: "h.example", Incarnation: "inc", InstallationStamp: 1, Entries: []hostbox.StopInventoryEntry{f.Entry}} t.Run("installation", func(t *testing.T) { if err := (ClaimRootHTTP{Roots: roots}).verifyInventoryPath(f.Entry); err != nil { t.Fatal(err) } }) t.Run("state-reuse", func(t *testing.T) { retained := in retained.Entries = append([]hostbox.StopInventoryEntry(nil), in.Entries...) owner := sha256.Sum256([]byte("h.example\x00inc\x001\x00box")) retained.Entries[0].Ownership = hex.EncodeToString(owner[:]) a := ClaimRoleAssembly{ManagedRoots: roots} changed, err := a.prepareStateInventory(ClaimLaunchInputs{DBPath: filepath.Join(roots["data"], "state", "db.sqlite")}, &retained, hostbox.Actor{Box: "box"}) if err != nil || changed { t.Fatal("retained state was not reused", changed, err) } }) t.Run("payload", func(t *testing.T) { if err := generationPayloadFiles(t.Context(), roots["data"], []hostbox.GenerationPayload{{Inventory: in}}); err != nil { t.Fatal(err) } }) t.Run("stopped-installation", func(t *testing.T) { if _, _, err := installationCoverage(roots, in, true); err != nil { t.Fatal(err) } }) t.Run("migration", func(t *testing.T) { version, err := filepath.Rel(roots["opt"], f.Generation.Path) if err != nil { t.Fatal(err) } if err := hostbox.CheckMigrationRoots(t.Context(), roots, map[string][]string{"units": {"app.service"}, "opt": {version}}, []hostbox.StopInventory{in}); err != nil { t.Fatal(err) } }) t.Run("named-unit", func(t *testing.T) { source := proofBindingSource{openFile: rootstate.OpenTrustedFile, persistent: rootstate.PersistentIdentity} _, now, err := source.trustedBytes(f.Unit.Path) if err != nil { t.Fatal(err) } if !sameProofFiles([]proofUnitFile{f.Unit}, []proofUnitFile{now}) || !bytes.Equal(proofFileSource(f.Unit), proofFileSource(now)) { t.Fatal("unit source changed across boot") } bad := f.Unit bad.Persistent.Filesystem = "foreign" if sameProofFiles([]proofUnitFile{bad}, []proofUnitFile{now}) { t.Fatal("foreign unit accepted") } }) t.Run("old-generation", func(t *testing.T) { if _, err := generationCurrentFiles(f.Generation, rootstate.OpenTrustedFile); err == nil { t.Fatal("old record without persistent evidence crossed boot") } }) t.Run("release-pins", func(t *testing.T) { for _, name := range []string{"old-pins", "stable-pins"} { state, err := rootstate.Open(filepath.Join(persistedRebootDir, name), false, false) if err != nil { t.Fatal(err) } pins := release.RootGenerationPins{Ledger: &release.RootGenerations{Store: state}, Verify: func(context.Context, []release.GenerationReservation) error { return nil }} _, err = pins.StagedPins(t.Context(), roots["opt"]) state.Close() if (err != nil) != (name == "old-pins") { t.Fatal(name, err) } } }) t.Run("cgroup", func(t *testing.T) { r := release.GenerationReservation{Binding: f.Generation} r.Binding.Binding.Host = "h.example" root := filepath.Join(persistedRebootDir, "cgroups") group := filepath.Join(root, generationBindingGroup(r.Binding)) if err := os.MkdirAll(group, 0755); err != nil { t.Fatal(err) } r.Process.Group = generationBindingGroup(r.Binding) r.Process.PID = 42 if err := generationGroupGone(t.Context(), root, r); err == nil { t.Fatal("reused cgroup from another boot accepted") } if err := os.Remove(group); err != nil { t.Fatal(err) } if err := generationGroupGone(t.Context(), root, r); err != nil { t.Fatal("absent old group refused", err) } }) t.Run("substitution", func(t *testing.T) { changed := f.Entry changed.Device++ changed.Identity.Device++ changed.Identity.Mount++ if err := hostbox.VerifyInventoryIdentity(roots["data"], changed); err != nil { t.Fatal("boot-local numbers used", err) } changed.Identity.Filesystem = "foreign" if err := hostbox.VerifyInventoryIdentity(roots["data"], changed); err == nil { t.Fatal("foreign filesystem accepted") } path := filepath.Join(roots["data"], "state") if err := unix.Mount(path, path, "", unix.MS_BIND, ""); err != nil { t.Fatal(err) } err := hostbox.VerifyInventoryIdentity(roots["data"], f.Entry) if e := unix.Unmount(path, 0); e != nil { t.Fatal(e) } if err == nil { t.Fatal("substituted mount accepted") } }) t.Run("stop-descriptors", func(t *testing.T) { k := LinuxStopKernel{ManagedRoots: roots} files, err := k.rebootStopFiles(t.Context(), in, []hostbox.StopFileIdentity{f.File}) if err != nil { t.Fatal(err) } database := filepath.Join(roots["data"], "state", "db.sqlite") moved := filepath.Join(persistedRebootDir, "renamed.sqlite") if err = os.Rename(database, moved); err != nil { t.Fatal(err) } held, err := os.Open(moved) if err != nil { t.Fatal(err) } err = scanStopDescriptors(t.Context(), "/proc", files, []string{database}) held.Close() if err == nil { t.Fatal("renamed descriptor escaped reboot deny set") } if err = scanStopDescriptors(t.Context(), "/proc", files, []string{database}); err != nil { t.Fatal("closed descriptor refused", err) } if err = os.Rename(moved, database); err != nil { t.Fatal(err) } bad := in bad.Entries = append([]hostbox.StopInventoryEntry(nil), in.Entries...) bad.Entries[0].Identity.Filesystem = "foreign" if _, err = k.rebootStopFiles(t.Context(), bad, []hostbox.StopFileIdentity{f.File}); err == nil { t.Fatal("foreign filesystem rebased") } }) t.Run("retirement", func(t *testing.T) { p := hostbox.TypedParker{Roots: roots, ParkedRoot: roots["data"], Incarnation: "inc", Host: "h.example", Retirement: "retired"} if err := p.Park(t.Context(), "state", f.Artifact); err != nil { t.Fatal(err) } if err := p.Park(t.Context(), "state", f.Artifact); err != nil { t.Fatal("durable park replay", err) } entry := f.Entry entry.RelativePath = filepath.Join(".parked", "inc", "h.example", "retired", "data", "state") if err := generationPayloadFiles(t.Context(), roots["data"], []hostbox.GenerationPayload{{Inventory: hostbox.StopInventory{Entries: []hostbox.StopInventoryEntry{entry}}}}); err != nil { t.Fatal("parked payload", err) } }) t.Run("historic-migration", TestPlan14RootMigrationHistoricRootRunner) t.Run("retry-delivery", TestPlan14RootRetryProjectionDelivery) after, err := s.Read("fixture.json") if err != nil || !bytes.Equal(raw, after) { t.Fatal("reboot rewrote authority", err) } }