| 1 | //go:build linux && rootrunner |
| 2 | |
| 3 | package host |
| 4 | |
| 5 | import ( |
| 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. |
| 39 | type 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 | |
| 50 | type 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. |
| 59 | func 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. |
| 241 | func TestRootServingParkAuthenticatedPeer(t *testing.T) { |
| 242 | runServingParkAuthenticatedPeer(t, false, false) |
| 243 | } |
| 244 | |
| 245 | func TestRootServingParkAuthenticatedHandover(t *testing.T) { |
| 246 | runServingParkAuthenticatedPeer(t, true, false) |
| 247 | } |
| 248 | |
| 249 | func runServingParkAuthenticatedPeer(t *testing.T, handover, writer bool) { |
| 250 | t.Helper() |
| 251 | runServingParkPeerNamespace(t, handover, writer, "") |
| 252 | } |
| 253 | |
| 254 | func TestRootServingNamespaceStartup(t *testing.T) { |
| 255 | runServingParkPeerNamespace(t, true, false, "parent") |
| 256 | } |
| 257 | |
| 258 | func TestRootServingNamespaceUnrecordedStartup(t *testing.T) { |
| 259 | runServingParkPeerNamespace(t, true, false, "fresh") |
| 260 | } |
| 261 | |
| 262 | func 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. |
| 797 | func 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. |
| 804 | type servingPeerParkTrace struct { |
| 805 | servingParkAuthority |
| 806 | root *servingParkRoot |
| 807 | t *testing.T |
| 808 | } |
| 809 | |
| 810 | func (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 | |
| 832 | func (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 | |
| 838 | func TestRootFilesSeedDelegatedController(t *testing.T) { |
| 839 | runServingParkPeerNamespace(t, false, false, "seed-delegation") |
| 840 | } |
| 841 | |
| 842 | func TestRootFilesSeedLiveTransport(t *testing.T) { |
| 843 | runServingParkPeerNamespace(t, false, false, "seed-live") |
| 844 | } |
| 845 | |
| 846 | func 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 | |
| 887 | func TestRootFilesSeedKilledControllerRerun(t *testing.T) { |
| 888 | runServingParkPeerNamespace(t, false, false, "seed-killed") |
| 889 | } |
| 890 | |
| 891 | type servingSeedRecoveryTrace struct { |
| 892 | LinuxStopKernel |
| 893 | t *testing.T |
| 894 | } |
| 895 | |
| 896 | func (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 | |