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 "bytes"
7 "context"
8 "crypto/sha256"
9 "encoding/hex"
10 "encoding/json"
11 "os"
12 "path/filepath"
13 "strings"
14 "testing"
15
16 "github.com/carlosframework/platform/internal/hostbox"
17 "github.com/carlosframework/platform/internal/release"
18 "github.com/carlosframework/platform/internal/rootstate"
19 "golang.org/x/sys/unix"
20)
21
22const persistedRebootDir = "/var/lib/carlos-rootrunner-p3-persisted"
23
24type persistedReboot struct {
25 Entry hostbox.StopInventoryEntry
26 Artifact hostbox.Artifact
27 Unit proofUnitFile
28 Generation release.GenerationBinding
29 File hostbox.StopFileIdentity
30}
31
32func TestRootRebootPersistedFamiliesPhase1(t *testing.T) {
33 if os.Geteuid() != 0 {
34 t.Fatal("root required")
35 }
36 if err := os.Mkdir(persistedRebootDir, 0755); err != nil {
37 t.Fatal(err)
38 }
39 for _, name := range []string{"data", "opt", "units", "data/state"} {
40 if err := os.Mkdir(filepath.Join(persistedRebootDir, name), 0755); err != nil {
41 t.Fatal(err)
42 }
43 }
44 path := filepath.Join(persistedRebootDir, "data/state")
45 f, err := hostbox.OpenInventoryFile(path)
46 if err != nil {
47 t.Fatal(err)
48 }
49 stable, err := rootstate.ObjectIdentity(f)
50 f.Close()
51 if err != nil {
52 t.Fatal(err)
53 }
54 database := filepath.Join(path, "db.sqlite")
55 if err := os.WriteFile(database, []byte("db"), 0600); err != nil {
56 t.Fatal(err)
57 }
58 fileID, err := stopIdentity(database)
59 if err != nil {
60 t.Fatal(err)
61 }
62 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}
63 unitPath := filepath.Join(persistedRebootDir, "units/app.service")
64 if err := os.WriteFile(unitPath, []byte("[Service]\nExecStart=/bin/true\n"), 0644); err != nil {
65 t.Fatal(err)
66 }
67 source := proofBindingSource{openFile: rootstate.OpenTrustedFile, persistent: rootstate.PersistentIdentity}
68 _, unit, err := source.trustedBytes(unitPath)
69 if err != nil {
70 t.Fatal(err)
71 }
72 versionPath := release.VersionPath(filepath.Join(persistedRebootDir, "opt"), "bab", "v1", "km")
73 if err := os.MkdirAll(filepath.Dir(versionPath), 0755); err != nil {
74 t.Fatal(err)
75 }
76 if err := os.WriteFile(versionPath, []byte("executable"), 0555); err != nil {
77 t.Fatal(err)
78 }
79 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)}
80 for path, dst := range map[string]*rootstate.FileIdentity{g.Path: &g.Executable, g.Tree: &g.TreeIdentity} {
81 opened, err := hostbox.OpenInventoryFile(path)
82 if err != nil {
83 t.Fatal(err)
84 }
85 *dst, err = rootstate.Identity(opened)
86 opened.Close()
87 if err != nil {
88 t.Fatal(err)
89 }
90 }
91 for _, name := range []string{"old-pins", "stable-pins"} {
92 state, err := rootstate.Open(filepath.Join(persistedRebootDir, name), true, false)
93 if err != nil {
94 t.Fatal(err)
95 }
96 ledger := &release.RootGenerations{Store: state}
97 binding := g
98 if name == "stable-pins" {
99 binding.ExecutableProof = testPersistentInventory(t, g.Path)
100 binding.TreeProof = testPersistentInventory(t, g.Tree)
101 }
102 if _, err = ledger.Reserve(binding); err != nil {
103 t.Fatal(err)
104 }
105 pins := release.RootGenerationPins{Ledger: ledger, Verify: func(context.Context, []release.GenerationReservation) error { return nil }}
106 if _, err = pins.StagedPins(t.Context(), filepath.Join(persistedRebootDir, "opt")); err != nil {
107 t.Fatal("initial pins", err)
108 }
109 state.Close()
110 }
111 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"}
112 a, err := parker.CaptureArtifact("data", "state", "owner", hostbox.ArtifactPath)
113 if err != nil {
114 t.Fatal(err)
115 }
116 s, err := rootstate.Open(filepath.Join(persistedRebootDir, "authority"), true, false)
117 if err != nil {
118 t.Fatal(err)
119 }
120 defer s.Close()
121 raw, _ := json.Marshal(persistedReboot{File: fileID, Entry: e, Artifact: a, Unit: unit, Generation: g})
122 if err = s.Write("fixture.json", raw); err != nil {
123 t.Fatal(err)
124 }
125 t.Logf("boot=%s device=%d mount=%d", stable.Boot, stable.Device, stable.Mount)
126}
127func TestRootRebootPersistedFamiliesPhase2(t *testing.T) {
128 defer os.RemoveAll(persistedRebootDir)
129 s, err := rootstate.Open(filepath.Join(persistedRebootDir, "authority"), false, false)
130 if err != nil {
131 t.Fatal(err)
132 }
133 defer s.Close()
134 raw, err := s.Read("fixture.json")
135 if err != nil {
136 t.Fatal(err)
137 }
138 var f persistedReboot
139 if err = json.Unmarshal(raw, &f); err != nil {
140 t.Fatal(err)
141 }
142 boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
143 if err != nil || strings.TrimSpace(string(boot)) == f.Entry.Identity.Boot {
144 t.Fatal("actual reboot required", err)
145 }
146 roots := map[string]string{"data": filepath.Join(persistedRebootDir, "data"), "opt": filepath.Join(persistedRebootDir, "opt"), "units": filepath.Join(persistedRebootDir, "units")}
147 in := hostbox.StopInventory{Complete: true, Host: "h.example", Incarnation: "inc", InstallationStamp: 1, Entries: []hostbox.StopInventoryEntry{f.Entry}}
148 t.Run("installation", func(t *testing.T) {
149 if err := (ClaimRootHTTP{Roots: roots}).verifyInventoryPath(f.Entry); err != nil {
150 t.Fatal(err)
151 }
152 })
153 t.Run("state-reuse", func(t *testing.T) {
154 retained := in
155 retained.Entries = append([]hostbox.StopInventoryEntry(nil), in.Entries...)
156 owner := sha256.Sum256([]byte("h.example\x00inc\x001\x00box"))
157 retained.Entries[0].Ownership = hex.EncodeToString(owner[:])
158 a := ClaimRoleAssembly{ManagedRoots: roots}
159 changed, err := a.prepareStateInventory(ClaimLaunchInputs{DBPath: filepath.Join(roots["data"], "state", "db.sqlite")}, &retained, hostbox.Actor{Box: "box"})
160 if err != nil || changed {
161 t.Fatal("retained state was not reused", changed, err)
162 }
163 })
164 t.Run("payload", func(t *testing.T) {
165 if err := generationPayloadFiles(t.Context(), roots["data"], []hostbox.GenerationPayload{{Inventory: in}}); err != nil {
166 t.Fatal(err)
167 }
168 })
169 t.Run("stopped-installation", func(t *testing.T) {
170 if _, _, err := installationCoverage(roots, in, true); err != nil {
171 t.Fatal(err)
172 }
173 })
174 t.Run("migration", func(t *testing.T) {
175 version, err := filepath.Rel(roots["opt"], f.Generation.Path)
176 if err != nil {
177 t.Fatal(err)
178 }
179 if err := hostbox.CheckMigrationRoots(t.Context(), roots, map[string][]string{"units": {"app.service"}, "opt": {version}}, []hostbox.StopInventory{in}); err != nil {
180 t.Fatal(err)
181 }
182 })
183 t.Run("named-unit", func(t *testing.T) {
184 source := proofBindingSource{openFile: rootstate.OpenTrustedFile, persistent: rootstate.PersistentIdentity}
185 _, now, err := source.trustedBytes(f.Unit.Path)
186 if err != nil {
187 t.Fatal(err)
188 }
189 if !sameProofFiles([]proofUnitFile{f.Unit}, []proofUnitFile{now}) || !bytes.Equal(proofFileSource(f.Unit), proofFileSource(now)) {
190 t.Fatal("unit source changed across boot")
191 }
192 bad := f.Unit
193 bad.Persistent.Filesystem = "foreign"
194 if sameProofFiles([]proofUnitFile{bad}, []proofUnitFile{now}) {
195 t.Fatal("foreign unit accepted")
196 }
197 })
198 t.Run("old-generation", func(t *testing.T) {
199 if _, err := generationCurrentFiles(f.Generation, rootstate.OpenTrustedFile); err == nil {
200 t.Fatal("old record without persistent evidence crossed boot")
201 }
202 })
203 t.Run("release-pins", func(t *testing.T) {
204 for _, name := range []string{"old-pins", "stable-pins"} {
205 state, err := rootstate.Open(filepath.Join(persistedRebootDir, name), false, false)
206 if err != nil {
207 t.Fatal(err)
208 }
209 pins := release.RootGenerationPins{Ledger: &release.RootGenerations{Store: state}, Verify: func(context.Context, []release.GenerationReservation) error { return nil }}
210 _, err = pins.StagedPins(t.Context(), roots["opt"])
211 state.Close()
212 if (err != nil) != (name == "old-pins") {
213 t.Fatal(name, err)
214 }
215 }
216 })
217 t.Run("cgroup", func(t *testing.T) {
218 r := release.GenerationReservation{Binding: f.Generation}
219 r.Binding.Binding.Host = "h.example"
220 root := filepath.Join(persistedRebootDir, "cgroups")
221 group := filepath.Join(root, generationBindingGroup(r.Binding))
222 if err := os.MkdirAll(group, 0755); err != nil {
223 t.Fatal(err)
224 }
225 r.Process.Group = generationBindingGroup(r.Binding)
226 r.Process.PID = 42
227 if err := generationGroupGone(t.Context(), root, r); err == nil {
228 t.Fatal("reused cgroup from another boot accepted")
229 }
230 if err := os.Remove(group); err != nil {
231 t.Fatal(err)
232 }
233 if err := generationGroupGone(t.Context(), root, r); err != nil {
234 t.Fatal("absent old group refused", err)
235 }
236 })
237 t.Run("substitution", func(t *testing.T) {
238 changed := f.Entry
239 changed.Device++
240 changed.Identity.Device++
241 changed.Identity.Mount++
242 if err := hostbox.VerifyInventoryIdentity(roots["data"], changed); err != nil {
243 t.Fatal("boot-local numbers used", err)
244 }
245 changed.Identity.Filesystem = "foreign"
246 if err := hostbox.VerifyInventoryIdentity(roots["data"], changed); err == nil {
247 t.Fatal("foreign filesystem accepted")
248 }
249 path := filepath.Join(roots["data"], "state")
250 if err := unix.Mount(path, path, "", unix.MS_BIND, ""); err != nil {
251 t.Fatal(err)
252 }
253 err := hostbox.VerifyInventoryIdentity(roots["data"], f.Entry)
254 if e := unix.Unmount(path, 0); e != nil {
255 t.Fatal(e)
256 }
257 if err == nil {
258 t.Fatal("substituted mount accepted")
259 }
260 })
261 t.Run("stop-descriptors", func(t *testing.T) {
262 k := LinuxStopKernel{ManagedRoots: roots}
263 files, err := k.rebootStopFiles(t.Context(), in, []hostbox.StopFileIdentity{f.File})
264 if err != nil {
265 t.Fatal(err)
266 }
267 database := filepath.Join(roots["data"], "state", "db.sqlite")
268 moved := filepath.Join(persistedRebootDir, "renamed.sqlite")
269 if err = os.Rename(database, moved); err != nil {
270 t.Fatal(err)
271 }
272 held, err := os.Open(moved)
273 if err != nil {
274 t.Fatal(err)
275 }
276 err = scanStopDescriptors(t.Context(), "/proc", files, []string{database})
277 held.Close()
278 if err == nil {
279 t.Fatal("renamed descriptor escaped reboot deny set")
280 }
281 if err = scanStopDescriptors(t.Context(), "/proc", files, []string{database}); err != nil {
282 t.Fatal("closed descriptor refused", err)
283 }
284 if err = os.Rename(moved, database); err != nil {
285 t.Fatal(err)
286 }
287 bad := in
288 bad.Entries = append([]hostbox.StopInventoryEntry(nil), in.Entries...)
289 bad.Entries[0].Identity.Filesystem = "foreign"
290 if _, err = k.rebootStopFiles(t.Context(), bad, []hostbox.StopFileIdentity{f.File}); err == nil {
291 t.Fatal("foreign filesystem rebased")
292 }
293 })
294 t.Run("retirement", func(t *testing.T) {
295 p := hostbox.TypedParker{Roots: roots, ParkedRoot: roots["data"], Incarnation: "inc", Host: "h.example", Retirement: "retired"}
296 if err := p.Park(t.Context(), "state", f.Artifact); err != nil {
297 t.Fatal(err)
298 }
299 if err := p.Park(t.Context(), "state", f.Artifact); err != nil {
300 t.Fatal("durable park replay", err)
301 }
302 entry := f.Entry
303 entry.RelativePath = filepath.Join(".parked", "inc", "h.example", "retired", "data", "state")
304 if err := generationPayloadFiles(t.Context(), roots["data"], []hostbox.GenerationPayload{{Inventory: hostbox.StopInventory{Entries: []hostbox.StopInventoryEntry{entry}}}}); err != nil {
305 t.Fatal("parked payload", err)
306 }
307 })
308 t.Run("historic-migration", TestPlan14RootMigrationHistoricRootRunner)
309 t.Run("retry-delivery", TestPlan14RootRetryProjectionDelivery)
310 after, err := s.Read("fixture.json")
311 if err != nil || !bytes.Equal(raw, after) {
312 t.Fatal("reboot rewrote authority", err)
313 }
314}
315