//go:build linux package host import ( "context" "errors" "os" "path/filepath" "reflect" "testing" "time" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/provision" "golang.org/x/sys/unix" ) func servingRebootRecordFixture(t *testing.T, phase string) (*servingParkDiskFixture, servingParkRecord) { t.Helper() f := newServingParkDiskFixture(t) a, receipts := f.evidence(1, true, "missing") f.put("", "last database") f.put("-wal", "last WAL") f.put("-shm", "last SHM") if err := f.fs.recordNamespaceParent(t.Context(), a.Successor, a.Directory); err != nil { t.Fatal(err) } root, _, err := f.fs.namespace() if err != nil { t.Fatal(err) } root.Close() var r servingParkRecord if phase == "captured" || phase == "prepared" || phase == "rename-reply" { r, err = f.ledger.capture(t.Context(), a, receipts, nil) if err != nil { t.Fatal(err) } if err = f.ledger.save(r); err != nil { t.Fatal(err) } save := func() error { return f.ledger.save(r) } if phase != "captured" { if err = f.fs.Prepare(&r, save); err != nil { t.Fatal(err) } } if phase == "rename-reply" { f.fs.rename = func(from int, old string, to int, name string, flags uint) error { if err := unix.Renameat2(from, old, to, name, flags); err != nil { return err } return unix.EIO } if err = f.fs.Move(&r, save); !errors.Is(err, unix.EIO) { t.Fatal("missing lost rename reply", err) } f.fs.rename = unix.Renameat2 } } else { r, err = f.park(a, receipts) if err != nil { t.Fatal(err) } if phase != "parked" { f.complete(a) } if phase == "complete" { f.put("", "successor database") } if phase == "unlink-reply" || phase == "partial-unlink" || phase == "rmdir-reply" || phase == "closed" { f.now = f.now.Add(8 * 24 * time.Hour) calls := 0 f.fs.unlink = func(fd int, name string, flags int) error { calls++ if phase == "partial-unlink" && calls == 2 { return unix.EIO } if err := unix.Unlinkat(fd, name, flags); err != nil { return err } if phase == "unlink-reply" || phase == "rmdir-reply" && flags == unix.AT_REMOVEDIR { return unix.EIO } return nil } err = f.ledger.withNamespace(t.Context(), func() error { return f.ledger.reapLocked(t.Context(), a.ID) }) if phase == "closed" && err != nil || phase != "closed" && !errors.Is(err, unix.EIO) { t.Fatal(phase, err) } f.fs.unlink = unix.Unlinkat } } r, err = f.ledger.read(a.ID) if err != nil { t.Fatal(err) } f.fs.boot = func() (string, error) { return servingOtherBoot, nil } return f, r } func TestServingParkRebootRecordReadOnly(t *testing.T) { for _, phase := range []string{"captured", "prepared", "rename-reply", "parked", "complete", "unlink-reply", "partial-unlink", "rmdir-reply", "closed"} { t.Run(phase, func(t *testing.T) { f, r := servingRebootRecordFixture(t, phase) before := servingBootSnapshot(t, f.disk.dir) var got servingParkRebootRecord err := f.ledger.withNamespace(t.Context(), func() error { var err error got, err = f.fs.inspectRebootRecord(t.Context(), r.Authorization.ID) return err }) if err != nil || !reflect.DeepEqual(got.Record, r) || got.Topology.BootID != servingOtherBoot { t.Fatal("record inspection", got, err) } if r.Complete && got.Source != (servingArtifactDirectory{}) { t.Fatal("completed park depended on live source") } if after := servingBootSnapshot(t, f.disk.dir); !reflect.DeepEqual(before, after) { t.Fatal("inspection wrote authority") } if err := f.fs.CheckParked(&r); err == nil { t.Fatal("observation granted old-boot effects") } }) } } func TestServingParkRebootRecordRefusesChangedEvidence(t *testing.T) { for _, fault := range []string{"no-source-proof", "no-file-proof", "no-event-proof", "source-boot", "source-handle", "event-handle", "file-handle", "file-filesystem", "file-mode", "event-mode", "namespace", "source-replaced", "event-replaced", "file-replaced", "file-hardlink", "file-symlink", "unexpected-child", "missing-file", "duplicate-source", "old-format", "source-race", "event-race", "record-race", "cancel-race"} { t.Run(fault, func(t *testing.T) { f, r := servingRebootRecordFixture(t, "parked") event := filepath.Dir(f.path(r.Authorization, "")) source := filepath.Dir(provisionDBPath(f)) ctx, cancel := context.WithCancel(t.Context()) defer cancel() switch fault { case "no-source-proof": r.Source.Persistent = servingParkPersistentDirectory{} case "no-file-proof": r.Source.Files[0].Persistent = servingParkPersistentFile{} case "no-event-proof": r.EventPersistent = servingParkPersistentDirectory{} case "source-boot": r.Source.BootID = servingOtherBoot case "source-handle": r.Source.Persistent.Handle = "aa" case "event-handle": r.EventPersistent.Handle = "aa" case "file-handle": r.Source.Files[0].Persistent.Handle = "aa" case "file-filesystem": r.Source.Files[0].Persistent.Filesystem[0]++ case "file-mode": r.Source.Files[0].Persistent.Mode ^= 0100 case "event-mode": r.EventPersistent.Mode ^= 0100 case "namespace": r.Namespace.Inode++ case "source-replaced", "event-replaced": path := source if fault == "event-replaced" { path = event } if err := os.Rename(path, path+"-old"); err != nil { t.Fatal(err) } if err := os.Mkdir(path, 0700); err != nil { t.Fatal(err) } case "file-replaced", "file-hardlink", "file-symlink", "missing-file": path := f.path(r.Authorization, "") if fault == "file-hardlink" { if err := os.Link(path, path+"-link"); err != nil { t.Fatal(err) } break } if err := os.Rename(path, path+"-old"); err != nil { t.Fatal(err) } if fault == "file-symlink" { if err := os.Symlink(path+"-old", path); err != nil { t.Fatal(err) } } if fault == "file-replaced" { if err := os.WriteFile(path, []byte("replacement"), 0600); err != nil { t.Fatal(err) } } // Put the retained original outside the event so unknown-child checks // cannot hide a missing/replaced-file validation bug. if err := os.Rename(path+"-old", source+"/saved-original"); err != nil { t.Fatal(err) } case "unexpected-child": if err := os.WriteFile(filepath.Join(event, "unknown"), []byte("retain"), 0600); err != nil { t.Fatal(err) } case "duplicate-source": f.put("", "replacement") case "old-format": r.Source.Persistent = servingParkPersistentDirectory{} r.Source.BootID = "" r.EventPersistent = servingParkPersistentDirectory{} for i := range r.Source.Files { r.Source.Files[i].Persistent = servingParkPersistentFile{} } case "source-race", "event-race", "record-race", "cancel-race": calls := 0 f.fs.handle = func(d *os.File) (servingParkPersistentDirectory, error) { p, err := servingParkPersistentDir(d) calls++ // The first topology inspection takes four handles; the pinned // topology takes two, followed by source and event. if calls == 8 { switch fault { case "cancel-race": cancel() case "record-race": changed := r changed.Receipts = nil if err := f.ledger.write("retirement-"+r.Authorization.ID+".json", changed); err != nil { t.Fatal(err) } default: path := source if fault == "event-race" { path = event } if err := os.Rename(path, path+"-old"); err != nil { t.Fatal(err) } if err := os.Mkdir(path, 0700); err != nil { t.Fatal(err) } } } return p, err } } if err := f.ledger.write("retirement-"+r.Authorization.ID+".json", r); err != nil { t.Fatal(err) } before := servingBootSnapshot(t, f.disk.dir) got, err := f.fs.inspectRebootRecord(ctx, r.Authorization.ID) if err == nil || !reflect.DeepEqual(got, servingParkRebootRecord{}) { t.Fatal("accepted changed record", got, err) } if fault != "record-race" { if after := servingBootSnapshot(t, f.disk.dir); !reflect.DeepEqual(before, after) { t.Fatal("refusal wrote authority") } } }) } } func provisionDBPath(f *servingParkDiskFixture) string { return filepath.Join(f.data, "instances", provision.InstanceID(f.base.Host), f.base.Host+".db") } func TestServingParkPersistentEvidenceNeverUpgradesOldIntent(t *testing.T) { for _, phase := range []string{"intent", "consumption"} { t.Run(phase, func(t *testing.T) { f := newServingParkDiskFixture(t) a, receipts := f.evidence(1, true, "missing") f.put("", "legacy") r, err := f.ledger.capture(t.Context(), a, receipts, nil) if err != nil { t.Fatal(err) } r.Source.Persistent = servingParkPersistentDirectory{} r.Source.BootID = "" for i := range r.Source.Files { r.Source.Files[i].Persistent = servingParkPersistentFile{} } if phase == "intent" { err = f.ledger.save(r) } else { err = f.ledger.write(servingLegacyKey("legacy", a.Successor), servingLegacyConsumption{Authorization: a, Source: r.Source, Receipts: receipts}) } if err != nil { t.Fatal(err) } parked, err := f.park(a, receipts) if err != nil { t.Fatal("old intent replay", err) } if parked.Source.Persistent != (servingParkPersistentDirectory{}) || parked.Source.BootID != "" || parked.EventPersistent != (servingParkPersistentDirectory{}) { t.Fatal("upgraded old evidence") } f.complete(a) }) } } func TestServingParkRebootFileRequiresDeleteIntent(t *testing.T) { f, r := servingRebootRecordFixture(t, "unlink-reply") r.Source.Files[0].DeleteIntent = false if err := f.ledger.write("retirement-"+r.Authorization.ID+".json", r); err != nil { t.Fatal(err) } if _, err := f.fs.inspectRebootRecord(t.Context(), r.Authorization.ID); !errors.Is(err, hostbox.ErrChanged) { t.Fatal("absent artifact without delete intent accepted", err) } } func TestServingParkRebootRecordChangedMount(t *testing.T) { f, r := servingRebootRecordFixture(t, "rename-reply") oldMount := r.Source.Parent.Mount + 123 r.Source.Parent.Mount = oldMount r.Source.Directory.Mount = oldMount r.Authorization.Directory.Mount = oldMount r.Event.Mount = oldMount r.Namespace.Mount = oldMount for i := range r.Source.Files { if r.Source.Files[i].Present { r.Source.Files[i].Mount = oldMount } } if err := f.ledger.save(r); err != nil { t.Fatal(err) } parent, err := f.fs.namespaceParent() if err != nil { t.Fatal(err) } parent.Parent.Mount = oldMount if err := f.ledger.write("namespace-parent.json", parent); err != nil { t.Fatal(err) } var n servingParkNamespace b, err := f.disk.Read("namespace.json") if err != nil || servingParkDecode(b, &n) != nil { t.Fatal(err) } n.Parent.Mount = oldMount n.Root.Mount = oldMount if err := f.ledger.write("namespace.json", n); err != nil { t.Fatal(err) } got, err := f.fs.inspectRebootRecord(t.Context(), r.Authorization.ID) if err != nil || got.Source.Mount == oldMount || got.Event.Mount == oldMount || got.Source.Mount != got.Event.Mount || !reflect.DeepEqual(got.Record, r) { t.Fatal("changed-mount inspection", got, err) } } func TestServingParkRebootCompletedDoesNotReadSource(t *testing.T) { f, r := servingRebootRecordFixture(t, "complete") path := filepath.Dir(provisionDBPath(f)) if err := os.Rename(path, path+"-removed"); err != nil { t.Fatal(err) } if err := os.Symlink(path+"-removed", path); err != nil { t.Fatal(err) } got, err := f.fs.inspectRebootRecord(t.Context(), r.Authorization.ID) if err != nil || got.Source != (servingArtifactDirectory{}) { t.Fatal("completed park consulted source", got, err) } } func TestServingParkPersistentLegacyConsumption(t *testing.T) { f, r := servingRebootRecordFixture(t, "parked") c, err := f.ledger.legacyConsumption(r.Authorization.Successor) if err != nil || c == nil { t.Fatal(err) } if !sameServingLegacyConsumption(*c, r) || !sameServingLegacySource(r.Source, c.Source) { t.Fatal("fixture consumption mismatch") } old := r.Source old.Persistent = servingParkPersistentDirectory{} old.BootID = "" for i := range old.Files { old.Files[i].Persistent = servingParkPersistentFile{} } partial := old partial.Files[0].Persistent = r.Source.Files[0].Persistent if sameServingLegacySource(old, partial) { t.Fatal("partial consumed proof accepted as old evidence") } for _, field := range []string{"boot", "directory", "file"} { t.Run(field, func(t *testing.T) { changed := *c switch field { case "boot": changed.Source.BootID = servingOtherBoot case "directory": changed.Source.Persistent.Handle = "aa" case "file": changed.Source.Files[0].Persistent.Handle = "aa" } if sameServingLegacyConsumption(changed, r) || sameServingLegacySource(r.Source, changed.Source) { t.Fatal("changed immutable consumption accepted") } }) } } func TestServingParkPersistentEventCaptureFailure(t *testing.T) { f := newServingParkDiskFixture(t) a, receipts := f.evidence(1, true, "missing") f.put("", "preserve source") f.fs.handle = func(d *os.File) (servingParkPersistentDirectory, error) { if d.Name() == a.ID { return servingParkPersistentDirectory{}, unix.EIO } return servingParkPersistentDir(d) } if _, err := f.park(a, receipts); !errors.Is(err, unix.EIO) { t.Fatal("event capture failure", err) } r, err := f.ledger.read(a.ID) if err != nil || r.Prepared || r.EventPersistent != (servingParkPersistentDirectory{}) { t.Fatal("failed capture gained event authority", err) } body, err := os.ReadFile(provisionDBPath(f)) if err != nil || string(body) != "preserve source" { t.Fatal("capture failure changed source", err) } f.fs.handle = servingParkPersistentDir if _, err := f.park(a, receipts); err != nil { t.Fatal("same-boot event capture recovery", err) } }