carlos / platform Public

WIP files seed checkpoint 10 (not for merge)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 44659fe519ea2390f5ebcb7ff30c76b48137189d parent 7169220
2 files changed, +564 −0
  • internal/host/claimservinghandover_linux_test.go +118 −0
  • internal/host/claimservingpeer_rootrunner_linux_test.go +446 −0

CI passed — run details

CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok  	amadan.net/carlos/platform.git/launchctx	0.011s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok  	github.com/carlosframework/platform	344.338s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	2.986s
ok  	github.com/carlosframework/platform/cmd/moveprobe	92.300s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.712s
ok  	github.com/carlosframework/platform/internal/activator	234.070s
ok  	github.com/carlosframework/platform/internal/agentsock	0.019s
ok  	github.com/carlosframework/platform/internal/alerting	0.004s
ok  	github.com/carlosframework/platform/internal/analytics	0.019s
ok  	github.com/carlosframework/platform/internal/appconfig	2.361s
ok  	github.com/carlosframework/platform/internal/apperr	0.071s
ok  	github.com/carlosframework/platform/internal/applog	0.152s
ok  	github.com/carlosframework/platform/internal/awsinv	0.003s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.004s
ok  	github.com/carlosframework/platform/internal/boxid	0.972s
?   	github.com/carlosframework/platform/internal/boxid/boxidtest	[no test files]
?   	github.com/carlosframework/platform/internal/boxid/internal/signtest	[no test files]
ok  	github.com/carlosframework/platform/internal/boxvolumes	0.008s
ok  	github.com/carlosframework/platform/internal/caps	0.012s
ok  	github.com/carlosframework/platform/internal/certwarm	0.003s
ok  	github.com/carlosframework/platform/internal/cgroup	0.694s
ok  	github.com/carlosframework/platform/internal/changelog	0.294s
ok  	github.com/carlosframework/platform/internal/clamd	0.055s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	367.110s
?   	github.com/carlosframework/platform/internal/controlname	[no test files]
?   	github.com/carlosframework/platform/internal/controlsock	[no test files]
ok  	github.com/carlosframework/platform/internal/dbdoor	5.703s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.009s
ok  	github.com/carlosframework/platform/internal/dbverify	0.004s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.003s
ok  	github.com/carlosframework/platform/internal/docsite	0.005s
ok  	github.com/carlosframework/platform/internal/domains	2.134s
ok  	github.com/carlosframework/platform/internal/doorkey	0.006s
ok  	github.com/carlosframework/platform/internal/economics	0.004s
ok  	github.com/carlosframework/platform/internal/edge	26.159s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.008s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.121s
ok  	github.com/carlosframework/platform/internal/gate	1.654s
ok  	github.com/carlosframework/platform/internal/genselect	0.015s
ok  	github.com/carlosframework/platform/internal/geo	0.014s
ok  	github.com/carlosframework/platform/internal/geoip	0.030s
ok  	github.com/carlosframework/platform/internal/host	138.210s
ok  	github.com/carlosframework/platform/internal/hostactors	0.345s
ok  	github.com/carlosframework/platform/internal/hostbox	2.241s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.499s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.007s
ok  	github.com/carlosframework/platform/internal/hostexec	4.986s
ok  	github.com/carlosframework/platform/internal/hostfence	0.039s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.563s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.012s
ok  	github.com/carlosframework/platform/internal/hostguard	0.981s
ok  	github.com/carlosframework/platform/internal/hostops	1.457s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.005s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.002s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.463s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.142s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.002s
ok  	github.com/carlosframework/platform/internal/infraview	0.002s
ok  	github.com/carlosframework/platform/internal/instancectl	1.819s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.736s
ok  	github.com/carlosframework/platform/internal/instancefs	0.005s
ok  	github.com/carlosframework/platform/internal/instancekey	0.004s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.007s
ok  	github.com/carlosframework/platform/internal/layout	1.431s
ok  	github.com/carlosframework/platform/internal/lease	9.462s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.007s
ok  	github.com/carlosframework/platform/internal/ledger	0.014s
ok  	github.com/carlosframework/platform/internal/mail	0.160s
ok  	github.com/carlosframework/platform/internal/maintenance	0.084s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.540s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.081s
ok  	github.com/carlosframework/platform/internal/metrics	0.005s
ok  	github.com/carlosframework/platform/internal/moves	21.087s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.770s
ok  	github.com/carlosframework/platform/internal/objstore	0.180s
ok  	github.com/carlosframework/platform/internal/ops	0.160s
ok  	github.com/carlosframework/platform/internal/park	0.218s
ok  	github.com/carlosframework/platform/internal/privatefile	0.005s
ok  	github.com/carlosframework/platform/internal/provision	13.578s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.006s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	4.064s
ok  	github.com/carlosframework/platform/internal/relay	7.254s
ok  	github.com/carlosframework/platform/internal/release	19.935s
ok  	github.com/carlosframework/platform/internal/rollout	3.776s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.003s
ok  	github.com/carlosframework/platform/internal/rootaws	0.007s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.007s
ok  	github.com/carlosframework/platform/internal/rootstate	0.025s
ok  	github.com/carlosframework/platform/internal/s3compat	0.005s
ok  	github.com/carlosframework/platform/internal/safefile	0.004s
ok  	github.com/carlosframework/platform/internal/schedule	2.329s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	9.878s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.837s
ok  	github.com/carlosframework/platform/internal/status	0.038s
ok  	github.com/carlosframework/platform/internal/steering	0.007s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.180s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.004s
ok  	github.com/carlosframework/platform/internal/termfence	0.004s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.002s
ok  	github.com/carlosframework/platform/internal/updatewords	0.004s
ok  	github.com/carlosframework/platform/internal/usage	0.004s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.564s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
  minio: cached, digest verified
  mc: cached, digest verified
  minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
  buckets and scoped user ready
=== RUN   TestDeliveredContractMinio
=== RUN   TestDeliveredContractMinio/put
=== RUN   TestDeliveredContractMinio/get_round-trips_the_body
=== RUN   TestDeliveredContractMinio/list_shows_the_key
=== RUN   TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN   TestDeliveredContractMinio/delete_removes_it
=== RUN   TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
    --- PASS: TestDeliveredContractMinio/put (0.00s)
    --- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
    --- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
    --- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
    --- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
    --- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok  	github.com/carlosframework/platform/internal/objstore	0.023s
=== RUN   TestT4StorageProbeMinio
2026/10/14 17:59:00 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE772DE387707E, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok  	github.com/carlosframework/platform/internal/host	0.022s
minio contract: PASS
diff --git a/internal/host/claimservinghandover_linux_test.go b/internal/host/claimservinghandover_linux_test.go
new file mode 100644
index 000000000..1b2640714
--- /dev/null
+++ b/internal/host/claimservinghandover_linux_test.go
@@ -0,0 +1,118 @@
+//go:build linux
+
+package host
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/registry"
+ "golang.org/x/sys/unix"
+)
+
+// Characterize the installation gate still separating the preparation fixture
+// from production. ColdHandover parks the whole state directory. A serving
+// handover must preserve it before the database-only retirement can be bound.
+func TestServingParkColdHandoverRequiresStatePreservation(t *testing.T) {
+ runServingColdHandoverBoundary(t, nil)
+}
+
+func runServingColdHandoverBoundary(t *testing.T, mount func(*testing.T, string)) {
+ t.Helper()
+ f := newServingRootFixture(t, true)
+ if mount != nil {
+ mount(t, f.f.guard.DataDir)
+ }
+ ctx := t.Context()
+ inventory := f.p.root.Journals.Inventory
+ captured := hostguard.Captured{Incarnation: f.prior.Incarnation, Stamp: f.prior.Stamp}
+ in, _, err := inventory.Snapshot(ctx, f.prior.Host, captured)
+ if err != nil || len(in.Entries) != 1 {
+ t.Fatal("predecessor state inventory", err)
+ }
+ owner := registry.ClaimOwnership{ClaimIncarnation: f.prior.Incarnation, ClaimStamp: f.prior.Stamp, ClaimOwner: in.Entries[0].Ownership, ClaimRowID: "prior-route"}
+ reg := restartTestRegistry(t)
+ row := registry.Route{Host: f.prior.Host, Account: f.admitted.Claim.Holder.Account, App: f.admitted.Claim.Holder.App, Kind: "instance", DBPath: f.db, Socket: provision.NestedSocket(f.prior.Host), ClaimOwnership: owner}
+ if err := reg.Upsert(row); err != nil {
+ t.Fatal(err)
+ }
+ in.Entries = append(in.Entries, hostbox.StopInventoryEntry{Kind: "route", ArtifactID: owner.ClaimRowID, ManagedRoot: "registry", RelativePath: row.Host, RowIdentity: owner.ClaimRowID, Ownership: owner.ClaimOwner, Materialization: "materialized"})
+ if err := inventory.WriteStopInventory(ctx, in); err != nil {
+ t.Fatal(err)
+ }
+ _, revision, err := inventory.Snapshot(ctx, row.Host, captured)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Fixture bootstrap only. The transition below uses production handover,
+ // root inventory refresh, exact receipt verification and the typed parker.
+ f.mutateRoot(func(m map[string]any) { m["Installation"].(map[string]any)["InventoryRevision"] = revision })
+ guard := f.f.guard
+ guard.Root = hostguard.NewRootAuthorityWithInventory(f.rootDisk, func(ctx context.Context, host string, c hostguard.Captured) (uint64, error) {
+ _, rev, err := inventory.Snapshot(ctx, host, c)
+ return rev, err
+ })
+ stateDir := filepath.Dir(f.db)
+ for name, body := range map[string]string{"files/upload": "tenant upload", "env": "tenant configuration"} {
+ path := filepath.Join(stateDir, name)
+ if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(path, []byte(body), 0600); err != nil {
+ t.Fatal(err)
+ }
+ }
+ before := f.f.read()
+ next := hostbox.ColdSuccessor{Host: f.current.Host, Incarnation: f.current.Incarnation, Box: f.actor.Box, Enrollment: f.actor.Enrollment, DirectoryRevision: "7", ETag: before.ETag, Generation: f.current.Generation, Stamp: f.current.Stamp, Session: f.current.Session}
+ handover := hostbox.ColdHandover{Guard: guard, Claims: f.f.claims, Registry: reg, Inventory: inventory, Launches: f.f.journal, Roots: f.p.root.Roots, JournalDir: filepath.Join(t.TempDir(), "handover"), VerifyReceipt: verifyRetirementReceipt}
+ err = handover.Run(ctx, f.receipt.Target, next)
+ if mount != nil {
+ if !errors.Is(err, unix.EXDEV) {
+ t.Fatalf("production cold handover across the instance bind: %v", err)
+ }
+ for name, want := range map[string]string{filepath.Base(f.db): "served bytes", filepath.Base(f.db) + ".gen": "G", filepath.Base(f.db) + "-wal": "WAL", filepath.Base(f.db) + "-shm": "SHM", "files/upload": "tenant upload", "env": "tenant configuration"} {
+ body, err := os.ReadFile(filepath.Join(stateDir, name))
+ if err != nil || string(body) != want {
+ t.Fatalf("failed handover changed %s: %q %v", name, body, err)
+ }
+ }
+ if f.f.read().ETag != before.ETag {
+ t.Fatal("failed handover rewrote the claim")
+ }
+ return
+ }
+ if err != nil {
+ t.Fatal("production cold handover", err)
+ }
+ if _, err := os.Lstat(stateDir); !os.IsNotExist(err) {
+ t.Fatalf("cold handover now preserves state; replace this gate witness with the serving integration: %v", err)
+ }
+ sum := sha256.Sum256([]byte(fmt.Sprintf("%s\x00%s\x00%d\x00%s", next.Host, next.Incarnation, next.Stamp, next.Box)))
+ parked := filepath.Join(guard.DataDir, ".parked", next.Incarnation, next.Host, "handover-"+hex.EncodeToString(sum[:])+"-old", "data", "instances", provision.InstanceID(next.Host))
+ for name, want := range map[string]string{filepath.Base(f.db): "served bytes", filepath.Base(f.db) + ".gen": "G", filepath.Base(f.db) + "-wal": "WAL", filepath.Base(f.db) + "-shm": "SHM", "files/upload": "tenant upload", "env": "tenant configuration"} {
+ body, err := os.ReadFile(filepath.Join(parked, name))
+ if err != nil || string(body) != want {
+ t.Fatalf("cold handover retained %s: %q %v", name, body, err)
+ }
+ }
+ f.p.root.Stop.Coordinator.Guard = guard
+ if _, err := f.park(); err == nil {
+ t.Fatal("database retirement accepted the removed source")
+ }
+ after := f.f.read()
+ if after.ETag != before.ETag {
+ t.Fatal("handover or refused park rewrote claim tags")
+ }
+ if names, err := f.disk.Names(); err != nil || len(names) != 0 {
+ t.Fatalf("removed source gained serving retirement authority: %v %v", names, err)
+ }
+}
diff --git a/internal/host/claimservingpeer_rootrunner_linux_test.go b/internal/host/claimservingpeer_rootrunner_linux_test.go
new file mode 100644
index 000000000..bd21f49d0
--- /dev/null
+++ b/internal/host/claimservingpeer_rootrunner_linux_test.go
@@ -0,0 +1,446 @@
+//go:build linux && rootrunner
+
+package host
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net"
+ "os"
+ "os/exec"
+ "os/user"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/cgroup"
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/instancectl"
+ "github.com/carlosframework/platform/internal/rootstate"
+)
+
+// The control socket coordinates the test only. Every effect request uses the
+// production journal client and its separately authenticated Unix connection.
+type servingPeerCommand struct {
+ Action string
+ Current, Prior servingArtifactIdentity
+ Target hostguard.FenceTarget
+ ID string
+}
+
+type servingPeerReply struct {
+ Peer StopPeer
+ Authorization servingParkAuthorization
+ Launch hostbox.LaunchInventory
+ Error string
+}
+
+// Invoked only by the disposable edge unit, or its same-UID sibling control.
+func TestRootServingParkPeerPayload(t *testing.T) {
+ control, socket := os.Getenv("SERVING_PEER_CONTROL"), os.Getenv("SERVING_PEER_SOCKET")
+ if control == "" || socket == "" || os.Geteuid() == 0 {
+ t.Fatal("invoke through TestRootServingParkAuthenticatedPeer")
+ }
+ call := rootJournalClient(socket)
+ if os.Getenv("SERVING_PEER_SIBLING") == "1" {
+ var q servingPeerCommand
+ if err := json.NewDecoder(os.Stdin).Decode(&q); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := (hostbox.ServingParkClient{Call: call}).Park(t.Context(), q.Current, q.Prior); err == nil {
+ t.Fatal("same-UID sibling was authorized")
+ }
+ return
+ }
+ conn, err := net.DialTimeout("unix", control, 5*time.Second)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer conn.Close()
+ if err := conn.SetDeadline(time.Now().Add(2 * time.Minute)); err != nil {
+ t.Fatal(err)
+ }
+ start, err := claimConstructionProcessStart()
+ if err != nil {
+ t.Fatal(err)
+ }
+ controller, err := (productionStopControllers{}).VerifiedController(t.Context(), os.Getpid())
+ if err != nil {
+ t.Fatal(err)
+ }
+ boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
+ if err != nil {
+ t.Fatal(err)
+ }
+ peer := StopPeer{UID: uint32(os.Geteuid()), PID: os.Getpid(), ProcessStart: start, BootID: strings.TrimSpace(string(boot)), ControllerID: controller}
+ encode, decode := json.NewEncoder(conn), json.NewDecoder(conn)
+ if err := encode.Encode(servingPeerReply{Peer: peer}); err != nil {
+ t.Fatal(err)
+ }
+ client := hostbox.ServingParkClient{Call: call}
+ var manager *cgroup.Manager
+ for {
+ var q servingPeerCommand
+ if err := decode.Decode(&q); errors.Is(err, io.EOF) {
+ return
+ } else if err != nil {
+ t.Fatal(err)
+ }
+ var reply servingPeerReply
+ var callErr error
+ switch q.Action {
+ case "park":
+ reply.Authorization, callErr = client.Park(t.Context(), q.Current, q.Prior)
+ case "complete":
+ callErr = client.Complete(t.Context(), q.ID, q.Current)
+ case "sibling":
+ binary, err := os.Executable()
+ if err != nil {
+ t.Fatal(err)
+ }
+ cmd := exec.CommandContext(t.Context(), binary, "-test.run=^TestRootServingParkPeerPayload$", "-test.v", "-test.timeout=20s")
+ cmd.Env = append(os.Environ(), "SERVING_PEER_SIBLING=1")
+ body, err := json.Marshal(q)
+ if err != nil {
+ t.Fatal(err)
+ }
+ cmd.Stdin = strings.NewReader(string(body))
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ callErr = fmt.Errorf("sibling: %w: %s", err, out)
+ }
+ case "prelaunch":
+ if manager == nil {
+ manager = cgroup.Setup(t.Logf)
+ if manager == nil {
+ t.Fatal("edge unit has no delegated cgroup")
+ }
+ }
+ target := q.Target
+ target.BootID, target.ControllerID = peer.BootID, peer.ControllerID
+ roles := &ClaimRoleAssembly{Manager: manager}
+ parent, group, err := roles.preparePrelaunchGroup(claimContainmentName(target), cgroup.TierDefault)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { _ = parent.Close() })
+ launch := hostbox.LaunchInventory{Target: target, ControllerPID: peer.PID, ProcessStart: peer.ProcessStart, NoState: true, Groups: []hostbox.LaunchGroup{group}}
+ journal, _ := hostbox.RemoteJournals(call)
+ // Each forged observation must be refused before any launch record.
+ for _, field := range []string{"pid", "start", "controller", "boot", "inode", "path"} {
+ bad := launch
+ bad.Groups = append([]hostbox.LaunchGroup(nil), launch.Groups...)
+ switch field {
+ case "pid":
+ bad.ControllerPID++
+ case "start":
+ bad.ProcessStart += "0"
+ case "controller":
+ bad.Target.ControllerID = "forged"
+ case "boot":
+ bad.Target.BootID = "forged"
+ case "inode":
+ bad.Groups[0].Inode++
+ case "path":
+ bad.Groups[0].RelativePath += "-foreign"
+ }
+ if err := journal.Prelaunch(t.Context(), bad); err == nil {
+ t.Fatalf("forged prelaunch %s was authorized", field)
+ }
+ }
+ callErr = journal.Prelaunch(t.Context(), launch)
+ reply.Launch = launch
+ default:
+ t.Fatalf("unknown test action %q", q.Action)
+ }
+ if callErr != nil {
+ reply.Error = callErr.Error()
+ }
+ if err := encode.Encode(reply); err != nil {
+ t.Fatal(err)
+ }
+ }
+}
+
+// This witness needs an unused carlos-edge.service name because production
+// authentication resolves that exact unit. It refuses existing deployments.
+// Claim, installation, predecessor stop/start and roster/census data are fixtures;
+// peer authentication, prelaunch containment, park authority and IO are real.
+func TestRootServingParkAuthenticatedPeer(t *testing.T) {
+ servingRootRunner(t)
+ pid1, err := os.ReadFile("/proc/1/comm")
+ if err != nil || strings.TrimSpace(string(pid1)) != "systemd" {
+ t.Fatal("root runner needs systemd as PID 1")
+ }
+ probe, probeErr := exec.CommandContext(t.Context(), "systemctl", "show", "carlos-edge.service", "-p", "LoadState", "--value").Output()
+ if strings.TrimSpace(string(probe)) != "not-found" {
+ t.Fatalf("refusing existing or unreadable carlos-edge.service: %s %v", probe, probeErr)
+ }
+ // The runner is disposable, but only a user created here is removed here.
+ account, err := user.Lookup("carlos")
+ if _, absent := err.(user.UnknownUserError); absent {
+ if _, groupErr := user.LookupGroup("carlos"); groupErr == nil {
+ t.Fatal("refusing pre-existing carlos group without its user")
+ } else if _, unknown := groupErr.(user.UnknownGroupError); !unknown {
+ t.Fatal(groupErr)
+ }
+ out, createErr := exec.CommandContext(t.Context(), "useradd", "--system", "--no-create-home", "--user-group", "--shell", "/usr/sbin/nologin", "carlos").CombinedOutput()
+ if createErr != nil {
+ t.Fatalf("create runner user: %v %s", createErr, out)
+ }
+ t.Cleanup(func() {
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+ if out, err := exec.CommandContext(ctx, "userdel", "carlos").CombinedOutput(); err != nil {
+ t.Errorf("remove runner user: %v %s", err, out)
+ return
+ }
+ if _, err := user.LookupGroup("carlos"); err == nil {
+ if out, err := exec.CommandContext(ctx, "groupdel", "carlos").CombinedOutput(); err != nil {
+ t.Errorf("remove runner group: %v %s", err, out)
+ }
+ }
+ })
+ account, err = user.Lookup("carlos")
+ }
+ if err != nil || account == nil || account.Uid == "0" {
+ t.Fatalf("runner service user: %v", err)
+ }
+ root, err := os.MkdirTemp("/var/lib", "carlos-rootrunner-peer-")
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() {
+ if err := os.RemoveAll(root); err != nil {
+ t.Error(err)
+ }
+ })
+ if err := os.Chmod(root, 0755); err != nil {
+ t.Fatal(err)
+ }
+ binary, err := os.Executable()
+ if err != nil {
+ t.Fatal(err)
+ }
+ body, err := os.ReadFile(binary)
+ if err != nil {
+ t.Fatal(err)
+ }
+ installed := filepath.Join(root, "runner.test")
+ if err := os.WriteFile(installed, body, 0755); err != nil {
+ t.Fatal(err)
+ }
+ f := newServingRootFixture(t, true)
+ servingRunnerMount(t, f.f.guard.DataDir)
+ // Refresh the state inventory after the bind mount. This remains a fixture
+ // installation; it does not exercise the production installation door.
+ f.install(f.admitted)
+ state, err := rootstate.Open(filepath.Join(root, "parks"), true, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { _ = state.Close() })
+ f.p.ledger.state = state
+ f.p.ledger.fs = newServingParkFS(f.f.guard.DataDir, state)
+ coordinator := f.f.root(nil)
+ coordinator.Evidence = f.f.claims
+ production, err := NewProductionClaimStopHandler(coordinator, f.f.journal, productionStopControllers{}, f.p.root.Roots)
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.p.root.Stop = production.(ClaimStopHTTP)
+ f.p.root.Reconcile.Box, f.p.root.Reconcile.Enrollment = f.actor.Box, f.actor.Enrollment
+ var admitted atomic.Bool
+ admitted.Store(true)
+ f.p.root.RequireNewWork = func(context.Context) error {
+ if !admitted.Load() {
+ return hostbox.ErrUnsupported
+ }
+ return nil
+ }
+ h := servingTransportRoot(t, f)
+ path := filepath.Join(root, "root.sock")
+ listener, err := net.Listen("unix", path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Chmod(path, 0666); err != nil {
+ listener.Close()
+ t.Fatal(err)
+ }
+ server := &instancectl.Server{ClaimStop: h, PeerOK: func(net.Conn) error { return nil }, Logf: t.Logf}
+ ctx, cancel := context.WithCancel(context.Background())
+ done := make(chan error, 1)
+ go func() { done <- server.Serve(ctx, listener) }()
+ t.Cleanup(func() {
+ cancel()
+ if err := <-done; err != nil {
+ t.Error(err)
+ }
+ })
+ controlPath := filepath.Join(root, "control.sock")
+ control, err := net.ListenUnix("unix", &net.UnixAddr{Name: controlPath, Net: "unix"})
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { control.Close() })
+ if err := os.Chmod(controlPath, 0666); err != nil {
+ t.Fatal(err)
+ }
+ raw, err := unitsFS.ReadFile("units/carlos-edge.service")
+ if err != nil {
+ t.Fatal(err)
+ }
+ var lines []string
+ for _, line := range strings.Split(string(raw), "\n") {
+ 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=") {
+ continue
+ }
+ lines = append(lines, line)
+ }
+ // Add a Service section because the embedded unit ends in Install.
+ 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)
+ unitPath := "/run/systemd/system/carlos-edge.service"
+ unit, err := os.OpenFile(unitPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() {
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+ if out, err := exec.CommandContext(ctx, "systemctl", "stop", "carlos-edge.service").CombinedOutput(); err != nil {
+ t.Errorf("stop test edge: %v %s", err, out)
+ }
+ if err := os.Remove(unitPath); err != nil {
+ t.Error(err)
+ }
+ if out, err := exec.CommandContext(ctx, "systemctl", "daemon-reload").CombinedOutput(); err != nil {
+ t.Errorf("reload test edge: %v %s", err, out)
+ }
+ })
+ _, writeErr := io.WriteString(unit, content)
+ if err := errors.Join(writeErr, unit.Close()); err != nil {
+ t.Fatal(err)
+ }
+ servingRunnerSystemctl(t, "daemon-reload")
+ connect := func() (net.Conn, *json.Encoder, *json.Decoder, StopPeer) {
+ t.Helper()
+ servingRunnerSystemctl(t, "start", "carlos-edge.service")
+ if err := control.SetDeadline(time.Now().Add(15 * time.Second)); err != nil {
+ t.Fatal(err)
+ }
+ conn, err := control.Accept()
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { conn.Close() })
+ if err := conn.SetDeadline(time.Now().Add(90 * time.Second)); err != nil {
+ t.Fatal(err)
+ }
+ enc, dec := json.NewEncoder(conn), json.NewDecoder(conn)
+ var hello servingPeerReply
+ if err := dec.Decode(&hello); err != nil {
+ t.Fatal(err)
+ }
+ peer, err := h.Stop.Authenticate(t.Context(), conn)
+ if err != nil || peer != hello.Peer || strconv.FormatUint(uint64(peer.UID), 10) != account.Uid {
+ t.Fatalf("production peer: %+v %+v %v", peer, hello.Peer, err)
+ }
+ return conn, enc, dec, peer
+ }
+ old, _, _, first := connect()
+ servingRunnerSystemctl(t, "stop", "carlos-edge.service")
+ _, encode, decode, next := connect()
+ if first == next {
+ t.Fatal("edge restart kept the process tuple")
+ }
+ if _, err := f.p.parkServingDatabase(t.Context(), old, f.current, f.prior); err == nil {
+ t.Fatal("old controller connection authorized retirement")
+ }
+ command := func(q servingPeerCommand, refused bool) servingPeerReply {
+ t.Helper()
+ if err := encode.Encode(q); err != nil {
+ t.Fatal(err)
+ }
+ var reply servingPeerReply
+ if err := decode.Decode(&reply); err != nil {
+ t.Fatal(err)
+ }
+ if (reply.Error != "") != refused {
+ t.Fatalf("%s: expected refusal=%v: %s", q.Action, refused, reply.Error)
+ }
+ return reply
+ }
+ q := servingPeerCommand{Action: "park", Current: f.current, Prior: f.prior}
+ if _, err := (hostbox.ServingParkClient{Call: rootJournalClient(path)}).Park(t.Context(), f.current, f.prior); err == nil {
+ t.Fatal("root caller authorized retirement")
+ }
+ q.Action = "sibling"
+ command(q, false)
+ q.Action = "park"
+ admitted.Store(false)
+ command(q, true)
+ admitted.Store(true)
+ bad := q
+ bad.Current.Generation = "foreign-generation"
+ command(bad, true)
+ before, err := os.ReadFile(f.db)
+ if err != nil || string(before) != "served bytes" {
+ t.Fatal("refusal changed source", err)
+ }
+ names, err := state.Names()
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, name := range names {
+ if name != "namespace.lock" {
+ t.Fatalf("refusal wrote park authority: %s", name)
+ }
+ }
+ target := recoveryTuple(f.f.read(), claimStopLocal{Actor: f.actor})
+ pre := command(servingPeerCommand{Action: "prelaunch", Target: target}, false)
+ actual, err := f.f.journal.Read(t.Context(), pre.Launch.Target)
+ if err != nil || actual.ControllerPID != next.PID || actual.ProcessStart != next.ProcessStart || len(actual.Groups) != 1 || actual.Groups[0] != pre.Launch.Groups[0] {
+ t.Fatalf("prelaunch not retained: %+v %v", actual, err)
+ }
+ parked := command(q, false).Authorization
+ if parked.ID == "" {
+ t.Fatal("empty park identity")
+ }
+ command(servingPeerCommand{Action: "complete", ID: parked.ID, Current: f.current}, true)
+ f.tags(f.current.Stamp)
+ command(servingPeerCommand{Action: "complete", ID: parked.ID, Current: f.current}, false)
+ r, err := f.p.ledger.read(parked.ID)
+ if err != nil || !r.Complete || r.Disposition != servingParkRetain {
+ t.Fatalf("completed park: %+v %v", r, err)
+ }
+ if err := f.p.ledger.fs.CheckParked(&r); err != nil {
+ t.Fatal(err)
+ }
+ for suffix, want := range map[string]string{"": "served bytes", ".gen": "G", "-wal": "WAL", "-shm": "SHM"} {
+ if _, err := os.Lstat(f.db + suffix); !errors.Is(err, os.ErrNotExist) {
+ t.Fatal("park left source", suffix, err)
+ }
+ body, err := os.ReadFile(filepath.Join(f.f.guard.DataDir, "instances", ".serving-parked", parked.ID, filepath.Base(f.db)+suffix))
+ if err != nil || string(body) != want {
+ t.Fatal("park changed bytes", suffix, err)
+ }
+ }
+ if replay := command(q, false).Authorization; replay != parked {
+ t.Fatal("authenticated replay changed retirement")
+ }
+}
+
+// Exercise the actual earlier cold-handover parker on the nested mount. This
+// is a refusal witness, not successful serving installation or Restore.
+func TestRootServingParkColdHandoverBoundary(t *testing.T) {
+ servingRootRunner(t)
+ runServingColdHandoverBoundary(t, servingRunnerMount)
+}