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 "encoding/json"
9 "errors"
10 "fmt"
11 "io"
12 "net"
13 "net/http"
14 "os"
15 "os/exec"
16 "os/user"
17 "path/filepath"
18 "reflect"
19 "strconv"
20 "strings"
21 "sync/atomic"
22 "syscall"
23 "testing"
24 "time"
25
26 "github.com/carlosframework/platform/internal/activator"
27 "github.com/carlosframework/platform/internal/cgroup"
28 "github.com/carlosframework/platform/internal/hostbox"
29 "github.com/carlosframework/platform/internal/hostclaim"
30 "github.com/carlosframework/platform/internal/hostguard"
31 "github.com/carlosframework/platform/internal/instancectl"
32 "github.com/carlosframework/platform/internal/registry"
33 "github.com/carlosframework/platform/internal/rootstate"
34 "golang.org/x/sys/unix"
35)
36
37// The control socket coordinates the test only. Every effect request uses the
38// production journal client and its separately authenticated Unix connection.
39type servingPeerCommand struct {
40 Action string
41 Current, Prior servingArtifactIdentity
42 Target hostguard.FenceTarget
43 ID string
44 Previous hostclaim.Observed
45 Admitted hostclaim.Observed
46 Actor hostbox.Actor
47 Stop StopRequest
48}
49
50type servingPeerReply struct {
51 Peer StopPeer
52 Authorization servingParkAuthorization
53 Launch hostbox.LaunchInventory
54 Stopped StopResult
55 Error string
56}
57
58// Invoked only by the disposable edge unit, or its same-UID sibling control.
59func TestRootServingParkPeerPayload(t *testing.T) {
60 control, socket := os.Getenv("SERVING_PEER_CONTROL"), os.Getenv("SERVING_PEER_SOCKET")
61 if control == "" || socket == "" || os.Geteuid() == 0 {
62 t.Fatal("invoke through TestRootServingParkAuthenticatedPeer")
63 }
64 call := rootJournalClient(socket)
65 if os.Getenv("SERVING_PEER_SIBLING") == "1" {
66 var q servingPeerCommand
67 if err := json.NewDecoder(os.Stdin).Decode(&q); err != nil {
68 t.Fatal(err)
69 }
70 if _, err := (hostbox.ServingParkClient{Call: call}).Park(t.Context(), q.Current, q.Prior); err == nil {
71 t.Fatal("same-UID sibling was authorized")
72 }
73 return
74 }
75 conn, err := net.DialTimeout("unix", control, 5*time.Second)
76 if err != nil {
77 t.Fatal(err)
78 }
79 defer conn.Close()
80 if err := conn.SetDeadline(time.Now().Add(2 * time.Minute)); err != nil {
81 t.Fatal(err)
82 }
83 start, err := claimConstructionProcessStart()
84 if err != nil {
85 t.Fatal(err)
86 }
87 controller, err := (productionStopControllers{}).VerifiedController(t.Context(), os.Getpid())
88 if err != nil {
89 t.Fatal(err)
90 }
91 boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
92 if err != nil {
93 t.Fatal(err)
94 }
95 peer := StopPeer{UID: uint32(os.Geteuid()), PID: os.Getpid(), ProcessStart: start, BootID: strings.TrimSpace(string(boot)), ControllerID: controller}
96 encode, decode := json.NewEncoder(conn), json.NewDecoder(conn)
97 if err := encode.Encode(servingPeerReply{Peer: peer}); err != nil {
98 t.Fatal(err)
99 }
100 client := hostbox.ServingParkClient{Call: call}
101 var manager *cgroup.Manager
102 var seedServer *filesSeedServer
103 defer func() {
104 if seedServer != nil {
105 seedServer.close(t.Context())
106 }
107 }()
108 for {
109 var q servingPeerCommand
110 if err := decode.Decode(&q); errors.Is(err, io.EOF) {
111 return
112 } else if err != nil {
113 t.Fatal(err)
114 }
115 var reply servingPeerReply
116 var callErr error
117 switch q.Action {
118 case "control-request":
119 transport := &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
120 return (&net.Dialer{}).DialContext(ctx, "unix", socket)
121 }}
122 response, err := (&http.Client{Transport: transport, Timeout: 3 * time.Second}).Get("http://root/claim/retire")
123 if err != nil {
124 callErr = err
125 } else {
126 response.Body.Close()
127 if response.StatusCode != http.StatusMethodNotAllowed {
128 callErr = fmt.Errorf("control status %d", response.StatusCode)
129 }
130 }
131 transport.CloseIdleConnections()
132 case "seed-listen":
133 ready := make(chan struct{})
134 close(ready)
135 seedServer, callErr = startFilesSeedSocket(t.Context(), ready, filesSeedSocket, func(ctx context.Context, c *net.UnixConn) error {
136 return authenticateFilesSeedPeer(ctx, c, os.Geteuid(), false)
137 }, func(ctx context.Context, host string, out io.Writer) error {
138 _, err := io.WriteString(out, "accepted "+host+"\n")
139 return err
140 })
141 case "seed-flat-client":
142 binary, err := os.Executable()
143 if err != nil {
144 t.Fatal(err)
145 }
146 cmd := exec.CommandContext(t.Context(), binary, "-test.run=^TestRootFilesSeedClientPayload$", "-test.v")
147 cmd.Env = append(os.Environ(), "FILES_SEED_EXPECT_REFUSAL=1", "FILES_SEED_CLIENT=1")
148 if out, err := cmd.CombinedOutput(); err != nil {
149 callErr = fmt.Errorf("flat client: %w: %s", err, out)
150 }
151 case "seed-manager":
152 manager, callErr = filesSeedManager(t.Context())
153 case "seed-capability":
154 callErr = NewClaimRoleRootClient(socket).KindCapability(t.Context(), "files-seed")
155 case "stop":
156 reply.Stopped, callErr = CollectClaimStop(t.Context(), socket, q.Stop, stopDrainFunc(func(_ context.Context, req StopRequest, fence string) (DrainAcknowledgement, error) {
157 return DrainClaimAcknowledgement(fence, req.ControllerID, peer.ProcessStart, nil), nil
158 }))
159 case "handover":
160 hook := coldHandoverWith(handoverRecoveryIdentity(t, q.Actor), socket, func(context.Context, hostclaim.Observed) (hostguard.FenceReceipt, error) {
161 return hostguard.FenceReceipt{Target: q.Target}, nil
162 })
163 callErr = hook(t.Context(), q.Previous, q.Admitted)
164 case "park":
165 reply.Authorization, callErr = client.Park(t.Context(), q.Current, q.Prior)
166 case "complete":
167 callErr = client.Complete(t.Context(), q.ID, q.Current)
168 case "sibling":
169 binary, err := os.Executable()
170 if err != nil {
171 t.Fatal(err)
172 }
173 cmd := exec.CommandContext(t.Context(), binary, "-test.run=^TestRootServingParkPeerPayload$", "-test.v", "-test.timeout=20s")
174 cmd.Env = append(os.Environ(), "SERVING_PEER_SIBLING=1")
175 body, err := json.Marshal(q)
176 if err != nil {
177 t.Fatal(err)
178 }
179 cmd.Stdin = strings.NewReader(string(body))
180 out, err := cmd.CombinedOutput()
181 if err != nil {
182 callErr = fmt.Errorf("sibling: %w: %s", err, out)
183 }
184 case "prelaunch":
185 if manager == nil {
186 manager = cgroup.Setup(t.Logf)
187 if manager == nil {
188 t.Fatal("edge unit has no delegated cgroup")
189 }
190 }
191 target := q.Target
192 target.BootID, target.ControllerID = peer.BootID, peer.ControllerID
193 roles := &ClaimRoleAssembly{Manager: manager}
194 parent, group, err := roles.preparePrelaunchGroup(claimContainmentName(target), cgroup.TierDefault)
195 if err != nil {
196 t.Fatal(err)
197 }
198 t.Cleanup(func() { _ = parent.Close() })
199 launch := hostbox.LaunchInventory{Target: target, ControllerPID: peer.PID, ProcessStart: peer.ProcessStart, NoState: true, Groups: []hostbox.LaunchGroup{group}}
200 journal, _ := hostbox.RemoteJournals(call)
201 // Each forged observation must be refused before any launch record.
202 for _, field := range []string{"pid", "start", "controller", "boot", "inode", "path"} {
203 bad := launch
204 bad.Groups = append([]hostbox.LaunchGroup(nil), launch.Groups...)
205 switch field {
206 case "pid":
207 bad.ControllerPID++
208 case "start":
209 bad.ProcessStart += "0"
210 case "controller":
211 bad.Target.ControllerID = "forged"
212 case "boot":
213 bad.Target.BootID = "forged"
214 case "inode":
215 bad.Groups[0].Inode++
216 case "path":
217 bad.Groups[0].RelativePath += "-foreign"
218 }
219 if err := journal.Prelaunch(t.Context(), bad); err == nil {
220 t.Fatalf("forged prelaunch %s was authorized", field)
221 }
222 }
223 callErr = journal.Prelaunch(t.Context(), launch)
224 reply.Launch = launch
225 default:
226 t.Fatalf("unknown test action %q", q.Action)
227 }
228 if callErr != nil {
229 reply.Error = callErr.Error()
230 }
231 if err := encode.Encode(reply); err != nil {
232 t.Fatal(err)
233 }
234 }
235}
236
237// This witness needs an unused carlos-edge.service name because production
238// authentication resolves that exact unit. It refuses existing deployments.
239// Claim, installation, predecessor stop/start and roster/census data are fixtures;
240// peer authentication, prelaunch containment, park authority and IO are real.
241func TestRootServingParkAuthenticatedPeer(t *testing.T) {
242 runServingParkAuthenticatedPeer(t, false, false)
243}
244
245func TestRootServingParkAuthenticatedHandover(t *testing.T) {
246 runServingParkAuthenticatedPeer(t, true, false)
247}
248
249func runServingParkAuthenticatedPeer(t *testing.T, handover, writer bool) {
250 t.Helper()
251 runServingParkPeerNamespace(t, handover, writer, "")
252}
253
254func TestRootServingNamespaceStartup(t *testing.T) {
255 runServingParkPeerNamespace(t, true, false, "parent")
256}
257
258func TestRootServingNamespaceUnrecordedStartup(t *testing.T) {
259 runServingParkPeerNamespace(t, true, false, "fresh")
260}
261
262func runServingParkPeerNamespace(t *testing.T, handover, writer bool, namespace string) {
263 t.Helper()
264 servingRootRunner(t)
265 pid1, err := os.ReadFile("/proc/1/comm")
266 if err != nil || strings.TrimSpace(string(pid1)) != "systemd" {
267 t.Fatal("root runner needs systemd as PID 1")
268 }
269 probe, probeErr := exec.CommandContext(t.Context(), "systemctl", "show", "carlos-edge.service", "-p", "LoadState", "--value").Output()
270 if strings.TrimSpace(string(probe)) != "not-found" {
271 t.Fatalf("refusing existing or unreadable carlos-edge.service: %s %v", probe, probeErr)
272 }
273 // The runner is disposable, but only a user created here is removed here.
274 account, err := user.Lookup("carlos")
275 if _, absent := err.(user.UnknownUserError); absent {
276 if _, groupErr := user.LookupGroup("carlos"); groupErr == nil {
277 t.Fatal("refusing pre-existing carlos group without its user")
278 } else if _, unknown := groupErr.(user.UnknownGroupError); !unknown {
279 t.Fatal(groupErr)
280 }
281 out, createErr := exec.CommandContext(t.Context(), "useradd", "--system", "--no-create-home", "--user-group", "--shell", "/usr/sbin/nologin", "carlos").CombinedOutput()
282 if createErr != nil {
283 t.Fatalf("create runner user: %v %s", createErr, out)
284 }
285 t.Cleanup(func() {
286 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
287 defer cancel()
288 if out, err := exec.CommandContext(ctx, "userdel", "carlos").CombinedOutput(); err != nil {
289 t.Errorf("remove runner user: %v %s", err, out)
290 return
291 }
292 if _, err := user.LookupGroup("carlos"); err == nil {
293 if out, err := exec.CommandContext(ctx, "groupdel", "carlos").CombinedOutput(); err != nil {
294 t.Errorf("remove runner group: %v %s", err, out)
295 }
296 }
297 })
298 account, err = user.Lookup("carlos")
299 }
300 if err != nil || account == nil || account.Uid == "0" {
301 t.Fatalf("runner service user: %v", err)
302 }
303 root, err := os.MkdirTemp("/var/lib", "carlos-rootrunner-peer-")
304 if err != nil {
305 t.Fatal(err)
306 }
307 t.Cleanup(func() {
308 if err := os.RemoveAll(root); err != nil {
309 t.Error(err)
310 }
311 })
312 if err := os.Chmod(root, 0755); err != nil {
313 t.Fatal(err)
314 }
315 binary, err := os.Executable()
316 if err != nil {
317 t.Fatal(err)
318 }
319 body, err := os.ReadFile(binary)
320 if err != nil {
321 t.Fatal(err)
322 }
323 installed := filepath.Join(root, "runner.test")
324 if err := os.WriteFile(installed, body, 0755); err != nil {
325 t.Fatal(err)
326 }
327 if namespace == "seed-cli" {
328 rootFilesSeedDelegatedCLI(t, installed)
329 return
330 }
331 if namespace == "seed-two-host" {
332 rootFilesSeedTwoHost(t, installed)
333 return
334 }
335 if namespace == "seed-isolated" {
336 rootFilesSeedIsolated(t, installed)
337 return
338 }
339 f := newServingRootFixture(t, true)
340 servingRunnerMount(t, f.f.guard.DataDir)
341 for name, body := range map[string]string{"files/upload": "tenant upload", "env": "tenant configuration"} {
342 path := filepath.Join(filepath.Dir(f.db), name)
343 if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
344 t.Fatal(err)
345 }
346 if err := os.WriteFile(path, []byte(body), 0600); err != nil {
347 t.Fatal(err)
348 }
349 }
350 if handover {
351 servingHandoverRoot(t, f)
352 } else {
353 f.install(f.admitted)
354 }
355 state, err := rootstate.Open(filepath.Join(root, "parks"), true, false)
356 if err != nil {
357 t.Fatal(err)
358 }
359 t.Cleanup(func() { _ = state.Close() })
360 f.p.ledger.state = state
361 f.p.ledger.fs = newServingParkFS(f.f.guard.DataDir, state)
362 coordinator := f.f.root(nil)
363 coordinator.Evidence = f.f.claims
364 coordinator.Inventory = f.p.root.Journals.Inventory
365 production, err := NewProductionClaimStopHandler(coordinator, f.f.journal, productionStopControllers{}, f.p.root.Roots)
366 if err != nil {
367 t.Fatal(err)
368 }
369 f.p.root.Stop = production.(ClaimStopHTTP)
370 if namespace == "seed-killed" {
371 f.p.root.Stop.Coordinator.Kernel = servingSeedRecoveryTrace{LinuxStopKernel: f.p.root.Stop.Coordinator.Kernel.(LinuxStopKernel), t: t}
372 }
373 kernelResults := make(chan error, 8)
374 if writer {
375 kernel := f.p.root.Stop.Coordinator.Kernel
376 f.p.root.Stop.Coordinator.Kernel = stopKernelFunc(func(ctx context.Context, target hostguard.FenceTarget, inventory StopInventory, ack DrainAcknowledgement) (StopKernelEvidence, error) {
377 evidence, err := kernel.VerifyStopped(ctx, target, inventory, ack)
378 kernelResults <- err
379 return evidence, err
380 })
381 }
382 f.p.root.Reconcile.Box, f.p.root.Reconcile.Enrollment = f.actor.Box, f.actor.Enrollment
383 var admitted atomic.Bool
384 admitted.Store(true)
385 f.p.root.RequireNewWork = func(context.Context) error {
386 if !admitted.Load() {
387 return hostbox.ErrUnsupported
388 }
389 return nil
390 }
391 h := servingTransportRoot(t, f)
392 path := filepath.Join(root, "root.sock")
393 listener, err := net.Listen("unix", path)
394 if err != nil {
395 t.Fatal(err)
396 }
397 if err := os.Chmod(path, 0666); err != nil {
398 listener.Close()
399 t.Fatal(err)
400 }
401 var handler http.Handler = h
402 if strings.HasPrefix(namespace, "seed-") {
403 capability := newLaunchFixture(t, hostclaim.Op{Kind: hostclaim.KindServing, Exec: "box-1"}).root
404 capability.Authenticate = h.Stop.Authenticate
405 capability.SeedController = verifyFilesSeedDelegation
406 handler = ClaimRootWithLaunch{Root: h, Launch: capability}
407 }
408 if namespace == "seed-construction" {
409 filesSeedInstanceControlChild(t, installed, root, listener.(*net.UnixListener))
410 } else {
411 server := &instancectl.Server{ClaimStop: handler, PeerOK: func(net.Conn) error { return nil }, Logf: t.Logf}
412 ctx, cancel := context.WithCancel(context.Background())
413 if lifecycle, ok := handler.(interface{ Start(context.Context) }); ok {
414 lifecycle.Start(ctx)
415 }
416 done := make(chan error, 1)
417 go func() { done <- server.Serve(ctx, listener) }()
418 t.Cleanup(func() {
419 cancel()
420 if err := <-done; err != nil {
421 t.Error(err)
422 }
423 })
424 }
425 controlPath := filepath.Join(root, "control.sock")
426 control, err := net.ListenUnix("unix", &net.UnixAddr{Name: controlPath, Net: "unix"})
427 if err != nil {
428 t.Fatal(err)
429 }
430 t.Cleanup(func() { control.Close() })
431 if err := os.Chmod(controlPath, 0666); err != nil {
432 t.Fatal(err)
433 }
434 raw, err := unitsFS.ReadFile("units/carlos-edge.service")
435 if err != nil {
436 t.Fatal(err)
437 }
438 var lines []string
439 for _, line := range strings.Split(string(raw), "\n") {
440 if strings.HasPrefix(line, "Requires=") || strings.HasPrefix(line, "After=") || strings.HasPrefix(line, "Wants=") || strings.HasPrefix(line, "ExecStart=") || strings.HasPrefix(line, "ExecReload=") || strings.HasPrefix(line, "EnvironmentFile=") || strings.HasPrefix(line, "ReadWritePaths=") || strings.HasPrefix(line, "Restart=") || strings.HasPrefix(line, "Slice=") {
441 continue
442 }
443 lines = append(lines, line)
444 }
445 // Add a Service section because the embedded unit ends in Install.
446 content := strings.Join(lines, "\n") + fmt.Sprintf("\n[Service]\nType=simple\nRestart=no\nTimeoutStopSec=10s\nExecStart=%s -test.run=^TestRootServingParkPeerPayload$ -test.v -test.timeout=2m\nEnvironment=SERVING_PEER_CONTROL=%s SERVING_PEER_SOCKET=%s\nIPAddressDeny=any\n", installed, controlPath, path)
447 if namespace == "seed-live" {
448 if err := os.Mkdir("/run/carlos", 0755); err != nil {
449 t.Fatal("refusing existing runtime directory", err)
450 }
451 t.Cleanup(func() { os.RemoveAll("/run/carlos") })
452 uid, _ := strconv.Atoi(account.Uid)
453 gid, _ := strconv.Atoi(account.Gid)
454 if err := os.Chown("/run/carlos", uid, gid); err != nil {
455 t.Fatal(err)
456 }
457 content += "ReadWritePaths=/run/carlos\n"
458 }
459 unitPath := "/run/systemd/system/carlos-edge.service"
460 unit, err := os.OpenFile(unitPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
461 if err != nil {
462 t.Fatal(err)
463 }
464 t.Cleanup(func() {
465 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
466 defer cancel()
467 if out, err := exec.CommandContext(ctx, "systemctl", "stop", "carlos-edge.service").CombinedOutput(); err != nil {
468 t.Errorf("stop test edge: %v %s", err, out)
469 }
470 if err := os.Remove(unitPath); err != nil {
471 t.Error(err)
472 }
473 if out, err := exec.CommandContext(ctx, "systemctl", "daemon-reload").CombinedOutput(); err != nil {
474 t.Errorf("reload test edge: %v %s", err, out)
475 }
476 })
477 _, writeErr := io.WriteString(unit, content)
478 if err := errors.Join(writeErr, unit.Close()); err != nil {
479 t.Fatal(err)
480 }
481 servingRunnerSystemctl(t, "daemon-reload")
482 connect := func() (net.Conn, *json.Encoder, *json.Decoder, StopPeer) {
483 t.Helper()
484 servingRunnerSystemctl(t, "start", "carlos-edge.service")
485 if err := control.SetDeadline(time.Now().Add(15 * time.Second)); err != nil {
486 t.Fatal(err)
487 }
488 conn, err := control.Accept()
489 if err != nil {
490 t.Fatal(err)
491 }
492 t.Cleanup(func() { conn.Close() })
493 if err := conn.SetDeadline(time.Now().Add(90 * time.Second)); err != nil {
494 t.Fatal(err)
495 }
496 enc, dec := json.NewEncoder(conn), json.NewDecoder(conn)
497 var hello servingPeerReply
498 if err := dec.Decode(&hello); err != nil {
499 t.Fatal(err)
500 }
501 peer, err := h.Stop.Authenticate(t.Context(), conn)
502 if err != nil || peer != hello.Peer || strconv.FormatUint(uint64(peer.UID), 10) != account.Uid {
503 t.Fatalf("production peer: %+v %+v %v", peer, hello.Peer, err)
504 }
505 return conn, enc, dec, peer
506 }
507 old, _, _, first := connect()
508 servingRunnerSystemctl(t, "stop", "carlos-edge.service")
509 _, encode, decode, next := connect()
510 if first == next {
511 t.Fatal("edge restart kept the process tuple")
512 }
513 if _, err := f.p.parkServingDatabase(t.Context(), old, f.current, f.prior); err == nil {
514 t.Fatal("old controller connection authorized retirement")
515 }
516 command := func(q servingPeerCommand, refused bool) servingPeerReply {
517 t.Helper()
518 t.Logf("authenticated peer request: action=%s expect-refusal=%v", q.Action, refused)
519 if err := encode.Encode(q); err != nil {
520 t.Fatal(err)
521 }
522 var reply servingPeerReply
523 if err := decode.Decode(&reply); err != nil {
524 t.Fatal(err)
525 }
526 if (reply.Error != "") != refused {
527 t.Fatalf("%s: expected refusal=%v: %s", q.Action, refused, reply.Error)
528 }
529 return reply
530 }
531 if namespace == "seed-construction" {
532 command(servingPeerCommand{Action: "control-request"}, false)
533 command(servingPeerCommand{Action: "seed-manager"}, false)
534 command(servingPeerCommand{Action: "seed-capability"}, false)
535 return
536 }
537 if strings.HasPrefix(namespace, "seed-") {
538 command(servingPeerCommand{Action: "seed-capability"}, true)
539 command(servingPeerCommand{Action: "seed-manager"}, false)
540 command(servingPeerCommand{Action: "seed-capability"}, false)
541 if namespace == "seed-killed" {
542 observed := f.f.read()
543 attempt := hostclaim.WakeRetryAttempt{ID: "killed-attempt", Box: f.actor.Box, Enrollment: f.actor.Enrollment, BootID: next.BootID, ControllerID: next.ControllerID, Session: observed.Claim.Holder.Session, InvocationID: "killed-invocation", LaunchInputs: "seed-inputs"}
544 observed, err = hostclaim.Update(t.Context(), f.f.claims, observed, hostclaim.ReserveWakeRetryAttempt(hostclaim.WakeRetryReservation{HolderIncarnation: observed.Claim.Holder.Incarnation, EpisodeID: "killed-seed", Attempt: attempt, Now: f.f.now}))
545 if err != nil {
546 t.Fatal(err)
547 }
548 target := recoveryTuple(observed, claimStopLocal{Actor: f.actor})
549 pre := command(servingPeerCommand{Action: "prelaunch", Target: target}, false)
550 former := next
551 servingRunnerSystemctl(t, "kill", "--signal=KILL", "carlos-edge.service")
552 // Start waits for systemd to reap the killed MainPID; no graceful cleanup ran.
553 servingRunnerSystemctl(t, "stop", "carlos-edge.service")
554 _, encode, decode, next = connect()
555 if former.PID == next.PID || former.ControllerID == next.ControllerID {
556 t.Fatal("kill did not replace controller")
557 }
558 command(servingPeerCommand{Action: "seed-manager"}, false)
559 command(servingPeerCommand{Action: "seed-capability"}, false)
560 rt := registry.Route{Host: observed.Host, DBPath: f.db}
561 files := &movesFiles{DataDir: f.f.guard.DataDir, EnvFile: func(host, db string) (claimEnvFile, error) {
562 return claimEnvFile{Nested: true, Dir: filepath.Dir(db), Name: host + ".env"}, nil
563 }}
564 service := &activator.Service{}
565 exclusion, err := service.AcquireSeedExclusion(t.Context(), rt.Host, nil)
566 if err != nil {
567 t.Fatal(err)
568 }
569 defer exclusion.Release()
570 stops := 0
571 retries := activator.ServingRetries{Claims: f.f.claims, Box: f.actor.Box, Enrollment: f.actor.Enrollment, BootID: next.BootID, ControllerID: next.ControllerID, Now: func() time.Time { return f.f.now }, NewID: f.f.newID, Inputs: func(context.Context, registry.Route) (string, error) { return "seed-inputs", nil }, ExactHealth: func(context.Context, *exec.Cmd, hostclaim.WakeRetryAttempt) error { return hostbox.ErrChanged }, StopAttempt: func(ctx context.Context, o hostclaim.Observed, a hostclaim.WakeRetryAttempt) error {
572 stops++
573 req := StopRequest{Region: f.actor.Region, Recover: true, Host: o.Host, Incarnation: o.Claim.Holder.Incarnation, InstallationStamp: o.Claim.Holder.Stamp, WriterSession: o.Claim.Holder.Session, Box: f.actor.Box, Enrollment: f.actor.Enrollment, BootID: a.BootID, ControllerID: a.ControllerID, ExpectedClaimETag: o.ETag, ExpectedDirectoryRevision: pre.Launch.Target.DirectoryRevision, RosterRevision: pre.Launch.Target.RosterRevision}
574 command(servingPeerCommand{Action: "stop", Stop: req}, false)
575 return nil
576 }}
577 settled, err := settleFilesSeedRerun(t.Context(), rt, files, retries, observed, exclusion)
578 if err != nil {
579 t.Fatal("killed rerun settlement", err)
580 }
581 retry := settled.Claim.Holder.WakeRetry
582 if retry == nil || retry.State != "open" || retry.AttemptsReserved != 1 || retry.Attempt != nil || stops != 1 {
583 t.Fatal("retry debt not settled exactly", retry, stops)
584 }
585 again, err := settleFilesSeedRerun(t.Context(), rt, files, retries, settled, exclusion)
586 if err != nil || again.ETag != settled.ETag || stops != 1 {
587 t.Fatal("rerun repeated stop or reservation", err)
588 }
589 return
590 }
591 if namespace == "seed-live" {
592 command(servingPeerCommand{Action: "seed-listen"}, false)
593 uid, _ := strconv.Atoi(account.Uid)
594 gid, _ := strconv.Atoi(account.Gid)
595 cmd := exec.CommandContext(t.Context(), installed, "-test.run=^TestRootFilesSeedClientPayload$", "-test.v")
596 cmd.Dir = root
597 cmd.Env = append(os.Environ(), "FILES_SEED_CLIENT=1")
598 cmd.SysProcAttr = &syscall.SysProcAttr{Credential: &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}}
599 if out, err := cmd.CombinedOutput(); err != nil {
600 t.Fatalf("external client: %v %s", err, out)
601 }
602 command(servingPeerCommand{Action: "seed-flat-client"}, false)
603 c, err := net.Dial("unix", filesSeedSocket)
604 if err != nil {
605 t.Fatal(err)
606 }
607 json.NewEncoder(c).Encode(filesSeedRequest{Verb: "seed"})
608 c.SetReadDeadline(time.Now().Add(time.Second))
609 if _, err := c.Read(make([]byte, 1)); err == nil {
610 t.Fatal("root client accepted")
611 }
612 c.Close()
613 }
614 if err := verifyFilesSeedDelegation(t.Context(), first); err == nil {
615 t.Fatal("former controller accepted")
616 }
617 forged := next
618 forged.UID = 0
619 if err := verifyFilesSeedDelegation(t.Context(), forged); err == nil {
620 t.Fatal("root peer accepted")
621 }
622 forged = next
623 forged.ControllerID = "changed"
624 if err := verifyFilesSeedDelegation(t.Context(), forged); err == nil {
625 t.Fatal("changed controller accepted")
626 }
627 return
628 }
629 q := servingPeerCommand{Action: "park", Current: f.current, Prior: f.prior}
630 if _, err := (hostbox.ServingParkClient{Call: rootJournalClient(path)}).Park(t.Context(), f.current, f.prior); err == nil {
631 t.Fatal("root caller authorized retirement")
632 }
633 q.Action = "sibling"
634 command(q, false)
635 q.Action = "park"
636 admitted.Store(false)
637 command(q, true)
638 admitted.Store(true)
639 bad := q
640 bad.Current.Generation = "foreign-generation"
641 command(bad, true)
642 before, err := os.ReadFile(f.db)
643 if err != nil || string(before) != "served bytes" {
644 t.Fatal("refusal changed source", err)
645 }
646 names, err := state.Names()
647 if err != nil {
648 t.Fatal(err)
649 }
650 for _, name := range names {
651 if name != "namespace.lock" {
652 t.Fatalf("refusal wrote park authority: %s", name)
653 }
654 }
655 if handover {
656 transfer := servingPeerCommand{Action: "handover", Previous: f.predecessor, Admitted: f.f.read(), Actor: f.actor, Target: f.receipt.Target}
657 captured := hostguard.Captured{Incarnation: f.prior.Incarnation, Stamp: f.prior.Stamp}
658 oldInventory, _, err := f.p.root.Journals.Inventory.Snapshot(t.Context(), f.current.Host, captured)
659 if err != nil {
660 t.Fatal(err)
661 }
662 admitted.Store(false)
663 command(transfer, true)
664 admitted.Store(true)
665 bad := transfer
666 bad.Target.FenceID = "foreign-receipt"
667 command(bad, true)
668 command(transfer, false)
669 command(transfer, false)
670 installed, err := f.f.guard.Capture(f.current.Host)
671 if err != nil || installed != (hostguard.Captured{Incarnation: f.current.Incarnation, Stamp: f.current.Stamp}) {
672 t.Fatalf("authenticated handover installation: %+v %v", installed, err)
673 }
674 retained, _, err := f.p.root.Journals.Inventory.Snapshot(t.Context(), f.current.Host, captured)
675 if err != nil || !reflect.DeepEqual(retained, oldInventory) {
676 t.Fatal("handover changed predecessor inventory", err)
677 }
678 for suffix, want := range map[string]string{"": "served bytes", ".gen": "G", "-wal": "WAL", "-shm": "SHM"} {
679 body, err := os.ReadFile(f.db + suffix)
680 if err != nil || string(body) != want {
681 t.Fatal("handover changed source", suffix, err)
682 }
683 }
684 }
685 target := recoveryTuple(f.f.read(), claimStopLocal{Actor: f.actor})
686 pre := command(servingPeerCommand{Action: "prelaunch", Target: target}, false)
687 actual, err := f.f.journal.Read(t.Context(), pre.Launch.Target)
688 if err != nil || actual.ControllerPID != next.PID || actual.ProcessStart != next.ProcessStart || len(actual.Groups) != 1 || actual.Groups[0] != pre.Launch.Groups[0] {
689 t.Fatalf("prelaunch not retained: %+v %v", actual, err)
690 }
691 if namespace != "" {
692 fs := f.p.ledger.fs.(*servingParkFS)
693 parkPath := filepath.Join(fs.data, "instances", ".serving-parked")
694 if err := os.Mkdir(parkPath, 0700); err != nil {
695 t.Fatal(err)
696 }
697 if err := os.WriteFile(filepath.Join(parkPath, "unknown"), []byte("retained unknown bytes"), 0600); err != nil {
698 t.Fatal(err)
699 }
700 // Parent evidence comes from authenticated installation checks. Lose
701 // either its reply or the later fresh-root identity write.
702 fs.state = quarantineWriteFault{Store: state, before: func(name string, body []byte) error {
703 var q servingParkQuarantine
704 if namespace == "fresh" && name == "namespace-quarantine.json" && servingParkDecode(body, &q) == nil && q.Fresh.Inode != 0 {
705 return unix.ENOSPC
706 }
707 return nil
708 }, after: func(name string, _ []byte) error {
709 if namespace == "parent" && name == "namespace-parent.json" {
710 return unix.EIO
711 }
712 return nil
713 }}
714 command(q, true)
715 fs.state = state
716 if namespace == "parent" {
717 if _, err := state.Read("namespace-quarantine.json"); !errors.Is(err, os.ErrNotExist) {
718 t.Fatal("lost parent reply did not stop before quarantine", err)
719 }
720 } else if err := os.WriteFile(filepath.Join(parkPath, "uncertain"), []byte("unrecorded bytes"), 0600); err != nil {
721 t.Fatal(err)
722 }
723 before := f.f.read()
724 restarted := *f.p
725 ledger := *f.p.ledger
726 ledger.fs = newServingParkFS(fs.data, state)
727 restarted.ledger = &ledger
728 page, err := restarted.recoverServingParks(t.Context(), "", servingParkRecoveryLimit)
729 if err != nil || len(page.Results) != 0 || page.More {
730 t.Fatalf("root namespace startup: %+v %v", page, err)
731 }
732 var quarantine servingParkQuarantine
733 body, err := state.Read("namespace-quarantine.json")
734 if err != nil || servingParkDecode(body, &quarantine) != nil || !quarantine.Complete {
735 t.Fatal("startup did not complete quarantine", err)
736 }
737 if quarantine.Parent.Mount != quarantine.Obstruction.Mount || quarantine.Parent.Mount != quarantine.Fresh.Mount {
738 t.Fatal("startup crossed the bind mount")
739 }
740 if namespace == "fresh" {
741 if !quarantine.UnrecordedMoved || quarantine.Unrecorded.Mount != quarantine.Parent.Mount || quarantine.Unrecorded == quarantine.Fresh {
742 t.Fatal("startup adopted unrecorded root")
743 }
744 body, err = os.ReadFile(filepath.Join(filepath.Dir(parkPath), quarantine.unrecordedName(), "uncertain"))
745 if err != nil || string(body) != "unrecorded bytes" {
746 t.Fatal("startup changed unrecorded bytes", err)
747 }
748 }
749 body, err = os.ReadFile(filepath.Join(filepath.Dir(parkPath), quarantine.name(), "unknown"))
750 if err != nil || string(body) != "retained unknown bytes" || !reflect.DeepEqual(before, f.f.read()) {
751 t.Fatal("startup changed unknown bytes or claim", err)
752 }
753 body, err = os.ReadFile(f.db)
754 if err != nil || string(body) != "served bytes" {
755 t.Fatal("namespace recovery changed source", err)
756 }
757 }
758 parked := command(q, false).Authorization
759 if parked.ID == "" {
760 t.Fatal("empty park identity")
761 }
762 command(servingPeerCommand{Action: "complete", ID: parked.ID, Current: f.current}, true)
763 f.tags(f.current.Stamp)
764 command(servingPeerCommand{Action: "complete", ID: parked.ID, Current: f.current}, false)
765 r, err := f.p.ledger.read(parked.ID)
766 if err != nil || !r.Complete || r.Disposition != servingParkRetain {
767 t.Fatalf("completed park: %+v %v", r, err)
768 }
769 if err := f.p.ledger.fs.CheckParked(&r); err != nil {
770 t.Fatal(err)
771 }
772 for suffix, want := range map[string]string{"": "served bytes", ".gen": "G", "-wal": "WAL", "-shm": "SHM"} {
773 if _, err := os.Lstat(f.db + suffix); !errors.Is(err, os.ErrNotExist) {
774 t.Fatal("park left source", suffix, err)
775 }
776 body, err := os.ReadFile(filepath.Join(f.f.guard.DataDir, "instances", ".serving-parked", parked.ID, filepath.Base(f.db)+suffix))
777 if err != nil || string(body) != want {
778 t.Fatal("park changed bytes", suffix, err)
779 }
780 }
781 if replay := command(q, false).Authorization; replay != parked {
782 t.Fatal("authenticated replay changed retirement")
783 }
784 for name, want := range map[string]string{"files/upload": "tenant upload", "env": "tenant configuration"} {
785 body, err := os.ReadFile(filepath.Join(filepath.Dir(f.db), name))
786 if err != nil || string(body) != want {
787 t.Fatal("retirement changed preserved state", name, err)
788 }
789 }
790 if writer {
791 runServingPeerStoppedWriter(t, f, installed, pre.Launch, next, command, kernelResults)
792 }
793}
794
795// Transfer the serving installation on the nested mount without moving its
796// state directory, then park the database through the root transport.
797func TestRootServingParkColdHandoverBoundary(t *testing.T) {
798 servingRootRunner(t)
799 runServingColdHandoverBoundary(t, servingRunnerMount)
800}
801
802// Keep root errors in the test log; the public transport intentionally returns
803// only uncertainty after a durable request may have made filesystem changes.
804type servingPeerParkTrace struct {
805 servingParkAuthority
806 root *servingParkRoot
807 t *testing.T
808}
809
810func (p servingPeerParkTrace) parkServingDatabase(ctx context.Context, conn net.Conn, current, prior servingArtifactIdentity) (servingParkAuthorization, error) {
811 a, err := p.servingParkAuthority.parkServingDatabase(ctx, conn, current, prior)
812 p.t.Logf("authenticated peer park authority: %v", err)
813 if err != nil {
814 check := p.root.withAuthority(ctx, conn, current, func(o hostclaim.Observed, view hostguard.FenceView, actor hostbox.Actor, dir servingArtifactDirectory, tags uint64) error {
815 selected, e := servingPredecessor(o.Claim, current, tags, prior.Generation)
816 p.t.Logf("park diagnostic predecessor: exact=%v error=%v", selected == prior, e)
817 receipts, e := servingRootStops(ctx, view, prior, actor)
818 p.t.Logf("park diagnostic receipts: count=%d error=%v", len(receipts), e)
819 source, e := p.root.ledger.fs.Capture(servingParkAuthorization{TagsFrom: tags, Predecessor: prior, OriginalSuccessor: current, Successor: current, Directory: dir, ID: servingRetirementID(prior, current, false)})
820 p.t.Logf("park diagnostic source capture: %v", e)
821 if e == nil {
822 disposition, e := servingGenerationParkDisposition(ctx, p.root.ledger.journal, prior, receipts, source.Files[0].Present)
823 p.t.Logf("park diagnostic disposition: %v error=%v", disposition, e)
824 }
825 return nil
826 })
827 p.t.Logf("park diagnostic root installation: %v", check)
828 }
829 return a, err
830}
831
832func (p servingPeerParkTrace) completeServingDatabasePark(ctx context.Context, conn net.Conn, id string, current servingArtifactIdentity) error {
833 err := p.servingParkAuthority.completeServingDatabasePark(ctx, conn, id, current)
834 p.t.Logf("authenticated peer completion authority: %v", err)
835 return err
836}
837
838func TestRootFilesSeedDelegatedController(t *testing.T) {
839 runServingParkPeerNamespace(t, false, false, "seed-delegation")
840}
841
842func TestRootFilesSeedLiveTransport(t *testing.T) {
843 runServingParkPeerNamespace(t, false, false, "seed-live")
844}
845
846func TestRootFilesSeedClientPayload(t *testing.T) {
847 if os.Getenv("FILES_SEED_CLIENT") != "1" {
848 t.Fatal("invoke through live transport witness")
849 }
850 t.Setenv("CARLOS_HOST_CLAIM", "1")
851 t.Setenv("CARLOS_HOST_CLAIM_ORIGINS", "1")
852 t.Setenv("CARLOS_MOVES_PHASE1", "")
853 var out bytes.Buffer
854 err := runSystemFiles(t.Context(), []string{"seed", "--host", recoveryHost}, &out)
855 if os.Getenv("FILES_SEED_EXPECT_REFUSAL") == "1" {
856 if err == nil {
857 t.Fatal("tenant admitted")
858 }
859 return
860 }
861 if err != nil || out.String() != "accepted "+recoveryHost+"\n" {
862 t.Fatal(err, out.String())
863 }
864 // A same-UID impostor can own a pathname but cannot impersonate MainPID.
865 held := filesSeedSocket + ".held"
866 if err := os.Rename(filesSeedSocket, held); err != nil {
867 t.Fatal(err)
868 }
869 defer func() {
870 if err := os.Rename(held, filesSeedSocket); err != nil {
871 t.Error(err)
872 }
873 }()
874 fake, err := net.ListenUnix("unix", &net.UnixAddr{Name: filesSeedSocket, Net: "unix"})
875 if err != nil {
876 t.Fatal(err)
877 }
878 defer fake.Close()
879 if err := os.Chmod(filesSeedSocket, 0600); err != nil {
880 t.Fatal(err)
881 }
882 if err := runSystemFiles(t.Context(), []string{"seed"}, io.Discard); !errors.Is(err, ErrClaimStopIdentity) {
883 t.Fatal("impostor accepted", err)
884 }
885}
886
887func TestRootFilesSeedKilledControllerRerun(t *testing.T) {
888 runServingParkPeerNamespace(t, false, false, "seed-killed")
889}
890
891type servingSeedRecoveryTrace struct {
892 LinuxStopKernel
893 t *testing.T
894}
895
896func (k servingSeedRecoveryTrace) RecoverStopped(ctx context.Context, target hostguard.FenceTarget, in StopInventory) (StopKernelEvidence, DrainAcknowledgement, error) {
897 evidence, ack, err := k.LinuxStopKernel.RecoverStopped(ctx, target, in)
898 k.t.Logf("killed controller root recovery: %v", err)
899 return evidence, ack, err
900}
901