carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux && rootrunner
2
3package host
4
5import (
6 "context"
7 "errors"
8 "fmt"
9 "os"
10 "os/exec"
11 "path/filepath"
12 "testing"
13
14 "github.com/carlosframework/platform/internal/hostguard"
15 "github.com/carlosframework/platform/internal/rootparticipation"
16 "github.com/carlosframework/platform/internal/rootstate"
17)
18
19func TestPlan14RootMigrationRootRunner(t *testing.T) {
20 for _, point := range []string{"rename", "parent-sync"} {
21 for _, after := range []bool{false, true} {
22 for cut := 1; cut <= 8; cut++ {
23 t.Run(fmt.Sprintf("%s/%t/%d", point, after, cut), func(t *testing.T) {
24 m, s, _ := migrationFixture(t)
25 dir := generationRootRunnerDir(t)
26 open := func(name string) rootstate.Store {
27 state, err := rootstate.Open(filepath.Join(dir, name), true, false)
28 if err != nil {
29 t.Fatal(err)
30 }
31 return state
32 }
33 state, execution, fences := open("migration"), open("exec"), open("fences")
34 defer execution.Close()
35 m.state = state
36 m.execution = execution
37 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"}}
38 n := 0
39 injected := errors.New("interrupted migration syscall")
40 fault := func(_ string, stage string, op func() error) error {
41 if stage != point {
42 return op()
43 }
44 n++
45 if n != cut {
46 return op()
47 }
48 if after {
49 if err := op(); err != nil {
50 return err
51 }
52 }
53 return injected
54 }
55 m.state = rootstate.WithWriteFault(state, fault)
56 root := hostguard.NewRootAuthority(rootstate.WithWriteFault(fences, fault))
57 m.deny = func(ctx context.Context, item rootMigrationItem) error {
58 return root.RetainMigrationDeny(ctx, item.Installation, item.Session)
59 }
60 if err := m.run(t.Context()); !errors.Is(err, injected) {
61 t.Fatal("fault not reached", n, err)
62 }
63 state.Close()
64 fences.Close()
65 // Reopen real checked descriptors; the service-owned data directory is gone.
66 state = open("migration")
67 fences = open("fences")
68 defer state.Close()
69 defer fences.Close()
70 m.state = state
71 root = hostguard.NewRootAuthority(fences)
72 if err := m.run(t.Context()); err != nil {
73 t.Fatal("resume", err)
74 }
75 c, err := (rootparticipation.Ledger{State: execution}).Census()
76 if err != nil || !c.Complete {
77 t.Fatal("no complete census", c, err)
78 }
79 view, err := root.View(t.Context(), "a.example", hostguard.Captured{Incarnation: "inc", Stamp: 1})
80 if err != nil || len(view.MigrationDenies) != 1 || view.MigrationDenies[0].Session != 9 {
81 t.Fatal("lost old session deny", view, err)
82 }
83 })
84 }
85 }
86 }
87}
88
89func TestPlan14RootMigrationMaskRootRunner(t *testing.T) {
90 if os.Geteuid() != 0 {
91 t.Fatal("root required")
92 }
93 unit := "/usr/lib/systemd/system/carlos-edge.service"
94 for _, path := range []string{unit, "/etc/systemd/system/carlos-edge.service", "/run/systemd/system/carlos-edge.service"} {
95 if _, err := os.Lstat(path); !os.IsNotExist(err) {
96 t.Fatal("existing edge unit", path, err)
97 }
98 }
99 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 {
100 t.Fatal(err)
101 }
102 run := func(args ...string) {
103 t.Helper()
104 if out, err := exec.Command("systemctl", args...).CombinedOutput(); err != nil {
105 t.Fatalf("systemctl %v: %v %s", args, err, out)
106 }
107 }
108 t.Cleanup(func() {
109 exec.Command("systemctl", "unmask", "--runtime", "carlos-edge.service").Run()
110 os.Remove(unit)
111 exec.Command("systemctl", "daemon-reload").Run()
112 })
113 run("daemon-reload")
114 oldOwner, oldMarker := claimQuiesceOwner, claimEdgeMaskOwner
115 oldSystemctl := claimSystemctl
116 claimSystemctl = func(ctx context.Context, args ...string) ([]byte, error) {
117 out, err := exec.CommandContext(ctx, "systemctl", args...).CombinedOutput()
118 if err != nil {
119 t.Logf("systemctl %v: %v %s", args, err, out)
120 }
121 return out, err
122 }
123 t.Cleanup(func() { claimSystemctl = oldSystemctl })
124 claimQuiesceOwner = func() (int, int, error) { return 65534, 65534, nil }
125 claimEdgeMaskOwner = filepath.Join(generationRootRunnerDir(t), "edge-mask")
126 t.Cleanup(func() { claimQuiesceOwner = oldOwner; claimEdgeMaskOwner = oldMarker })
127 // An /etc unit takes precedence over a runtime mask. Refuse that conflict;
128 // a lower-priority fixture then exercises the existing mask protocol itself.
129 etcUnit := "/etc/systemd/system/carlos-edge.service"
130 body, err := os.ReadFile(unit)
131 if err != nil {
132 t.Fatal(err)
133 }
134 if err := os.WriteFile(etcUnit, body, 0644); err != nil {
135 t.Fatal(err)
136 }
137 t.Cleanup(func() { os.Remove(etcUnit) })
138 run("daemon-reload")
139 run("mask", "--runtime", "carlos-edge.service")
140 conflict, _, _ := migrationFixture(t)
141 conflict.quiesce = quiesceLegacyClaimJournals
142 if conflict.run(t.Context()) == nil {
143 t.Fatal("ineffective runtime mask allowed migration")
144 }
145 if err := os.Remove(etcUnit); err != nil {
146 t.Fatal(err)
147 }
148 run("daemon-reload")
149 // The operator owns this mask. Both failed and complete migration retain it.
150 run("mask", "--runtime", "carlos-edge.service")
151 before, err := os.Lstat(claimEdgeRuntimeMask)
152 if err != nil {
153 t.Fatal(err)
154 }
155 for _, fail := range []bool{true, false} {
156 m, _, _ := migrationFixture(t)
157 m.quiesce = quiesceLegacyClaimJournals
158 if fail {
159 m.ready = func(context.Context, rootMigrationSnapshot) error { return os.ErrPermission }
160 }
161 err := m.run(t.Context())
162 if (err != nil) != fail {
163 t.Fatal("migration", fail, err)
164 }
165 after, err := os.Lstat(claimEdgeRuntimeMask)
166 if err != nil || !os.SameFile(before, after) {
167 t.Fatal("operator mask changed", err)
168 }
169 }
170}
171