//go:build linux && rootrunner package host import ( "context" "errors" "fmt" "os" "os/exec" "path/filepath" "testing" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/rootparticipation" "github.com/carlosframework/platform/internal/rootstate" ) func TestPlan14RootMigrationRootRunner(t *testing.T) { for _, point := range []string{"rename", "parent-sync"} { for _, after := range []bool{false, true} { for cut := 1; cut <= 8; cut++ { t.Run(fmt.Sprintf("%s/%t/%d", point, after, cut), func(t *testing.T) { m, s, _ := migrationFixture(t) dir := generationRootRunnerDir(t) open := func(name string) rootstate.Store { state, err := rootstate.Open(filepath.Join(dir, name), true, false) if err != nil { t.Fatal(err) } return state } state, execution, fences := open("migration"), open("exec"), open("fences") defer execution.Close() m.state = state m.execution = execution s.Items = []rootMigrationItem{{Installation: hostguard.RootInstallation{Host: "a.example", Incarnation: "inc", Stamp: 1, Account: "bab", App: "app", Record: "record", Box: "box", Enrollment: "en", Migration: 1}, Session: 9, ClaimETag: "claim"}} n := 0 injected := errors.New("interrupted migration syscall") fault := func(_ string, stage string, op func() error) error { if stage != point { return op() } n++ if n != cut { return op() } if after { if err := op(); err != nil { return err } } return injected } m.state = rootstate.WithWriteFault(state, fault) root := hostguard.NewRootAuthority(rootstate.WithWriteFault(fences, fault)) m.deny = func(ctx context.Context, item rootMigrationItem) error { return root.RetainMigrationDeny(ctx, item.Installation, item.Session) } if err := m.run(t.Context()); !errors.Is(err, injected) { t.Fatal("fault not reached", n, err) } state.Close() fences.Close() // Reopen real checked descriptors; the service-owned data directory is gone. state = open("migration") fences = open("fences") defer state.Close() defer fences.Close() m.state = state root = hostguard.NewRootAuthority(fences) if err := m.run(t.Context()); err != nil { t.Fatal("resume", err) } c, err := (rootparticipation.Ledger{State: execution}).Census() if err != nil || !c.Complete { t.Fatal("no complete census", c, err) } view, err := root.View(t.Context(), "a.example", hostguard.Captured{Incarnation: "inc", Stamp: 1}) if err != nil || len(view.MigrationDenies) != 1 || view.MigrationDenies[0].Session != 9 { t.Fatal("lost old session deny", view, err) } }) } } } } func TestPlan14RootMigrationMaskRootRunner(t *testing.T) { if os.Geteuid() != 0 { t.Fatal("root required") } unit := "/usr/lib/systemd/system/carlos-edge.service" for _, path := range []string{unit, "/etc/systemd/system/carlos-edge.service", "/run/systemd/system/carlos-edge.service"} { if _, err := os.Lstat(path); !os.IsNotExist(err) { t.Fatal("existing edge unit", path, err) } } if err := os.WriteFile(unit, []byte("[Unit]\nDescription=Migration test\n[Service]\nType=oneshot\nExecStart=/usr/bin/true\n[Install]\nWantedBy=multi-user.target\n"), 0644); err != nil { t.Fatal(err) } run := func(args ...string) { t.Helper() if out, err := exec.Command("systemctl", args...).CombinedOutput(); err != nil { t.Fatalf("systemctl %v: %v %s", args, err, out) } } t.Cleanup(func() { exec.Command("systemctl", "unmask", "--runtime", "carlos-edge.service").Run() os.Remove(unit) exec.Command("systemctl", "daemon-reload").Run() }) run("daemon-reload") oldOwner, oldMarker := claimQuiesceOwner, claimEdgeMaskOwner oldSystemctl := claimSystemctl claimSystemctl = func(ctx context.Context, args ...string) ([]byte, error) { out, err := exec.CommandContext(ctx, "systemctl", args...).CombinedOutput() if err != nil { t.Logf("systemctl %v: %v %s", args, err, out) } return out, err } t.Cleanup(func() { claimSystemctl = oldSystemctl }) claimQuiesceOwner = func() (int, int, error) { return 65534, 65534, nil } claimEdgeMaskOwner = filepath.Join(generationRootRunnerDir(t), "edge-mask") t.Cleanup(func() { claimQuiesceOwner = oldOwner; claimEdgeMaskOwner = oldMarker }) // An /etc unit takes precedence over a runtime mask. Refuse that conflict; // a lower-priority fixture then exercises the existing mask protocol itself. etcUnit := "/etc/systemd/system/carlos-edge.service" body, err := os.ReadFile(unit) if err != nil { t.Fatal(err) } if err := os.WriteFile(etcUnit, body, 0644); err != nil { t.Fatal(err) } t.Cleanup(func() { os.Remove(etcUnit) }) run("daemon-reload") run("mask", "--runtime", "carlos-edge.service") conflict, _, _ := migrationFixture(t) conflict.quiesce = quiesceLegacyClaimJournals if conflict.run(t.Context()) == nil { t.Fatal("ineffective runtime mask allowed migration") } if err := os.Remove(etcUnit); err != nil { t.Fatal(err) } run("daemon-reload") // The operator owns this mask. Both failed and complete migration retain it. run("mask", "--runtime", "carlos-edge.service") before, err := os.Lstat(claimEdgeRuntimeMask) if err != nil { t.Fatal(err) } for _, fail := range []bool{true, false} { m, _, _ := migrationFixture(t) m.quiesce = quiesceLegacyClaimJournals if fail { m.ready = func(context.Context, rootMigrationSnapshot) error { return os.ErrPermission } } err := m.run(t.Context()) if (err != nil) != fail { t.Fatal("migration", fail, err) } after, err := os.Lstat(claimEdgeRuntimeMask) if err != nil || !os.SameFile(before, after) { t.Fatal("operator mask changed", err) } } }