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/ed25519"
9 "encoding/base64"
10 "encoding/json"
11 "errors"
12 "github.com/carlosframework/platform/internal/hostfloor"
13 "golang.org/x/sys/unix"
14 "net/http"
15 "net/http/httptest"
16 "os"
17 "os/exec"
18 "path/filepath"
19 "strings"
20 "testing"
21 "time"
22
23 "github.com/carlosframework/platform/internal/hostbox"
24 "github.com/carlosframework/platform/internal/hostclaim"
25 "github.com/carlosframework/platform/internal/hostguard"
26 "github.com/carlosframework/platform/internal/moves"
27 "github.com/carlosframework/platform/internal/provision"
28 "github.com/carlosframework/platform/internal/release"
29 "github.com/carlosframework/platform/internal/rootparticipation"
30 "github.com/carlosframework/platform/internal/rootstate"
31)
32
33func init() {
34 if os.Getenv("PLAN14_R9_LEGACY_MUTATOR") == "1" {
35 os.Stdout.Write([]byte("ready\n"))
36 var b [1]byte
37 os.Stdin.Read(b[:])
38 os.Exit(0)
39 }
40}
41
42func TestPlan14InitialMigrationExclusionRootRunner(t *testing.T) {
43 testRootMigrationBootstrap(t, false, false)
44}
45
46func plan14InitialMigrationExclusion(t *testing.T, store release.Store, r ClaimRuntimeResources, owners ClaimOwnerBindings, actor hostbox.Actor, journals *hostbox.JournalService, launches *hostbox.LaunchJournal) {
47 for _, args := range [][]string{{"adopt"}, {"system", "apply"}} {
48 cmd := exec.Command(os.Args[0], args...)
49 cmd.Env = append(os.Environ(), "PLAN14_R9_LEGACY_MUTATOR=1")
50 stdin, err := cmd.StdinPipe()
51 if err != nil {
52 t.Fatal(err)
53 }
54 stdout, err := cmd.StdoutPipe()
55 if err != nil {
56 t.Fatal(err)
57 }
58 if err := cmd.Start(); err != nil {
59 t.Fatal(err)
60 }
61 t.Cleanup(func() { stdin.Close(); cmd.Process.Kill(); cmd.Wait() })
62 var b [6]byte
63 if _, err := stdout.Read(b[:]); err != nil {
64 t.Fatal(err)
65 }
66 if err := migrateRootClaimAuthority(t.Context(), store, r, owners, actor, journals, launches); err == nil {
67 t.Fatal("migration overlapped legacy mutator", args)
68 }
69 if _, err := os.Stat(filepath.Join(rootparticipation.Namespace, "census.json")); !os.IsNotExist(err) {
70 t.Fatal("census created before quiescence", err)
71 }
72 stdin.Close()
73 cmd.Wait()
74 }
75 old := rootMigrationQuiesce
76 defer func() { rootMigrationQuiesce = old }()
77 rootMigrationQuiesce = func(context.Context) (func(), error) {
78 if err := verifyOfflineRootMigration(t.Context()); err != nil {
79 t.Fatal(err)
80 }
81 return nil, os.ErrPermission
82 }
83 if err := migrateRootClaimAuthority(t.Context(), store, r, owners, actor, journals, launches); !errors.Is(err, os.ErrPermission) {
84 t.Fatal("exclusion witness", err)
85 }
86 before, err := os.ReadDir(rootReleasePrivate)
87 if err != nil {
88 t.Fatal(err)
89 }
90 for _, flag := range []string{"", "1"} {
91 t.Setenv("CARLOS_HOST_CLAIM", flag)
92 plan14NoCensusCommands(t)
93 if err := withClaimRootRelease(t.Context(), false, func() (rootstate.Store, error) {
94 t.Fatal("release opened after aborted migration")
95 return nil, os.ErrPermission
96 }, func() error { return nil }); err != nil {
97 t.Fatal(err)
98 }
99 for _, recover := range []bool{false, true} {
100 f := newApplyFixture(t, "canary", "v1.0.0")
101 f.a.participation = nil
102 f.a.rootMoves = rootMovesFloorIdentity
103 if err := f.a.runWithRelease(t.Context(), recover); err != nil {
104 t.Fatal("legacy apply after aborted migration", recover, err)
105 }
106 }
107 }
108 after, err := os.ReadDir(rootReleasePrivate)
109 if err != nil || len(after) != len(before) {
110 t.Fatal("new release state after abort", err, before, after)
111 }
112}
113
114func TestPlan14RepairExecutorEndToEndRootRunner(t *testing.T) {
115 opt := generationRootRunnerDir(t)
116 pub, key, err := ed25519.GenerateKey(nil)
117 if err != nil {
118 t.Fatal(err)
119 }
120 t.Setenv("CARLOS_RELEASE_PUBKEY", base64.StdEncoding.EncodeToString(pub))
121 id := rootFloorIdentity{Deployment: "deploy", Box: "box", Enrollment: "en"}
122 f, err := openRootMovesFloors(opt, id, true)
123 if err != nil {
124 t.Fatal(err)
125 }
126 defer f.close()
127 bound := moves.Floor{Min: 99, MinRelease: "v99.0.0"}
128 if err := f.reconstruct(t.Context(), rootFloorReconstruction{Identity: id, Floor: &bound, Source: "raised bound", Digest: floorTestDigest, Authorization: floorTestAuthorization(t, f)}); err != nil {
129 t.Fatal(err)
130 }
131 for _, path := range []string{systemBinaryPath(opt), systemPrevPath(opt), filepath.Join(systemGuardDir(opt), "carlos"), filepath.Join(systemRootDir(opt), "staging", "carlos")} {
132 if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
133 t.Fatal(err)
134 }
135 rootFloorSignRunnerBytes(t, opt, path, "v2.0.0", []byte("#!/bin/sh\nprintf 'v2.0.0\\n'\n"), key)
136 }
137 if err := verifyRootMigrationFloors(opt, id); err != nil {
138 t.Fatal("repair controller cannot start", err)
139 }
140 a := systemApplier{optDir: opt, dataDir: t.TempDir(), rootMoves: func() (rootFloorIdentity, bool, error) { return id, true, nil }, ops: productionSysOps()}
141 for _, path := range []string{systemBinaryPath(opt), filepath.Join(systemGuardDir(opt), "carlos")} {
142 if out, err := a.guardRun(t.Context(), time.Second, path, "version"); err != nil || out == "" {
143 t.Fatal("certified helper blocked", out, err)
144 }
145 }
146 if _, err := a.guardRun(t.Context(), time.Second, filepath.Join(systemRootDir(opt), "staging", "carlos"), "version"); err == nil {
147 t.Fatal("below-floor replacement executed")
148 }
149 if err := a.floorAllowsForward("v2.0.0"); err != nil {
150 t.Fatal("installed repair executor cannot finish recovery", err)
151 }
152 j := rootFloorJudge{floors: f, current: systemBinaryPath(opt), prev: systemPrevPath(opt), binary: func(p string) (rootFloorBinary, error) { return readRootFloorBinary(opt, p) }, idle: func(context.Context) error { return nil }}
153 if err := j.judge(t.Context()); err != nil {
154 t.Fatal(err)
155 }
156 var v rootFloorVerdict
157 raw, err := f.public.Read("prev.json")
158 if err != nil {
159 t.Fatal(err)
160 }
161 if err := json.Unmarshal(raw, &v); err != nil {
162 t.Fatal(err)
163 }
164 v.Prev.File.Device++
165 raw, _ = json.Marshal(v)
166 if err := f.public.Write("prev.json", raw); err != nil {
167 t.Fatal(err)
168 }
169 rootFloorSignRunnerBytes(t, opt, systemBinaryPath(opt), "v3.0.0", []byte("#!/bin/sh\nexit 99\n"), key)
170 selected, err := rootFloorBootCandidate(t.Context(), j, true, func(context.Context, string) error { return nil }, func(context.Context) error { return nil })
171 if err != nil || selected != systemPrevPath(opt) {
172 t.Fatal("renumbered signed prev lost recovery", selected, err)
173 }
174 if out, err := a.guardRun(t.Context(), time.Second, selected, "version"); err != nil || out == "" {
175 t.Fatal("selected recovery image failed", out, err)
176 }
177 for _, recovery := range []bool{false, true} {
178 f := newApplyFixture(t, "canary", "v2.0.0")
179 f.a.optDir = opt
180 f.a.rootMoves = a.rootMoves
181 f.a.ops.run = productionSysOps().run
182 f.edgeStartedThisBoot = false
183 rootFloorSignRunnerBytes(t, opt, systemBinaryPath(opt), "v2.0.0", []byte("#!/bin/sh\nexit 99\n"), key)
184 prevDir := filepath.Join(f.a.stagingDir(), "prev")
185 unitDir := filepath.Join(prevDir, "etc", "systemd", "system")
186 if err := os.MkdirAll(unitDir, 0755); err != nil {
187 t.Fatal(err)
188 }
189 if err := os.WriteFile(filepath.Join(unitDir, "carlos-edge.service"), []byte("[Unit]\n"), 0644); err != nil {
190 t.Fatal(err)
191 }
192 if err := os.MkdirAll(filepath.Join(prevDir, "etc", "tmpfiles.d"), 0755); err != nil {
193 t.Fatal(err)
194 }
195 if err := os.WriteFile(filepath.Join(prevDir, "etc", "tmpfiles.d", "carlos.conf"), nil, 0644); err != nil {
196 t.Fatal(err)
197 }
198 if err := writeJSONDurable(filepath.Join(prevDir, "plan.json"), bootstrapPlan{}, 0600); err != nil {
199 t.Fatal(err)
200 }
201 if err := writeJSONDurable(f.a.file("journal.json"), systemJournal{Phase: systemPhaseInstalled, Target: "v2.0.0", PrevBuild: "v2.0.0", StartedAt: f.now}, 0600); err != nil {
202 t.Fatal(err)
203 }
204 if err := f.a.Run(t.Context(), recovery); err != nil {
205 t.Fatalf("production recovery=%v: %v", recovery, err)
206 }
207 if journal, err := readSystemJournal(opt); err != nil || journal != nil {
208 t.Fatal("repair journal not closed", journal, err)
209 }
210 if result := f.result(); result == nil || result.Outcome != systemOutcomeRolledBack {
211 t.Fatal("repair result", result)
212 }
213 if out, err := f.a.guardRun(t.Context(), time.Second, systemBinaryPath(opt), "version"); err != nil || out == "" {
214 t.Fatal("repaired current cannot execute", out, err)
215 }
216 }
217}
218
219func TestPlan14PendingInventoryStartupRootRunner(t *testing.T) {
220 if err := os.Mkdir("/var/lib/carlos-release", 0700); err != nil {
221 t.Fatal(err)
222 }
223 t.Cleanup(func() { os.RemoveAll("/var/lib/carlos-release") })
224 for _, mode := range []string{"instance-control", "system"} {
225 t.Run(mode, func(t *testing.T) {
226 t.Setenv("PLAN14_R9_PENDING", mode)
227 runServingParkPeerNamespace(t, false, false, "seed-construction")
228 })
229 }
230}
231
232type r9PendingBoundary struct {
233 rootstate.Store
234 failed bool
235}
236
237func (s *r9PendingBoundary) Write(name string, raw []byte) error {
238 if err := s.Store.Write(name, raw); err != nil {
239 return err
240 }
241 if !s.failed {
242 s.failed = true
243 return os.ErrPermission
244 }
245 return nil
246}
247
248func filesSeedPendingConstruction(t *testing.T, store release.Store, resources ClaimRuntimeResources, owners ClaimOwnerBindings, actor hostbox.Actor) {
249 if err := unix.Mount("", "/", "", unix.MS_REC|unix.MS_PRIVATE, ""); err != nil {
250 t.Fatal(err)
251 }
252 for _, path := range []string{"/run/carlos", "/run/carlos/hostguard"} {
253 if err := os.Mkdir(path, 0755); err == nil {
254 t.Cleanup(func() { os.RemoveAll(path) })
255 } else if !os.IsExist(err) {
256 t.Fatal(err)
257 }
258 }
259 emptyUnits := t.TempDir()
260 if err := unix.Mount(emptyUnits, "/etc/systemd/system", "", unix.MS_BIND, ""); err != nil {
261 t.Fatal(err)
262 }
263 t.Cleanup(func() { unix.Unmount("/etc/systemd/system", 0) })
264 host := os.Getenv("PLAN14_R10_INSTALL_HOST")
265 if host == "" {
266 host = recoveryHost
267 }
268 f := newRecoveryFixture(t, host)
269 obs := f.read()
270 obs.Claim.Op.Exec = actor.Box
271 obs.Claim.Op.Participants = []string{actor.Box}
272 obs.Claim.Touched.Boxes = []string{actor.Box}
273 claimBody, err := json.Marshal(obs.Claim)
274 if err != nil {
275 t.Fatal(err)
276 }
277 claimKey, err := hostclaim.Key(obs.Host)
278 if err != nil {
279 t.Fatal(err)
280 }
281 if _, err := f.claims.Put(t.Context(), claimKey, claimBody, obs.ETag); err != nil {
282 t.Fatal(err)
283 }
284 h := obs.Claim.Holder
285 plan14PutRecord(t, f.claims, provision.Record{Schema: 1, Host: obs.Host, Account: h.Account, App: h.App, Channel: "stable", CreatedAt: f.now})
286 keys, err := f.claims.List(t.Context(), "")
287 if err != nil {
288 t.Fatal(err)
289 }
290 for _, key := range keys {
291 raw, _, err := f.claims.Get(t.Context(), key)
292 if err != nil {
293 t.Fatal(err)
294 }
295 if _, err = store.Put(t.Context(), key, raw, ""); err != nil {
296 t.Fatal(err)
297 }
298 }
299 obs, err = hostclaim.Read(t.Context(), store, obs.Host)
300 if err != nil {
301 t.Fatal(err)
302 }
303 _, etag, err := store.Get(t.Context(), h.Key)
304 if err != nil {
305 t.Fatal(err)
306 }
307 resources.DataDir = "/data"
308 resources.UnitsDir = "/etc/systemd/system"
309 reg, err := openServiceRegistry("/data/registry.db")
310 if err != nil {
311 t.Fatal(err)
312 }
313 defer reg.Close()
314 journal, err := openClaimRootJournal()
315 if err != nil {
316 t.Fatal(err)
317 }
318 defer journal.Close()
319 journals, err := hostbox.NewJournalService(journal)
320 if err != nil {
321 t.Fatal(err)
322 }
323 if os.Getenv("PLAN14_R10_RETIRED") != "" {
324 if err := checkClaimJournalMigration(t.Context(), journals, filepath.Join("/data", ".hostclaims")); err != nil {
325 t.Fatal("journal startup before installation", err)
326 }
327 }
328 state, err := openClaimRootAuthority()
329 if err != nil {
330 t.Fatal(err)
331 }
332 defer state.Close()
333 boundary := &r9PendingBoundary{Store: state, failed: os.Getenv("PLAN14_R10_RETIRED") != ""}
334 authority := hostguard.NewRootAuthority(boundary)
335 roots := map[string]string{"data": "/data", "opt": resources.OptDir, "units": "/etc/systemd/system"}
336 guard := hostguard.Guard{Root: authority, DataDir: "/data", Claims: hostguard.StoreReader{Store: store}}
337 census := &hostbox.EmptyInstallationCensus{Inventory: journals.Inventory, Registry: reg, Launches: journal, Box: actor.Box, Enrollment: actor.Enrollment, DirectoryRevision: "1", Roots: roots, SharedPaths: claimGlobalSharedPaths(resources)}
338 root := ClaimRootHTTP{history: productionCreationHistory, Journals: journals, Launches: journal, Roots: roots, InventoryActor: func(context.Context) (hostbox.Actor, error) { return actor, nil }, Reconcile: hostbox.ReconcileAdapter{Registry: reg, Inventory: journals.Inventory, Census: census, Box: actor.Box, Enrollment: actor.Enrollment}, Stop: ClaimStopHTTP{Coordinator: ClaimStopCoordinator{Guard: guard, Evidence: store}}}
339 _, client := hostbox.RemoteJournals(func(ctx context.Context, q hostbox.JournalRequest) (hostbox.JournalResponse, error) {
340 out, err := root.commitRootInventory(ctx, q)
341 t.Logf("pending commit: %v", err)
342 return out, err
343 })
344 _, err = client.WriteExpected(t.Context(), hostbox.StopInventory{Complete: true, Host: obs.Host, Incarnation: h.Incarnation, InstallationStamp: h.Stamp, Box: actor.Box, Enrollment: actor.Enrollment, DirectoryRevision: "1"}, hostbox.InventoryExpectation{RecordKey: h.Key, RecordETag: etag, Target: hostguard.FenceTarget{Host: obs.Host, Incarnation: h.Incarnation, Stamp: h.Stamp, Session: h.Session, Box: actor.Box, Enrollment: actor.Enrollment, ClaimETag: obs.ETag, Op: obs.Claim.Op.ID, Phase: obs.Claim.Op.Phase}})
345 if os.Getenv("PLAN14_R10_RETIRED") != "" {
346 if err != nil {
347 t.Fatal("complete installation", err)
348 }
349 if obs.Host == "sibling.example" {
350 if err := guard.With(t.Context(), obs.Host, hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp}, func(*hostclaim.Claim) error { return nil }, func() error { return nil }); err != nil {
351 t.Fatal("sibling admission", err)
352 }
353 return
354 }
355 want := hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp}
356 in, err := authority.Installation(t.Context(), obs.Host, want)
357 if err != nil {
358 t.Fatal(err)
359 }
360 interrupted := os.Getenv("PLAN14_R10_RETIRED") == "interrupted"
361 err = guard.RetireInstallationWith(t.Context(), obs.Host, want, func(*hostclaim.Claim) error { return nil }, func() error {
362 if interrupted {
363 return os.ErrPermission
364 }
365 return nil
366 })
367 if interrupted != errors.Is(err, os.ErrPermission) {
368 t.Fatal("retirement boundary", err)
369 }
370 state.Close()
371 reopened, err := openClaimRootAuthority()
372 if err != nil {
373 t.Fatal(err)
374 }
375 defer reopened.Close()
376 authority = hostguard.NewRootAuthority(reopened)
377 retained, err := authority.Installation(t.Context(), obs.Host, want)
378 if err != nil || retained != in {
379 t.Fatal("retired identity lost", retained, in, err)
380 }
381 guard.Root = authority
382 if err := guard.With(t.Context(), obs.Host, want, func(*hostclaim.Claim) error { return nil }, func() error { t.Fatal("retired admitted"); return nil }); err == nil {
383 t.Fatal("retired admitted")
384 }
385 return
386 }
387 if err == nil || !boundary.failed {
388 t.Fatal("pending boundary not reached", err)
389 }
390 if pending, err := authority.PendingInstallations(); err != nil || len(pending) != 1 {
391 t.Fatal("pending intent not durable", len(pending), err)
392 }
393 if err := verifyRootMigrationStartup(t.Context(), rootFloorIdentity{Deployment: actor.ControlBucket, Box: actor.Box, Enrollment: actor.Enrollment}, journals.Inventory); err == nil {
394 t.Fatal("pending state admitted new work")
395 }
396}
397
398func TestPlan14GuardRecoveryLegacyPolicyRootRunner(t *testing.T) {
399 if os.Getenv("PLAN14_R9_GUARD_CHILD") == "1" {
400 RunSystemGuardExec([]string{"recover"})
401 t.Fatal("guard returned")
402 }
403 dir := generationRootRunnerDir(t)
404 t.Setenv("CARLOS_OPT_DIR", dir)
405 t.Setenv("CARLOS_DEPLOYMENT_BUCKET", "deploy")
406 pub, key, err := ed25519.GenerateKey(nil)
407 if err != nil {
408 t.Fatal(err)
409 }
410 t.Setenv("CARLOS_RELEASE_PUBKEY", base64.StdEncoding.EncodeToString(pub))
411 remote := filepath.Join(dir, "remote")
412 t.Setenv("CARLOS_DEPLOYMENT_DIR", remote)
413 floor, _ := json.Marshal(hostfloor.Floor{Min: 1, MinRelease: "v1.0.0"})
414 auth, err := release.SignRootEnvelope(release.RootSigner(key), hostfloor.Authorization{Schema: 1, Purpose: "moves-protocol", DeploymentBucket: "deploy", Key: hostfloor.Key, SHA256: hostfloor.Digest(floor)})
415 if err != nil {
416 t.Fatal(err)
417 }
418 policy, _ := json.Marshal(hostfloor.Policy{Schema: 1, DeploymentBucket: "deploy", Mode: "enforced", Floor: floor, Authorization: auth})
419 if err := os.MkdirAll(systemGuardDir(dir), 0755); err != nil {
420 t.Fatal(err)
421 }
422 if err := os.WriteFile(filepath.Join(systemGuardDir(dir), "policy.json"), policy, 0644); err != nil {
423 t.Fatal(err)
424 }
425 store := release.NewDirStore(remote)
426 other, _ := json.Marshal(hostfloor.Floor{Min: 2, MinRelease: "v2.0.0"})
427 otherAuth, err := release.SignRootEnvelope(release.RootSigner(key), hostfloor.Authorization{Schema: 1, Purpose: "moves-protocol", DeploymentBucket: "deploy", Key: hostfloor.Key, SHA256: hostfloor.Digest(other)})
428 if err != nil {
429 t.Fatal(err)
430 }
431 if _, err := store.Put(t.Context(), hostfloor.Key, other, ""); err != nil {
432 t.Fatal(err)
433 }
434 if _, err := store.Put(t.Context(), hostfloor.AuthKey, otherAuth, ""); err != nil {
435 t.Fatal(err)
436 }
437 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
438 key := strings.TrimPrefix(r.URL.Path, "/deploy/")
439 raw, _, err := store.Get(r.Context(), key)
440 if err != nil {
441 http.Error(w, "missing", http.StatusNotFound)
442 return
443 }
444 w.Write(raw)
445 }))
446 defer server.Close()
447 for k, v := range map[string]string{"CARLOS_S3_ENDPOINT": server.URL, "AWS_REGION": "eu-west-1", "AWS_DEFAULT_REGION": "eu-west-1", "AWS_ACCESS_KEY_ID": "test", "AWS_SECRET_ACCESS_KEY": "test", "AWS_SESSION_TOKEN": "", "AWS_EC2_METADATA_DISABLED": "true"} {
448 t.Setenv(k, v)
449 }
450 binary, _ := os.ReadFile("/usr/bin/echo")
451 if err := os.WriteFile(systemBinaryPath(dir), binary, 0755); err != nil {
452 t.Fatal(err)
453 }
454 for _, flag := range []string{"", "1"} {
455 cmd := exec.Command(os.Args[0], "-test.run=^TestPlan14GuardRecoveryLegacyPolicyRootRunner$")
456 cmd.Env = append(os.Environ(), "PLAN14_R9_GUARD_CHILD=1", "CARLOS_HOST_CLAIM="+flag)
457 out, err := cmd.CombinedOutput()
458 if err == nil || !strings.Contains(string(out), hostfloor.ErrFloor.Error()) {
459 t.Fatal("legacy recovery ignored reachable policy mismatch", string(out), err)
460 }
461 }
462}
463
464const r9FloorRebootDir = "/var/lib/carlos-rootrunner-r9-floor"
465
466func TestRootRebootFloorPrevAuthorizationPhase1(t *testing.T) {
467 if err := os.Mkdir(r9FloorRebootDir, 0755); err != nil {
468 t.Fatal(err)
469 }
470 pub, key, err := ed25519.GenerateKey(nil)
471 if err != nil {
472 t.Fatal(err)
473 }
474 t.Setenv("CARLOS_RELEASE_PUBKEY", base64.StdEncoding.EncodeToString(pub))
475 id := rootFloorIdentity{"deploy", "box", "en"}
476 f, err := openRootMovesFloors(r9FloorRebootDir, id, true)
477 if err != nil {
478 t.Fatal(err)
479 }
480 defer f.close()
481 bound := moves.Floor{Min: 1, MinRelease: "v2.0.0"}
482 if err := f.reconstruct(t.Context(), rootFloorReconstruction{Identity: id, Floor: &bound, Source: "accepted history", Digest: floorTestDigest, Authorization: floorTestAuthorization(t, f)}); err != nil {
483 t.Fatal(err)
484 }
485 for _, p := range []string{systemBinaryPath(r9FloorRebootDir), systemPrevPath(r9FloorRebootDir)} {
486 rootFloorSignRunnerBytes(t, r9FloorRebootDir, p, "v2.0.0", []byte("#!/bin/sh\nprintf recovered\n"), key)
487 }
488 j := rootFloorJudge{floors: f, current: systemBinaryPath(r9FloorRebootDir), prev: systemPrevPath(r9FloorRebootDir), binary: func(p string) (rootFloorBinary, error) { return readRootFloorBinary(r9FloorRebootDir, p) }, idle: func(context.Context) error { return nil }}
489 if err := j.judge(t.Context()); err != nil {
490 t.Fatal(err)
491 }
492 rootFloorSignRunnerBytes(t, r9FloorRebootDir, j.current, "v3.0.0", []byte("#!/bin/sh\nexit 99\n"), key)
493 if err := os.MkdirAll(systemRootDir(r9FloorRebootDir), 0755); err != nil {
494 t.Fatal(err)
495 }
496 if err := writeFileDurable(filepath.Join(systemRootDir(r9FloorRebootDir), "journal.json"), []byte("{}"), 0600); err != nil {
497 t.Fatal(err)
498 }
499 boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
500 if err != nil {
501 t.Fatal(err)
502 }
503 if err := writeFileDurable(filepath.Join(r9FloorRebootDir, "boot"), boot, 0600); err != nil {
504 t.Fatal(err)
505 }
506 if err := writeFileDurable(filepath.Join(r9FloorRebootDir, "pub"), []byte(base64.StdEncoding.EncodeToString(pub)), 0600); err != nil {
507 t.Fatal(err)
508 }
509}
510func TestRootRebootFloorPrevAuthorizationPhase2(t *testing.T) {
511 defer os.RemoveAll(r9FloorRebootDir)
512 before, err := os.ReadFile(filepath.Join(r9FloorRebootDir, "boot"))
513 if err != nil {
514 t.Fatal(err)
515 }
516 now, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
517 if err != nil || bytes.Equal(before, now) {
518 t.Fatal("actual reboot required", err)
519 }
520 pub, err := os.ReadFile(filepath.Join(r9FloorRebootDir, "pub"))
521 if err != nil {
522 t.Fatal(err)
523 }
524 t.Setenv("CARLOS_RELEASE_PUBKEY", string(pub))
525 f, err := openRootMovesFloors(r9FloorRebootDir, rootFloorIdentity{"deploy", "box", "en"}, false)
526 if err != nil {
527 t.Fatal(err)
528 }
529 defer f.close()
530 raw, err := f.public.Read("prev.json")
531 if err != nil {
532 t.Fatal(err)
533 }
534 var verdict rootFloorVerdict
535 if err := json.Unmarshal(raw, &verdict); err != nil {
536 t.Fatal(err)
537 }
538 prev, err := readRootFloorBinary(r9FloorRebootDir, systemPrevPath(r9FloorRebootDir))
539 if err != nil {
540 t.Fatal(err)
541 }
542 verdict.Prev.File.Device = prev.File.Device + 1
543 raw, _ = json.Marshal(verdict)
544 if err := f.public.Write("prev.json", raw); err != nil {
545 t.Fatal(err)
546 }
547 j := rootFloorJudge{floors: f, current: systemBinaryPath(r9FloorRebootDir), prev: systemPrevPath(r9FloorRebootDir), binary: func(p string) (rootFloorBinary, error) { return readRootFloorBinary(r9FloorRebootDir, p) }}
548 journal, err := rootFloorJournalPresent(r9FloorRebootDir)
549 if err != nil || !journal {
550 t.Fatal("interrupted update missing", err)
551 }
552 selected, err := rootFloorBootCandidate(t.Context(), j, journal, func(context.Context, string) error { return nil }, func(context.Context) error { return nil })
553 if err != nil || selected != j.prev {
554 t.Fatal("accepted signed prev rejected after reboot", selected, err)
555 }
556 out, err := exec.Command(selected, "system", "apply", "--recover").CombinedOutput()
557 if err != nil || string(out) != "recovered" {
558 t.Fatal(string(out), err)
559 }
560 if err := os.WriteFile(j.prev, []byte("substitution"), 0755); err != nil {
561 t.Fatal(err)
562 }
563 if err := rootFloorPrevVerdict(j); err == nil {
564 t.Fatal("substituted prev accepted")
565 }
566}
567