WIP claim Plan 14 track E checkpoint 25 (not for merge): active-pin retirement and kernel descriptor scanning fixes; four root-runner acceptance tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9 files changed,
+424
−102
internal/host/plan14_generation_census_root_linux_test.go+40 −26internal/host/plan14_generation_inventory_root_test.go+27 −26internal/host/plan14_generation_kernel_linux_test.go+19 −0internal/host/plan14_generation_retirement_binding_test.go+140 −0internal/host/plan14_generation_retirement_root_linux_test.go+112 −0internal/host/rootgeneration_kernel_linux.go+28 −10internal/host/rootgeneration_retirement.go+47 −24internal/provision/testdata/plan14-root-reads-e.tsv+8 −5internal/release/plan14_generation_completeness_root_test.go+3 −11
CI passed — run details
CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok amadan.net/carlos/platform.git/launchctx 0.012s
? 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 349.488s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 3.306s
ok github.com/carlosframework/platform/cmd/moveprobe 92.318s
ok github.com/carlosframework/platform/cmd/zddprobe 0.707s
ok github.com/carlosframework/platform/internal/activator 234.308s
ok github.com/carlosframework/platform/internal/agentsock 0.015s
ok github.com/carlosframework/platform/internal/alerting 0.004s
ok github.com/carlosframework/platform/internal/analytics 0.023s
ok github.com/carlosframework/platform/internal/appconfig 2.380s
ok github.com/carlosframework/platform/internal/apperr 0.074s
ok github.com/carlosframework/platform/internal/applog 0.139s
ok github.com/carlosframework/platform/internal/awsinv 0.004s
ok github.com/carlosframework/platform/internal/boxchoice 0.004s
ok github.com/carlosframework/platform/internal/boxid 0.650s
? github.com/carlosframework/platform/internal/boxid/boxidtest [no test files]
? github.com/carlosframework/platform/internal/boxid/internal/signtest [no test files]
ok github.com/carlosframework/platform/internal/boxvolumes 0.008s
ok github.com/carlosframework/platform/internal/caps 0.012s
ok github.com/carlosframework/platform/internal/certwarm 0.003s
ok github.com/carlosframework/platform/internal/cgroup 0.697s
ok github.com/carlosframework/platform/internal/changelog 0.303s
ok github.com/carlosframework/platform/internal/clamd 0.055s
? github.com/carlosframework/platform/internal/cliflag [no test files]
ok github.com/carlosframework/platform/internal/console 366.063s
? github.com/carlosframework/platform/internal/controlname [no test files]
? github.com/carlosframework/platform/internal/controlsock [no test files]
ok github.com/carlosframework/platform/internal/dbdoor 5.777s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.009s
ok github.com/carlosframework/platform/internal/dbverify 0.004s
ok github.com/carlosframework/platform/internal/dnsrec 0.004s
ok github.com/carlosframework/platform/internal/docsite 0.005s
ok github.com/carlosframework/platform/internal/domains 2.194s
ok github.com/carlosframework/platform/internal/doorkey 0.006s
ok github.com/carlosframework/platform/internal/economics 0.007s
ok github.com/carlosframework/platform/internal/edge 26.353s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.008s
ok github.com/carlosframework/platform/internal/fleetapi 0.126s
ok github.com/carlosframework/platform/internal/gate 1.656s
ok github.com/carlosframework/platform/internal/genselect 0.015s
ok github.com/carlosframework/platform/internal/geo 0.017s
ok github.com/carlosframework/platform/internal/geoip 0.030s
ok github.com/carlosframework/platform/internal/historyreadback 0.155s
ok github.com/carlosframework/platform/internal/host 135.176s
ok github.com/carlosframework/platform/internal/hostactors 0.286s
ok github.com/carlosframework/platform/internal/hostbox 2.299s
ok github.com/carlosframework/platform/internal/hostclaim 0.516s
ok github.com/carlosframework/platform/internal/hostevidence 0.017s
ok github.com/carlosframework/platform/internal/hostexec 5.084s
ok github.com/carlosframework/platform/internal/hostfence 0.035s
ok github.com/carlosframework/platform/internal/hostfiles 10.618s
ok github.com/carlosframework/platform/internal/hostfloor 0.017s
ok github.com/carlosframework/platform/internal/hostguard 0.975s
ok github.com/carlosframework/platform/internal/hostops 1.508s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.015s
ok github.com/carlosframework/platform/internal/hostquarantine 0.017s
ok github.com/carlosframework/platform/internal/hostrelocate 0.478s
ok github.com/carlosframework/platform/internal/hostresolve 0.145s
ok github.com/carlosframework/platform/internal/hostwrite 0.015s
ok github.com/carlosframework/platform/internal/infraview 0.015s
ok github.com/carlosframework/platform/internal/instancectl 1.828s
ok github.com/carlosframework/platform/internal/instanceexec 2.758s
ok github.com/carlosframework/platform/internal/instancefs 0.015s
ok github.com/carlosframework/platform/internal/instancekey 0.021s
ok github.com/carlosframework/platform/internal/instanceupdates 0.023s
ok github.com/carlosframework/platform/internal/layout 1.482s
ok github.com/carlosframework/platform/internal/lease 9.466s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.005s
ok github.com/carlosframework/platform/internal/ledger 0.012s
ok github.com/carlosframework/platform/internal/mail 0.159s
ok github.com/carlosframework/platform/internal/maintenance 0.091s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.562s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.093s
ok github.com/carlosframework/platform/internal/metrics 0.005s
ok github.com/carlosframework/platform/internal/moves 21.131s
ok github.com/carlosframework/platform/internal/moves/archive 0.787s
ok github.com/carlosframework/platform/internal/objstore 0.174s
ok github.com/carlosframework/platform/internal/ops 0.161s
ok github.com/carlosframework/platform/internal/park 0.210s
ok github.com/carlosframework/platform/internal/privatefile 0.006s
ok github.com/carlosframework/platform/internal/provision 13.888s
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 4.121s
ok github.com/carlosframework/platform/internal/relay 7.237s
ok github.com/carlosframework/platform/internal/release 22.294s
ok github.com/carlosframework/platform/internal/rollout 3.806s
ok github.com/carlosframework/platform/internal/rolloutwords 0.004s
ok github.com/carlosframework/platform/internal/rootaws 0.007s
ok github.com/carlosframework/platform/internal/rootparticipation 0.008s
ok github.com/carlosframework/platform/internal/rootstate 0.025s
ok github.com/carlosframework/platform/internal/s3compat 0.006s
ok github.com/carlosframework/platform/internal/safefile 0.004s
ok github.com/carlosframework/platform/internal/schedule 2.345s
ok github.com/carlosframework/platform/internal/sim/hostlife 10.209s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 3.725s
ok github.com/carlosframework/platform/internal/status 0.042s
ok github.com/carlosframework/platform/internal/steering 0.008s
ok github.com/carlosframework/platform/internal/stopevidence 0.273s
ok github.com/carlosframework/platform/internal/svcrole 0.002s
ok github.com/carlosframework/platform/internal/telemetry 0.005s
ok github.com/carlosframework/platform/internal/termfence 0.005s
ok github.com/carlosframework/platform/internal/tunnelclient 0.004s
ok github.com/carlosframework/platform/internal/unixdial 0.003s
ok github.com/carlosframework/platform/internal/updatewords 0.006s
ok github.com/carlosframework/platform/internal/usage 0.004s
ok github.com/carlosframework/platform/internal/version 0.004s
ok github.com/carlosframework/platform/internal/vet 1.573s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
minio: cached, digest verified
mc: cached, digest verified
minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
buckets and scoped user ready
=== RUN TestDeliveredContractMinio
=== RUN TestDeliveredContractMinio/put
=== RUN TestDeliveredContractMinio/get_round-trips_the_body
=== RUN TestDeliveredContractMinio/list_shows_the_key
=== RUN TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN TestDeliveredContractMinio/delete_removes_it
=== RUN TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
--- PASS: TestDeliveredContractMinio/put (0.00s)
--- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
--- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
--- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
--- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok github.com/carlosframework/platform/internal/objstore 0.023s
=== RUN TestT4StorageProbeMinio
2026/10/14 13:44:37 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE694C2A5F1253, 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.021s
minio contract: PASS
diff --git a/internal/host/plan14_generation_census_root_linux_test.go b/internal/host/plan14_generation_census_root_linux_test.go| index 31a2932e1..043bde8cb 100644 |
| --- a/internal/host/plan14_generation_census_root_linux_test.go |
| +++ b/internal/host/plan14_generation_census_root_linux_test.go |
| @@ -1,4 +1,4 @@ |
| -//go:build linux |
| +//go:build linux && rootrunner |
| package host |
| @@ -8,7 +8,7 @@ import ( |
| "path/filepath" |
| "testing" |
| - "github.com/carlosframework/platform/internal/rootstate" |
| + "golang.org/x/sys/unix" |
| ) |
| func TestPlan14GenerationKernelCensusRootRunner(t *testing.T) { |
| @@ -44,32 +44,46 @@ func TestPlan14GenerationKernelCensusRootRunner(t *testing.T) { |
| if _, _, err := productionGenerationKernelCensus(t.Context()); err != nil { |
| t.Fatal("closed file still retained", err) |
| } |
| - return |
| - } |
| - parent := os.Getenv("PLAN14_GENERATION_INVENTORY_ROOT_RUNNER") |
| - if parent == "" { |
| - t.Skip("requires a designated disposable Linux root runner with unshare and cgroup support") |
| - } |
| - if os.Geteuid() != 0 { |
| - t.Fatal("designated runner must execute as root") |
| - } |
| - state, err := rootstate.Open(parent, false, true) |
| - if err != nil { |
| - t.Fatal(err) |
| - } |
| - state.Close() |
| - dir, err := os.MkdirTemp(parent, "generation-kernel-") |
| - if err != nil { |
| - t.Fatal(err) |
| - } |
| - t.Cleanup(func() { |
| - if err := os.RemoveAll(dir); err != nil { |
| - t.Error(err) |
| + if err := os.WriteFile(path, []byte("root census reference"), 0644); err != nil { |
| + t.Fatal(err) |
| + } |
| + alias := filepath.Join(dir, "alias") |
| + if err := os.Mkdir(alias, 0755); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := unix.Mount(filepath.Dir(path), alias, "", unix.MS_BIND, ""); err != nil { |
| + t.Fatal(err) |
| } |
| - }) |
| - if err := os.Chmod(dir, 0755); err != nil { |
| - t.Fatal(err) |
| + mounted := true |
| + t.Cleanup(func() { |
| + if mounted { |
| + if err := unix.Unmount(alias, 0); err != nil { |
| + t.Error(err) |
| + } |
| + } |
| + }) |
| + if _, _, err := productionGenerationKernelCensus(t.Context()); err == nil { |
| + t.Fatal("bind alias certified") |
| + } |
| + if err := unix.Unmount(alias, 0); err != nil { |
| + t.Fatal(err) |
| + } |
| + mounted = false |
| + ns, err := os.Open("/proc/self/ns/mnt") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, _, err := productionGenerationKernelCensus(t.Context()); err == nil { |
| + ns.Close() |
| + t.Fatal("retained namespace descriptor certified") |
| + } |
| + ns.Close() |
| + if _, _, err := productionGenerationKernelCensus(t.Context()); err != nil { |
| + t.Fatal("released references still retained", err) |
| + } |
| + return |
| } |
| + dir := generationRootRunnerDir(t) |
| // A private PID/mount namespace gives the scan a controlled all-root census. |
| // No generated tenant unit or running-tenant containment is accepted here. |
| cmd := exec.CommandContext(t.Context(), "unshare", "--mount", "--pid", "--fork", "--mount-proc", os.Args[0], "-test.run=^TestPlan14GenerationKernelCensusRootRunner$", "-test.count=1") |
diff --git a/internal/host/plan14_generation_inventory_root_test.go b/internal/host/plan14_generation_inventory_root_test.go| index 88bca6946..97012a507 100644 |
| --- a/internal/host/plan14_generation_inventory_root_test.go |
| +++ b/internal/host/plan14_generation_inventory_root_test.go |
| @@ -1,4 +1,4 @@ |
| -//go:build linux |
| +//go:build linux && rootrunner |
| package host |
| @@ -14,31 +14,7 @@ import ( |
| ) |
| func TestPlan14GenerationInventoryCheckedRootReader(t *testing.T) { |
| - parent := os.Getenv("PLAN14_GENERATION_INVENTORY_ROOT_RUNNER") |
| - if parent == "" { |
| - t.Skip("requires a designated disposable root runner and its publicly traversable scratch parent") |
| - } |
| - if os.Geteuid() != 0 { |
| - t.Fatal("designated runner must execute as root") |
| - } |
| - check, err := rootstate.Open(parent, false, true) |
| - if err != nil { |
| - t.Fatal(err) |
| - } |
| - check.Close() |
| - // Match the production journal ancestry checked by OpenTrustedFile. |
| - dir, err := os.MkdirTemp(parent, "generation-inventory-") |
| - if err != nil { |
| - t.Fatal(err) |
| - } |
| - t.Cleanup(func() { |
| - if err := os.RemoveAll(dir); err != nil { |
| - t.Error(err) |
| - } |
| - }) |
| - if err := os.Chmod(dir, 0755); err != nil { |
| - t.Fatal(err) |
| - } |
| + dir := generationRootRunnerDir(t) |
| censusPath, fencesPath, journalPath := filepath.Join(dir, "census"), filepath.Join(dir, "fences"), filepath.Join(dir, "journals.db") |
| census, err := rootstate.Open(censusPath, true, false) |
| if err != nil { |
| @@ -120,3 +96,28 @@ func TestPlan14GenerationInventoryCheckedRootReader(t *testing.T) { |
| }) |
| } |
| } |
| + |
| +func generationRootRunnerDir(t *testing.T) string { |
| + t.Helper() |
| + if os.Geteuid() != 0 { |
| + t.Fatal("designated runner must execute as root") |
| + } |
| + dir, err := os.MkdirTemp("/var/lib", "plan14-generation-") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + t.Cleanup(func() { |
| + if err := os.RemoveAll(dir); err != nil { |
| + t.Error(err) |
| + } |
| + }) |
| + if err := os.Chmod(dir, 0755); err != nil { |
| + t.Fatal(err) |
| + } |
| + check, err := rootstate.Open(dir, false, true) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + check.Close() |
| + return dir |
| +} |
diff --git a/internal/host/plan14_generation_kernel_linux_test.go b/internal/host/plan14_generation_kernel_linux_test.go| index e390bfd8d..60ad8f0f3 100644 |
| --- a/internal/host/plan14_generation_kernel_linux_test.go |
| +++ b/internal/host/plan14_generation_kernel_linux_test.go |
| @@ -68,6 +68,25 @@ func generationRelink(t *testing.T, target, path string) { |
| generationLink(t, target, path) |
| } |
| +func TestPlan14GenerationKernelOwnDescriptors(t *testing.T) { |
| + f := generationProc(t) |
| + if err := os.Remove(f.task + "/fd"); err != nil { |
| + t.Fatal(err) |
| + } |
| + generationLink(t, "/proc/self/fd", f.task+"/fd") |
| + if _, err := (generationKernel{proc: f.proc}).absent(t.Context(), f.reservation); err != nil { |
| + t.Fatal("scanner's own descriptor disappeared during inspection", err) |
| + } |
| + file, err := os.Open(f.reservation.Binding.Path) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer file.Close() |
| + if _, err := (generationKernel{proc: f.proc}).absent(t.Context(), f.reservation); err == nil { |
| + t.Fatal("real open staged file ignored") |
| + } |
| +} |
| + |
| func TestPlan14GenerationKernelReferences(t *testing.T) { |
| for _, attack := range []string{"none", "exe", "fd", "deleted-fd", "cwd", "root", "mapped", "mount", "foreign-mount-ns", "foreign-pid-ns", "namespace-fd", "uncontained", "malformed-stat", "malformed-maps", "missing-status", "missing-fds", "missing-mounts", "missing-boot", "cancel", "thread"} { |
| t.Run(attack, func(t *testing.T) { |
diff --git a/internal/host/plan14_generation_retirement_binding_test.go b/internal/host/plan14_generation_retirement_binding_test.go| index 8a6ea95b8..41036596c 100644 |
| --- a/internal/host/plan14_generation_retirement_binding_test.go |
| +++ b/internal/host/plan14_generation_retirement_binding_test.go |
| @@ -3,8 +3,10 @@ package host |
| import ( |
| "context" |
| "os" |
| + "strings" |
| "testing" |
| + "github.com/carlosframework/platform/internal/hostbox" |
| "github.com/carlosframework/platform/internal/release" |
| "github.com/carlosframework/platform/internal/rootstate" |
| ) |
| @@ -105,6 +107,61 @@ func TestPlan14GenerationRetirementAfterLostStarted(t *testing.T) { |
| } |
| } |
| +func TestPlan14GenerationRetirementKeepsActivePin(t *testing.T) { |
| + for _, mode := range []string{"running", "running-wrong-controller", "running-wrong-inputs", "running-wrong-process", "settled-open-writers", "closed-unsettled", "closed", "wrong-controller", "wrong-inputs", "kernel"} { |
| + t.Run(mode, func(t *testing.T) { |
| + f, disk, _ := generationLaunchFixture(t) |
| + if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil { |
| + t.Fatal(err) |
| + } |
| + r := readGenerationLaunch(t, disk) |
| + if !strings.HasPrefix(mode, "running") && mode != "closed-unsettled" { |
| + if err := f.root.stop(t.Context(), f.peer, *f.req.Reservation); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + if !strings.HasPrefix(mode, "running") && mode != "settled-open-writers" { |
| + if err := f.journal.CloseWriters(t.Context(), f.target); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + if mode == "wrong-controller" || mode == "running-wrong-controller" { |
| + r.Binding.ControllerPID++ |
| + } |
| + if mode == "wrong-inputs" || mode == "running-wrong-inputs" { |
| + r.Binding.Inputs = "other" |
| + } |
| + if mode == "running-wrong-process" { |
| + r.Process.PID++ |
| + } |
| + calls := 0 |
| + v := generationRetirement{guard: f.root.Guard, journal: f.journal, kernel: func(context.Context, release.GenerationReservation) (string, error) { |
| + calls++ |
| + if mode == "kernel" { |
| + return "", os.ErrPermission |
| + } |
| + return r.Binding.Digest, nil |
| + }} |
| + err := v.retire(t.Context(), &release.RootGenerations{Store: disk}, r) |
| + got := readGenerationLaunch(t, disk) |
| + switch mode { |
| + case "running", "settled-open-writers", "closed-unsettled": |
| + if err != nil || calls != 0 || got != r { |
| + t.Fatalf("active pin changed or blocked the pass: %v calls=%d got=%+v", err, calls, got) |
| + } |
| + case "closed": |
| + if err != nil || calls != 1 || got.Retirement == "" { |
| + t.Fatalf("closed pin not retired: %v calls=%d got=%+v", err, calls, got) |
| + } |
| + default: |
| + if err == nil || got.Retirement != "" { |
| + t.Fatalf("uncertain pin released: %v %+v", err, got) |
| + } |
| + } |
| + }) |
| + } |
| +} |
| + |
| func TestPlan14GenerationRetirementCensusGate(t *testing.T) { |
| for _, flag := range []string{"", "1"} { |
| for _, mode := range []string{"legacy", "census", "unreadable"} { |
| @@ -155,3 +212,86 @@ func TestPlan14GenerationRetirementCensusGate(t *testing.T) { |
| } |
| } |
| } |
| + |
| +func TestPlan14GenerationRetirementContinuesPastActivePin(t *testing.T) { |
| + f, disk, _ := generationLaunchFixture(t) |
| + if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil { |
| + t.Fatal(err) |
| + } |
| + running := readGenerationLaunch(t, disk) |
| + target := f.target |
| + target.ControllerID = "closed-controller" |
| + prior, err := f.journal.Read(t.Context(), f.target) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + prior.Target, prior.Reservations = target, nil |
| + prior.Files, prior.ProducerIDs, prior.NoState = nil, nil, true |
| + if err := f.journal.Prelaunch(t.Context(), prior); err != nil { |
| + t.Fatal(err) |
| + } |
| + b := running.Binding |
| + b.Target = generationOwner(target) |
| + b.Attempt, b.Invocation = "second:main", "second-invocation" |
| + g := &release.RootGenerations{Store: disk} |
| + second, err := g.Reserve(b) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + unit := *f.req.Reservation |
| + unit.Target, unit.ID, unit.InvocationID = target, b.Attempt, b.Invocation |
| + if err := f.journal.IntendUnit(t.Context(), unit); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := f.journal.SettleUnit(t.Context(), target, unit.ID); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := f.journal.CloseWriters(t.Context(), target); err != nil { |
| + t.Fatal(err) |
| + } |
| + calls := 0 |
| + v := generationRetirement{guard: f.root.Guard, journal: f.journal, kernel: func(_ context.Context, r release.GenerationReservation) (string, error) { |
| + calls++ |
| + if r.Binding != second.Binding { |
| + t.Fatal("active reservation reached kernel retirement") |
| + } |
| + return r.Binding.Digest, nil |
| + }} |
| + if err := v.retireAll(t.Context(), g, []release.GenerationReservation{running, second}); err != nil { |
| + t.Fatal(err) |
| + } |
| + got, err := g.Read(running.Binding) |
| + if err != nil || got != running { |
| + t.Fatalf("active pin changed: %+v %v", got, err) |
| + } |
| + got, err = g.Read(second.Binding) |
| + if err != nil || calls != 1 || got.Retirement == "" { |
| + t.Fatalf("later closed pin not retired: %+v %v calls=%d", got, err, calls) |
| + } |
| + launch, err := f.journal.Read(t.Context(), f.target) |
| + if err != nil || launch.WritersClosed || launch.Reservations[0].State != hostbox.UnitStarted { |
| + t.Fatalf("active launch changed: %+v %v", launch, err) |
| + } |
| +} |
| + |
| +func TestPlan14GenerationRetirementKeepsPrelaunchPin(t *testing.T) { |
| + f, disk, _ := generationLaunchFixture(t) |
| + permit, err := f.root.generations.reserve(t.Context(), f.root, f.peer, f.req) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + r := permit.reservation |
| + v := generationRetirement{guard: f.root.Guard, journal: f.journal, kernel: func(context.Context, release.GenerationReservation) (string, error) { |
| + t.Fatal("prelaunch reservation reached kernel retirement") |
| + return "", os.ErrPermission |
| + }} |
| + if err := v.retire(t.Context(), &release.RootGenerations{Store: disk}, r); err != nil { |
| + t.Fatal(err) |
| + } |
| + if got := readGenerationLaunch(t, disk); got != r { |
| + t.Fatalf("prelaunch pin changed: %+v", got) |
| + } |
| + if err := v.closed(t.Context(), r); err == nil { |
| + t.Fatal("prelaunch mistaken for closed authority") |
| + } |
| +} |
diff --git a/internal/host/plan14_generation_retirement_root_linux_test.go b/internal/host/plan14_generation_retirement_root_linux_test.go| new file mode 100644 |
| index 000000000..f578133f4 |
| --- /dev/null |
| +++ b/internal/host/plan14_generation_retirement_root_linux_test.go |
| @@ -0,0 +1,112 @@ |
| +//go:build linux && rootrunner |
| + |
| +package host |
| + |
| +import ( |
| + "os" |
| + "os/exec" |
| + "path/filepath" |
| + "strconv" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/release" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| +) |
| + |
| +func TestPlan14GenerationKernelRetirementRootRunner(t *testing.T) { |
| + dir := os.Getenv("PLAN14_E25_RETIRE_KERNEL_CHILD") |
| + if dir == "" { |
| + dir = generationRootRunnerDir(t) |
| + cmd := exec.CommandContext(t.Context(), "unshare", "--mount", "--pid", "--fork", "--mount-proc", os.Args[0], "-test.run=^TestPlan14GenerationKernelRetirementRootRunner$", "-test.count=1") |
| + cmd.Dir = dir |
| + cmd.Env = append(os.Environ(), "PLAN14_E25_RETIRE_KERNEL_CHILD="+dir) |
| + if out, err := cmd.CombinedOutput(); err != nil { |
| + t.Fatalf("isolated root retirement: %v %s", err, out) |
| + } |
| + return |
| + } |
| + t.Setenv("CARLOS_OPT_DIR", dir) |
| + path := release.VersionPath(dir, "bab", "v1", "shop") |
| + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(path, []byte("retained generation"), 0644); err != nil { |
| + t.Fatal(err) |
| + } |
| + identity := func(path string) rootstate.FileIdentity { |
| + t.Helper() |
| + file, err := os.Open(path) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer file.Close() |
| + id, err := rootstate.Identity(file) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + return id |
| + } |
| + k := generationKernel{proc: "/proc"} |
| + boot, err := k.boot() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + controller := exec.CommandContext(t.Context(), "/bin/sleep", "60") |
| + controller.Dir = dir |
| + if err := controller.Start(); err != nil { |
| + t.Fatal(err) |
| + } |
| + joined := false |
| + t.Cleanup(func() { |
| + if !joined { |
| + _ = controller.Process.Kill() |
| + _ = controller.Wait() |
| + } |
| + }) |
| + raw, err := generationRead(filepath.Join("/proc", strconv.Itoa(controller.Process.Pid), "stat")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + start, err := generationStart(raw) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + r := release.GenerationReservation{Binding: release.GenerationBinding{ |
| + Binding: release.HistoryBinding{Host: filepath.Base(dir) + ".example", Account: "bab", App: "shop"}, |
| + Target: release.GenerationOwner{BootID: boot}, Version: "v1", Path: path, Tree: filepath.Dir(path), |
| + Executable: identity(path), TreeIdentity: identity(filepath.Dir(path)), |
| + ControllerPID: controller.Process.Pid, ControllerStart: start, |
| + }} |
| + if _, err := productionGenerationAbsence(t.Context(), r); err == nil { |
| + t.Fatal("live controller released") |
| + } |
| + if err := controller.Process.Kill(); err != nil { |
| + t.Fatal(err) |
| + } |
| + _ = controller.Wait() |
| + joined = true |
| + if digest, err := productionGenerationAbsence(t.Context(), r); err != nil || len(digest) != 64 { |
| + t.Fatalf("joined controller: %s %v", digest, err) |
| + } |
| + file, err := os.Open(path) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := productionGenerationAbsence(t.Context(), r); err == nil { |
| + file.Close() |
| + t.Fatal("controller join erased an open reference") |
| + } |
| + file.Close() |
| + if err := os.Rename(filepath.Dir(path), filepath.Dir(path)+".old"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Mkdir(filepath.Dir(path), 0755); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(path, []byte("replacement"), 0644); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := productionGenerationAbsence(t.Context(), r); err == nil { |
| + t.Fatal("replaced staged tree discharged the original identity") |
| + } |
| +} |
diff --git a/internal/host/rootgeneration_kernel_linux.go b/internal/host/rootgeneration_kernel_linux.go| index dc99ca0a6..524c4b140 100644 |
| --- a/internal/host/rootgeneration_kernel_linux.go |
| +++ b/internal/host/rootgeneration_kernel_linux.go |
| @@ -305,6 +305,33 @@ func generationRootTask(raw []byte) (bool, error) { |
| } |
| return kernel, nil |
| } |
| + |
| +func generationTaskFDs(ctx context.Context, path string, b release.GenerationBinding, refs map[rootstate.FileIdentity]bool) error { |
| + f, err := os.Open(path) |
| + if err != nil { |
| + return err |
| + } |
| + // Our own descriptor appears when scanning this process. Keep it open |
| + // until inspection finishes, so it cannot manufacture a missing entry. |
| + defer f.Close() |
| + names, err := f.Readdirnames(8193) |
| + if err != nil && err != io.EOF { |
| + return err |
| + } |
| + if len(names) > 8192 { |
| + return os.ErrPermission |
| + } |
| + for _, name := range names { |
| + if err := ctx.Err(); err != nil { |
| + return err |
| + } |
| + if err := generationLinkRef(filepath.Join(path, name), b, refs); err != nil { |
| + return err |
| + } |
| + } |
| + return nil |
| +} |
| + |
| func (k generationKernel) task(ctx context.Context, path string, b release.GenerationBinding, ns map[string]string, refs map[rootstate.FileIdentity]bool) (string, error) { |
| if err := ctx.Err(); err != nil { |
| return "", err |
| @@ -340,18 +367,9 @@ func (k generationKernel) task(ctx context.Context, path string, b release.Gener |
| return "", err |
| } |
| } |
| - fds, err := generationNames(path + "/fd") |
| - if err != nil { |
| + if err := generationTaskFDs(ctx, path+"/fd", b, refs); err != nil { |
| return "", err |
| } |
| - for _, fd := range fds { |
| - if err := ctx.Err(); err != nil { |
| - return "", err |
| - } |
| - if err := generationLinkRef(path+"/fd/"+fd, b, refs); err != nil { |
| - return "", err |
| - } |
| - } |
| maps, err := generationRead(path + "/maps") |
| if err != nil { |
| return "", err |
diff --git a/internal/host/rootgeneration_retirement.go b/internal/host/rootgeneration_retirement.go| index fe09845f9..2868c794e 100644 |
| --- a/internal/host/rootgeneration_retirement.go |
| +++ b/internal/host/rootgeneration_retirement.go |
| @@ -17,56 +17,74 @@ type generationRetirement struct { |
| kernel func(context.Context, release.GenerationReservation) (string, error) |
| } |
| -// closed verifies root's existing irreversible closure. It never stops a unit, |
| -// closes another controller's admission, or interprets a missing journal as join. |
| -func (v generationRetirement) closed(ctx context.Context, r release.GenerationReservation) error { |
| +func (v generationRetirement) closure(ctx context.Context, r release.GenerationReservation) (bool, error) { |
| if v.guard.Root == nil || v.journal == nil || v.journal.Remote() || v.kernel == nil { |
| - return os.ErrPermission |
| + return false, os.ErrPermission |
| } |
| b := r.Binding |
| t := b.Target |
| in, err := v.guard.Root.Installation(ctx, t.Host, hostguard.Captured{Incarnation: t.Incarnation, Stamp: t.Stamp}) |
| if err != nil { |
| - return err |
| + return false, err |
| } |
| // Installation admits an older capture only when root retained it as |
| // retired. Rebind preserves this identity's account, enrollment and migration. |
| // Its old launch journal and kernel obligations still have to settle below. |
| if in.Incarnation != t.Incarnation || in.Stamp < t.Stamp { |
| - return os.ErrPermission |
| + return false, os.ErrPermission |
| } |
| in.Stamp = t.Stamp |
| if in.Host != b.Binding.Host || in.Account != b.Binding.Account || in.App != b.Binding.App || in.Box != t.Box || in.Enrollment != t.Enrollment || in.InventoryRevision == 0 || in.Record != b.Record || rootHistoryInstallation(in) != b.Installation { |
| - return os.ErrPermission |
| + return false, os.ErrPermission |
| } |
| - launches, err := v.journal.HostLaunches(ctx, t.Host) |
| + launches, err := v.journal.GenerationLaunches(ctx) |
| if err != nil { |
| - return err |
| + return false, err |
| } |
| - matches := 0 |
| + matches, controllers := 0, 0 |
| + closed := false |
| for _, launch := range launches { |
| target := launch.Target |
| if target.Host != t.Host || target.Incarnation != t.Incarnation || target.Stamp != t.Stamp || target.Session != t.Session || target.Box != t.Box || target.Enrollment != t.Enrollment || target.BootID != t.BootID || target.ControllerID != t.ControllerID { |
| continue |
| } |
| - if !launch.WritersClosed || launch.ControllerPID != b.ControllerPID || launch.ProcessStart != b.ControllerStart { |
| - return os.ErrPermission |
| + controllers++ |
| + if launch.ControllerPID != b.ControllerPID || launch.ProcessStart != b.ControllerStart { |
| + return false, os.ErrPermission |
| } |
| for _, unit := range launch.Reservations { |
| if unit.ID != b.Attempt { |
| continue |
| } |
| - if !hostbox.IsUnitReservation(unit) || unit.State != hostbox.UnitSettled || !hostbox.SameLaunchOwner(unit.Target, target) || unit.Inputs != b.Inputs || unit.InvocationID != b.Invocation || unit.Executable.Device != b.Executable.Device || unit.Executable.Inode != b.Executable.Inode || unit.Group.RelativePath != instancectl.ClaimUnitControlGroup(t.Host) { |
| - return os.ErrPermission |
| + if !hostbox.IsUnitReservation(unit) || (unit.State != hostbox.UnitIntended && unit.State != hostbox.UnitStarted && unit.State != hostbox.UnitSettled) || !hostbox.SameLaunchOwner(unit.Target, target) || unit.Inputs != b.Inputs || unit.InvocationID != b.Invocation || unit.Executable.Device != b.Executable.Device || unit.Executable.Inode != b.Executable.Inode || unit.Group.RelativePath != instancectl.ClaimUnitControlGroup(t.Host) { |
| + return false, os.ErrPermission |
| } |
| p := r.Process |
| if p != (release.GenerationProcess{}) && (unit.PID != p.PID || unit.ProcessStart != p.Start || unit.Group.RelativePath != p.Group || unit.Group.InvocationID != p.Invocation || unit.Group.Device != p.GroupIdentity.Device || unit.Group.Inode != p.GroupIdentity.Inode) { |
| - return os.ErrPermission |
| + return false, os.ErrPermission |
| } |
| matches++ |
| + closed = launch.WritersClosed && unit.State == hostbox.UnitSettled |
| } |
| } |
| - if matches != 1 { |
| + // Reserve precedes IntendUnit. Keep that exact prelaunch obligation pinned. |
| + if controllers == 1 && matches == 0 && !r.Closed && r.Process == (release.GenerationProcess{}) { |
| + return false, nil |
| + } |
| + if controllers != 1 || matches != 1 || (r.Closed && !closed) { |
| + return false, os.ErrPermission |
| + } |
| + return closed, nil |
| +} |
| + |
| +// closed verifies root's existing irreversible closure. It never stops a unit, |
| +// closes another controller's admission, or interprets a missing journal as join. |
| +func (v generationRetirement) closed(ctx context.Context, r release.GenerationReservation) error { |
| + closed, err := v.closure(ctx, r) |
| + if err != nil { |
| + return err |
| + } |
| + if !closed { |
| return os.ErrPermission |
| } |
| return nil |
| @@ -88,7 +106,13 @@ func (v generationRetirement) verify(ctx context.Context, r release.GenerationRe |
| // The caller holds release exclusion; host exclusion remains held through both |
| // ledger commits and the kernel check, in the same order used by launch. |
| func (v generationRetirement) retire(ctx context.Context, g *release.RootGenerations, r release.GenerationReservation) error { |
| - return v.guard.WithLocal(ctx, r.Binding.Binding.Host, func() error { return g.Retire(ctx, r, v.closed, v.verify) }) |
| + return v.guard.WithLocal(ctx, r.Binding.Binding.Host, func() error { |
| + closed, err := v.closure(ctx, r) |
| + if err != nil || !closed { |
| + return err |
| + } |
| + return g.Retire(ctx, r, v.closed, v.verify) |
| + }) |
| } |
| func productionRetireGenerations(ctx context.Context, g *release.RootGenerations) error { |
| @@ -105,22 +129,21 @@ func productionRetireGenerations(ctx context.Context, g *release.RootGenerations |
| return err |
| } |
| defer state.Close() |
| - file, err := rootstate.OpenTrustedFile("/var/lib/carlos-claims/journals.db") |
| - if err != nil { |
| - return err |
| - } |
| - file.Close() |
| - journal, err := hostbox.OpenLaunchJournal("/var/lib/carlos-claims/journals.db") |
| + journal, err := openGenerationLaunchInventory("/var/lib/carlos-claims/journals.db") |
| if err != nil { |
| return err |
| } |
| defer journal.Close() |
| verifier := generationRetirement{guard: hostguard.Guard{DataDir: dataDir(), Root: hostguard.NewRootAuthority(state)}, journal: journal, kernel: productionGenerationAbsence} |
| + return verifier.retireAll(ctx, g, records) |
| +} |
| + |
| +func (v generationRetirement) retireAll(ctx context.Context, g *release.RootGenerations, records []release.GenerationReservation) error { |
| for _, r := range records { |
| if r.Retirement != "" { |
| continue |
| } |
| - if err := verifier.retire(ctx, g, r); err != nil { |
| + if err := v.retire(ctx, g, r); err != nil { |
| return err |
| } |
| } |
diff --git a/internal/provision/testdata/plan14-root-reads-e.tsv b/internal/provision/testdata/plan14-root-reads-e.tsv| index acb6645b3..5e004d0c8 100644 |
| --- a/internal/provision/testdata/plan14-root-reads-e.tsv |
| +++ b/internal/provision/testdata/plan14-root-reads-e.tsv |
| @@ -54,7 +54,7 @@ internal/release/roothistory_replace.go replacementReconstruction authority E1 S |
| internal/release/signedhead.go StageSignedHead authority E1 Canonical route is supplied by the root unit verifier. Verifies signed channel, digest ratchet and manifest/artifact before staging; unavailable pin census never authorizes restaging differing bytes. Fixtures: TestPlan14Incident20261006WithLedger and TestPlan14SignedNamedRecoveryAuthority. registry:Account@202,registry:Account@302,registry:Account@403,registry:App@214,registry:App@415,registry:Channel@222,registry:Host@434 ca02f04e55556ba0233dfa0848b0c533a9d9aa06cad6abd33cfd2968b3bd838a |
| internal/rootstate/link_linux.go ReadTrustedLink authority A,E1 Checks the full root-owned parent chain before descriptor-based live-link reading. Fixture: TestPlan14TrustedLiveLink; privileged acceptance remains required. file:Open@156 90f8e8de4e2c71f07b5d749cbe4bb1c1dfa426576a82138823449f56fc9ec5ab |
| internal/rootstate/link_linux.go directory.readLink authority A,E1 Pins the symlink inode with O_PATH and checks owner/type/link count before readlinkat. Fixture: TestPlan14TrustedLiveLink. file:Fstat@356,file:Openat@183,file:Readlinkat@554 a8559fe63dce48830916004ecfe7fbbe5d419568c7daf7a3073cf1b5bb12c3d7 |
| -@coverage internal/host 7a0f83f08620422a955ded1e353b5dd4e77aeaf0f07580c7bff85d7393923de2 |
| +@coverage internal/host a164d7f7e44edd30db1657361141593e267d4982d1d0cbca63677722429c5e0d |
| @coverage internal/release 2bb546a86c1ab49b02e424eb0b366b46776857a1249baa2d6fb61ead4209d6bd |
| @coverage internal/rootstate 6338f74a6b2059cf77cf478bd6111b37b441eb9d65a309464c378359d4bdac05 |
| internal/release/roothistory_status.go RootHistory.MigrationStatus authority E1 Exact completion binds the accepted authorization digest from durable private history to enrolled-root signed readback. Pending originals, changed evidence and canonical sources cannot supply retirement completion. Fixtures: TestPlan14HistoryReconstructionCompletion, TestPlan14HistoryRetirementCompletion, TestPlan14ReadReconstructionCompletion, TestPlan14ReadRetirementCompletion, TestPlan14RecoveryRetirementProductionCallback, TestPlan14RootHistoryReconstructionCrash, TestPlan14HistoryRetirementProcessCrash. file:Read@1062,file:Read@3891 f0ab7ad2d2d70f8c22ebfd575f5a1c6931f7eba30ccdba80ad43d0efffc88154 |
| @@ -233,7 +233,7 @@ internal/host/rootgeneration_kernel_linux.go generationIDRef authority E2 E21: b |
| 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. file:Readlink@312 18269bf4b64e05138dff05e7894d4925bc24bcc012fa64a336723d981a9c52dd |
| internal/host/rootgeneration_kernel_linux.go generationKernel.boot 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. c3cc84ed2af21ee0e57e91160561a3e4996e75990df2617a14865b084b176ced |
| internal/host/rootgeneration_kernel_linux.go generationKernel.controllerGone 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. fa19ddebc7bf12297313e1431e4d8e534949032ff495b37be0038252bad355d3 |
| -internal/host/rootgeneration_kernel_linux.go generationKernel.task 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. file:Readlink@607 d5d6f2f6b04c52914095e0060bee2fe715632493909e944eeecd90aac0fa5a36 |
| +internal/host/rootgeneration_kernel_linux.go generationKernel.task authority E2 E25: keep the descriptor-listing handle open through inspection, including when scanning the current process. Check every listed descriptor against paths and artifact identities. Fixtures: TestPlan14GenerationKernelOwnDescriptors and TestPlan14GenerationKernelReferences. Root-runner acceptance pending: TestPlan14GenerationKernelCensusRootRunner and TestPlan14GenerationKernelRetirementRootRunner. file:Readlink@607 f965b29343f3e5e5e464eaf1f4929f9ac4e5ff3c76b0e87750dad64839caece8 |
| internal/host/rootgeneration_kernel_linux.go generationLinkRef 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. file:Readlink@128,file:Stat@423 1b9492ac2f9066f0ab6de429fafd707a7c826db76f3ac7b244d924385130ef5c |
| internal/host/rootgeneration_kernel_linux.go generationMajor 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. 48d037aef3349cd11492bb1cf7e9608f05bfde15e004f67f3bcad62a0518ab82 |
| internal/host/rootgeneration_kernel_linux.go generationMaps 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. 7549bed808c1431fb382c48cddde8d1730713aba73b59467ad0677901d72becc |
| @@ -251,10 +251,10 @@ internal/host/rootgeneration_kernel_linux.go productionGenerationAbsence authori |
| internal/host/rootgeneration_kernel_linux.go type generationKernel 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. 5a8673c8cf4c7b2e660688c4cd0a1043271159cebb11835da2b00bdfef798f69 |
| internal/host/rootgeneration_kernel_linux.go type generationMount 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. b4cd115a225b26a7906357404f8d04692a8d1b062662f80672280972049a8acb |
| internal/host/rootgeneration_kernel_other.go productionGenerationAbsence authority E2 E21: non-Linux production generation retirement refuses. Darwin, Windows and Linux ARM64 compile gates; no non-Linux retirement authority. de47d997019075326e1d5513d7576390d085566fd69edc35efd00f067fb1db24 |
| -internal/host/rootgeneration_retirement.go generationRetirement.closed authority E2 E23: the root installation reader admits an old capture only from its retained retired set; restore that same-incarnation stamp solely to check the original installation identity. Require exact closed launch and independent kernel absence afterward. Fixtures: TestPlan14GenerationRetirementRetainedRebind, TestPlan14GenerationRetirementRootBinding. Running-tenant kernel acceptance remains pending. f871fd29171851a2300b68c947124d5d014ed97cdef45eef496645411df76107 |
| -internal/host/rootgeneration_retirement.go generationRetirement.retire authority E2 E21: release then host exclusion; root installation and exact settled launch with irreversibly closed writers bind retirement before and after independent kernel verification. Production invokes it from the census-only pin source. Fixtures: TestPlan14GenerationRetirementRootBinding, TestPlan14GenerationRetirementAfterLostStarted, TestPlan14GenerationRetirementBindingProcessCrash, TestPlan14GenerationRetirementCensusGate. Privileged production opener acceptance remains pending. e4caafad8e42625aa21927f447f5f50a82f0890778635b74c3498c1d86efe1e2 |
| +internal/host/rootgeneration_retirement.go generationRetirement.closed authority E2 E25: validate installation, controller, exact unit and process before retaining an active or prelaunch pin. Only closed writers plus a settled unit reach durable retirement and independent kernel absence. Use the checked read-only journal and continue past active pins. Fixtures: TestPlan14GenerationRetirementKeepsActivePin, TestPlan14GenerationRetirementKeepsPrelaunchPin, TestPlan14GenerationRetirementContinuesPastActivePin, TestPlan14GenerationRetirementBindingProcessCrash, TestPlan14GenerationRetirementCensusGate. Running-tenant completeness remains unfinished. e69c69a74695791f8432e24a33397e49c22c47288e1bba66e56f11e605bf9881 |
| +internal/host/rootgeneration_retirement.go generationRetirement.retire authority E2 E25: validate installation, controller, exact unit and process before retaining an active or prelaunch pin. Only closed writers plus a settled unit reach durable retirement and independent kernel absence. Use the checked read-only journal and continue past active pins. Fixtures: TestPlan14GenerationRetirementKeepsActivePin, TestPlan14GenerationRetirementKeepsPrelaunchPin, TestPlan14GenerationRetirementContinuesPastActivePin, TestPlan14GenerationRetirementBindingProcessCrash, TestPlan14GenerationRetirementCensusGate. Running-tenant completeness remains unfinished. d11a2871718fdd7bcbcca568ab0b0717496e92176d3bfd296007914bf6d08c99 |
| internal/host/rootgeneration_retirement.go generationRetirement.verify authority E2 E21: release then host exclusion; root installation and exact settled launch with irreversibly closed writers bind retirement before and after independent kernel verification. Production invokes it from the census-only pin source. Fixtures: TestPlan14GenerationRetirementRootBinding, TestPlan14GenerationRetirementAfterLostStarted, TestPlan14GenerationRetirementBindingProcessCrash, TestPlan14GenerationRetirementCensusGate. Privileged production opener acceptance remains pending. 7bf7540ad8bac41c5ea23d7bbf2f09c870b9bed78cf912fb9645c1a621289f70 |
| -internal/host/rootgeneration_retirement.go productionRetireGenerations authority E2 E21: release then host exclusion; root installation and exact settled launch with irreversibly closed writers bind retirement before and after independent kernel verification. Production invokes it from the census-only pin source. Fixtures: TestPlan14GenerationRetirementRootBinding, TestPlan14GenerationRetirementAfterLostStarted, TestPlan14GenerationRetirementBindingProcessCrash, TestPlan14GenerationRetirementCensusGate. Privileged production opener acceptance remains pending. file:Open@292 fbe2431f88b9f2bd904f046855b63f619d20d5bdde6bcb0dc6d962388046a3c7 |
| +internal/host/rootgeneration_retirement.go productionRetireGenerations authority E2 E25: validate installation, controller, exact unit and process before retaining an active or prelaunch pin. Only closed writers plus a settled unit reach durable retirement and independent kernel absence. Use the checked read-only journal and continue past active pins. Fixtures: TestPlan14GenerationRetirementKeepsActivePin, TestPlan14GenerationRetirementKeepsPrelaunchPin, TestPlan14GenerationRetirementContinuesPastActivePin, TestPlan14GenerationRetirementBindingProcessCrash, TestPlan14GenerationRetirementCensusGate. Running-tenant completeness remains unfinished. file:Open@292 0c2baaf1832ab91e36c93a70468428f3e2584c6bc69579ad55f11728477e4385 |
| internal/host/rootgeneration_retirement.go type generationRetirement authority E2 E21: release then host exclusion; root installation and exact settled launch with irreversibly closed writers bind retirement before and after independent kernel verification. Production invokes it from the census-only pin source. Fixtures: TestPlan14GenerationRetirementRootBinding, TestPlan14GenerationRetirementAfterLostStarted, TestPlan14GenerationRetirementBindingProcessCrash, TestPlan14GenerationRetirementCensusGate. Privileged production opener acceptance remains pending. 942978edbce75390dd3fcbfee5ec8b1d86102053f99da7915b7afdd89a503b8e |
| internal/release/rootgenerationpins.go RootGenerations.Reservations authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. file:Read@541,file:Read@890 87f58596d5807a0e9eaed6dbd0cf5ba4e21a0906fba5959a2d974a68e9365d8c |
| internal/host/movesheldrelease_production.go WithProductionMovesHeldRelease authority B2,C,E1,E2 Root held-release constructor requires root guard and private release store, authenticates peer and reads root health history. E22: root generation pins guard existing-tree artifact and manifest writes in adoption and moves-held staging/publication. Held snapshot is read under release exclusion before the leaf held-stage lock; failures remain restrictive for writes. Claims-off source returns no root pins. Fresh wholly absent version trees retain signed staging. Fixtures: TestPlan14GenerationSiblingWrites, TestPlan14GenerationVerifiedSiblingWrites, TestPlan14GenerationHeldStage (production source assembly, both flag values without census, lock order, pin/error/fresh cases). Production completeness remains unavailable; no new completeness grant. file:Open@1365,file:Read@1612 9981c473733dd5214ab5baf79fa4402e8112ec3ca9095d1ff742fcde10119d5f |
| @@ -307,3 +307,6 @@ internal/host/rootgeneration_inventory.go productionGenerationInventory authorit |
| internal/host/rootgeneration_inventory.go type generationInventory authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel tests are runner-pending. 398337e54b33efb37b21b181520f1bf9bcce48c34a0d66430f2e58505ae7bf00 |
| internal/host/rootgeneration_inventory.go type generationReconciler authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel tests are runner-pending. a979cdff1cd6311cf57d4a93e4501fb8dc2f65772d0013e622175b2a1f610d91 |
| internal/hostguard/generationinventory.go RootAuthority.GenerationInstallations authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel tests are runner-pending. 593768e6ba937cf2cfa2635c8f796f767d6c2354737a72073cc351f514faed20 |
| +internal/host/rootgeneration_retirement.go generationRetirement.closure authority E2 E25: validate installation, controller, exact unit and process before retaining an active or prelaunch pin. Only closed writers plus a settled unit reach durable retirement and independent kernel absence. Use the checked read-only journal and continue past active pins. Fixtures: TestPlan14GenerationRetirementKeepsActivePin, TestPlan14GenerationRetirementKeepsPrelaunchPin, TestPlan14GenerationRetirementContinuesPastActivePin, TestPlan14GenerationRetirementBindingProcessCrash, TestPlan14GenerationRetirementCensusGate. Running-tenant completeness remains unfinished. f698ed949535287a55d5a8fc103f7dc5bf0f2c5020e27ff96552f42852024d03 |
| +internal/host/rootgeneration_retirement.go generationRetirement.retireAll authority E2 E25: validate installation, controller, exact unit and process before retaining an active or prelaunch pin. Only closed writers plus a settled unit reach durable retirement and independent kernel absence. Use the checked read-only journal and continue past active pins. Fixtures: TestPlan14GenerationRetirementKeepsActivePin, TestPlan14GenerationRetirementKeepsPrelaunchPin, TestPlan14GenerationRetirementContinuesPastActivePin, TestPlan14GenerationRetirementBindingProcessCrash, TestPlan14GenerationRetirementCensusGate. Running-tenant completeness remains unfinished. c2ef0459f6836cceddd5dab65127f10323ffc797fe738d2c5f6040fadb1a6465 |
| +internal/host/rootgeneration_kernel_linux.go generationTaskFDs authority E2 E25: keep the descriptor-listing handle open through inspection, including when scanning the current process. Check every listed descriptor against paths and artifact identities. Fixtures: TestPlan14GenerationKernelOwnDescriptors and TestPlan14GenerationKernelReferences. Root-runner acceptance pending: TestPlan14GenerationKernelCensusRootRunner and TestPlan14GenerationKernelRetirementRootRunner. file:Open@143,file:Readdirnames@367 06417eff13beac0b675b55f2664183f938afee33a733d51bb571c9a0ad3d50dc |
diff --git a/internal/release/plan14_generation_completeness_root_test.go b/internal/release/plan14_generation_completeness_root_test.go| index 65b0e230c..2a7816f85 100644 |
| --- a/internal/release/plan14_generation_completeness_root_test.go |
| +++ b/internal/release/plan14_generation_completeness_root_test.go |
| @@ -1,3 +1,5 @@ |
| +//go:build linux && rootrunner |
| + |
| package release |
| import ( |
| @@ -10,20 +12,10 @@ import ( |
| ) |
| func TestPlan14GenerationCompletenessCheckedRootStore(t *testing.T) { |
| - parent := os.Getenv("PLAN14_GENERATION_ROOT_RUNNER") |
| - if parent == "" { |
| - t.Skip("requires a designated disposable root runner and its private scratch parent") |
| - } |
| if os.Geteuid() != 0 { |
| t.Fatal("designated runner must execute as root") |
| } |
| - // The runner supplies a root-owned private parent, outside production state. |
| - check, err := rootstate.Open(parent, false, false) |
| - if err != nil { |
| - t.Fatal(err) |
| - } |
| - check.Close() |
| - dir, err := os.MkdirTemp(parent, "generation-certificate-") |
| + dir, err := os.MkdirTemp("/var/lib", "plan14-generation-certificate-") |
| if err != nil { |
| t.Fatal(err) |
| } |