carlos / platform Public

Plan 14 integrated: review round 4 fixes, partial (not for merge)

Kernel verifier classifies legitimate platform services and processes; active sibling no longer blocks retirement scan; retired-host reinstall refuses cleanly instead of ErrLost; no-census adoption selects participation first; plus stale closed containment, data-root ancestry, disappearing-task census and unlaunched-installation inventory fixes. E2E extended with the real activation gate and an active sibling.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 595c0db29543f53c3229d9f73882c50f4d67b919 parent f33c87f
39 files changed, +1177 −146
  • .claude/hooks +0 −0
  • .claude/launch.json +0 −0
  • .claude/loop.md +0 −0
  • .claude/output-styles +0 −0
  • .claude/routines +0 −0
  • .claude/settings.json +0 −0
  • .claude/workflows +0 −0
  • .mcp.json +0 −0
  • internal/host/agent.go +5 −1
  • internal/host/claimstop_run.go +15 −8
  • internal/host/inventory_reboot_root_linux_test.go +15 −0
  • internal/host/plan14_generation_containment_linux_test.go +8 −2
  • internal/host/plan14_generation_creation_test.go +93 −0
  • internal/host/plan14_generation_kernel_linux_test.go +72 −0
  • internal/host/plan14_lifecycle_crash_root_linux_test.go +26 −30
  • internal/host/plan14_lifecycle_diagnostic_root_linux_test.go +41 −0
  • internal/host/plan14_lifecycle_reinstall_root_linux_test.go +121 −0
  • internal/host/plan14_lifecycle_retire_root_linux_test.go +2 −0
  • internal/host/plan14_lifecycle_root_linux_test.go +120 −40
  • internal/host/plan14_r10_root_linux_test.go +46 −7
  • internal/host/rootadoption_options.go +11 −0
  • internal/host/rootadoption_options_test.go +89 −0
  • internal/host/rootexecution_authority.go +11 −0
  • internal/host/rootexecution_transport.go +10 −0
  • internal/host/rootexecution_transport_test.go +10 −0
  • internal/host/rootgeneration_census_linux.go +16 −1
  • internal/host/rootgeneration_containment_linux.go +1 −1
  • internal/host/rootgeneration_inventory.go +2 −1
  • internal/host/rootgeneration_kernel_linux.go +33 −16
  • internal/host/rootgeneration_payload_linux.go +8 −1
  • internal/host/rootgeneration_requirements.go +38 −1
  • internal/host/rootgeneration_retired_linux.go +102 −9
  • internal/host/rootgeneration_retired_linux_test.go +7 −2
  • internal/host/rootgeneration_services_linux.go +172 −0
  • internal/hostbox/inventory_identity_linux.go +13 −3
  • internal/hostguard/retired_inventory_test.go +6 −0
  • internal/hostguard/rootretired_host.go +33 −0
  • internal/provision/plan14_p3_census_test.go +4 −3
  • internal/provision/testdata/plan14-root-reads-p3.tsv +47 −20

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	296.161s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	2.544s
ok  	github.com/carlosframework/platform/cmd/moveprobe	93.201s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.702s
ok  	github.com/carlosframework/platform/internal/activator	250.177s
ok  	github.com/carlosframework/platform/internal/agentsock	0.017s
ok  	github.com/carlosframework/platform/internal/alerting	0.003s
ok  	github.com/carlosframework/platform/internal/analytics	0.025s
ok  	github.com/carlosframework/platform/internal/appconfig	2.461s
ok  	github.com/carlosframework/platform/internal/apperr	0.071s
ok  	github.com/carlosframework/platform/internal/applog	0.132s
ok  	github.com/carlosframework/platform/internal/awsinv	0.005s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.004s
ok  	github.com/carlosframework/platform/internal/boxid	0.876s
?   	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.007s
ok  	github.com/carlosframework/platform/internal/caps	0.012s
ok  	github.com/carlosframework/platform/internal/certwarm	0.002s
ok  	github.com/carlosframework/platform/internal/cgroup	0.693s
ok  	github.com/carlosframework/platform/internal/changelog	0.306s
ok  	github.com/carlosframework/platform/internal/clamd	0.054s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	401.874s
?   	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	6.407s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.008s
ok  	github.com/carlosframework/platform/internal/dbverify	0.004s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.002s
ok  	github.com/carlosframework/platform/internal/docsite	0.004s
ok  	github.com/carlosframework/platform/internal/domains	2.309s
ok  	github.com/carlosframework/platform/internal/doorkey	0.006s
ok  	github.com/carlosframework/platform/internal/economics	0.003s
ok  	github.com/carlosframework/platform/internal/edge	44.788s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.007s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.130s
ok  	github.com/carlosframework/platform/internal/gate	1.655s
ok  	github.com/carlosframework/platform/internal/genselect	0.014s
ok  	github.com/carlosframework/platform/internal/geo	0.016s
ok  	github.com/carlosframework/platform/internal/geoip	0.029s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.155s
ok  	github.com/carlosframework/platform/internal/host	178.938s
ok  	github.com/carlosframework/platform/internal/hostactors	0.757s
ok  	github.com/carlosframework/platform/internal/hostbox	2.723s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.580s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.006s
ok  	github.com/carlosframework/platform/internal/hostexec	4.758s
ok  	github.com/carlosframework/platform/internal/hostfence	0.040s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.402s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.012s
ok  	github.com/carlosframework/platform/internal/hostguard	0.989s
ok  	github.com/carlosframework/platform/internal/hostops	1.572s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.002s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.002s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.476s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.148s
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	5.213s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.732s
ok  	github.com/carlosframework/platform/internal/instancefs	0.018s
ok  	github.com/carlosframework/platform/internal/instancekey	0.002s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.021s
ok  	github.com/carlosframework/platform/internal/layout	1.530s
ok  	github.com/carlosframework/platform/internal/lbhealth	0.015s
ok  	github.com/carlosframework/platform/internal/lease	9.804s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.006s
ok  	github.com/carlosframework/platform/internal/ledger	0.015s
ok  	github.com/carlosframework/platform/internal/mail	0.178s
ok  	github.com/carlosframework/platform/internal/maintenance	0.078s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.490s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.077s
ok  	github.com/carlosframework/platform/internal/metrics	0.006s
ok  	github.com/carlosframework/platform/internal/moves	21.049s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.803s
ok  	github.com/carlosframework/platform/internal/objstore	0.183s
ok  	github.com/carlosframework/platform/internal/ops	0.180s
ok  	github.com/carlosframework/platform/internal/park	0.216s
ok  	github.com/carlosframework/platform/internal/privatefile	0.004s
ok  	github.com/carlosframework/platform/internal/provision	32.708s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.007s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	5.469s
ok  	github.com/carlosframework/platform/internal/relay	7.219s
ok  	github.com/carlosframework/platform/internal/release	21.625s
ok  	github.com/carlosframework/platform/internal/rollout	3.658s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.003s
ok  	github.com/carlosframework/platform/internal/rootaws	0.008s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.057s
ok  	github.com/carlosframework/platform/internal/rootstate	0.026s
ok  	github.com/carlosframework/platform/internal/runs	0.574s
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.482s
ok  	github.com/carlosframework/platform/internal/seeds	0.010s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	8.986s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.104s
ok  	github.com/carlosframework/platform/internal/status	0.045s
ok  	github.com/carlosframework/platform/internal/steering	0.005s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.306s
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.001s
ok  	github.com/carlosframework/platform/internal/vet	1.567s
=== 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
linux/arm64 carlos binary: 48693410 bytes (ceiling 52000000)
--- 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.021s
=== RUN   TestT4StorageProbeMinio
2026/10/17 04:13:19 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DF35DCF5BF7696, 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.020s
minio contract: PASS
diff --git a/.claude/hooks b/.claude/hooks
deleted file mode 100644
index e69de29bb..000000000
diff --git a/.claude/launch.json b/.claude/launch.json
deleted file mode 100644
index e69de29bb..000000000
diff --git a/.claude/loop.md b/.claude/loop.md
deleted file mode 100644
index e69de29bb..000000000
diff --git a/.claude/output-styles b/.claude/output-styles
deleted file mode 100644
index e69de29bb..000000000
diff --git a/.claude/routines b/.claude/routines
deleted file mode 100644
index e69de29bb..000000000
diff --git a/.claude/settings.json b/.claude/settings.json
deleted file mode 100644
index e69de29bb..000000000
diff --git a/.claude/workflows b/.claude/workflows
deleted file mode 100644
index e69de29bb..000000000
diff --git a/.mcp.json b/.mcp.json
deleted file mode 100644
index e69de29bb..000000000
diff --git a/internal/host/agent.go b/internal/host/agent.go
index b1c7db1b2..25296c5e5 100644
--- a/internal/host/agent.go
+++ b/internal/host/agent.go
@@ -4637,7 +4637,7 @@ func runAdoptUnderRootRelease(args []string, buildClaims ClaimFactory, ensureUni
linkRootHistory(history, restartChecks, release.OptDir(), os.Getenv)
}
recovery := productionNamedUnitRecovery(store, pub, release.OptDir(), claims.Writes)
- passOpts := []release.PassOption{release.WithRootGenerationPins(generationPins), release.WithRootHistory(history), recovery.passOption(reg, restartChecks, os.Getenv), release.WithCapabilities(productionClaimCapsOps(caps.Ops{}, restartChecks.allowLegacy)), release.BakeDir(dataDir()), release.WithRestartAttemptSettled(restartChecks.finish),
+ passOpts := []release.PassOption{release.WithRootGenerationPins(generationPins), release.WithCapabilities(productionClaimCapsOps(caps.Ops{}, restartChecks.allowLegacy)), release.BakeDir(dataDir()), release.WithRestartAttemptSettled(restartChecks.finish),
release.WithRunningVersion(restartChecks.runningVersion),
release.WithSelectedRestarts(
rootSelectedRestart(reg, systemdUnits{}, release.OptDir(), os.Getenv, settled, restartChecks, false),
@@ -4652,6 +4652,10 @@ func runAdoptUnderRootRelease(args []string, buildClaims ClaimFactory, ensureUni
// Settings spec §4.3: root alone resolves install requests and
// writes /opt/carlos/consumed/<host>, before it stages.
updatesPassOption(dataDir(), true, updateProviders...)}
+ passOpts, err = rootAdoptionOptions(passOpts, restartChecks, history, func() release.PassOption { return recovery.passOption(reg, restartChecks, os.Getenv) })
+ if err != nil {
+ return err
+ }
if !*stageOnly {
passOpts = append(passOpts, release.NagExpiredCanaries(os.Stdout))
}
diff --git a/internal/host/claimstop_run.go b/internal/host/claimstop_run.go
index 304add7e6..ed8b38c33 100644
--- a/internal/host/claimstop_run.go
+++ b/internal/host/claimstop_run.go
@@ -77,26 +77,33 @@ func RunInstanceControl(args []string) {
if err != nil {
log.Fatal(err)
}
+ if err := activateClaimRoot(ctx, startup, handler); err != nil {
+ log.Fatal(err)
+ }
+ finishStartup()
+ fenceDone := make(chan struct{})
+ go func() { defer close(fenceDone); runHostFencePass(ctx) }()
+ defer func() { cancel(); <-fenceDone }()
+ instancectl.RunWithClaim(args, handler, heldReleaseRouteHandler(handler, claimEntryEnv(ctx)), rootInstallationHostApp(entry.Assembly.Guard, entry.Assembly.Claims))
+}
+
+func activateClaimRoot(ctx, startup context.Context, handler http.Handler) error {
recovery, ok := handler.(interface{ RecoverPending(context.Context) error })
if !ok {
- log.Fatal(ErrClaimStopUnavailable)
+ return ErrClaimStopUnavailable
}
if err := recovery.RecoverPending(startup); err != nil {
log.Print(err)
}
if err := claimActivationReady(startup, rootparticipation.OpenForUpdate); err != nil {
- log.Fatal(err)
+ return err
}
- finishStartup()
lifecycle, ok := handler.(interface{ Start(context.Context) })
if !ok {
- log.Fatal(ErrClaimStopUnavailable)
+ return ErrClaimStopUnavailable
}
lifecycle.Start(ctx)
- fenceDone := make(chan struct{})
- go func() { defer close(fenceDone); runHostFencePass(ctx) }()
- defer func() { cancel(); <-fenceDone }()
- instancectl.RunWithClaim(args, handler, heldReleaseRouteHandler(handler, claimEntryEnv(ctx)), rootInstallationHostApp(entry.Assembly.Guard, entry.Assembly.Claims))
+ return nil
}
// heldReleaseRouteHandler is the handler instancectl registers for the moves
diff --git a/internal/host/inventory_reboot_root_linux_test.go b/internal/host/inventory_reboot_root_linux_test.go
index 10453c9ab..deb168f49 100644
--- a/internal/host/inventory_reboot_root_linux_test.go
+++ b/internal/host/inventory_reboot_root_linux_test.go
@@ -208,6 +208,21 @@ func TestRootRebootInventoryIdentityPhase2(t *testing.T) {
if err := check(); err == nil {
t.Error("accepted untrusted root ancestry")
}
+ if err := hostbox.VerifyInventoryIdentityForOwner(inventoryRebootRoot, e, 65534); err != nil {
+ t.Fatal("explicit managed-root owner refused after reboot", err)
+ }
+ if err := hostbox.VerifyInventoryIdentityForOwner(inventoryRebootRoot, e, 65533); err == nil {
+ t.Fatal("wrong managed-root owner accepted")
+ }
+ if err := os.Chmod(inventoryRebootRoot, 0777); err != nil {
+ t.Fatal(err)
+ }
+ if err := hostbox.VerifyInventoryIdentityForOwner(inventoryRebootRoot, e, 65534); err == nil {
+ t.Fatal("writable managed-root accepted")
+ }
+ if err := os.Chmod(inventoryRebootRoot, 0700); err != nil {
+ t.Fatal(err)
+ }
if err := os.Chown(inventoryRebootRoot, 0, 0); err != nil {
t.Fatal(err)
}
diff --git a/internal/host/plan14_generation_containment_linux_test.go b/internal/host/plan14_generation_containment_linux_test.go
index ded0f0076..b506834df 100644
--- a/internal/host/plan14_generation_containment_linux_test.go
+++ b/internal/host/plan14_generation_containment_linux_test.go
@@ -16,7 +16,7 @@ import (
const containedStatus = "Uid:\t65534\t65534\t65534\t65534\nCapInh:\t0000000000000000\nCapPrm:\t0000000000000000\nCapEff:\t0000000000000000\nCapBnd:\t0000000000000000\nCapAmb:\t0000000000000000\nNoNewPrivs:\t1\nSeccomp:\t2\n"
func TestPlan14GenerationContainedTask(t *testing.T) {
- for _, attack := range []string{"none", "old record", "retired", "group identity", "main lifetime", "launcher", "namespace", "capability", "privilege gain", "missing filter", "unmasked", "other bind", "wrong app", "task group", "foreign proc", "foreign proc descriptor bind", "unrecognized mount root"} {
+ for _, attack := range []string{"none", "closed history", "old record", "retired", "group identity", "main lifetime", "launcher", "namespace", "capability", "privilege gain", "missing filter", "unmasked", "other bind", "wrong app", "task group", "foreign proc", "foreign proc descriptor bind", "unrecognized mount root"} {
t.Run(attack, func(t *testing.T) {
f := generationProc(t)
group := instancectl.ClaimUnitControlGroup(f.reservation.Binding.Binding.Host)
@@ -99,8 +99,14 @@ func TestPlan14GenerationContainedTask(t *testing.T) {
generationWrite(t, f.task+"/cgroup", "0::/other\n")
}
k := generationKernel{proc: f.proc, cgroups: cgroups, contained: []release.GenerationReservation{r}, owner: uint32(os.Geteuid())}
+ if attack == "closed history" {
+ old := r
+ old.Closed = true
+ old.Process.Start = "dead"
+ k.contained = append([]release.GenerationReservation{old}, k.contained...)
+ }
ok, err := k.containedTask(t.Context(), f.task, status)
- if attack == "none" {
+ if attack == "none" || attack == "closed history" {
if !ok || err != nil {
t.Fatalf("contained tenant refused: %v %v", ok, err)
}
diff --git a/internal/host/plan14_generation_creation_test.go b/internal/host/plan14_generation_creation_test.go
index b19138928..cf31b6a38 100644
--- a/internal/host/plan14_generation_creation_test.go
+++ b/internal/host/plan14_generation_creation_test.go
@@ -5,8 +5,10 @@ import (
"os"
"testing"
+ "github.com/carlosframework/platform/internal/hostbox"
"github.com/carlosframework/platform/internal/hostclaim"
"github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/provision"
"github.com/carlosframework/platform/internal/release"
"github.com/carlosframework/platform/internal/rootparticipation"
"github.com/carlosframework/platform/internal/rootstate"
@@ -184,3 +186,94 @@ func TestPlan14GenerationCreationFromRegistration(t *testing.T) {
t.Fatalf("registered creation incomplete: %+v %v", c, err)
}
}
+
+func TestPlan14GenerationUnwokenSibling(t *testing.T) {
+ for _, attack := range []string{"none", "no-proof", "consumed", "state-exists", "changed-during-scan"} {
+ t.Run(attack, func(t *testing.T) {
+ f, _, _ := generationLaunchFixture(t)
+ views, err := f.root.Guard.Root.GenerationInstallations(t.Context())
+ if err != nil {
+ t.Fatal(err)
+ }
+ views[0].Installation.Empty = false
+ views[0].Installation.Artifacts.StateDirectory = true
+ in := views[0].Installation
+ t.Setenv("CARLOS_DATA_DIR", t.TempDir())
+ snapshot := generationInventory{Census: rootparticipation.Census{Schema: 1, Box: in.Box, Enrollment: in.Enrollment, Migration: in.Migration, Revision: 1, LaunchRevision: 1, Complete: true}, Installations: views, Payloads: generationPayloadFixture(views)}
+ history := &release.RootHistory{Store: &rollbackDisk{dir: t.TempDir()}}
+ binding := release.HistoryBinding{Host: in.Host, Account: in.Account, App: in.App}
+ if err := history.RecordCreation(binding, rootHistoryInstallation(in)); err != nil {
+ t.Fatal(err)
+ }
+ if attack == "consumed" {
+ if err := history.ConsumeCreation(binding); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if attack == "state-exists" {
+ if err := os.MkdirAll(provision.InstanceDir(dataDir(), in.Host), 0700); err != nil {
+ t.Fatal(err)
+ }
+ }
+ fresh := func(in hostguard.RootInstallation) error {
+ ok, err := history.FreshInstallation(binding, rootHistoryInstallation(in))
+ if err != nil {
+ return err
+ }
+ if !ok {
+ return os.ErrPermission
+ }
+ return nil
+ }
+ if attack == "no-proof" {
+ fresh = nil
+ }
+ v := generationReconciler{read: func(ctx context.Context) (generationInventory, error) {
+ return generationLocalObligations(ctx, snapshot, fresh)
+ }, kernel: func(context.Context) (string, string, error) {
+ if attack == "changed-during-scan" {
+ if err := history.ConsumeCreation(binding); err != nil {
+ t.Fatal(err)
+ }
+ }
+ return "boot", generationInventoryDigest("kernel"), nil
+ }}
+ _, err = v.reconcile(t.Context(), nil)
+ if (err == nil) != (attack == "none") {
+ t.Fatalf("unwoken installation: %v", err)
+ }
+ })
+ }
+}
+
+func TestPlan14GenerationFreshPrelaunchHandover(t *testing.T) {
+ if ok, err := generationUnlaunched(hostguard.FenceView{Installation: hostguard.RootInstallation{Empty: true}}, generationInventory{}, func(hostguard.RootInstallation) error {
+ t.Fatal("empty installation requested new proof")
+ return os.ErrPermission
+ }); err != nil || ok {
+ t.Fatal(ok, err)
+ }
+
+ view := hostguard.FenceView{Host: "h.example", Installation: hostguard.RootInstallation{Host: "h.example", Incarnation: "inc", Stamp: 2}, Retired: []hostguard.Captured{{Incarnation: "inc", Stamp: 1}}}
+ fresh := func(in hostguard.RootInstallation) error {
+ if in.Host == "h.example" && in.Incarnation == "inc" && in.Stamp == 1 {
+ return nil
+ }
+ return os.ErrPermission
+ }
+ if ok, err := generationUnlaunched(view, generationInventory{}, fresh); err != nil || !ok {
+ t.Fatal(ok, err)
+ }
+ view.Retired[0].Incarnation = "other"
+ if ok, err := generationUnlaunched(view, generationInventory{}, fresh); err == nil || ok {
+ t.Fatal(ok, err)
+ }
+ view.Retired[0].Incarnation = "inc"
+ inv := generationInventory{Launches: []hostbox.LaunchInventory{{Target: hostguard.FenceTarget{Host: view.Host}}}}
+ if ok, err := generationUnlaunched(view, inv, fresh); err != nil || ok {
+ t.Fatal("launched installation treated as fresh", ok, err)
+ }
+ if ok, err := generationUnlaunched(view, generationInventory{}, func(hostguard.RootInstallation) error { return os.ErrInvalid }); err == nil || ok {
+ t.Fatal("unreadable creation accepted", ok, err)
+ }
+}
diff --git a/internal/host/plan14_generation_kernel_linux_test.go b/internal/host/plan14_generation_kernel_linux_test.go
index 650ee0c29..687efe2d0 100644
--- a/internal/host/plan14_generation_kernel_linux_test.go
+++ b/internal/host/plan14_generation_kernel_linux_test.go
@@ -263,3 +263,75 @@ func generationRetainedFiles(b release.GenerationBinding, open func(string) (*os
_, err := generationCurrentFiles(b, open)
return err
}
+
+func TestPlan14GenerationServiceReferences(t *testing.T) {
+ for _, mode := range []string{"service", "unknown", "fd", "mapped", "target mount"} {
+ t.Run(mode, func(t *testing.T) {
+ f := generationProc(t)
+ b := f.reservation.Binding
+ generationWrite(t, f.task+"/status", "Uid:\t1001\t1001\t1001\t1001\nKthread:\t0\n")
+ generationRelink(t, "private-service", f.task+"/ns/mnt")
+ switch mode {
+ case "fd":
+ generationLink(t, b.Path, f.task+"/fd/3")
+ case "mapped":
+ generationWrite(t, f.task+"/maps", fmt.Sprintf("1000-2000 r-xp 0000 %x:%x %d %s\n", generationMajor(b.Executable.Device), generationMinor(b.Executable.Device), b.Executable.Inode, b.Path))
+ case "target mount":
+ generationWrite(t, f.task+"/mountinfo", fmt.Sprintf("1 0 %d:%d / / rw - ext4 disk rw\n2 1 %d:%d %s /bound ro - ext4 disk ro\n", generationMajor(b.Executable.Device), generationMinor(b.Executable.Device), generationMajor(b.Executable.Device), generationMinor(b.Executable.Device), b.Tree))
+ }
+ k := generationKernel{proc: f.proc, service: func(string, []byte) (bool, error) { return mode != "unknown", nil }}
+ _, err := k.absent(t.Context(), f.reservation)
+ if (err == nil) != (mode == "service") {
+ t.Fatalf("service reference boundary: %v", err)
+ }
+ })
+ }
+}
+
+func TestPlan14GenerationCensusRetriesWholeObservation(t *testing.T) {
+ for _, refusal := range []error{os.ErrNotExist, os.ErrPermission} {
+ calls := 0
+ boot, digest, err := generationCensusRetry(t.Context(), func() (string, string, error) {
+ calls++
+ if calls == 1 {
+ return "", "", fmt.Errorf("task exited: %w", refusal)
+ }
+ return "boot", "digest", nil
+ })
+ if refusal == os.ErrNotExist {
+ if err != nil || calls != 2 || boot != "boot" || digest != "digest" {
+ t.Fatal(calls, boot, digest, err)
+ }
+ } else if err == nil || calls != 1 {
+ t.Fatal("retried unexplained task", calls, err)
+ }
+ }
+ calls := 0
+ _, _, err := generationCensusRetry(t.Context(), func() (string, string, error) { calls++; return "", "", os.ErrNotExist })
+ if err == nil || calls != 3 {
+ t.Fatal("unbounded or incomplete scan accepted", calls, err)
+ }
+}
+
+func TestPlan14GenerationServiceLookupKeepsStrictRootScan(t *testing.T) {
+ for _, mode := range []string{"root", "nonroot", "private-namespace", "fd", "ancestor-mount"} {
+ t.Run(mode, func(t *testing.T) {
+ f := generationProc(t)
+ switch mode {
+ case "nonroot":
+ generationWrite(t, f.task+"/status", "Uid:\t1001\t1001\t1001\t1001\nKthread:\t0\n")
+ case "private-namespace":
+ generationRelink(t, "private-service", f.task+"/ns/mnt")
+ case "fd":
+ generationLink(t, f.reservation.Binding.Path, f.task+"/fd/3")
+ case "ancestor-mount":
+ generationWrite(t, f.task+"/mountinfo", "2 1 0:1 "+filepath.Dir(f.reservation.Binding.Tree)+" /bound ro - tmpfs tmpfs ro\n")
+ }
+ k := generationKernel{proc: f.proc, service: func(string, []byte) (bool, error) { return false, os.ErrPermission }}
+ _, err := k.absent(t.Context(), f.reservation)
+ if (err == nil) != (mode == "root") {
+ t.Fatalf("strict root scan: %v", err)
+ }
+ })
+ }
+}
diff --git a/internal/host/plan14_lifecycle_crash_root_linux_test.go b/internal/host/plan14_lifecycle_crash_root_linux_test.go
index 9190931e9..0afc38565 100644
--- a/internal/host/plan14_lifecycle_crash_root_linux_test.go
+++ b/internal/host/plan14_lifecycle_crash_root_linux_test.go
@@ -7,9 +7,8 @@ import (
"errors"
"os"
"os/exec"
- "syscall"
+ "strings"
"testing"
- "time"
"github.com/carlosframework/platform/internal/controlsock"
"github.com/carlosframework/platform/internal/hostguard"
@@ -80,39 +79,36 @@ func lifecycleControlFaults(t *testing.T) {
func lifecycleStartControl(t *testing.T, endpoint string) func() {
t.Helper()
- log, err := os.OpenFile(lifecycleHome+"/controller.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
- lifecycleCheck(t, "controller log", err)
- var cmd *exec.Cmd
- start := func() {
- if cmd != nil {
- cmd.Process.Kill()
- cmd.Wait()
+ raw, err := unitsFS.ReadFile("units/carlos-instance-control.service")
+ lifecycleCheck(t, "controller template", err)
+ var lines []string
+ for _, line := range strings.Split(string(raw), "\n") {
+ key, _, _ := strings.Cut(line, "=")
+ if key == "ExecStart" || key == "Requires" || key == "After" {
+ continue
}
+ lines = append(lines, line)
+ }
+ lines = append(lines, "[Service]", "ExecStart=/opt/carlos/carlos -test.run=^TestPlan14LifecycleControlProcess$ -test.v -test.timeout=12m", "Environment=PLAN14_LIFECYCLE_CONTROL=1", "Environment=PLAN14_LIFECYCLE_CRASH="+os.Getenv("PLAN14_LIFECYCLE_CRASH"), "Environment=CARLOS_S3_ENDPOINT="+endpoint, "ReadWritePaths=/data /opt/carlos /var/lib/carlos-claims /var/lib/carlos-release /sys/fs/cgroup "+lifecycleHome, "RestrictAddressFamilies=", "RestrictAddressFamilies=AF_UNIX AF_INET", "MemoryMax=512M", "StandardOutput=append:"+lifecycleHome+"/controller.log", "StandardError=append:"+lifecycleHome+"/controller.log")
+ path := "/run/systemd/system/carlos-instance-control.service"
+ f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
+ lifecycleCheck(t, "exclusive controller unit", err)
+ _, err = f.WriteString(strings.Join(lines, "\n"))
+ f.Close()
+ lifecycleCheck(t, "controller unit", err)
+ out, err := exec.Command("systemctl", "daemon-reload").CombinedOutput()
+ lifecycleCheck(t, string(out), err)
+ start := func() {
+ exec.Command("systemctl", "stop", "carlos-instance-control.service").Run()
os.Remove("/run/carlos-control/lifecycle-ready")
os.Remove(controlsock.Path)
- cmd = exec.Command("/opt/carlos/carlos", "-test.run=^TestPlan14LifecycleControlProcess$", "-test.v", "-test.timeout=8m")
- cmd.Env = append(os.Environ(), "PLAN14_LIFECYCLE_CONTROL=1", "CARLOS_S3_ENDPOINT="+endpoint)
- cmd.Stdout, cmd.Stderr = log, log
- lifecycleCheck(t, "start controller", cmd.Start())
- deadline := time.Now().Add(time.Minute)
- for time.Now().Before(deadline) {
- if _, err := os.Stat("/run/carlos-control/lifecycle-ready"); err == nil {
- return
- }
- time.Sleep(20 * time.Millisecond)
- }
- t.Fatal("controller startup timeout")
+ out, err := exec.Command("systemctl", "start", "carlos-instance-control.service").CombinedOutput()
+ lifecycleCheck(t, string(out), err)
}
t.Cleanup(func() {
- if cmd != nil {
- if t.Failed() {
- cmd.Process.Signal(syscall.SIGQUIT)
- time.Sleep(100 * time.Millisecond)
- }
- cmd.Process.Kill()
- cmd.Wait()
- }
- log.Close()
+ exec.Command("systemctl", "stop", "carlos-instance-control.service").Run()
+ os.Remove(path)
+ exec.Command("systemctl", "daemon-reload").Run()
body, _ := os.ReadFile(lifecycleHome + "/controller.log")
t.Logf("controller log: %s", body)
})
diff --git a/internal/host/plan14_lifecycle_diagnostic_root_linux_test.go b/internal/host/plan14_lifecycle_diagnostic_root_linux_test.go
new file mode 100644
index 000000000..a05ac00aa
--- /dev/null
+++ b/internal/host/plan14_lifecycle_diagnostic_root_linux_test.go
@@ -0,0 +1,41 @@
+//go:build linux && rootrunner
+
+package host
+
+import (
+ "context"
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/rootparticipation"
+ "github.com/carlosframework/platform/internal/rootstate"
+ "testing"
+)
+
+func lifecycleActivationDiagnostic(t *testing.T, ctx context.Context) {
+ err := participationRelease.With(ctx, func(ctx context.Context) error {
+ store, err := rootstate.Open(release.RootGenerationsDir, false, false)
+ if err != nil {
+ return err
+ }
+ defer store.Close()
+ ledger := &release.RootGenerations{Store: store}
+ rs, err := ledger.Reservations(ctx)
+ t.Log("diagnostic reservations", len(rs), err)
+ if err != nil {
+ return err
+ }
+ inv, err := generationInventoryAt(ctx, rootparticipation.Namespace, "/var/lib/carlos-claims/authority/fences", "/var/lib/carlos-claims/journals.db")
+ t.Log("diagnostic inventory", len(inv.Installations), len(inv.Launches), len(inv.Payloads), err)
+ inv.Handovers, err = productionGenerationHandovers(ctx)
+ t.Log("diagnostic handovers", len(inv.Handovers), err)
+ inv.Retirements, err = productionGenerationRetirements(ctx)
+ t.Log("diagnostic retirements", len(inv.Retirements), err)
+ inv, err = generationLocalObligations(ctx, inv, productionGenerationCreation)
+ t.Log("diagnostic obligations", err)
+ _, err = productionGenerationRegistryInventory(ctx, inv)
+ t.Log("diagnostic registry", err)
+ v := generationReconciler{read: productionLocalGenerationInventory, kernel: productionGenerationKernelCensus}
+ _, err = v.reconcile(ctx, rs)
+ return err
+ })
+ t.Log("diagnostic certificate", err)
+}
diff --git a/internal/host/plan14_lifecycle_reinstall_root_linux_test.go b/internal/host/plan14_lifecycle_reinstall_root_linux_test.go
new file mode 100644
index 000000000..1790e4075
--- /dev/null
+++ b/internal/host/plan14_lifecycle_reinstall_root_linux_test.go
@@ -0,0 +1,121 @@
+//go:build linux && rootrunner
+
+package host
+
+import (
+ "context"
+ "crypto/ed25519"
+ "crypto/sha256"
+ "errors"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/controlsock"
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ execlease "github.com/carlosframework/platform/internal/hostexec"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+func lifecycleActiveSibling(t *testing.T) {
+ t.Helper()
+ s, err := openClaimRootAuthority()
+ lifecycleCheck(t, "sibling authority", err)
+ defer s.Close()
+ views, err := hostguard.NewRootAuthority(s).GenerationInstallations(t.Context())
+ lifecycleCheck(t, "sibling installations", err)
+ for _, view := range views {
+ if view.Host != lifecycleSibling {
+ continue
+ }
+ if view.Installation.Stamp == 0 || view.Barrier != nil {
+ t.Fatal("sibling is not active")
+ }
+ for _, old := range view.Retired {
+ if old.Incarnation == view.Installation.Incarnation && old.Stamp == view.Installation.Stamp {
+ t.Fatal("sibling retired")
+ }
+ }
+ return
+ }
+ t.Fatal("missing production sibling installation")
+}
+
+func lifecycleRetainedBytes(t *testing.T) map[string][32]byte {
+ t.Helper()
+ out := map[string][32]byte{}
+ for _, root := range []string{"/var/lib/carlos-claims", "/data/.parked"} {
+ lifecycleCheck(t, "retained evidence", filepath.WalkDir(root, func(path string, e fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ if !e.Type().IsRegular() || strings.HasSuffix(path, ".lock") {
+ return nil
+ }
+ raw, err := os.ReadFile(path)
+ if err != nil {
+ return err
+ }
+ if strings.HasPrefix(path, "/data/.parked/") || strings.Contains(string(raw), lifecycleHost) {
+ out[path] = sha256.Sum256(raw)
+ }
+ return nil
+ }))
+ }
+ return out
+}
+
+func lifecycleReinstall(t *testing.T, store release.Store, key ed25519.PrivateKey, actor hostbox.Actor, pointer release.SystemPointer) {
+ t.Helper()
+ lifecycleActiveSibling(t)
+ before := lifecycleRetainedBytes(t)
+ lifecycleCreate(t, store, key, actor, pointer, provision.Record{Schema: 1, Host: lifecycleHost, Account: "bab", App: "reinstall", Channel: "stable", CreatedAt: time.Now().UTC()})
+ lifecycleCheck(t, "request reinstall", os.WriteFile("/run/carlos/lifecycle-reinstall", []byte("ready"), 0644))
+ deadline := time.Now().Add(time.Minute)
+ for {
+ if _, err := os.Stat("/run/carlos/lifecycle-reinstall-refused"); err == nil {
+ break
+ }
+ if time.Now().After(deadline) {
+ t.Fatal("reinstall refusal timeout")
+ }
+ time.Sleep(20 * time.Millisecond)
+ }
+ if !reflect.DeepEqual(before, lifecycleRetainedBytes(t)) {
+ t.Fatal("reinstall changed retained evidence")
+ }
+ lifecycleActiveSibling(t)
+}
+
+func lifecycleAgentReinstall(t *testing.T, ctx context.Context, store release.Store) {
+ t.Helper()
+ deadline := time.Now().Add(2 * time.Minute)
+ refused := false
+ for time.Now().Before(deadline) {
+ if _, err := os.Stat("/run/carlos/lifecycle-finish"); err == nil {
+ return
+ }
+ if _, err := os.Stat("/run/carlos/lifecycle-reinstall"); err == nil && !refused {
+ obs, err := hostclaim.Read(ctx, store, lifecycleHost)
+ lifecycleCheck(t, "reinstall claim", err)
+ q := executionExpectation(obs.Claim)
+ q.ID, q.Operation = "reinstall", "executionView"
+ _, err = rootExecutionClient(controlsock.Path)(ctx, q)
+ want := lifecycleHost + ": hostname was retired on this box; reinstalling it is not yet supported"
+ if err == nil || errors.Is(err, execlease.ErrLost) || err.Error() != want {
+ t.Fatalf("reinstall refusal: %v", err)
+ }
+ lifecycleCheck(t, "reinstall refused", os.WriteFile("/run/carlos/lifecycle-reinstall-refused", []byte("ready"), 0644))
+ refused = true
+ }
+ time.Sleep(20 * time.Millisecond)
+ }
+ t.Fatal("lifecycle verification timeout")
+}
diff --git a/internal/host/plan14_lifecycle_retire_root_linux_test.go b/internal/host/plan14_lifecycle_retire_root_linux_test.go
index 88b331e80..13ddb4367 100644
--- a/internal/host/plan14_lifecycle_retire_root_linux_test.go
+++ b/internal/host/plan14_lifecycle_retire_root_linux_test.go
@@ -82,6 +82,7 @@ func lifecycleDeleteDriver(t *testing.T, phase string, raw release.Store, key ed
func lifecycleVerifyRetiredPins(t *testing.T, store release.Store, key ed25519.PrivateKey) {
t.Helper()
ctx := t.Context()
+ lifecycleActiveSibling(t)
recordKey := provision.RecordKey("bab", "lifecycle", lifecycleHost)
_, _, err := store.Get(ctx, recordKey)
if !errors.Is(err, os.ErrNotExist) {
@@ -154,6 +155,7 @@ func lifecycleVerifyRetiredPins(t *testing.T, store release.Store, key ed25519.P
}
return nil
}))
+ lifecycleActiveSibling(t)
body := []byte("signed sibling")
manifest := release.Manifest{App: "sibling", Version: "v9", Kind: "binary", Artifacts: map[string]release.Artifact{release.AdoptPlatform(): {}}}
lifecycleCheck(t, "ship sibling", release.Ship(ctx, store, "bab", manifest, map[string][]byte{release.AdoptPlatform(): body}))
diff --git a/internal/host/plan14_lifecycle_root_linux_test.go b/internal/host/plan14_lifecycle_root_linux_test.go
index 188f8d39e..0225c1f37 100644
--- a/internal/host/plan14_lifecycle_root_linux_test.go
+++ b/internal/host/plan14_lifecycle_root_linux_test.go
@@ -48,6 +48,7 @@ import (
)
const lifecycleHost = "lifecycle.bab.example"
+const lifecycleSibling = "sibling.bab.example"
const lifecycleHome = "/var/lib/plan14-lifecycle"
func init() {
@@ -71,7 +72,14 @@ func init() {
}
func lifecyclePayloadArgs(args []string) bool {
- return len(args) == 5 && args[1] == "--socket" && args[2] == filepath.Join(provision.InstanceRunDir(lifecycleHost), "app.sock") && args[3] == "--db" && args[4] == provision.NestedDBPath("/data", lifecycleHost)
+ if len(args) != 5 {
+ return false
+ }
+ host := lifecycleHost
+ if args[2] == filepath.Join(provision.InstanceRunDir(lifecycleSibling), "app.sock") {
+ host = lifecycleSibling
+ }
+ return args[1] == "--socket" && args[2] == filepath.Join(provision.InstanceRunDir(host), "app.sock") && args[3] == "--db" && args[4] == provision.NestedDBPath("/data", host)
}
func TestPlan14LifecyclePayloadDispatch(t *testing.T) {
@@ -267,6 +275,7 @@ func lifecycleRun(t *testing.T) {
image, err := os.ReadFile(binary)
lifecycleCheck(t, "test image", err)
if !resume {
+ lifecycleCheck(t, "initial version namespace", os.MkdirAll("/opt/carlos/versions", 0755))
lifecycleCheck(t, "executor", os.WriteFile("/opt/carlos/carlos", image, 0755))
lifecycleCheck(t, "guard executor", os.WriteFile("/opt/carlos/system/guard/carlos", image, 0755))
}
@@ -295,6 +304,21 @@ func lifecycleRun(t *testing.T) {
lifecycleCheck(t, "retained record", e)
rec = *found
}
+ withSibling := phase == "1" || phase == "2" || phase == "crash"
+ if withSibling && !resume {
+ sibling := provision.Record{Schema: 1, Host: lifecycleSibling, Account: "bab", App: "sibling", Channel: "stable", CreatedAt: time.Now().UTC()}
+ completeSibling := lifecycleCreate(t, store, key, actor, pointer, sibling)
+ completeMain := completeCreate
+ completeCreate = func() error {
+ if err := completeMain(); err != nil {
+ return err
+ }
+ return completeSibling()
+ }
+ manifest := release.Manifest{App: "sibling", Version: "v1", Kind: "binary", Artifacts: map[string]release.Artifact{release.AdoptPlatform(): {}}}
+ lifecycleCheck(t, "ship active sibling", release.Ship(t.Context(), store, "bab", manifest, map[string][]byte{release.AdoptPlatform(): image}))
+ lifecycleCheck(t, "promote active sibling", release.Promote(t.Context(), store, key, "bab", "sibling", "stable", "v1"))
+ }
// Only deployment inputs are service-owned. Root authority remains private.
lifecycleCheck(t, "deployment owner", filepath.Walk(lifecycleHome+"/deployment", func(path string, info os.FileInfo, err error) error {
if err != nil {
@@ -313,12 +337,31 @@ func lifecycleRun(t *testing.T) {
_, _, err = release.StageSignedHead(ctx, store, key.Public().(ed25519.PublicKey), nil, "/opt/carlos", registry.Route{Host: rec.Host, Account: rec.Account, App: rec.App, Channel: "stable"})
lifecycleCheck(t, "signed stage", err)
lifecycleCheck(t, "point initial version", release.PointLive("/opt/carlos", rec.Host, rec.Account, "v1", rec.App))
+ if withSibling {
+ _, _, err := release.StageSignedHead(ctx, store, key.Public().(ed25519.PublicKey), nil, "/opt/carlos", registry.Route{Host: lifecycleSibling, Account: "bab", App: "sibling", Channel: "stable"})
+ lifecycleCheck(t, "signed sibling stage", err)
+ lifecycleCheck(t, "sibling live", release.PointLive("/opt/carlos", lifecycleSibling, "bab", "v1", "sibling"))
+ }
}, lifecycleDeleteDriver(t, phase, store, key, actor, pointer), recoverControl)
if phase == "1" {
lifecycleSaveBucket(t, model)
}
if phase == "2" || phase == "crash" {
lifecycleVerifyRetiredPins(t, store, key)
+ lifecycleReinstall(t, store, key, actor, pointer)
+ lifecycleCheck(t, "release agent", os.WriteFile("/run/carlos/lifecycle-finish", []byte("ready"), 0644))
+ deadline := time.Now().Add(time.Minute)
+ for {
+ state, _ := exec.Command("systemctl", "show", "carlos-edge.service", "-p", "ActiveState", "--value").Output()
+ if strings.TrimSpace(string(state)) == "inactive" {
+ break
+ }
+ if strings.TrimSpace(string(state)) == "failed" || time.Now().After(deadline) {
+ log, _ := exec.Command("journalctl", "-u", "carlos-edge.service", "--no-pager", "-n", "80").CombinedOutput()
+ t.Fatalf("agent completion: %s", log)
+ }
+ time.Sleep(20 * time.Millisecond)
+ }
if phase == "crash" {
if _, err := os.Stat(lifecycleCrashPath); err != nil {
t.Fatal("crash point was not reached", err)
@@ -423,6 +466,16 @@ func lifecycleStartAgent(t *testing.T, endpoint string, completeCreate func() er
}
func lifecycleAgent(t *testing.T) {
+ deadlineReady := time.Now().Add(3 * time.Minute)
+ for {
+ if _, err := os.Stat("/run/carlos-control/lifecycle-ready"); err == nil {
+ break
+ }
+ if time.Now().After(deadlineReady) {
+ t.Fatal("controller activation timeout")
+ }
+ time.Sleep(20 * time.Millisecond)
+ }
lifecycleEnvironment(t, os.Getenv("CARLOS_S3_ENDPOINT"))
completed := false
defer func() {
@@ -535,6 +588,16 @@ func lifecycleAgent(t *testing.T) {
time.Sleep(100 * time.Millisecond)
}
}
+ var finishSibling func()
+ {
+ sibling, err := reg.Get(lifecycleSibling)
+ lifecycleCheck(t, "sibling route", err)
+ if sibling == nil {
+ t.Fatal("missing sibling installation")
+ }
+ finishSibling = lifecycleLaunch(t, ctx, providers.Wake, assembly.Wake, *sibling)
+ lifecycleCheck(t, "active sibling ready", os.WriteFile("/run/carlos/lifecycle-sibling", []byte("active"), 0644))
+ }
lifecycleCheck(t, "request deletion", os.WriteFile("/run/carlos/lifecycle-delete", []byte("ready"), 0600))
deadline := time.Now().Add(time.Minute)
for time.Now().Before(deadline) {
@@ -547,6 +610,11 @@ func lifecycleAgent(t *testing.T) {
obs, e := hostclaim.Read(ctx, store, lifecycleHost)
lifecycleCheck(t, "retirement observation", e)
if obs.Claim.Free() {
+ lifecycleCheck(t, "agent checkpoint", os.WriteFile("/run/carlos/lifecycle-result", []byte("pass"), 0600))
+ lifecycleAgentReinstall(t, ctx, store)
+ if finishSibling != nil {
+ finishSibling()
+ }
completed = true
return
}
@@ -558,8 +626,12 @@ func lifecycleAgent(t *testing.T) {
}
func lifecycleWake(t *testing.T, ctx context.Context, runtime *activator.ClaimRuntime, wake *ClaimWakeAssembly, route registry.Route) {
+ lifecycleLaunch(t, ctx, runtime, wake, route)()
+}
+
+func lifecycleLaunch(t *testing.T, ctx context.Context, runtime *activator.ClaimRuntime, wake *ClaimWakeAssembly, route registry.Route) func() {
t.Helper()
- observed, err := runtime.Read(ctx, lifecycleHost)
+ observed, err := runtime.Read(ctx, route.Host)
lifecycleCheck(t, "read for wake", err)
policy := activator.StartPolicy{MaxStarts: 5, Window: 30 * time.Second}
prerequisiteDeadline := time.Now().Add(2 * time.Minute)
@@ -568,7 +640,7 @@ func lifecycleWake(t *testing.T, ctx context.Context, runtime *activator.ClaimRu
if e == nil {
break
}
- fresh, readErr := runtime.Read(ctx, lifecycleHost)
+ fresh, readErr := runtime.Read(ctx, route.Host)
retry := errors.Is(e, hostbox.ErrChanged) && readErr == nil && fresh.ETag != observed.ETag
if errors.Is(e, hostclaim.ErrWrongState) && readErr == nil && fresh.Claim.Holder.WakeRetry != nil && fresh.Claim.Holder.WakeRetry.State == "open" {
retry = true
@@ -590,31 +662,13 @@ func lifecycleWake(t *testing.T, ctx context.Context, runtime *activator.ClaimRu
launch, err := start.PrepareLaunch(ctx)
lifecycleCheck(t, "launch preparation", err)
cmd, _, settle, _, err := launch.Start(ctx, "main", start.CurrentAttempt(), start.Build, func() error { return ctx.Err() })
- if settle != nil {
- defer func() {
- cleanup, cancel := context.WithTimeout(context.Background(), 30*time.Second)
- defer cancel()
- for {
- stopErr := settle(cleanup)
- if stopErr == nil {
- return
- }
- if cleanup.Err() != nil {
- t.Error("settle failed launch", stopErr)
- return
- }
- time.Sleep(100 * time.Millisecond)
- }
- }()
- }
lifecycleCheck(t, "root launch", err)
lifecycleCheck(t, "tenant ready", start.Ready(ctx, cmd))
prefix, err := hostbox.GenerationPrefix(*admitted.Claim.Holder)
lifecycleCheck(t, "generation prefix", err)
rep, err := start.Replicate(ctx, prefix)
lifecycleCheck(t, "replicate", err)
- defer rep.Close(context.Background())
- current, err := runtime.Read(ctx, lifecycleHost)
+ current, err := runtime.Read(ctx, route.Host)
lifecycleCheck(t, "read publication", err)
_, err = start.Publish(ctx, current)
lifecycleCheck(t, "publish", err)
@@ -631,11 +685,13 @@ func lifecycleWake(t *testing.T, ctx context.Context, runtime *activator.ClaimRu
if response.StatusCode != 200 || string(raw) != "lifecycle tenant" {
t.Fatalf("tenant response %d %q", response.StatusCode, raw)
}
- lifecycleCheck(t, "stop tenant", lifecycleRetry(ctx, settle))
- lifecycleCheck(t, "stop replication", rep.Close(ctx))
- current, err = runtime.Read(ctx, lifecycleHost)
- lifecycleCheck(t, "read stopped session", err)
- lifecycleCheck(t, "stop session", lifecycleRetry(ctx, func(ctx context.Context) error { return wake.Stop(ctx, current) }))
+ return func() {
+ lifecycleCheck(t, "stop tenant", lifecycleRetry(ctx, settle))
+ lifecycleCheck(t, "stop replication", rep.Close(ctx))
+ current, err = runtime.Read(ctx, route.Host)
+ lifecycleCheck(t, "read stopped session", err)
+ lifecycleCheck(t, "stop session", lifecycleRetry(ctx, func(ctx context.Context) error { return wake.Stop(ctx, current) }))
+ }
}
func lifecycleInstallUnits(t *testing.T) {
@@ -656,9 +712,11 @@ func lifecycleInstallUnits(t *testing.T) {
})
}
t.Cleanup(func() {
+ exec.Command("systemctl", "stop", instancectl.UnitName(instancectl.RoleRun, lifecycleSibling)).Run()
+ exec.Command("systemctl", "stop", instancectl.SliceName(lifecycleSibling)).Run()
exec.Command("systemctl", "stop", instancectl.UnitName(instancectl.RoleRun, lifecycleHost)).Run()
exec.Command("systemctl", "stop", instancectl.SliceName(lifecycleHost)).Run()
- for _, name := range []string{instancectl.UnitName(instancectl.RoleRun, lifecycleHost), instancectl.SliceName(lifecycleHost)} {
+ for _, name := range []string{instancectl.UnitName(instancectl.RoleRun, lifecycleHost), instancectl.SliceName(lifecycleHost), instancectl.UnitName(instancectl.RoleRun, lifecycleSibling), instancectl.SliceName(lifecycleSibling)} {
os.RemoveAll(filepath.Join("/run/systemd/system", name+".d"))
os.RemoveAll(filepath.Join("/run/systemd/system.control", name+".d"))
}
@@ -696,18 +754,20 @@ func lifecycleCreate(t *testing.T, raw release.Store, key ed25519.PrivateKey, ac
lifecycleCheck(t, "read deployment floor", err)
roster, err := directory.Read(ctx)
lifecycleCheck(t, "read actor roster", err)
- roster.Roster.Publication, roster.Roster.FloorSHA256 = "published", floor.Digest
- roster.Roster.Regions = map[string]hostactors.Region{actor.Region: {Account: actor.Cloud.Account, AWSRegion: actor.Cloud.Region, Namespaces: []string{actor.Namespace}, Pools: []string{"default"}}}
- roster.Roster.Consoles["console"] = hostactors.Console{Enrollment: strings.Repeat("d", 32), InstanceID: "console", Namespaces: []string{actor.Namespace}, DeploymentBucket: actor.ControlBucket}
- _, err = directory.Replace(ctx, signer, roster, roster.Roster)
- lifecycleCheck(t, "publish actors", err)
+ if roster.Roster.Publication != "published" {
+ roster.Roster.Publication, roster.Roster.FloorSHA256 = "published", floor.Digest
+ roster.Roster.Regions = map[string]hostactors.Region{actor.Region: {Account: actor.Cloud.Account, AWSRegion: actor.Cloud.Region, Namespaces: []string{actor.Namespace}, Pools: []string{"default"}}}
+ roster.Roster.Consoles["console"] = hostactors.Console{Enrollment: strings.Repeat("d", 32), InstanceID: "console", Namespaces: []string{actor.Namespace}, DeploymentBucket: actor.ControlBucket}
+ _, err = directory.Replace(ctx, signer, roster, roster.Roster)
+ lifecycleCheck(t, "publish actors", err)
+ }
controller, authority, control := lifecycleController(t, raw, key, actor, pointer)
data := controller.Store
appID, err := control.EnrollNewApp(ctx, rec.Account, rec.App)
lifecycleCheck(t, "enroll app", err)
before, err := hostclaim.Read(ctx, data, rec.Host)
lifecycleCheck(t, "read before registration", err)
- id := "create-lifecycle"
+ id := "create-" + rec.App
start, err := control.RegisterStart(ctx, rec.Account, rec.App, appID, id, hostops.Registration{Kind: "create", Host: rec.Host, Owner: hostops.RegistrationOwner{Console: "console", Enrollment: strings.Repeat("d", 32), Controller: "console-controller"}, AppID: appID, Epoch: 1, Claim: hostops.SavedObservation(before)}, time.Now())
lifecycleCheck(t, "register create", err)
rec.Acquisition = id
@@ -722,12 +782,18 @@ func lifecycleCreate(t *testing.T, raw release.Store, key ed25519.PrivateKey, ac
observed, err := authority.StartCreate(createCtx, before, record, id, "runner")
lifecycleCheck(t, "production begin create", err)
lifecycleCheck(t, "production record effects", authority.RecordEffects(createCtx, observed, record))
+ created := false
return func() error {
+ if created {
+ return nil
+ }
_, err := controller.Complete(createCtx, rec.Host, id)
if err != nil {
return err
}
- return control.SettleRegistration(ctx, rec.Account, rec.App, appID, id, time.Now())
+ err = control.SettleRegistration(ctx, rec.Account, rec.App, appID, id, time.Now())
+ created = err == nil
+ return err
}
}
@@ -817,6 +883,24 @@ func TestPlan14LifecycleControlProcess(t *testing.T) {
return finish, close, err
}
t.Cleanup(func() { rootHTTP.Root.executionRuntime.Close(context.Background()) })
+ deadline := time.Now().Add(time.Minute)
+ for {
+ out, _ := exec.Command("systemctl", "show", "carlos-edge.service", "-p", "MainPID", "--value").Output()
+ if strings.TrimSpace(string(out)) != "" && strings.TrimSpace(string(out)) != "0" {
+ break
+ }
+ if time.Now().After(deadline) {
+ t.Fatal("edge startup timeout")
+ }
+ time.Sleep(20 * time.Millisecond)
+ }
+ startup, finishStartup := context.WithTimeout(ctx, 2*time.Minute)
+ defer finishStartup()
+ if err := activateClaimRoot(ctx, startup, rootHTTP); err != nil {
+ lifecycleActivationDiagnostic(t, ctx)
+ lifecycleCheck(t, "production activation gate", err)
+ }
+ finishStartup()
listener, err := net.Listen("unix", controlsock.Path)
lifecycleCheck(t, "root socket", err)
lifecycleCheck(t, "root socket mode", os.Chmod(controlsock.Path, 0660))
@@ -827,12 +911,8 @@ func TestPlan14LifecycleControlProcess(t *testing.T) {
done := make(chan error, 1)
go func() { done <- control.Serve(controlCtx, listener) }()
defer func() { cancelControl(); lifecycleCheck(t, "control shutdown", <-done) }()
- if err := rootHTTP.RecoverPending(ctx); err != nil {
- t.Log("root recovery pending", err)
- }
- rootHTTP.Start(ctx)
- lifecycleCheck(t, "controller ready", os.WriteFile("/run/carlos-control/lifecycle-ready", []byte("ready"), 0600))
+ lifecycleCheck(t, "controller ready", os.WriteFile("/run/carlos-control/lifecycle-ready", []byte("ready"), 0644))
<-ctx.Done()
}
diff --git a/internal/host/plan14_r10_root_linux_test.go b/internal/host/plan14_r10_root_linux_test.go
index 420e91e22..02b4bccc1 100644
--- a/internal/host/plan14_r10_root_linux_test.go
+++ b/internal/host/plan14_r10_root_linux_test.go
@@ -227,12 +227,14 @@ func TestRootRebootPlan14AcceptanceSeedBatchPhase2(t *testing.T) {
}
func plan14RebootGroups(t *testing.T, phase string, groups []string) {
for _, group := range groups {
- cmd := exec.CommandContext(t.Context(), os.Args[0], "-test.run=^TestRootReboot"+group+"Phase"+phase+"$", "-test.v", "-test.timeout=8m")
- out, err := cmd.CombinedOutput()
- t.Log(string(out))
- if err != nil {
- t.Fatal(err)
- }
+ t.Run(group, func(t *testing.T) {
+ cmd := exec.CommandContext(t.Context(), os.Args[0], "-test.run=^TestRootReboot"+group+"Phase"+phase+"$", "-test.v", "-test.timeout=8m")
+ out, err := cmd.CombinedOutput()
+ t.Log(string(out))
+ if err != nil {
+ t.Fatal(err)
+ }
+ })
}
}
@@ -274,7 +276,7 @@ func TestPlan14LocalBackingStateRootRunner(t *testing.T) {
}
func TestRootRebootPlan14FinalSuitePhase1(t *testing.T) {
- cmd := exec.CommandContext(t.Context(), os.Args[0], "-test.run="+"^(TestPlan14ServingHandoverPinsRootRunner|TestPlan14DeletedRecordRetirementRootRunner|TestPlan14SettingsSuccessorProductionRootRunner|TestClaimRestoreOwnWriteMaterializes|TestClaimRestoreWriteOnlyRetagsPair|TestFilesSeedCLIStopFixture|TestFilesSeedExecutorLifetime|TestFilesSeedLiveFixtureShape|TestPlan14ActivationNamedUnitPreflight|TestPlan14BootLocalRecoveryRootRunner|TestPlan14BoundNetworkEdgeRootRunner|TestPlan14BoundRouteProgramRootRunner|TestPlan14BoundStartupRetirementRootRunner|TestPlan14CombinedBoundRetirementRootRunner|TestPlan14ConsoleLibraryRootRunner|TestPlan14ExecutionHostileCensusRootRunner|TestPlan14FloorRiseWithoutPlatformUpdateRootRunner|TestPlan14GenerationBackendInstalledRootRunner|TestPlan14GenerationContainedRunningRootRunner|TestPlan14GenerationDescriptorTransferRootRunner|TestPlan14GenerationExactRetirementRootRunner|TestPlan14GenerationInstalledUnitRootRunner|TestPlan14GenerationInventoryCheckedRootReader|TestPlan14GenerationKernelCensusRootRunner|TestPlan14GenerationKernelNamespaceMountRootRunner|TestPlan14GenerationKernelRetirementRootRunner|TestPlan14GenerationParkedRootRunner|TestPlan14GenerationPayloadPhysicalRootRunner|TestPlan14GenerationPrivateJournalRootRunner|TestPlan14GenerationRelocationUIDRootRunner|TestPlan14GenerationRequiredArtifactsRootRunner|TestPlan14GenerationRunningTenantPinRootRunner|TestPlan14GuardExecEdgeRootRunner|TestPlan14GuardRecoveryLegacyPolicyRootRunner|TestPlan14InitialMigrationExclusionRootRunner|TestPlan14InstalledClaimLaunchRootRunner|TestPlan14LaunchVerifierRootRunner|TestPlan14LocalBackingStateRootRunner|TestPlan14MaterializedBoundRouteRootRunner|TestPlan14MigratedBoundRouteRootRunner|TestPlan14NamedClaimLaunchRootRunner|TestPlan14NamedStartupDefersReplacementRootRunner|TestPlan14NoCensusReleaseRootRunner|TestPlan14OrdinaryUnitAuthorizationRootRunner|TestPlan14PendingAcquisitionStartupRootRunner|TestPlan14PendingInventoryStartupRootRunner|TestPlan14RealConsoleRootRunner|TestPlan14RealWoodstarRootRunner|TestPlan14RebootStopDescriptorIdentity|TestPlan14RepairExecutorEndToEndRootRunner|TestPlan14RetiredInstallationStartupRootRunner|TestPlan14RetiredMigrationPayloadRootRunner|TestPlan14RootCapabilitiesBothPaths|TestPlan14RootFailureProductionOpener|TestPlan14RootFloorAdoptRefreshRootRunner|TestPlan14RootFloorBootSelectors|TestPlan14RootFloorCrashOrderingRootRunner|TestPlan14RootFloorHealthyAdoptRootRunner|TestPlan14RootFloorRecoveryRollbackRootRunner|TestPlan14RootFloorSignedTargetRootRunner|TestPlan14RootFloorSyscallCrashOrdering|TestPlan14RootFloorUnknownCrashRecoveryRootRunner|TestPlan14RootInventoryPersistentProducer|TestPlan14RootInventoryRejectsOldBootMaterialization|TestPlan14RootJournalNamespaceRootRunner|TestPlan14RootMigrationBootstrapRootRunner|TestPlan14RootMigrationFreshStopRootRunner|TestPlan14RootMigrationHistoricRootRunner|TestPlan14RootMigrationLedgerExclusionRootRunner|TestPlan14RootMigrationMaskRootRunner|TestPlan14RootMigrationRootRunner|TestPlan14RootMigrationTimerAdoptionRootRunner|TestPlan14RootMigrationUnarmedRootRunner|TestPlan14RootRetryHardenedController|TestPlan14RootRetryProjectionDelivery|TestPlan14RootRuntimeInstalledCompletion|TestPlan14RootRuntimeInstalledFailure|TestPlan14RootSeedsFloorRootRunner|TestPlan14RootSeedsFloorSyscallCrashOrdering|TestPlan14SwapFailureHostileRootRunner|TestPlan14SwapFailureNoCensusRootRunner|TestPlan14TimerIntentionRootRunner|TestPlan14TimerRetirementRootRunner|TestPlan14UnitBindingRootRunner|TestRootFilesSeedCombinedRecoveryAllowsUpdate|TestRootFilesSeedDelegatedController|TestRootFilesSeedDelegatedOneShot|TestRootFilesSeedInstanceControlConstruction|TestRootFilesSeedIsolatedTenant|TestRootFilesSeedIsolationNegativeControl|TestRootFilesSeedKilledControllerRerun|TestRootFilesSeedLiveTransport|TestRootFilesSeedRealOriginPreparation|TestRootFilesSeedRealOriginTenant|TestRootFilesSeedSocketCompleteExecutor|TestRootFilesSeedSocketDisconnectExecutor|TestRootFilesSeedSocketShutdownExecutor|TestRootFilesSeedTwoHostIsolation|TestRootFilesSeedWholeExecutorKilledRerun|TestRootServingHandoverDirectoryProof|TestRootServingNamespaceStartup|TestRootServingNamespaceUnrecordedStartup|TestRootServingParkAuthenticatedHandover|TestRootServingParkAuthenticatedPeer|TestRootServingParkColdHandoverBoundary|TestRootServingParkCollection|TestRootServingParkDisplacedRoot|TestRootServingParkInstalledUnit|TestRootServingParkNestedBindRestore|TestRootServingParkQuarantine|TestRootServingParkRetainedDoesNotBlockRetirement|TestRootServingParkSnapshotClone|TestRootServingParkStoppedWriterHandover|TestRootServingParkUnrecordedReplacement)$", "-test.v", "-test.timeout=25m")
+ cmd := exec.CommandContext(t.Context(), os.Args[0], "-test.run="+"^(TestPlan14FirstInstallWithVendorUnitsRootRunner|TestPlan14LifecycleColdWakeRootRunner|TestPlan14LifecyclePayloadDispatch|TestPlan14ServingHandoverPinsRootRunner|TestPlan14DeletedRecordRetirementRootRunner|TestPlan14SettingsSuccessorProductionRootRunner|TestClaimRestoreOwnWriteMaterializes|TestClaimRestoreWriteOnlyRetagsPair|TestFilesSeedCLIStopFixture|TestFilesSeedExecutorLifetime|TestFilesSeedLiveFixtureShape|TestPlan14ActivationNamedUnitPreflight|TestPlan14BootLocalRecoveryRootRunner|TestPlan14BoundNetworkEdgeRootRunner|TestPlan14BoundRouteProgramRootRunner|TestPlan14BoundStartupRetirementRootRunner|TestPlan14CombinedBoundRetirementRootRunner|TestPlan14ConsoleLibraryRootRunner|TestPlan14ExecutionHostileCensusRootRunner|TestPlan14FloorRiseWithoutPlatformUpdateRootRunner|TestPlan14GenerationBackendInstalledRootRunner|TestPlan14GenerationContainedRunningRootRunner|TestPlan14GenerationDescriptorTransferRootRunner|TestPlan14GenerationExactRetirementRootRunner|TestPlan14GenerationInstalledUnitRootRunner|TestPlan14GenerationInventoryCheckedRootReader|TestPlan14GenerationKernelCensusRootRunner|TestPlan14GenerationKernelNamespaceMountRootRunner|TestPlan14GenerationKernelRetirementRootRunner|TestPlan14GenerationParkedRootRunner|TestPlan14GenerationPayloadPhysicalRootRunner|TestPlan14GenerationPrivateJournalRootRunner|TestPlan14GenerationRelocationUIDRootRunner|TestPlan14GenerationRequiredArtifactsRootRunner|TestPlan14GenerationRunningTenantPinRootRunner|TestPlan14GuardExecEdgeRootRunner|TestPlan14GuardRecoveryLegacyPolicyRootRunner|TestPlan14InitialMigrationExclusionRootRunner|TestPlan14InstalledClaimLaunchRootRunner|TestPlan14LaunchVerifierRootRunner|TestPlan14LocalBackingStateRootRunner|TestPlan14MaterializedBoundRouteRootRunner|TestPlan14MigratedBoundRouteRootRunner|TestPlan14NamedClaimLaunchRootRunner|TestPlan14NamedStartupDefersReplacementRootRunner|TestPlan14NoCensusReleaseRootRunner|TestPlan14OrdinaryUnitAuthorizationRootRunner|TestPlan14PendingAcquisitionStartupRootRunner|TestPlan14PendingInventoryStartupRootRunner|TestPlan14RealConsoleRootRunner|TestPlan14RealWoodstarRootRunner|TestPlan14RebootStopDescriptorIdentity|TestPlan14RepairExecutorEndToEndRootRunner|TestPlan14RetiredInstallationStartupRootRunner|TestPlan14RetiredMigrationPayloadRootRunner|TestPlan14RootCapabilitiesBothPaths|TestPlan14RootFailureProductionOpener|TestPlan14RootFloorAdoptRefreshRootRunner|TestPlan14RootFloorBootSelectors|TestPlan14RootFloorCrashOrderingRootRunner|TestPlan14RootFloorHealthyAdoptRootRunner|TestPlan14RootFloorRecoveryRollbackRootRunner|TestPlan14RootFloorSignedTargetRootRunner|TestPlan14RootFloorSyscallCrashOrdering|TestPlan14RootFloorUnknownCrashRecoveryRootRunner|TestPlan14RootInventoryPersistentProducer|TestPlan14RootInventoryRejectsOldBootMaterialization|TestPlan14RootJournalNamespaceRootRunner|TestPlan14RootMigrationBootstrapRootRunner|TestPlan14RootMigrationFreshStopRootRunner|TestPlan14RootMigrationHistoricRootRunner|TestPlan14RootMigrationLedgerExclusionRootRunner|TestPlan14RootMigrationMaskRootRunner|TestPlan14RootMigrationRootRunner|TestPlan14RootMigrationTimerAdoptionRootRunner|TestPlan14RootMigrationUnarmedRootRunner|TestPlan14RootRetryHardenedController|TestPlan14RootRetryProjectionDelivery|TestPlan14RootRuntimeInstalledCompletion|TestPlan14RootRuntimeInstalledFailure|TestPlan14RootSeedsFloorRootRunner|TestPlan14RootSeedsFloorSyscallCrashOrdering|TestPlan14SwapFailureHostileRootRunner|TestPlan14SwapFailureNoCensusRootRunner|TestPlan14TimerIntentionRootRunner|TestPlan14TimerRetirementRootRunner|TestPlan14UnitBindingRootRunner|TestRootFilesSeedCombinedRecoveryAllowsUpdate|TestRootFilesSeedDelegatedController|TestRootFilesSeedDelegatedOneShot|TestRootFilesSeedInstanceControlConstruction|TestRootFilesSeedIsolatedTenant|TestRootFilesSeedIsolationNegativeControl|TestRootFilesSeedKilledControllerRerun|TestRootFilesSeedLiveTransport|TestRootFilesSeedRealOriginPreparation|TestRootFilesSeedRealOriginTenant|TestRootFilesSeedSocketCompleteExecutor|TestRootFilesSeedSocketDisconnectExecutor|TestRootFilesSeedSocketShutdownExecutor|TestRootFilesSeedTwoHostIsolation|TestRootFilesSeedWholeExecutorKilledRerun|TestRootServingHandoverDirectoryProof|TestRootServingNamespaceStartup|TestRootServingNamespaceUnrecordedStartup|TestRootServingParkAuthenticatedHandover|TestRootServingParkAuthenticatedPeer|TestRootServingParkColdHandoverBoundary|TestRootServingParkCollection|TestRootServingParkDisplacedRoot|TestRootServingParkInstalledUnit|TestRootServingParkNestedBindRestore|TestRootServingParkQuarantine|TestRootServingParkRetainedDoesNotBlockRetirement|TestRootServingParkSnapshotClone|TestRootServingParkStoppedWriterHandover|TestRootServingParkUnrecordedReplacement)$", "-test.v", "-test.timeout=25m")
out, err := cmd.CombinedOutput()
t.Log(string(out))
if err != nil {
@@ -299,3 +301,40 @@ func TestRootRebootPlan14R11FinalPhase1(t *testing.T) {
func TestRootRebootPlan14R11FinalPhase2(t *testing.T) {
plan14RebootGroups(t, "2", []string{"FilesSeedTwoHostRenumber"})
}
+
+func TestRootRebootPlan14R12RemainingPhase1(t *testing.T) {
+ plan14RebootGroups(t, "1", []string{"Plan14MigrationInstalled", "ServingParkGrownDisk", "FilesSeedTwoHostRenumber"})
+}
+func TestRootRebootPlan14R12RemainingPhase2(t *testing.T) {
+ plan14RebootGroups(t, "2", []string{"Plan14MigrationInstalled", "ServingParkGrownDisk", "FilesSeedTwoHostRenumber"})
+}
+
+func TestRootRebootPlan14R12FullPhase1(t *testing.T) {
+ t.Run("suite", TestRootRebootPlan14FinalSuitePhase1)
+ t.Run("review", TestRootRebootPlan14ReviewBatchPhase1)
+ t.Run("lifecycle", TestRootRebootPlan14LifecycleEndToEndPhase1)
+}
+
+func TestRootRebootPlan14R12FullPhase2(t *testing.T) {
+ t.Run("lifecycle", TestRootRebootPlan14LifecycleEndToEndPhase2)
+ t.Run("suite", TestRootRebootPlan14FinalSuitePhase2)
+ t.Run("review", TestRootRebootPlan14ReviewBatchPhase2)
+}
+
+func TestRootRebootPlan14R12RecoveryBatchPhase1(t *testing.T) {
+ t.Run("regressions", func(t *testing.T) {
+ cmd := exec.CommandContext(t.Context(), os.Args[0], "-test.run=^(TestPlan14BootLocalRecoveryRootRunner|TestPlan14BoundNetworkEdgeRootRunner|TestPlan14BoundRouteProgramRootRunner|TestPlan14BoundStartupRetirementRootRunner|TestPlan14CombinedBoundRetirementRootRunner|TestPlan14ConsoleLibraryRootRunner|TestPlan14DeletedRecordRetirementRootRunner|TestPlan14GenerationExactRetirementRootRunner|TestPlan14GenerationKernelCensusRootRunner|TestPlan14GenerationKernelNamespaceMountRootRunner|TestPlan14GenerationKernelRetirementRootRunner|TestPlan14GenerationParkedRootRunner|TestPlan14GenerationPayloadPhysicalRootRunner|TestPlan14GenerationRequiredArtifactsRootRunner|TestPlan14InitialMigrationExclusionRootRunner|TestPlan14InstalledClaimLaunchRootRunner|TestPlan14LaunchVerifierRootRunner|TestPlan14MaterializedBoundRouteRootRunner|TestPlan14MigratedBoundRouteRootRunner|TestPlan14NamedClaimLaunchRootRunner|TestPlan14OrdinaryUnitAuthorizationRootRunner|TestPlan14RealConsoleRootRunner|TestPlan14RealWoodstarRootRunner|TestPlan14RetiredMigrationPayloadRootRunner|TestPlan14RootMigrationBootstrapRootRunner|TestPlan14RootMigrationFreshStopRootRunner|TestPlan14RootMigrationHistoricRootRunner|TestPlan14RootRuntimeInstalledCompletion)$", "-test.v", "-test.timeout=18m")
+ out, err := cmd.CombinedOutput()
+ t.Log(string(out))
+ if err != nil {
+ t.Fatal(err)
+ }
+ })
+ plan14R12RecoveryPairs(t, "1")
+}
+func TestRootRebootPlan14R12RecoveryBatchPhase2(t *testing.T) {
+ plan14R12RecoveryPairs(t, "2")
+}
+func plan14R12RecoveryPairs(t *testing.T, phase string) {
+ plan14RebootGroups(t, phase, []string{"(InventoryIdentity|Plan14Migration|PersistedFamilies|ClaimUnitMigration|GenerationPinIdentity|GenerationDevices|ClaimUnitDevice|FloorPrevAuthorization)", "ServingParkAcceptance", "FilesSeedTwoHost"})
+}
diff --git a/internal/host/rootadoption_options.go b/internal/host/rootadoption_options.go
new file mode 100644
index 000000000..484f479f0
--- /dev/null
+++ b/internal/host/rootadoption_options.go
@@ -0,0 +1,11 @@
+package host
+
+import "github.com/carlosframework/platform/internal/release"
+
+func rootAdoptionOptions(opts []release.PassOption, checks rootRestartChecks, history *release.RootHistoryPass, recovery func() release.PassOption) ([]release.PassOption, error) {
+ legacy, err := checks.allowLegacy()
+ if err != nil || legacy {
+ return opts, err
+ }
+ return append(opts, release.WithRootHistory(history), recovery()), nil
+}
diff --git a/internal/host/rootadoption_options_test.go b/internal/host/rootadoption_options_test.go
new file mode 100644
index 000000000..f28bebf84
--- /dev/null
+++ b/internal/host/rootadoption_options_test.go
@@ -0,0 +1,89 @@
+package host
+
+import (
+ "context"
+ "encoding/gob"
+ "io"
+ "reflect"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+func TestPlan14NoCensusAdoptionTrace(t *testing.T) {
+ for _, flag := range []string{"false", "true"} {
+ for _, shape := range []string{"empty", "non-adoptable", "ordinary"} {
+ t.Run(flag+"/"+shape, func(t *testing.T) {
+ t.Setenv("CARLOS_HOST_CLAIM", flag)
+ var rows []registry.Route
+ if shape != "empty" {
+ rows = []registry.Route{{Host: "shop.example", Account: "bab", App: "shop", Channel: "stable", Kind: "instance", Socket: "/run/shop.sock"}}
+ }
+ if shape == "non-adoptable" {
+ rows[0].Kind = "static"
+ rows[0].Channel = ""
+ }
+ run := func(protected bool) []string {
+ input, send := io.Pipe()
+ recv, output := io.Pipe()
+ var trace []string
+ done := make(chan struct{})
+ go func() {
+ defer close(done)
+ defer output.Close()
+ defer input.Close()
+ enc, dec := gob.NewEncoder(output), gob.NewDecoder(input)
+ type response struct {
+ Failed bool
+ Routes []registry.Route
+ Route *registry.Route
+ Times map[string]int64
+ }
+ enc.Encode(response{})
+ for {
+ var q struct{ Method, Host string }
+ if dec.Decode(&q) != nil {
+ return
+ }
+ trace = append(trace, q.Method+":"+q.Host)
+ if enc.Encode(response{Routes: rows, Times: map[string]int64{}}) != nil {
+ return
+ }
+ }
+ }()
+ reg, err := registry.OpenRemote("trace", send, recv, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var opts []release.PassOption
+ if protected {
+ opts, err = rootAdoptionOptions(opts, rootRestartChecks{legacy: func() (bool, error) { return true, nil }}, nil, func() release.PassOption {
+ return release.WithRecovery(func(_ context.Context, rt registry.Route) (bool, error) {
+ _, err := reg.Get(rt.Host)
+ return false, err
+ })
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ err = release.Pass(t.Context(), reg, release.NewDirStore(t.TempDir()), nil, t.TempDir(), func(string) error { return nil }, opts...)
+ if err != nil {
+ t.Fatal(err)
+ }
+ reg.Close()
+ <-done
+ return trace
+ }
+ want, got := run(false), run(true)
+ if !reflect.DeepEqual(got, want) {
+ t.Fatalf("no-census trace %v, main %v", got, want)
+ }
+ if len(got) == 0 {
+ t.Fatal("pass skipped registry")
+ }
+ })
+ }
+ }
+}
diff --git a/internal/host/rootexecution_authority.go b/internal/host/rootexecution_authority.go
index b587d6f53..2b5c1c861 100644
--- a/internal/host/rootexecution_authority.go
+++ b/internal/host/rootexecution_authority.go
@@ -18,6 +18,8 @@ import (
"github.com/carlosframework/platform/internal/rootparticipation"
)
+var errReinstallRetired = errors.New("hostname was retired on this box; reinstalling it is not yet supported")
+
func (h ClaimRootHTTP) executionAuthority(ctx context.Context, q execlease.ExecutionRequest, _ execlease.ControllerProcess) (execlease.ExecutionAuthority, error) {
var out execlease.ExecutionAuthority
if h.InventoryActor == nil || h.Stop.Coordinator.Guard.Root == nil || h.Reconcile.Census == nil || h.Journals == nil {
@@ -81,6 +83,15 @@ func (h ClaimRootHTTP) executionAuthority(ctx context.Context, q execlease.Execu
return out, err
}
if errors.Is(err, hostguard.ErrStampMismatch) {
+ if execlease.PreinstallationParticipant(c, actor.Box, actor.Enrollment) == nil {
+ retired, checkErr := h.Stop.Coordinator.Guard.Root.HostRetired(ctx, q.Host)
+ if checkErr != nil {
+ return out, checkErr
+ }
+ if retired {
+ return out, fmt.Errorf("%s: %w", q.Host, errReinstallRetired)
+ }
+ }
return h.retainedExecutionSource(ctx, q, actor, c, out)
}
if errors.Is(err, hostguard.ErrFenced) {
diff --git a/internal/host/rootexecution_transport.go b/internal/host/rootexecution_transport.go
index 8c8f722f6..89e8349dc 100644
--- a/internal/host/rootexecution_transport.go
+++ b/internal/host/rootexecution_transport.go
@@ -4,6 +4,8 @@ import (
"bytes"
"context"
"encoding/json"
+ "errors"
+ "fmt"
"io"
"net"
"net/http"
@@ -25,6 +27,10 @@ func (h ClaimRootHTTP) serveExecution(w http.ResponseWriter, r *http.Request, pe
return
}
out, err := h.Execution.Execute(r.Context(), execlease.ControllerProcess{KernelBootID: peer.BootID, PID: peer.PID, Start: peer.ProcessStart}, q)
+ if errors.Is(err, errReinstallRetired) {
+ w.WriteHeader(http.StatusGone)
+ return
+ }
if err != nil {
w.WriteHeader(http.StatusConflict)
return
@@ -56,6 +62,10 @@ func rootExecutionClient(path string) func(context.Context, execlease.ExecutionR
err = e
continue
}
+ if resp.StatusCode == http.StatusGone {
+ resp.Body.Close()
+ return out, fmt.Errorf("%s: %w", q.Host, errReinstallRetired)
+ }
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return out, execlease.ErrLost
diff --git a/internal/host/rootexecution_transport_test.go b/internal/host/rootexecution_transport_test.go
index 9da19e71b..655e64532 100644
--- a/internal/host/rootexecution_transport_test.go
+++ b/internal/host/rootexecution_transport_test.go
@@ -6,6 +6,7 @@ import (
"bytes"
"context"
"encoding/json"
+ "errors"
"net"
"net/http"
"testing"
@@ -22,8 +23,12 @@ func TestPlan14ExecutionBrokerTransport(t *testing.T) {
peer := StopPeer{UID: 1001, PID: 42, ProcessStart: "start", BootID: "boot", ControllerID: "controller"}
root.Stop.Authenticate = func(context.Context, net.Conn) (StopPeer, error) { return peer, nil }
calls := 0
+ retired := false
b, err := execlease.NewRootBroker(execlease.RootBrokerConfig{State: &rollbackDisk{dir: t.TempDir()}, Remote: f.claims, Clock: execlease.SystemClock{}, BootID: peer.BootID, Identity: "broker", Authority: func(ctx context.Context, q execlease.ExecutionRequest, p execlease.ControllerProcess) (execlease.ExecutionAuthority, error) {
calls++
+ if retired {
+ return execlease.ExecutionAuthority{}, errReinstallRetired
+ }
a, e := root.executionAuthority(ctx, q, p)
a.Migration = 1
return a, e
@@ -67,6 +72,11 @@ func TestPlan14ExecutionBrokerTransport(t *testing.T) {
if resp.StatusCode != http.StatusBadRequest || calls != 0 {
t.Fatalf("oversized request reached authority: status=%d calls=%d", resp.StatusCode, calls)
}
+ retired = true
+ if _, err := rootExecutionClient(socket)(t.Context(), q); !errors.Is(err, errReinstallRetired) || errors.Is(err, execlease.ErrLost) || err.Error() != q.Host+": "+errReinstallRetired.Error() {
+ t.Fatalf("retired refusal lost: %v", err)
+ }
+ retired = false
out, err := rootExecutionClient(socket)(t.Context(), q)
if err != nil {
t.Fatal(err)
diff --git a/internal/host/rootgeneration_census_linux.go b/internal/host/rootgeneration_census_linux.go
index e794b8ea5..8b49b6f0f 100644
--- a/internal/host/rootgeneration_census_linux.go
+++ b/internal/host/rootgeneration_census_linux.go
@@ -4,6 +4,7 @@ package host
import (
"context"
+ "errors"
"io/fs"
"os"
"path/filepath"
@@ -85,5 +86,19 @@ func productionGenerationKernelCensus(ctx context.Context) (string, string, erro
return "", "", err
}
records, _ := ctx.Value(generationCensusReservationsKey{}).([]release.GenerationReservation)
- return (generationKernel{proc: "/proc", cgroups: "/sys/fs/cgroup", contained: records}).census(ctx, tree)
+ return generationCensusRetry(ctx, func() (string, string, error) {
+ return (generationKernel{proc: "/proc", cgroups: "/sys/fs/cgroup", contained: records, service: generationServiceReader(ctx)}).census(ctx, tree)
+ })
+}
+
+func generationCensusRetry(ctx context.Context, inspect func() (string, string, error)) (string, string, error) {
+ for attempt := 0; ; attempt++ {
+ if err := ctx.Err(); err != nil {
+ return "", "", err
+ }
+ boot, digest, err := inspect()
+ if attempt == 2 || (!errors.Is(err, os.ErrNotExist) && !errors.Is(err, unix.ESRCH)) {
+ return boot, digest, err
+ }
+ }
}
diff --git a/internal/host/rootgeneration_containment_linux.go b/internal/host/rootgeneration_containment_linux.go
index c22b8fad8..5a901e96d 100644
--- a/internal/host/rootgeneration_containment_linux.go
+++ b/internal/host/rootgeneration_containment_linux.go
@@ -30,7 +30,7 @@ func (k generationKernel) containedTask(ctx context.Context, path string, status
group := strings.TrimSpace(string(raw))
for _, r := range k.contained {
p := r.Process
- if p.Containment != 1 || r.Retirement != "" || group != "0::/"+p.Group {
+ if p.Containment != 1 || r.Closed || r.Retirement != "" || group != "0::/"+p.Group {
continue
}
if err := generationContainedStatus(status); err != nil {
diff --git a/internal/host/rootgeneration_inventory.go b/internal/host/rootgeneration_inventory.go
index 20d3edc6a..da186221e 100644
--- a/internal/host/rootgeneration_inventory.go
+++ b/internal/host/rootgeneration_inventory.go
@@ -24,6 +24,7 @@ type generationInventory struct {
Launches []hostbox.LaunchInventory
Payloads []hostbox.GenerationPayload
Records []generationRecord
+ Unlaunched map[string]string
}
type generationCensusReservationsKey struct{}
@@ -142,7 +143,7 @@ func (v generationReconciler) reconcile(ctx context.Context, records []release.G
}
}
for _, view := range before.Installations {
- if !view.Installation.Empty && !hosts[view.Host] {
+ if !view.Installation.Empty && !hosts[view.Host] && before.Unlaunched[view.Host] != rootHistoryInstallation(view.Installation) {
return zero, os.ErrPermission
}
}
diff --git a/internal/host/rootgeneration_kernel_linux.go b/internal/host/rootgeneration_kernel_linux.go
index 49053ee66..a9755220e 100644
--- a/internal/host/rootgeneration_kernel_linux.go
+++ b/internal/host/rootgeneration_kernel_linux.go
@@ -8,6 +8,7 @@ import (
"encoding/hex"
"encoding/json"
"errors"
+ "fmt"
"io"
"io/fs"
"os"
@@ -28,6 +29,7 @@ type generationKernel struct {
proc, cgroups, boundApp string
owner uint32
contained []release.GenerationReservation
+ service func(string, []byte) (bool, error)
}
func generationRead(path string) ([]byte, error) {
@@ -213,7 +215,7 @@ type generationMount struct {
device uint64
}
-func generationMounts(raw []byte, b release.GenerationBinding) error {
+func generationMounts(raw []byte, b release.GenerationBinding, trusted ...bool) error {
var mounts []generationMount
for _, line := range strings.Split(strings.TrimSpace(string(raw)), "\n") {
f := strings.Fields(line)
@@ -274,7 +276,7 @@ func generationMounts(raw []byte, b release.GenerationBinding) error {
}
below, e1 := filepath.Rel(tree, m.root)
above, e2 := filepath.Rel(m.root, tree)
- if (e1 == nil && filepath.IsLocal(below)) || (e2 == nil && filepath.IsLocal(above)) {
+ if (e1 == nil && filepath.IsLocal(below)) || (e2 == nil && filepath.IsLocal(above) && !(len(trusted) != 0 && trusted[0])) {
return os.ErrPermission
}
}
@@ -331,7 +333,7 @@ func generationTaskFDs(ctx context.Context, path string, b release.GenerationBin
return err
}
if err := generationLinkRef(filepath.Join(path, name), b, refs); err != nil {
- return err
+ return fmt.Errorf("%s: %w", name, err)
}
}
return nil
@@ -370,9 +372,19 @@ func (k generationKernel) task(ctx context.Context, path string, b release.Gener
return start, nil
}
}
- kernel, err := generationRootTask(status)
- if err != nil {
- return "", err
+ kernel, rootErr := generationRootTask(status)
+ trusted := false
+ if k.service != nil && !kernel {
+ trusted, err = k.service(path, status)
+ if err != nil {
+ trusted = false
+ if rootErr != nil {
+ return "", fmt.Errorf("service: %w", err)
+ }
+ }
+ }
+ if rootErr != nil && !trusted {
+ return "", rootErr
}
if !kernel {
for name, want := range ns {
@@ -380,17 +392,17 @@ func (k generationKernel) task(ctx context.Context, path string, b release.Gener
if err != nil {
return "", err
}
- if got != want {
+ if got != want && !trusted {
return "", os.ErrPermission
}
}
for _, name := range []string{"exe", "cwd", "root"} {
if err := generationLinkRef(path+"/"+name, b, refs); err != nil {
- return "", err
+ return "", fmt.Errorf("%s: %w", name, err)
}
}
if err := generationTaskFDs(ctx, path+"/fd", b, refs); err != nil {
- return "", err
+ return "", fmt.Errorf("fd: %w", err)
}
maps, err := generationRead(path + "/maps")
if err != nil {
@@ -403,8 +415,8 @@ func (k generationKernel) task(ctx context.Context, path string, b release.Gener
if err != nil {
return "", err
}
- if err := generationMounts(mounts, b); err != nil {
- return "", err
+ if err := generationMounts(mounts, b, trusted); err != nil {
+ return "", fmt.Errorf("mounts: %w", err)
}
}
raw, err = generationRead(path + "/stat")
@@ -474,7 +486,7 @@ func (k generationKernel) absent(ctx context.Context, r release.GenerationReserv
}
start, err := k.task(ctx, filepath.Join(k.proc, name, "task", task), r.Binding, ns, refs)
if err != nil {
- return "", err
+ return "", fmt.Errorf("%s: %w", filepath.Join(k.proc, name, "task", task), err)
}
observations[name+"/"+task] = start
}
@@ -508,10 +520,18 @@ func productionGenerationAbsence(ctx context.Context, r release.GenerationReserv
if err != nil {
return "", err
}
- return (generationKernel{proc: "/proc"}).absent(ctx, current)
+ return (generationKernel{proc: "/proc", service: generationServiceReader(ctx)}).absent(ctx, current)
}
func generationRecordedAbsence(ctx context.Context, r release.GenerationReservation) (release.GenerationReservation, error) {
+ current, err := generationRecordedTargets(ctx, r)
+ if err != nil {
+ return r, err
+ }
+ return current, (generationKernel{proc: "/proc"}).controllerGone(ctx, current.Binding)
+}
+
+func generationRecordedTargets(ctx context.Context, r release.GenerationReservation) (release.GenerationReservation, error) {
current, err := generationCurrentFiles(r.Binding, rootstate.OpenTrustedFile)
if err != nil {
return r, err
@@ -521,9 +541,6 @@ func generationRecordedAbsence(ctx context.Context, r release.GenerationReservat
return r, err
}
k := generationKernel{proc: "/proc"}
- if err := k.controllerGone(ctx, r.Binding); err != nil {
- return r, err
- }
// Missing cgroups alone never discharge a reservation. With its launch
// authority closed, both recorded process identities must also be gone.
for _, p := range []struct {
diff --git a/internal/host/rootgeneration_payload_linux.go b/internal/host/rootgeneration_payload_linux.go
index 1f03dfe26..947bcebb0 100644
--- a/internal/host/rootgeneration_payload_linux.go
+++ b/internal/host/rootgeneration_payload_linux.go
@@ -4,6 +4,7 @@ package host
import (
"context"
+ "errors"
"os"
"path/filepath"
@@ -30,7 +31,13 @@ func generationPayloadFiles(ctx context.Context, root string, payloads []hostbox
if e.Kind != "path" || e.ManagedRoot != "data" || e.Materialization != "materialized" || !filepath.IsLocal(e.RelativePath) || filepath.Clean(e.RelativePath) != e.RelativePath || e.RelativePath == "." || e.Device == 0 || e.Inode == 0 || (e.Type != unix.S_IFDIR && e.Type != unix.S_IFREG) {
return os.ErrPermission
}
- if err := hostbox.VerifyInventoryIdentity(root, e); err != nil {
+ err := hostbox.VerifyInventoryIdentity(root, e)
+ if errors.Is(err, os.ErrPermission) {
+ if uid, ok := lookupUID(serviceUser); ok && uid > 0 {
+ err = hostbox.VerifyInventoryIdentityForOwner(root, e, uint32(uid))
+ }
+ }
+ if err != nil {
return err
}
diff --git a/internal/host/rootgeneration_requirements.go b/internal/host/rootgeneration_requirements.go
index 2c87b24e4..e9887e9a9 100644
--- a/internal/host/rootgeneration_requirements.go
+++ b/internal/host/rootgeneration_requirements.go
@@ -71,6 +71,7 @@ func generationRequiredInventory(ctx context.Context, store release.Store, snaps
func generationLocalObligations(ctx context.Context, snapshot generationInventory, fresh func(hostguard.RootInstallation) error) (generationInventory, error) {
var zero generationInventory
+ snapshot.Unlaunched = map[string]string{}
installs := map[string]hostguard.FenceView{}
for _, view := range snapshot.Installations {
installs[view.Host] = view
@@ -92,6 +93,13 @@ func generationLocalObligations(ctx context.Context, snapshot generationInventor
if _, err := hostclaim.Key(in.Host); err != nil {
return zero, err
}
+ unlaunched, err := generationUnlaunched(view, snapshot, fresh)
+ if err != nil {
+ return zero, err
+ }
+ if unlaunched {
+ snapshot.Unlaunched[in.Host] = rootHistoryInstallation(in)
+ }
for _, record := range snapshot.Records {
if record.Key == in.Record && record.Record.DerivesPaths() {
in.Artifacts.StateDirectory = true
@@ -141,14 +149,43 @@ func generationLocalObligations(ctx context.Context, snapshot generationInventor
directories++
}
}
- if routes != 1 || (in.Artifacts.StateDirectory && directories != 1) {
+ if routes != 1 {
return zero, os.ErrPermission
}
+ if in.Artifacts.StateDirectory && directories != 1 {
+ if directories != 0 || payload.InstallationStamp != in.Stamp || len(view.Retired) != 0 || !unlaunched {
+ return zero, os.ErrPermission
+ }
+ if _, err := os.Lstat(filepath.Join(dataDir(), state)); !os.IsNotExist(err) {
+ return zero, os.ErrPermission
+ }
+ }
}
}
return snapshot, ctx.Err()
}
+func generationUnlaunched(view hostguard.FenceView, snapshot generationInventory, fresh func(hostguard.RootInstallation) error) (bool, error) {
+ if fresh == nil || view.Installation.Empty {
+ return false, nil
+ }
+ for _, launch := range snapshot.Launches {
+ if launch.Target.Host == view.Host {
+ return false, nil
+ }
+ }
+ in := view.Installation
+ for _, old := range view.Retired {
+ if old.Incarnation == in.Incarnation && old.Stamp < in.Stamp {
+ in.Stamp = old.Stamp
+ }
+ }
+ if err := fresh(in); err != nil {
+ return false, err
+ }
+ return true, nil
+}
+
func productionGenerationCreation(in hostguard.RootInstallation) error {
state, err := rootstate.Open(release.RootHistoryDir, false, false)
if err != nil {
diff --git a/internal/host/rootgeneration_retired_linux.go b/internal/host/rootgeneration_retired_linux.go
index 6c8327e68..0f7dc49b3 100644
--- a/internal/host/rootgeneration_retired_linux.go
+++ b/internal/host/rootgeneration_retired_linux.go
@@ -9,12 +9,14 @@ import (
"os"
"path/filepath"
"slices"
+ "strconv"
"strings"
"github.com/carlosframework/platform/internal/hostbox"
"github.com/carlosframework/platform/internal/hostguard"
"github.com/carlosframework/platform/internal/instancectl"
"github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/rootstate"
)
func isolatedGeneration(r release.GenerationReservation) bool {
@@ -54,10 +56,13 @@ func productionRetiredGenerationAbsence(ctx context.Context, r release.Generatio
if err := verifyStopKernelMounts("/sys/fs/cgroup", "/proc"); err != nil {
return "", err
}
- current, err := generationRecordedAbsence(ctx, r)
+ current, err := generationRecordedTargets(ctx, r)
if err != nil {
return "", err
}
+ if err := generationRetiredController(ctx, current.Binding); err != nil {
+ return "", err
+ }
boot, err := (generationKernel{proc: "/proc"}).boot()
if err != nil {
return "", err
@@ -70,19 +75,42 @@ func retiredGenerationSnapshot(ctx context.Context, inv generationInventory, rec
return false
}
views := map[string]hostguard.FenceView{}
+ all := map[string]hostguard.FenceView{}
for _, view := range inv.Installations {
in := view.Installation
- if view.Barrier != nil || !slices.Contains(view.Retired, hostguard.Captured{Incarnation: in.Incarnation, Stamp: in.Stamp}) {
+ all[view.Host] = view
+ if view.Barrier != nil {
return false
}
+ if !slices.Contains(view.Retired, hostguard.Captured{Incarnation: in.Incarnation, Stamp: in.Stamp}) {
+ continue
+ }
views[view.Host] = view
}
+ if len(views) == 0 {
+ return false
+ }
for _, r := range records {
+ if _, ok := all[r.Binding.Target.Host]; !ok {
+ return false
+ }
+ if _, retired := views[r.Binding.Target.Host]; !retired {
+ if !isolatedGeneration(r) {
+ return false
+ }
+ continue
+ }
if r.Retirement == "" || !r.Closed || !isolatedGeneration(r) || retiredGenerationReceipt(ctx, views[r.Binding.Target.Host], r.Binding.Target) != nil {
return false
}
}
for _, launch := range inv.Launches {
+ if _, ok := all[launch.Target.Host]; !ok {
+ return false
+ }
+ if _, retired := views[launch.Target.Host]; !retired {
+ continue
+ }
if !launch.WritersClosed || !launch.Completed.Valid() || retiredGenerationReceipt(ctx, views[launch.Target.Host], generationOwner(launch.Target)) != nil {
return false
}
@@ -104,7 +132,7 @@ func retiredGenerationSnapshot(ctx context.Context, inv generationInventory, rec
return true
}
-func emptyGenerationGroups(ctx context.Context, root string) error {
+func emptyGenerationGroups(ctx context.Context, root string, active ...release.GenerationReservation) error {
count := 0
return filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error {
if path == root && errors.Is(err, os.ErrNotExist) {
@@ -127,8 +155,23 @@ func emptyGenerationGroups(ctx context.Context, root string) error {
if err != nil {
return err
}
- if strings.TrimSpace(string(raw)) != "" {
- return os.ErrPermission
+ for _, pid := range strings.Fields(string(raw)) {
+ if _, err := strconv.ParseUint(pid, 10, 32); err != nil {
+ return os.ErrPermission
+ }
+ task := filepath.Join("/proc", pid)
+ status, err := generationRead(task + "/status")
+ if err != nil {
+ return err
+ }
+ k := generationKernel{proc: "/proc", cgroups: "/sys/fs/cgroup", contained: active}
+ ok, err := k.containedTask(ctx, task, status)
+ if err != nil {
+ return err
+ }
+ if !ok {
+ return os.ErrPermission
+ }
}
return nil
})
@@ -158,9 +201,21 @@ func productionRetiredGenerationKernelCensus(ctx context.Context) (string, strin
return "", "", err
}
for _, launch := range inv.Launches {
- b := release.GenerationBinding{Target: generationOwner(launch.Target), ControllerPID: launch.ControllerPID, ControllerStart: launch.ProcessStart}
- if err := k.controllerGone(ctx, b); err != nil {
- return "", "", err
+ retired := false
+ for _, view := range inv.Installations {
+ if view.Host == launch.Target.Host && slices.Contains(view.Retired, hostguard.Captured{Incarnation: launch.Target.Incarnation, Stamp: launch.Target.Stamp}) {
+ retired = true
+ }
+ }
+ if !retired {
+ continue
+ }
+ for _, r := range records {
+ if r.Binding.Target == generationOwner(launch.Target) {
+ if err := generationRetiredController(ctx, r.Binding); err != nil {
+ return "", "", err
+ }
+ }
}
for _, group := range launch.Groups {
if !filepath.IsLocal(group.RelativePath) || group.RelativePath == "." || filepath.Clean(group.RelativePath) != group.RelativePath {
@@ -171,7 +226,7 @@ func productionRetiredGenerationKernelCensus(ctx context.Context) (string, strin
}
}
}
- if err := emptyGenerationGroups(ctx, "/sys/fs/cgroup/carlos.slice/carlos-instances.slice"); err != nil {
+ if err := emptyGenerationGroups(ctx, "/sys/fs/cgroup/carlos.slice/carlos-instances.slice", records...); err != nil {
return "", "", err
}
if err := checkedGenerationTree(ctx, filepath.Join(release.OptDir(), "versions")); err != nil {
@@ -183,3 +238,41 @@ func productionRetiredGenerationKernelCensus(ctx context.Context) (string, strin
}
return boot, generationInventoryDigest([]any{"retired-isolated-census", inv, records, boot}), nil
}
+
+func generationRetiredController(ctx context.Context, b release.GenerationBinding) error {
+ k := generationKernel{proc: "/proc", service: generationServiceReader(ctx)}
+ if k.controllerGone(ctx, b) == nil {
+ return nil
+ }
+ boot, err := k.boot()
+ if err != nil || boot != b.Target.BootID {
+ return os.ErrPermission
+ }
+ path := filepath.Join("/proc", strconv.Itoa(b.ControllerPID))
+ start, err := claimLaunchProcessStart("/proc", b.ControllerPID)
+ if err != nil || start != b.ControllerStart {
+ return os.ErrPermission
+ }
+ b, err = generationCurrentFiles(b, rootstate.OpenTrustedFile)
+ if err != nil {
+ return err
+ }
+ refs, err := generationTree(ctx, b)
+ if err != nil {
+ return err
+ }
+ tasks, err := generationNames(path + "/task")
+ if err != nil {
+ return err
+ }
+ for _, tid := range tasks {
+ if _, err := k.task(ctx, path+"/task/"+tid, b, nil, refs); err != nil {
+ return err
+ }
+ }
+ again, err := claimLaunchProcessStart("/proc", b.ControllerPID)
+ if err != nil || again != start {
+ return os.ErrPermission
+ }
+ return nil
+}
diff --git a/internal/host/rootgeneration_retired_linux_test.go b/internal/host/rootgeneration_retired_linux_test.go
index b613d39e7..88ad9f797 100644
--- a/internal/host/rootgeneration_retired_linux_test.go
+++ b/internal/host/rootgeneration_retired_linux_test.go
@@ -16,7 +16,7 @@ import (
)
func TestPlan14RetiredGenerationBoundary(t *testing.T) {
- for _, mode := range []string{"retired", "live installation", "pending pin", "open pin", "named", "legacy process", "open writers", "pending unit", "foreign receipt", "bad receipt", "barrier", "missing installation", "missing launches", "missing reservation"} {
+ for _, mode := range []string{"retired", "active sibling", "live installation", "pending pin", "open pin", "named", "legacy process", "open writers", "pending unit", "foreign receipt", "bad receipt", "barrier", "missing installation", "missing launches", "missing reservation"} {
t.Run(mode, func(t *testing.T) {
target := hostguard.FenceTarget{Host: "shop.example", Incarnation: "inc", Stamp: 3, Session: 4, Box: "box", Enrollment: "en", BootID: "boot", ControllerID: "controller", FenceID: "fence", Op: "delete", ObligationID: "park"}
at := time.Now().UTC()
@@ -25,6 +25,11 @@ func TestPlan14RetiredGenerationBoundary(t *testing.T) {
inv := generationInventory{Installations: []hostguard.FenceView{view}, Launches: []hostbox.LaunchInventory{{Target: target, WritersClosed: true, Reservations: []hostbox.LaunchReservation{{Target: target, State: hostbox.UnitSettled, Group: hostbox.LaunchGroup{RelativePath: instancectl.ClaimUnitControlGroup(target.Host)}}}}}}
records := []release.GenerationReservation{{Binding: release.GenerationBinding{Target: generationOwner(target)}, Closed: true, Retirement: strings.Repeat("a", 64)}}
switch mode {
+ case "active sibling":
+ sibling := hostguard.FenceView{Host: "sibling.example", Installation: hostguard.RootInstallation{Host: "sibling.example", Incarnation: "sibling", Stamp: 1}}
+ inv.Installations = append(inv.Installations, sibling)
+ inv.Launches = append(inv.Launches, hostbox.LaunchInventory{Target: hostguard.FenceTarget{Host: "sibling.example", Incarnation: "sibling", Stamp: 1}})
+ records = append(records, release.GenerationReservation{Binding: release.GenerationBinding{Target: release.GenerationOwner{Host: "sibling.example", Incarnation: "sibling", Stamp: 1}}})
case "live installation":
inv.Installations[0].Retired = nil
case "pending pin":
@@ -52,7 +57,7 @@ func TestPlan14RetiredGenerationBoundary(t *testing.T) {
case "missing reservation":
records = nil
}
- if got := retiredGenerationSnapshot(t.Context(), inv, records); got != (mode == "retired") {
+ if got := retiredGenerationSnapshot(t.Context(), inv, records); got != (mode == "retired" || mode == "active sibling") {
t.Fatalf("boundary = %v", got)
}
})
diff --git a/internal/host/rootgeneration_services_linux.go b/internal/host/rootgeneration_services_linux.go
new file mode 100644
index 000000000..785e028d7
--- /dev/null
+++ b/internal/host/rootgeneration_services_linux.go
@@ -0,0 +1,172 @@
+//go:build linux
+
+package host
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "github.com/carlosframework/platform/internal/rootstate"
+ "golang.org/x/sys/unix"
+)
+
+type generationService struct {
+ group, executable string
+ main int
+}
+
+func generationServiceReader(ctx context.Context) func(string, []byte) (bool, error) {
+ units := map[string]generationService{}
+ return func(path string, status []byte) (bool, error) {
+ raw, err := generationRead(path + "/cgroup")
+ if err != nil {
+ return false, err
+ }
+ group := strings.TrimPrefix(strings.TrimSpace(string(raw)), "0::")
+ if !strings.HasPrefix(group, "/") || strings.Contains(group, "\n") || strings.Contains(group, "/carlos-instances.slice/") {
+ return false, os.ErrPermission
+ }
+ unit := filepath.Base(group)
+ for _, part := range strings.Split(group, "/") {
+ if strings.HasSuffix(part, ".service") {
+ unit = part
+ break
+ }
+ }
+ if !strings.HasSuffix(unit, ".service") && group != "/init.scope" {
+ return false, os.ErrPermission
+ }
+ s, ok := units[unit]
+ if !ok {
+ if group == "/init.scope" {
+ s = generationService{group: group, main: 1}
+ } else {
+ out, err := exec.CommandContext(ctx, "systemctl", "show", unit, "-p", "ControlGroup", "-p", "MainPID", "-p", "ExecStart", "-p", "FragmentPath", "-p", "DropInPaths").Output()
+ if err != nil {
+ return false, err
+ }
+ props := map[string]string{}
+ for _, line := range strings.Split(string(out), "\n") {
+ if k, v, ok := strings.Cut(line, "="); ok {
+ props[k] = v
+ }
+ }
+ s.group, s.main = props["ControlGroup"], 0
+ s.main, err = strconv.Atoi(props["MainPID"])
+ if err != nil || s.main <= 0 || props["FragmentPath"] == "" {
+ return false, os.ErrPermission
+ }
+ for _, file := range append([]string{props["FragmentPath"]}, strings.Fields(props["DropInPaths"])...) {
+ file, err = filepath.EvalSymlinks(file)
+ if err != nil {
+ return false, err
+ }
+ f, err := rootstate.OpenTrustedFile(file)
+ if err != nil {
+ return false, err
+ }
+ f.Close()
+ }
+ _, tail, found := strings.Cut(props["ExecStart"], "path=")
+ if !found {
+ return false, os.ErrPermission
+ }
+ s.executable, _, _ = strings.Cut(tail, " ;")
+ }
+ units[unit] = s
+ }
+ if !generationServiceGroup(group, s.group) {
+ return false, os.ErrPermission
+ }
+ name, err := os.Readlink(path + "/exe")
+ if err != nil {
+ return false, err
+ }
+ actual, err := generationServiceExecutable(path, name)
+ if err != nil {
+ return false, fmt.Errorf("%s: %w", name, err)
+ }
+
+ // Non-root services may execute only the program selected by their
+ // root-owned unit. UID and a service-looking cgroup are insufficient.
+ if _, err := generationRootTask(status); err != nil {
+ want, e := generationIdentity(s.executable)
+ if e != nil || want != actual {
+ return false, os.ErrPermission
+ }
+ }
+ mainGroup, err := generationRead("/proc/" + strconv.Itoa(s.main) + "/cgroup")
+ if err != nil || !generationServiceGroup(strings.TrimPrefix(strings.TrimSpace(string(mainGroup)), "0::"), s.group) {
+ return false, os.ErrPermission
+ }
+ return true, nil
+ }
+}
+
+func generationServiceGroup(group, base string) bool {
+ if group == base && base != "" {
+ return true
+ }
+ if filepath.Clean(group) != group || base == "" || !strings.HasPrefix(group, base+"/") {
+ return false
+ }
+ for p := group; ; p = filepath.Dir(p) {
+ var st unix.Stat_t
+ if unix.Lstat(filepath.Join("/sys/fs/cgroup", p), &st) != nil || st.Uid != 0 || st.Mode&0022 != 0 || st.Mode&unix.S_IFMT != unix.S_IFDIR {
+ return false
+ }
+ if p == base {
+ return true
+ }
+ }
+}
+
+func generationServiceExecutable(task, name string) (rootstate.FileIdentity, error) {
+ var zero rootstate.FileIdentity
+ if !filepath.IsAbs(name) || filepath.Clean(name) != name || name == "/" {
+ return zero, os.ErrPermission
+ }
+ root, err := os.Open(task + "/root")
+ if err != nil {
+ return zero, err
+ }
+ defer root.Close()
+ fd, err := unix.FcntlInt(root.Fd(), unix.F_DUPFD_CLOEXEC, 0)
+ if err != nil {
+ return zero, err
+ }
+ defer func() { unix.Close(fd) }()
+ parts := strings.Split(strings.TrimPrefix(name, "/"), "/")
+ for i := 0; ; i++ {
+ var st unix.Stat_t
+ if err := unix.Fstat(fd, &st); err != nil {
+ return zero, err
+ }
+ kind := uint32(unix.S_IFDIR)
+ if i == len(parts) {
+ kind = unix.S_IFREG
+ }
+ if st.Uid != 0 || st.Mode&unix.S_IFMT != kind || st.Mode&0022 != 0 || (kind == unix.S_IFREG && st.Nlink != 1) {
+ return zero, os.ErrPermission
+ }
+ if i == len(parts) {
+ id := rootstate.FileIdentity{Device: uint64(st.Dev), Inode: st.Ino}
+ actual, err := generationIdentity(task + "/exe")
+ if err != nil || actual != id {
+ return zero, os.ErrPermission
+ }
+ return id, nil
+ }
+ next, err := unix.Openat(fd, parts[i], unix.O_RDONLY|unix.O_NONBLOCK|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
+ if err != nil {
+ return zero, err
+ }
+ unix.Close(fd)
+ fd = next
+ }
+}
diff --git a/internal/hostbox/inventory_identity_linux.go b/internal/hostbox/inventory_identity_linux.go
index 789846801..9c6a9b12c 100644
--- a/internal/hostbox/inventory_identity_linux.go
+++ b/internal/hostbox/inventory_identity_linux.go
@@ -49,6 +49,10 @@ func inventoryIdentityAt(parent int, name string, symlink bool) (rootstate.FileI
}
func VerifyInventoryIdentity(root string, e StopInventoryEntry) error {
+ return VerifyInventoryIdentityForOwner(root, e, 0)
+}
+
+func VerifyInventoryIdentityForOwner(root string, e StopInventoryEntry, owner uint32) error {
id, err := CaptureInventoryIdentity(root, e.RelativePath)
if err != nil {
return err
@@ -57,12 +61,16 @@ func VerifyInventoryIdentity(root string, e StopInventoryEntry) error {
return ErrChanged
}
if e.Identity.Boot != id.Boot {
- return checkedInventoryAncestry(root)
+ return checkedInventoryAncestryOwner(root, owner)
}
return nil
}
func checkedInventoryAncestry(root string) error {
+ return checkedInventoryAncestryOwner(root, 0)
+}
+
+func checkedInventoryAncestryOwner(root string, owner uint32) error {
if !filepath.IsAbs(root) || filepath.Clean(root) != root {
return ErrTypedInventory
}
@@ -71,7 +79,8 @@ func checkedInventoryAncestry(root string) error {
return err
}
defer func() { unix.Close(fd) }()
- for _, part := range append([]string{""}, strings.Split(strings.TrimPrefix(root, "/"), "/")...) {
+ parts := append([]string{""}, strings.Split(strings.TrimPrefix(root, "/"), "/")...)
+ for index, part := range parts {
if part != "" {
next, err := unix.Openat(fd, part, unix.O_PATH|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
if err != nil {
@@ -84,7 +93,8 @@ func checkedInventoryAncestry(root string) error {
if err := unix.Fstat(fd, &st); err != nil {
return err
}
- if st.Uid != 0 || st.Mode&0022 != 0 {
+ allowedOwner := st.Uid == 0 || (index == len(parts)-1 && st.Uid == owner)
+ if !allowedOwner || st.Mode&0022 != 0 {
return os.ErrPermission
}
}
diff --git a/internal/hostguard/retired_inventory_test.go b/internal/hostguard/retired_inventory_test.go
index 6af88d7f9..0bd596f72 100644
--- a/internal/hostguard/retired_inventory_test.go
+++ b/internal/hostguard/retired_inventory_test.go
@@ -21,6 +21,9 @@ func TestPlan14RetiredInstallationEnumeration(t *testing.T) {
if err := f.g.EstablishRoot(t.Context(), in, allow); err != nil {
t.Fatal(err)
}
+ if retired, err := f.g.Root.HostRetired(t.Context(), target.Host); err != nil || retired {
+ t.Fatal("active host marked retired", retired, err)
+ }
if err := f.g.RetireInstallationWith(t.Context(), target.Host, captured, allow, func() error { return os.ErrPermission }); !errors.Is(err, os.ErrPermission) {
t.Fatal(err)
}
@@ -40,6 +43,9 @@ func TestPlan14RetiredInstallationEnumeration(t *testing.T) {
}
}
f.g.Root = NewRootAuthority(&rootTestStore{dir: store.dir})
+ if retired, err := f.g.Root.HostRetired(t.Context(), target.Host); err != nil || !retired {
+ t.Fatal("retired host unavailable", retired, err)
+ }
for _, enumerate := range []func(context.Context) ([]FenceView, error){f.g.Root.MigrationInstallations, f.g.Root.GenerationInstallations} {
views, err := enumerate(t.Context())
if err != nil || len(views) != 1 || views[0].Installation != in {
diff --git a/internal/hostguard/rootretired_host.go b/internal/hostguard/rootretired_host.go
new file mode 100644
index 000000000..c5ad13d97
--- /dev/null
+++ b/internal/hostguard/rootretired_host.go
@@ -0,0 +1,33 @@
+package hostguard
+
+import "context"
+
+func (a *RootAuthority) HostRetired(ctx context.Context, host string) (bool, error) {
+ if err := ctx.Err(); err != nil {
+ return false, err
+ }
+ if a == nil || a.store == nil || validateFileHost(host) != nil {
+ return false, ErrInvalidInput
+ }
+ unlock, err := a.store.Lock(ctx, host+".lock")
+ if err != nil {
+ return false, err
+ }
+ defer unlock()
+ r, err := a.read(host)
+ if err != nil {
+ return false, err
+ }
+ if r.quarantined || r.pendingInstallation != nil || r.barrier != nil {
+ return false, ErrFenced
+ }
+ if r.ActiveStamp != 0 {
+ return false, nil
+ }
+ for _, old := range r.rootRetired {
+ if old.Incarnation == r.installation.Incarnation && old.Stamp == r.installation.Stamp && old.Stamp != 0 {
+ return true, nil
+ }
+ }
+ return false, nil
+}
diff --git a/internal/provision/plan14_p3_census_test.go b/internal/provision/plan14_p3_census_test.go
index 9566c31b8..3fec3e018 100644
--- a/internal/provision/plan14_p3_census_test.go
+++ b/internal/provision/plan14_p3_census_test.go
@@ -47,7 +47,7 @@ func plan14P3WriteCensusInventory() []censusEntry {
if e.File == "internal/host/rootgeneration_inventory.go" && e.Unit == "generationReconciler.reconcile" {
continue
}
- if e.File == "internal/host/rootgeneration_requirements.go" && e.Unit == "generationRequiredInventory" {
+ if e.File == "internal/host/rootgeneration_requirements.go" && (e.Unit == "generationRequiredInventory" || e.Unit == "generationLocalObligations") {
continue
}
kept = append(kept, e)
@@ -55,11 +55,12 @@ func plan14P3WriteCensusInventory() []censusEntry {
return append(kept,
censusEntry{"internal/hostclaim/claim.go", "Claim.Validate", "key-site", "lit:instances", "ffeb10f34954f5b4c8566ee1d66a89a99eb303a8611e4671696dd312c5296c29"},
censusEntry{"internal/hostclaim/retainedloss.go", "validateRetainedAdmissionLosses", "reference", "lit:instances", "b32ddaed69411feaa06c45e84287fb1c09b0168498231a5386e3acd937d335e0"},
- censusEntry{"internal/host/rootexecution_authority.go", "ClaimRootHTTP.executionAuthority", "key-site", "key:RecordKey,lit:instances", "eff2c5806bff1c3d4beec4f54513b71e3890a4970b4b03d1f6a200db0146109e"},
+ censusEntry{"internal/host/rootexecution_authority.go", "ClaimRootHTTP.executionAuthority", "key-site", "key:RecordKey,lit:instances", "cca1bc79b5efe35afbda9df8b61e46ae2644bf99ed5234da3ae8d88fcc4a872d"},
censusEntry{"internal/host/claimproduction.go", "productionClaimRegional.View", "reference", "lit:instances", "bcefe9f8fd6420e795ef95a6170fbad1030e29f2261bcc2d677f76b6f6d577ce"},
censusEntry{"internal/host/claimordinaryparks.go", "productionClaimRegional.ordinaryHome", "reference", "lit:instances", "fa96f75f8af5fd1e3ba3dff81e27ac0fb7c6029663472292d698bfddaec99fe8"},
- censusEntry{"internal/host/rootgeneration_inventory.go", "generationReconciler.reconcile", "key-site", "key:RecordKey", "23995980eb4aa9c02618ce649b583344fc80ee1ee193ff84fb9a46f49d33ce68"},
+ censusEntry{"internal/host/rootgeneration_inventory.go", "generationReconciler.reconcile", "key-site", "key:RecordKey", "da4df99dcbcec0cd62025c8c50e8f6be19e64ac990034002bcad1a6b8332bcb6"},
censusEntry{"internal/host/rootgeneration_requirements.go", "generationRequiredInventory", "key-site", "key:RecordKey", "3c0eb88abc5680c67191d1e848e93ba528c66e81bcd521577098ce761fb827e0"},
+ censusEntry{"internal/host/rootgeneration_requirements.go", "generationLocalObligations", "key-site", "key:RecordKey", "1a5c493c0a6383df880d4d6e730f6d01822559cab804b57ac445e36bb4535f3c"},
censusEntry{"internal/host/proofbinding.go", "proofBindingSource.binding", "key-site", "key:RecordKey", "20ed721cbcaccb6ab813a0d317542a43fe057544811920ba43ac6431c3e73000"},
censusEntry{"internal/host/proofbinding.go", "proofBindingSource.unitBinding", "key-site", "key:RecordKey", "82e214d1cf66d29d0900f5c1ce9cfe0a2f31c070da4bf1497ef3fd5273c09aad"},
censusEntry{"internal/host/proofnamed.go", "proofBindingSource.record", "key-site", "key:RecordKey", "97f1501b9543e36c6a231170425c5c79c7c22489500aadf119e064abc1abde93"},
diff --git a/internal/provision/testdata/plan14-root-reads-p3.tsv b/internal/provision/testdata/plan14-root-reads-p3.tsv
index d1bc1f7c3..90ee7bb68 100644
--- a/internal/provision/testdata/plan14-root-reads-p3.tsv
+++ b/internal/provision/testdata/plan14-root-reads-p3.tsv
@@ -207,7 +207,7 @@ internal/hostguard/rootmigration.go RootAuthority.CompleteMigrationInventory aut
@coverage internal/rootparticipation 7798d05a815af052b9b40d4ff381834a58b2740082e8f673c173e34cb3ce4f22
@coverage internal/rootstate 292c259ffe1e7144029af096cc39691d6239b2dbc5f01583d76d2e3f4653b2cb
@coverage internal/safefile 7030f312f7a60487381f0dbae2cdecaaa83625b639b6af15104f912592550816
-internal/host/rootgeneration_kernel_linux.go generationKernel.absent authority E2 E21: bounded kernel reader retains executable, sibling, descriptor, mapping and mount references; checks controller boot/start and cgroup emptiness. Unknown nonroot tasks, foreign namespaces, missing kernel state or renamed trees refuse. Fixtures: TestPlan14GenerationKernelReferences, TestPlan14GenerationKernelMappingIdentity, TestPlan14GenerationKernelControllerIdentity, TestPlan14GenerationKernelRunningExecutable, TestPlan14GenerationKernelCgroup, TestPlan14GenerationKernelRetainedFiles, TestPlan14GenerationKernelSiblingAlias. Real proc executable tests are not full privileged namespace acceptance.;rev3=PROPOSED-p3;run=8;identity=root generation kernel census uid 0;basis=uses current-boot scanner identities only after retained filesystem and handle verification; immutable reservations remain unchanged;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures file:Readlink@312 0dd1a357c428ea9ebab2fc808d8fb602bbd42f73af595fd3dd16319ed7336ff3
+internal/host/rootgeneration_kernel_linux.go generationKernel.absent authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=bounded task and persistent generation inspection retains boot and process start checks; refusal diagnostics identify the failing task file:Readlink@312 9aee837e918fbc1cc6f16bbf8e6b5652a700cf0c767d99150cac3e87bb03d2a3
internal/host/rootgenerations_launch.go productionRootGenerations authority E2,E3 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=connects the census root generation source to independent runtime failure and served settlement;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures file:Open@386 e5a3f19159348abed074b2e2c01f9e611e47783395e5f3d063b7c1a657145a92
internal/host/rootgenerations_launch.go rootGenerationSource.resolve authority E2 E20: read the atomic root installation under the existing host exclusion without recursive lock acquisition; retain fresh claim, canonical record, signed artifact and final reservation checks. Fixtures: TestPlan14GenerationLaunchDoesNotReenterRootLock, TestPlan14GenerationLaunchRefusals, TestPlan14RootDispatchRejectsRegistryMutation. PROPOSED E47: root-owned immutable unit/shape binding, reserved launch placement and exact invocation stop; retained generation binding carries the canonical unit. Default unit paths remain unchanged. Production named-route wiring and unit/env/timer retirement are not complete.;rev3=PROPOSED-p3;run=8;identity=root claim launch uid 0;basis=captures persistent executable and tree proofs only after canonical installation and signed artifact verification under exclusion;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures file:Open@2501 3cd7c8132e9c451c983392f4d1cc676ddc14b761c90dec3e631d54ae800f9951
internal/host/rootgenerations_launch.go type rootGenerationSource authority E2,E3 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=binds production runtime authority and persistent file capture to the root launch source;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures f291da85d463e50322a32fed1208629c6bf991112c4e8f614d9bf30bdb0338f5
@@ -232,8 +232,8 @@ internal/rollout/rootfailures.go RootFailures.ImportLegacy authority E3 rev3=PRO
internal/rollout/rootfailures.go RootFailures.VerifiedRetry authority E3 rev3=PROPOSED-p3;run=8;identity=root adoption uid 0;basis=verifies deployment-root signature, tuple, freshness and exact root book epoch before existing durable retry transition f4ab9edee069dedf8748055ee612a6ef8905b9770718c82378cfc15b0316810e
internal/rollout/rootfailures.go rootFailureWriter authority E3 rev3=PROPOSED-p3;run=8;identity=root adoption and migration uid 0;basis=writes migration identity, failure epochs and import fingerprints atomically through checked private root store e32ffb3459fe2b13e0ecbe7b364bd405ef25325ed8cbd685b9ac1e076a8c197a
@coverage internal/rollout 56efcb8c1d7231a7f43d0464d97345930c83c2baf864762c1faa7a004286e984
-internal/host/agent.go runAdoptUnderRootRelease authority B3,E1,E2,E3 rev3=PROPOSED-p3;run=16;identity=root adopt/update/control uid 0;basis=no census selects legacy proof/history and update effects; proof observation cannot fetch signed recovery manifests on that path; participating adoption retains root release exclusion and independent generation pins whole:Route@11104,whole:Route@18293,whole:Route@18329,whole:Route@8406 a25835004cca39d1c624fc47e59b7bcac47eb175b55c1db583fac920eaa33068
-internal/host/agent.go runAdoptWithEnsureSystemUnits authority B3,C,D,E1,E2,E3,E4,F,J1 PROPOSED E45 / R-E45-3: root checks census presence before opening release exclusion; absent census uses main adoption with no release directory or lock. Retained census remains protected with either flag value. PROPOSED E45: root enforces census-gated release ordering, bounded acquisition and scoped recovery; registry timer primitives preserve supplied identity but producer integration remains pending.;rev3=PROPOSED-p3;run=8;identity=carlos adopt uid 0;basis=passes the same census source to release exclusion and root settings to preserve the no-census branch;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=passes both the census probe and P3 production update opener under the common root release exclusion file:Open@176 13c055021f5e0453138bf773673521b86851eb16d567f6ccf968f5b7bc4f9199
+internal/host/agent.go runAdoptUnderRootRelease authority B3,E1,E2,E3 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=selects census participation before adding root history or named recovery; absent census uses main pass options with no recovery pre-list or per-host lookup; trace covers either flag and empty, non-adoptable and ordinary routes whole:Route@11104,whole:Route@18400,whole:Route@18436,whole:Route@8406 3031d8ce5a8d23390dee282dd299856f2c837b7bf1fb15599e4cd74692535294
+internal/host/agent.go runAdoptWithEnsureSystemUnits authority B3,C,D,E1,E2,E3,E4,F,J1 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=supplies the census probe to root exclusion and adoption; runAdoptUnderRootRelease chooses participation before root history and named recovery options; no census retains main pass configuration file:Open@176 13c055021f5e0453138bf773673521b86851eb16d567f6ccf968f5b7bc4f9199
internal/host/claimassembly.go NewProductionRootClaimStop authority C,E2 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=installs root filesystem identity capture in the production inventory handler;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures a55fcaa85155e2376d5f680ca8f86de6b579072e6a5c3ad47e1329b354311a00
internal/host/claimlaunch_root.go ClaimLaunchRoot.startRecorded authority E1,E2,E3 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=persists the root failure intent before launch and independently revalidates readiness and the exact unit invocation before served/request completion;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=retains P3 runtime settlement and E host-aware readiness validation under the generation reservation and host guard file:Read@4103 b19cb431a122d33703a83b39cb9a19786b7f509eba22961ec1e86d95f618e909
internal/host/claimretire_transport.go ClaimRootHTTP.retire authority C,G,J1 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=requires persistent root-captured artifact evidence when constructing the protected retirement parker;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures registry:ClaimOwnership@1081,registry:Host@1062,registry:Host@792,whole:ClaimOwnership@1085,whole:ClaimOwnership@894,whole:Route@2018,whole:Route@854 a60c74d0e0461e0a432f9a338aa25799cc0cd9d163c7b6eb803cfb33d55d7c2a
@@ -250,7 +250,7 @@ internal/host/proofnamed.go proofBindingSource.record authority B3 rev3=PROPOSED
internal/host/proofnamed.go proofBindingSource.trustedBytes authority B3,J1 rev3=PROPOSED-p3;identity=root adoption and rollback proof binder uid 0;basis=captures persistent evidence from the checked root-owned unit file descriptor alongside its bytes file:ReadAll@422 aa6e98efc7a57b01ed38cd051642d8d2f0b6abf2d527d5f70533e7365746096e
internal/host/rootgeneration_kernel_linux.go generationGroupGone authority E2 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=refuses a present cgroup placement from another boot; an absent old placement carries no process identity to compare;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=requires a known boot before interpreting a present cgroup; same-boot exact scan and E reboot population scan use separate authority rules;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=requires a known boot before interpreting a present cgroup; same-boot exact scan and E reboot population scan use separate authority rules file:Lstat@194 1ed5a4aadf5334f37027e04b258d7fb4a6413356d2d21257ae5ef9c626655376
internal/host/rootgeneration_parked.go generationRetirements authority E2 rev3=PROPOSED-p3;run=16;identity=root claim/adoption controller uid 0;basis=verifies retained completed serving-handover journals against both root payloads, retired predecessor, exact stop receipt, ownership and persistent path identity; follows transfers through terminal retirement without removing launch reservations; TestPlan14ServingHandoverPins and TestPlan14ServingHandoverPinsRootRunner e7fb5fc50073e49b5e8f1c0d9a09e9e3cc8da8f96902229d2af4d2c7df8fb20e
-internal/host/rootgeneration_payload_linux.go generationPayloadFiles authority E2 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=verifies materialized payload entries by persistent filesystem evidence before admitting their files into the absence scan;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures 2c899f743f9953ce2c8d9223d15f00914e9a09c7675226260d1c4d4c1fe1fd5d
+internal/host/rootgeneration_payload_linux.go generationPayloadFiles authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=verifies exact retained persistent file identity; on a cross-boot ancestry refusal permits only the configured carlos owner at the managed root itself, retaining root-owned protected ancestors and refusing writable roots 58e62ff06eba73b12b0e892d862a338dafb62170a47a0abc7e92989f5f11b0d7
internal/host/rootgenerations_launch.go rootGenerationSource.reserve authority E2,E3 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=writes root runtime intent after the generation reservation and before launch under release and host exclusion b09d7390279bcb302d83c282a517680f43c44013e07fc490f37bc798fd1703ba
internal/host/rootgenerations_launch.go type generationLaunchPermit authority E2,E3 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=retains the root runtime completion and close callbacks for the reserved launch 964b8f1ea86c1a85f8a60d5d87168d71375e9c835e2c64859267dc822265c135
internal/host/rootinventory.go ClaimRootHTTP.commitRootInventoryPending authority E2 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=canonicalizes persistent evidence under root exclusion while preserving the original request digest and durable replay;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures file:Read@1454,file:Read@3494,registry:Account@4858,registry:Account@4873,registry:App@4894,registry:App@4905,registry:ClaimIncarnation@4364,registry:ClaimOwner@4414,registry:ClaimOwner@5544,registry:ClaimOwnership@4956,registry:ClaimOwnership@4978,registry:ClaimOwnership@5860,registry:ClaimOwnership@8042,registry:ClaimRowID@4578,registry:ClaimRowID@4655,registry:ClaimStamp@4392,registry:DBPath@4922,registry:DBPath@4936,registry:Host@4828,registry:Host@4840,registry:Host@8518,registry:Name@8541,whole:ClaimOwnership@4960,whole:ClaimOwnership@4988,whole:ClaimOwnership@5870,whole:ClaimOwnership@8053,whole:Route@10308,whole:Route@3686,whole:Route@4069,whole:Route@4085,whole:Route@4140,whole:Route@4791,whole:Route@4804,whole:Route@4813,whole:Route@5051,whole:Route@5283,whole:Route@6117,whole:Route@6253,whole:Route@6477,whole:Route@7886,whole:Route@9573,whole:Route@9634,whole:Timer@8526,whole:Timer@8549 18fcea255a124e5c970c32d943c960836602dd12d9f00669583d8b4bd72fe7e6
@@ -304,7 +304,7 @@ internal/host/movesfiles.go movesFiles.Establish authority C,J4 rev3=PROPOSED-p3
internal/hostbox/servinghandover.go ServingHandover.Run authority C,J4 rev3=PROPOSED-p3;run=12;identity=root handover controller uid 0;basis=preserves seed fresh boot-aware directory verification and persists its checked full filesystem identity in successor inventory only; immutable predecessor retained registry:Account@2154,registry:Addr@2373,registry:Addrs@2391,registry:App@2198,registry:Backing@2410,registry:Canary@2467,registry:ClaimOwner@5643,registry:ClaimOwnership@2841,registry:ClaimOwnership@3057,registry:ClaimOwnership@3090,registry:ClaimRowID@3120,registry:ClaimRowID@5714,registry:ClaimRowID@5888,registry:DBPath@2286,registry:Kind@2128,registry:Name@3174,registry:Name@5853,registry:Platform@2451,registry:Replica@2481,registry:Route@2841,registry:Route@3090,registry:Route@5643,registry:Route@5714,registry:Socket@2234,registry:Timers@3039,registry:Timers@3057,registry:Timers@3120,registry:Timers@3174,registry:Timers@5796,registry:Tunnel@2431,registry:Unit@2355,whole:ClaimOwnership@2853,whole:ClaimOwnership@3073,whole:ClaimOwnership@3102,whole:ColdRows@3514,whole:ColdRows@3522,whole:ColdRows@3530,whole:ColdRows@3537,whole:ColdRows@5645,whole:ColdRows@5716,whole:ColdRows@5798,whole:ColdRows@6379,whole:ColdRows@6389,whole:Route@2114,whole:Route@2643,whole:Route@2651,whole:Route@2701,whole:Route@2709,whole:Route@2847,whole:Route@3096,whole:Route@5651,whole:Route@5722,whole:Timer@2656,whole:Timer@2664,whole:Timer@2714,whole:Timer@2752,whole:Timer@3045,whole:Timer@3063,whole:Timer@3126,whole:Timer@3180,whole:Timer@5804 c1367b936e3e0caa9e2bbd0da117a9b89ce3b7b7d4f518069e7d265a48b89a04
internal/host/claimactivation_preflight.go claimActivationVerified authority G rev3=PROPOSED-p3;run=15;identity=agent uid carlos; root migration/adopt/control uid 0;basis=canonical records, installed local units and retained root bindings independently refuse incomplete named-unit activation before claim state 8420c4b5b416bfb653788d69fbdf0f2c3acaad739c85be8bb2c1462f2ad56474
internal/host/claimactivation_preflight.go var openClaimActivationState authority G rev3=PROPOSED-p3;run=13;identity=root migration/adopt/control uid 0;basis=opens retained root unit namespaces read-only without creating claim state file:Open@31 9b4c4aeddcc3756f3c7fbb267096147f5670511fc4fda809d88d98540ad685f6
-internal/host/claimstop_run.go RunInstanceControl authority C,D,G rev3=PROPOSED-p3;run=13;identity=instance-control uid 0;basis=requires retained recovery census before construction and complete launch verification after pending recovery before serving d55919f2c48d02f39bb2d0eda76c7854aaf2c7684b8c782483d8fb62e5f5bff4
+internal/host/claimstop_run.go RunInstanceControl authority C,D,G rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=production root startup builds authenticated execution and launch handlers then uses activateClaimRoot for recovery, the registered census launch verifier and lifecycle start 645d4c7ce78d43fb73ae0b7bf9eff60ed1b1361e0a5b1a7df2083e2e05ec82f8
internal/host/claimstop_run.go claimRecoveryReady authority C,G rev3=PROPOSED-p3;run=13;identity=instance-control uid 0;basis=requires valid retained recovery census but defers complete launch inventory until restricted recovery has settled 627ab800a8f5772b81c3a893cf546438d7f76ae30ed29949ae6af52b5c2a9a7b
internal/host/claimunit_binding.go claimUnitBindings.establish authority E2 rev3=PROPOSED-p3;run=13;identity=root unit controller uid 0;basis=production admission refuses binding while named-unit 4a is incomplete, including backing=unit ef56f2db27a3d4a95803407a4b389b402592ed5b450bb6723aeffac42a041cab
internal/host/claimunit_dispatch.go claimUnitDispatch.forStart authority E2 rev3=PROPOSED-p3;run=13;identity=root launch controller uid 0;basis=verifies effective ordinary templates as well as retained named bindings before start and at the reservation permit recheck 3d358cb14cd248cea66750f64a4ea49be91eddb563e6ca389c12c7664f6c6706
@@ -330,12 +330,12 @@ internal/host/rootgeneration_inventory.go readProductionGenerationInventory auth
internal/host/rootmigration_production_linux.go verifyOfflineRootMigration authority G rev3=PROPOSED-p3;run=15;identity=offline root migration uid 0;basis=requires offline masked inactive adopt, stage, config-sync, restart, update and recovery services and absence of concurrent legacy root mutators before creating census state 300a81411020b33318dd8e15418acc95654c3980eb03c8581276d40a951a1a15
internal/host/systemparticipation.go systemApplier.checkUnarmedParticipationRecovery authority G,F rev3=PROPOSED-p3;run=15;identity=root system update/recovery uid 0;basis=pending acquisition triggers production controller settlement even without an update journal; normal ledger admission stays refused until verified recovery clears the marker 4360d1c0c50c554afc06b3a9ad216acc646728f97750e00ca1fe038b1bccce51
internal/hostguard/generationinventory.go RootAuthority.generationInstallation authority E2 rev3=PROPOSED-p3;run=15;identity=root generation verification uid 0;basis=enumerates using preserved installation stamp including exact retired tuples; resyncs retained authority without admitting inactive installations or dropping payload obligations fc5ef57b638ee0eba8ae49b724ef9397b2a3ae097d1e892cab888880480eacd2
-internal/host/rootgeneration_inventory.go generationReconciler.reconcile authority E2 rev3=PROPOSED-p3;run=16;identity=root claim/adoption controller uid 0;basis=verifies retained completed serving-handover journals against both root payloads, retired predecessor, exact stop receipt, ownership and persistent path identity; follows transfers through terminal retirement without removing launch reservations; TestPlan14ServingHandoverPins and TestPlan14ServingHandoverPinsRootRunner 23995980eb4aa9c02618ce649b583344fc80ee1ee193ff84fb9a46f49d33ce68
+internal/host/rootgeneration_inventory.go generationReconciler.reconcile authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=checks matched root installations, payloads and launch reservations before and after kernel observation; a route-only installation without a launch requires the exact unlaunched identity derived by the independent reader from root creation evidence da4df99dcbcec0cd62025c8c50e8f6be19e64ac990034002bcad1a6b8332bcb6
internal/host/rootgeneration_requirements.go generationRequiredInventory authority E2 rev3=PROPOSED-p3;run=16;identity=root adopt/update/control uid 0;basis=active installations require canonical deployment records; retired exact root tuples use retained artifacts and verified completed retirement journals without requiring deleted remote records; local obligations and pins remain 3c0eb88abc5680c67191d1e848e93ba528c66e81bcd521577098ce761fb827e0
internal/host/rootgeneration_retirement.go generationRetirement.closure authority E2 rev3=PROPOSED-p3;run=16;identity=root adopt/update/control uid 0;basis=root installation and exact controller identity bind retirement; pre-intention cancellation requires no process and irreversible writer closure, followed by the existing independent kernel verifier and durable close/retirement writes 1035a55fba521c7c81631d14084684e2dd4cb94703a7604d982be880bcea0cdf
-internal/host/rootgeneration_inventory.go type generationInventory authority E2 rev3=PROPOSED-p3;run=16;identity=root claim/adoption controller uid 0;basis=verifies retained completed serving-handover journals against both root payloads, retired predecessor, exact stop receipt, ownership and persistent path identity; follows transfers through terminal retirement without removing launch reservations; TestPlan14ServingHandoverPins and TestPlan14ServingHandoverPinsRootRunner 42a3affbf3d0da492f0326545305a3d996841cd36c0e3f4373d057abc2e7374f
+internal/host/rootgeneration_inventory.go type generationInventory authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=root kernel generation verifier carries service classification and exact retained containment; all observations are independently checked before certification 979381265f27bb0dad39d184aa1f1e1aa22fd72268a688c99cadbaca6c9938f6
internal/host/rootgeneration_parked.go generationPhysicalPayloads authority E2 rev3=PROPOSED-p3;run=16;identity=root claim/adoption controller uid 0;basis=verifies retained completed serving-handover journals against both root payloads, retired predecessor, exact stop receipt, ownership and persistent path identity; follows transfers through terminal retirement without removing launch reservations; TestPlan14ServingHandoverPins and TestPlan14ServingHandoverPinsRootRunner;rev3=PROPOSED-p3;run=21;basis=shared recorded-mount location for typed retirement and generation pin verification; root inventory and completed receipt required;basis=recorded mount relocation only for CapturePersistent root-protected artifacts; legacy destinations unchanged 67aca6be358b8f87a98c72e03a65d2dcd240cface604b17a058d7360a883e382
-internal/host/rootgeneration_parked.go generationRetirementRecords authority E2 rev3=PROPOSED-p3;run=16;identity=root claim/adoption controller uid 0;basis=verifies retained completed serving-handover journals against both root payloads, retired predecessor, exact stop receipt, ownership and persistent path identity; follows transfers through terminal retirement without removing launch reservations; TestPlan14ServingHandoverPins and TestPlan14ServingHandoverPinsRootRunner file:Read@728 65cc967484b693f71e4f78c418b85dcef801e82d0eed1ffd2cdda21c8ba163ef
+internal/host/rootgeneration_parked.go generationRetirementRecords authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=enumerates and decodes retained retirement journals while excluding the serving-handover directory; generationPhysicalPayloads and its consumers perform subsequent receipt and artifact verification file:Read@728 65cc967484b693f71e4f78c418b85dcef801e82d0eed1ffd2cdda21c8ba163ef
internal/host/rootgeneration_rows.go generationRegistryInventory authority E2 rev3=PROPOSED-p3;run=16;identity=root claim/adoption controller uid 0;basis=verifies retained completed serving-handover journals against both root payloads, retired predecessor, exact stop receipt, ownership and persistent path identity; follows transfers through terminal retirement without removing launch reservations; TestPlan14ServingHandoverPins and TestPlan14ServingHandoverPinsRootRunner registry:ClaimOwnership@2394,registry:ClaimOwnership@2773,registry:ClaimRowID@2826,registry:Host@2372,registry:Host@2751,registry:Name@2692,registry:Registration@2804,whole:ClaimOwnership@1625,whole:ClaimOwnership@1734,whole:ClaimOwnership@1748,whole:ClaimOwnership@1762,whole:ClaimOwnership@2271,whole:ClaimOwnership@2400,whole:ClaimOwnership@2418,whole:ClaimOwnership@2473,whole:ClaimOwnership@2722,whole:ClaimOwnership@2779,whole:ClaimOwnership@2797,whole:ClaimOwnership@2892,whole:ClaimOwnership@2920,whole:Route@2289,whole:Timer@2578,whole:Timer@2656 4d4e526c596c7a7fe36a071ca2452acc63f4941b225d389971fb718f4c39dd33
internal/host/rootgeneration_handover.go generationReadHandovers authority E2 rev3=PROPOSED-p3;run=16;identity=root claim/adoption controller uid 0;basis=verifies retained completed serving-handover journals against both root payloads, retired predecessor, exact stop receipt, ownership and persistent path identity; follows transfers through terminal retirement without removing launch reservations; TestPlan14ServingHandoverPins and TestPlan14ServingHandoverPinsRootRunner file:Read@453 226a4fbdb227eed6993a4d94c380e8aef8f681c6ad4b56abed2a30acee10e12a
internal/host/rootgeneration_handover.go generationTransfers authority E2 rev3=PROPOSED-p3;run=16;identity=root claim/adoption controller uid 0;basis=verifies retained completed serving-handover journals against both root payloads, retired predecessor, exact stop receipt, ownership and persistent path identity; follows transfers through terminal retirement without removing launch reservations; TestPlan14ServingHandoverPins and TestPlan14ServingHandoverPinsRootRunner registry:ClaimOwner@2754,registry:ClaimOwner@2802,registry:ClaimRowID@2936,registry:ClaimRowID@2986,registry:ClaimRowID@3574,registry:ClaimRowID@3615,registry:Name@3541,registry:Route@2754,registry:Route@2802,registry:Route@2936,registry:Route@2986,registry:Timers@3483,registry:Timers@3615,whole:ColdRows@2756,whole:ColdRows@2804,whole:ColdRows@2938,whole:ColdRows@2988,whole:ColdRows@3485,whole:ColdRows@3617,whole:Route@2763,whole:Route@2810,whole:Route@2945,whole:Route@2994,whole:Timer@3492,whole:Timer@3623 c0c0d2d05d6945560ee7170e6953ad3e603b5054b8f256ee49d93e36a1ad1fe9
@@ -365,9 +365,9 @@ internal/hostguard/recoveryfence.go Guard.PendingStopFences authority C,J4 rev3=
internal/hostguard/recoveryfence.go Guard.pendingFences authority C,J2,J4,J1 rev3=PROPOSED-p3;run=19;identity=root;basis=reads root-owned records under host lock and keeps exact barrier classification separate from ordinary stop recovery; legacy behavior retained 20be9535fd54587a54e3027f5388581ae5a64e5fcfced5a6031ffc9a4fc76a3b
internal/host/claimroleassembly_linux.go verifiedClaimExecutable authority C,G,J4 rev3=PROPOSED-p3;run=19;identity=agent uid carlos;basis=pins root-owned executable bytes and keeps the verified descriptor while supplying its absolute path to the root launcher file:Fstat@1122,file:Fstat@579,file:OpenFile@376,file:Read@949 7a7e9af520e4157e60ae93d12c3268a629a9b0bd1e83b263cdd27103cf2e227c
internal/host/claimroleassembly_linux.go claimExecutableName authority C,G,J4 rev3=PROPOSED-p3;run=19;identity=agent uid carlos;basis=duplicates the existing descriptor with CLOEXEC and its validated absolute name; never reopens a potentially replaced path e887880731a16c9e7934aef33a08169b34e7236f6d3b85a195602d35f8b69107
-@coverage internal/host cf1597c7c4801e97f8312bcdeb0756b47a177f088fadec078d74a0e324904f5c
-@coverage internal/hostbox 23dce8b43c105e281a8e93f2c70c969e48e1db448632c8c72d21b0ed273cc246
-@coverage internal/hostguard 1b3341e3f1a3b56c46b1a85ec5e12cb08e0c26128b1b5c865a562ab5aa0994fb
+@coverage internal/host 60676a836eb61ff244de77b770fd0baa49bb5b8554a0558b96e5d78481fb7fb6
+@coverage internal/hostbox 98d6c395a50aa809e0a63c0fa2ccf4463dde57ee89eab90dff79a243f245305f
+@coverage internal/hostguard ca664497969680d692172abda403b6a5d6664f8d7c8bc16a6440f02dd8426be6
internal/host/claimstop.go ClaimStopCoordinator.FenceAndStop authority C,J4 rev3=PROPOSED-p3;run=20;identity=root instance-control uid 0;basis=authenticated session stop retains root fence before independent unjournaled absence check; proves no journaled installation, containment or open files before bypassing nonexistent writer closure; normal drain remains for recorded launches; no-census unchanged file:Read@1610 58147f3abbf278037fc660392aeec0b6e76b667e3fe8435845ebaedc62d2ead1
internal/host/claimstop_recovery.go ClaimStopCoordinator.RecoverStopped authority C,J4 rev3=PROPOSED-p3;run=20;identity=root instance-control uid 0;basis=pending exact root fence and exact installation inventory permit recovery through independent absence verification when no launch journal was created a91b528d9f9259612c767bf0a94e0ff7664294668f29f584a681a1a0f656ad90
internal/host/claimstop_recovery.go ClaimStopCoordinator.verifyUnjournaled authority C,J4 rev3=PROPOSED-p3;run=20;identity=root instance-control uid 0;basis=requires root authority, exact resumed fence and sql.ErrNoRows before allowing the independent kernel absence witness; no-census unchanged 0fa59df5d2e838b944c0b36ae60598f958b1057b978cb8c123de7edace143d7a
@@ -417,7 +417,7 @@ internal/host/claimordinaryparks.go productionClaimRegional.authorizeOrdinaryRet
internal/park/sweep.go protected authority C,E2,J4 rev3=PROPOSED-p3;run=21;identity=claim regional worker service UID under root stop or signed retirement evidence;basis=ordinary view distinguishes independently retained file versions from database generation pins; only explicit file namespace pins receive per-version protection af0ffc56bb49f87738a94350c4c92305a8b922ede0587bc8fad4bb79b10f20d0
internal/park/remote.go Engine.selected authority C,E2,J4 rev3=PROPOSED-p3;run=21;identity=claim regional worker service UID under root stop or signed retirement evidence;basis=ordinary generation retirement excludes exact live file versions from fresh selection and revalidates on manifest replay; replay selection mismatch refuses deletion 43dd4b71ef97868599cf56560a458026ac77bada65f75b314630c436e57112ca
@coverage internal/park b611c74d7f75c985a1546f0b79b2f7db79312a3d3d2450981c5184c04c8e324e
-internal/host/rootexecution_authority.go ClaimRootHTTP.executionAuthority authority C,E2,J4 rev3=PROPOSED-p3;run=21;identity=root authenticated execution broker uid 0;basis=ordinary home delete and parking participation only, exact claim root installation inventory and record version or verified absence; completed stops never confer execution file:Exec@1657,file:Read@553 eff2c5806bff1c3d4beec4f54513b71e3890a4970b4b03d1f6a200db0146109e
+internal/host/rootexecution_authority.go ClaimRootHTTP.executionAuthority authority C,E2,J4 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=independently reads canonical claim and record before root registration; a valid preinstallation over an exactly retired root hostname returns the deferred-reinstall refusal without mutating retained evidence; retained move-source authority remains separate file:Exec@1657,file:Read@553 cca1bc79b5efe35afbda9df8b61e46ae2644bf99ed5234da3ae8d88fcc4a872d
internal/host/rootexecution_settle.go ClaimRootHTTP.cleanupExecution authority C,E2,J4 rev3=PROPOSED-p3;run=22;identity=root execution broker uid 0;basis=canonical root stop receipt and irreversible retired installation plus closed launch journal discharge already-completed cleanup after cgroups vanish; live sessions retain fresh kernel checks and remote claim settlement remains mandatory file:Read@2928,file:Read@4251 3a7091afec06f61182a80bd59475d5005296ac1ac06a1e9c9ab6d48bd9a24810
internal/hostclaim/delete.go ToParking authority C,E2,J4 rev3=PROPOSED-p3;run=21;identity=production console or local regional worker using authenticated operation;basis=console ordinary delete transition under claim CAS; sets the previously empty ordinary deletion phase to park so exact root stop tuples can retire it; existing nonempty phases retained 9d5d3cf3fd1255e4a3cda50cedcd65197f65155ea4bfa57d50670ebad0e5492a
internal/hostbox/renewing.go renewingHeld.Release authority C,E2,J4 rev3=PROPOSED-p3;run=21;identity=production console or local regional worker using authenticated operation;basis=regional park worker release of held lease; cancels work immediately then issues exact conditional lease deletion under detached request bounded cleanup context; grants no new work 0cfd55e1a0f40851dd27be382187fc01cfab5dc2bca54262966f6a66a2799e39
@@ -432,15 +432,42 @@ internal/host/rootexecution_settle.go retiredExecutionLaunch authority C,E2,J4 r
internal/host/rootgenerationpins.go productionRootGenerationPins authority E2 rev3=PROPOSED-p3;run=22;identity=root uid 0 under release exclusion;basis=retired ordinary isolated cohort only: canonical root stop receipts, irreversible retired captures, closed journals, exact original process identities gone and all managed instance cgroups empty; live pending named or legacy state retains prior verifier; generic process identity accepts valid Linux zero boot ticks file:Open@195 2cf7afda7ffe06c85030ff80c1d1d44b3f67b1589267ca0c0dfde5c81d4827fc
internal/host/rootgeneration_retirement.go productionRetireGenerations authority E2 rev3=PROPOSED-p3;run=22;identity=root uid 0 under release exclusion;basis=retired ordinary isolated cohort only: canonical root stop receipts, irreversible retired captures, closed journals, exact original process identities gone and all managed instance cgroups empty; live pending named or legacy state retains prior verifier; generic process identity accepts valid Linux zero boot ticks file:Open@292 f1eca8626049f13e079b6fe34f5f38117c7abf97a11105250bc1c4760bbd8f0d
internal/host/rootgeneration_kernel_linux.go generationStart authority E2 rev3=PROPOSED-p3;run=22;identity=root uid 0 under release exclusion;basis=retired ordinary isolated cohort only: canonical root stop receipts, irreversible retired captures, closed journals, exact original process identities gone and all managed instance cgroups empty; live pending named or legacy state retains prior verifier; generic process identity accepts valid Linux zero boot ticks a10512a1faad1675d091227b19feade6f65a0f834c19be472014fd6f77f7f389
-internal/host/rootgeneration_kernel_linux.go productionGenerationAbsence authority E2 rev3=PROPOSED-p3;run=22;identity=root uid 0 under release exclusion;basis=retired ordinary isolated cohort only: canonical root stop receipts, irreversible retired captures, closed journals, exact original process identities gone and all managed instance cgroups empty; live pending named or legacy state retains prior verifier; generic process identity accepts valid Linux zero boot ticks c5cb757bffece96fa33c1055529427a61bf52fec9213c4bf12d1f6402d2e5a69
-internal/host/rootgeneration_kernel_linux.go generationRecordedAbsence authority E2 rev3=PROPOSED-p3;run=22;identity=root uid 0 under release exclusion;basis=retired ordinary isolated cohort only: canonical root stop receipts, irreversible retired captures, closed journals, exact original process identities gone and all managed instance cgroups empty; live pending named or legacy state retains prior verifier; generic process identity accepts valid Linux zero boot ticks 758367037ce72577707a346b38e3544d851ceb57018eea773a878fb698b14d33
+internal/host/rootgeneration_kernel_linux.go productionGenerationAbsence authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=checks recorded launch targets and controller absence then inspects all tasks with independently verified service classification 03a98919c6c32638e9ec5a2658a68c58d895d931b3186e4968533ad28e57eadc
+internal/host/rootgeneration_kernel_linux.go generationRecordedAbsence authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=general absence retains recorded controller death plus exact main/tenant process and containment absence; only retired isolated proofs use the scoped controller reference check c817b4c8ecc0045bde88afe7c07f040e309db684f3a0455908ca61d672f2b9f5
internal/host/rootgeneration_retired_linux.go isolatedGeneration authority E2 rev3=PROPOSED-p3;run=22;identity=root uid 0 under release exclusion;basis=retired ordinary isolated cohort only: canonical root stop receipts, irreversible retired captures, closed journals, exact original process identities gone and all managed instance cgroups empty; live pending named or legacy state retains prior verifier; generic process identity accepts valid Linux zero boot ticks 4030cee805e5e3c4b08e9e606548cc7c23c4a13898ecd6bffa43e1dc7cf56cbc
internal/host/rootgeneration_retired_linux.go retiredGenerationReceipt authority E2 rev3=PROPOSED-p3;run=22;identity=root uid 0 under release exclusion;basis=retired ordinary isolated cohort only: canonical root stop receipts, irreversible retired captures, closed journals, exact original process identities gone and all managed instance cgroups empty; live pending named or legacy state retains prior verifier; generic process identity accepts valid Linux zero boot ticks 258b9f88fe29b944d2dfafebcbb383ac9742b4920909eda57c4588d254939377
-internal/host/rootgeneration_retired_linux.go productionRetiredGenerationAbsence authority E2 rev3=PROPOSED-p3;run=22;identity=root uid 0 under release exclusion;basis=retired ordinary isolated cohort only: canonical root stop receipts, irreversible retired captures, closed journals, exact original process identities gone and all managed instance cgroups empty; live pending named or legacy state retains prior verifier; generic process identity accepts valid Linux zero boot ticks;receipt snapshot is read before host exclusion, exact installation and closed journal are freshly rechecked under exclusion before and after verification 48937ef0466c36e18f86e5bb5f3acaa57c88f4fe55b040d73d2b9a096c675fc2
-internal/host/rootgeneration_retired_linux.go retiredGenerationSnapshot authority E2 rev3=PROPOSED-p3;run=22;identity=root uid 0 under release exclusion;basis=retired ordinary isolated cohort only: canonical root stop receipts, irreversible retired captures, closed journals, exact original process identities gone and all managed instance cgroups empty; live pending named or legacy state retains prior verifier; generic process identity accepts valid Linux zero boot ticks 050b409f4b3e21cb71a9343e5bdd20b3c23cc9676ad96291cc3e2093aad6f0e8
-internal/host/rootgeneration_retired_linux.go emptyGenerationGroups authority E2 rev3=PROPOSED-p3;run=22;identity=root uid 0 under release exclusion;basis=retired ordinary isolated cohort only: canonical root stop receipts, irreversible retired captures, closed journals, exact original process identities gone and all managed instance cgroups empty; live pending named or legacy state retains prior verifier; generic process identity accepts valid Linux zero boot ticks file:ReadFile@481,file:WalkDir@89 37e5760c2c9d38725ec8ad24ef3d18830dcbb8c10b0a861e97e486844a4a334d
-internal/host/rootgeneration_retired_linux.go productionRetiredGenerationKernelCensus authority E2 rev3=PROPOSED-p3;run=22;identity=root uid 0 under release exclusion;basis=retired ordinary isolated cohort only: canonical root stop receipts, irreversible retired captures, closed journals, exact original process identities gone and all managed instance cgroups empty; live pending named or legacy state retains prior verifier; generic process identity accepts valid Linux zero boot ticks 4574da707f3c09c424b061b879f06ad367d093ce373c418f893c02d932b8aba3
-internal/host/rootgeneration_retired_other.go productionRetiredGenerationAbsence authority E2 rev3=PROPOSED-p3;run=22;identity=non-Linux root caller;basis=delegates to existing unavailable generation verifier; no new non-Linux authority;receipt snapshot is read before host exclusion, exact installation and closed journal are freshly rechecked under exclusion before and after verification a281b05dae1d2b65b0fbcffe6b7cb65002da5dabfdc4f2596bd05511594b3546
-internal/host/rootgeneration_retired_other.go productionRetiredGenerationKernelCensus authority E2 rev3=PROPOSED-p3;run=22;identity=non-Linux root caller;basis=delegates to existing unavailable generation verifier; no new non-Linux authority f61b35d6064ff8a5a4fa757e229167d1b8e71b0109f8d116e0f288c533552a5c
+internal/host/rootgeneration_retired_linux.go productionRetiredGenerationAbsence authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=requires isolated reservation and exact root retirement receipt before checking recorded target absence and scoped shared-controller references; generic or named paths retain the stricter verifier 8f95c6232d728233a3a2ea091894308e2c238cfc83ed33812c08b562da814a9b
+internal/host/rootgeneration_retired_linux.go retiredGenerationSnapshot authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=proves exact retired installations through receipts and closed matched journals; active isolated siblings remain separately retained and pinned rather than making retirement a whole-box proof a00bec36603a167bbbe9d3d724da30a7db0025f7769801868c8514466644ef21
+internal/host/rootgeneration_retired_linux.go emptyGenerationGroups authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=walks managed groups and refuses every process not independently matched to active root containment; recorded retired groups still require emptiness file:ReadFile@522,file:WalkDir@130 7cf453e76cda0e9235de0b5acfa45f875dad52ef51b177dde148c8d56e9ea247
+internal/host/rootgeneration_retired_linux.go productionRetiredGenerationKernelCensus authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=checks retired installations own recorded groups and controller references; all populated managed groups must match a live root reservation, so active isolated siblings pass but unknown processes refuse 543f196321218063c074314fb9d05e95bc205db553890b127e11f7a8f40815aa
+internal/host/rootgeneration_retired_other.go productionRetiredGenerationAbsence authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=requires isolated reservation and exact root retirement receipt before checking recorded target absence and scoped shared-controller references; generic or named paths retain the stricter verifier a281b05dae1d2b65b0fbcffe6b7cb65002da5dabfdc4f2596bd05511594b3546
+internal/host/rootgeneration_retired_other.go productionRetiredGenerationKernelCensus authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=checks retired installations own recorded groups and controller references; all populated managed groups must match a live root reservation, so active isolated siblings pass but unknown processes refuse f61b35d6064ff8a5a4fa757e229167d1b8e71b0109f8d116e0f288c533552a5c
internal/host/rootgeneration_retirement.go generationRetirement.retire authority E2 rev3=PROPOSED-p3;run=22;identity=root uid 0 holding release exclusion;basis=read immutable root retirement receipt snapshot before host exclusion; existing exact installation and journal closure checks run under exclusion before and after kernel verification; prevents recursive host locking aad12e0c897c3abab5dd04fc0f9377f56076a6341e42e92a15c4382e3bf22831
internal/host/rootgeneration_retirement.go type generationRetirement authority E2 rev3=PROPOSED-p3;run=22;identity=root uid 0 under release and host exclusion;basis=retirement verifier carries a separate root-retired proof callback; caller reads canonical receipt before host locking while closure and kernel checks remain inside 132edefce1c8427220dcfee4d8bdcfc8ffa2bd21012cb97b3d330a73af13a93b
+internal/host/claimstop_run.go activateClaimRoot authority C,D,E2,G rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=shared production startup recovers pending work, runs claimActivationReady under launch exclusion, then starts the lifecycle; the shipped-unit lifecycle witness uses this same function 32d687b7273cdc1d04ad7542e1a8f9a6bed9a566e581c4c08a12a709135a0f98
+internal/host/rootadoption_options.go rootAdoptionOptions authority B3,E1,E3 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=checks participation before constructing history or recovery options; missing census returns the original main option slice; census read errors refuse a076d56dfa12b4080a4592aaca3f21be89abb33896718f335ddaf08d8b8f4ccc
+internal/host/rootexecution_transport.go ClaimRootHTTP.serveExecution authority D rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=authenticated broker refusal for a retired hostname has a distinct Gone response; other failures retain Conflict; no success capability is published c43b871b9cb5c41035c9d1dbc82e76e8b8cfb7d8fe0a4b8b7dbe864195073a3d
+internal/host/rootexecution_transport.go rootExecutionClient authority D rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=preserves the root retired-host refusal instead of translating it into ErrLost; success still validates the exact request tuple and serving/token shape 2a3981bb5549db09accdb28bcbb2da93460fb677c8f8ed7652bcdb2dec964a8b
+internal/host/rootgeneration_census_linux.go productionGenerationKernelCensus authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=checks protected staged tree and hardlink policy; actual systemd services pass only through unit, cgroup and executable verification while recorded live tenants use independent containment cc2500efe71568d535f5c2699115f7dc5922836c27977d072a1784fe807666f6
+internal/host/rootgeneration_census_other.go productionGenerationKernelCensus authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=checks protected staged tree and hardlink policy; actual systemd services pass only through unit, cgroup and executable verification while recorded live tenants use independent containment a1217cf881e9d05336fc16d6050f7f7a1a2184a0c20ff2988d0cf0f9012eaa45
+internal/host/rootgeneration_containment_linux.go generationKernel.containedTask authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=matches only live unclosed containment reservations to current kernel group and process identity; completed historical reservations cannot shadow an active sibling wake 25b3655284fb97a02d0f2e090ec105a18f178669979c147645b5a0f1966c893a
+internal/host/rootgeneration_kernel_linux.go generationKernel.task authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=retains the original root-only scan with matching namespaces and strict reference checks when service lookup is unavailable; only independently verified unit, cgroup and executable identity permits nonroot or private-namespace services; every accepted service still has all references and process start rechecked file:Readlink@1197 d75cd84dda21cba70763794bfd3a996f212fc75f6d01e4ce0ffae93959b61bb7
+internal/host/rootgeneration_kernel_linux.go generationMounts authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=rejects target-tree bind mounts and nsfs references; independently verified system services may retain ancestor mounts used by shipped private mount namespaces; unknown tasks retain the strict namespace policy 8bfb9496d7c24fd4bca0f7b84cd0b9cf01a1c116a9112f66738be5dde93b1f75
+internal/host/rootgeneration_kernel_linux.go generationRecordedTargets authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=revalidates persistent executable/tree identity, exact cgroup absence and recorded main/tenant process death; shared controller is checked separately by the caller 220fa257f513ea71a6e0b19e4fd5b5ec04c4454d272e09493d9fa1977db70f6d
+internal/host/rootgeneration_kernel_linux.go generationTaskFDs authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=holds the inspected descriptor directory open and checks every live descriptor against the retained generation identities; reports the refusing descriptor without weakening namespace checks file:Open@143,file:Readdirnames@367 b8be888cc74e90cc0b0f3a6c4efd320b3edfd6f16a3c08e0ae9ad34549ea00de
+internal/host/rootgeneration_kernel_linux.go type generationKernel authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=root kernel generation verifier carries service classification and exact retained containment; all observations are independently checked before certification 3463afcbd055b35fa94988868d8cb721272c2bc066220a0bd59e3529e811131b
+internal/host/rootgeneration_kernel_other.go productionGenerationAbsence authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=checks recorded launch targets and controller absence then inspects all tasks with independently verified service classification de47d997019075326e1d5513d7576390d085566fd69edc35efd00f067fb1db24
+internal/host/rootgeneration_retired_linux.go generationRetiredController authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=for an exact retired isolated installation, an old controller may still serve another host only after its boot/start identity and all thread references to this target generation are independently checked 793dfb40f339824ca3aeb9ceecfde69d8b63452eadf1a8abda734722401f1b79
+internal/host/rootgeneration_services_linux.go generationServiceGroup authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=accepts exact systemd cgroup or root-owned non-writable descendants only; delegated service-owned subgroups cannot impersonate platform or system services file:Lstat@281 b82ebaadda3ee8a36a66d69c36dda2551329197b903c09875b41fc0bbff44b38
+internal/host/rootgeneration_services_linux.go generationServiceReader authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=checks kernel cgroup against systemd ControlGroup and MainPID, root-owned unit and drop-ins, and root-protected executable inode; nonroot executable must match ExecStart; managed tenant groups and unexplained service subgroups refuse file:CommandContext@919,file:EvalSymlinks@1633,file:Readlink@2131 38c4656a7fca9e16865418b835cc8344c3eff99899713a0095d6a51ad4daa6ef
+internal/hostguard/rootretired_host.go RootAuthority.HostRetired authority C rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=refusal-only host-locked read of the exact retained installation, inactive stamp and retired tuple; quarantine, pending installation and barrier refuse; no evidence is rewritten e9e93474a629358ab5dda2d845b2571e913d5a7ec14beb544851f55690b17b1b
+internal/host/rootgeneration_services_linux.go generationServiceExecutable authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=opens the kernel-selected process root and checks each executable-path component with descriptor-relative no-symlink traversal, root ownership and no group/world writes; final single-link regular inode must equal the running executable; private controller mounts cannot hide a legitimate host executable file:Fstat@576,file:Open@243,file:Openat@1123 b0380d86d61198c94f826dc48df3d46f1352dd983279b75387dbbe2b51cca59f
+internal/host/rootgeneration_payload_other.go generationPayloadFiles authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=verifies exact retained persistent file identity; on a cross-boot ancestry refusal permits only the configured carlos owner at the managed root itself, retaining root-owned protected ancestors and refusing writable roots candidate:Kind@183,candidate:Kind@204 182189d8409113d671cdc76fc5d2d73e7056057bca230d546e65300c428b2c18
+internal/hostbox/inventory_identity_linux.go VerifyInventoryIdentity authority E2,J2,J4 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=default verifier retains root-owned ancestry and delegates exact persistent identity checks to the owner-aware verifier 5fc8216d53453a86c90a59e49a31b4e6027772488bae4b7c3068f79409891b18
+internal/hostbox/inventory_identity_linux.go VerifyInventoryIdentityForOwner authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=reopens the exact managed object without symlinks, verifies persistent filesystem, handle, inode, ownership and mount shape; cross-boot ancestry admits only the explicitly selected owner at the final managed root 1d0985ac5f306797f311c60ca582a8323c0194f6c3bfc1785dfe3da9cfa36786
+internal/hostbox/inventory_identity_linux.go checkedInventoryAncestry authority E2,J2,J4 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=default ancestry remains root-owned and protected through the final directory 80004516dfdd0c3dff425853c7f4010813a98068c6a0918e5367c40a575fe569
+internal/hostbox/inventory_identity_linux.go checkedInventoryAncestryOwner authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=descriptor-relative no-symlink walk requires root ownership and no group/world writes for every ancestor; only the final managed directory can have the explicitly selected service owner file:Fstat@649,file:Open@172,file:Openat@456 d4e93ccc1794dfc0854f723a637597ad5d8cc7a7117ada380b6289a2d1c470b1
+internal/hostbox/inventory_identity_other.go VerifyInventoryIdentity authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=default verifier retains root-owned ancestry and delegates exact persistent identity checks to the owner-aware verifier 2c1db6b94ad3c11b95e6905ff935efe3e13396b6d4febb6d757c1eda38039921
+internal/host/rootgeneration_census_linux.go generationCensusRetry authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=restarts the entire independent kernel census at most three times only when a process disappears; unknown tasks and permission failures refuse immediately, and no partial census is accepted 3cfd54f395ed7e77b15c01b76567de0188573603aafc4e7ec72f0c3cfb9d512f
+internal/host/rootgeneration_requirements.go generationLocalObligations authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=requires retained route and state artifacts; a current route-only installation without predecessors or any launch is unlaunched only after exact root FreshInstallation proof and physical state-directory absence, recomputed on every read file:Lstat@3018 1a5c493c0a6383df880d4d6e730f6d01822559cab804b57ac445e36bb4535f3c
+internal/host/rootgeneration_requirements.go generationUnlaunched authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=requires absence of every root launch for this hostname, selects the earliest retained stamp of this same incarnation, and performs one durable unconsumed-creation read; any read error refuses; empty installations keep their existing path; consumers independently validate transfers and physical state 8041de414e9b90f044066c4cad51e7e8b43dea858a4e2ae6eb6f22418466a720