carlos / platform Public

WIP claim Plan 14 track E checkpoint 21 (not for merge): E2 production retirement caller, root installation and launch-journal binding, kernel reference checks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 9726694983ba80abfd4e0fa30ac7e1242f93bd4f parent c677a04
11 files changed, +1429 −29
  • internal/host/plan14_generation_kernel_linux_test.go +236 −0
  • internal/host/plan14_generation_kernel_process_linux_test.go +140 −0
  • internal/host/plan14_generation_retirement_binding_test.go +157 −0
  • internal/host/plan14_generation_retirement_process_test.go +124 −0
  • internal/host/plan14_recovery_pass_test.go +2 −0
  • internal/host/rootgeneration_kernel_linux.go +556 −0
  • internal/host/rootgeneration_kernel_other.go +14 −0
  • internal/host/rootgeneration_retirement.go +121 −0
  • internal/host/rootgenerationpins.go +9 −2
  • internal/provision/testdata/plan14-root-reads-e.tsv +35 −6
  • internal/release/rootgenerationpins.go +35 −21

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	350.614s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.048s
ok  	github.com/carlosframework/platform/cmd/moveprobe	93.348s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.707s
ok  	github.com/carlosframework/platform/internal/activator	235.011s
ok  	github.com/carlosframework/platform/internal/agentsock	0.018s
ok  	github.com/carlosframework/platform/internal/alerting	0.003s
ok  	github.com/carlosframework/platform/internal/analytics	0.019s
ok  	github.com/carlosframework/platform/internal/appconfig	2.340s
ok  	github.com/carlosframework/platform/internal/apperr	0.080s
ok  	github.com/carlosframework/platform/internal/applog	0.127s
ok  	github.com/carlosframework/platform/internal/awsinv	0.003s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.004s
ok  	github.com/carlosframework/platform/internal/boxid	0.810s
?   	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.002s
ok  	github.com/carlosframework/platform/internal/cgroup	0.703s
ok  	github.com/carlosframework/platform/internal/changelog	0.291s
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	368.507s
?   	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.666s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.009s
ok  	github.com/carlosframework/platform/internal/dbverify	0.005s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.003s
ok  	github.com/carlosframework/platform/internal/docsite	0.005s
ok  	github.com/carlosframework/platform/internal/domains	2.260s
ok  	github.com/carlosframework/platform/internal/doorkey	0.007s
ok  	github.com/carlosframework/platform/internal/economics	0.005s
ok  	github.com/carlosframework/platform/internal/edge	26.869s
?   	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.121s
ok  	github.com/carlosframework/platform/internal/gate	1.656s
ok  	github.com/carlosframework/platform/internal/genselect	0.014s
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.151s
ok  	github.com/carlosframework/platform/internal/host	138.271s
ok  	github.com/carlosframework/platform/internal/hostactors	0.390s
ok  	github.com/carlosframework/platform/internal/hostbox	2.189s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.503s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.013s
ok  	github.com/carlosframework/platform/internal/hostexec	5.492s
ok  	github.com/carlosframework/platform/internal/hostfence	0.030s
ok  	github.com/carlosframework/platform/internal/hostfiles	11.176s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.013s
ok  	github.com/carlosframework/platform/internal/hostguard	1.004s
ok  	github.com/carlosframework/platform/internal/hostops	1.400s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.012s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.013s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.444s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.142s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.012s
ok  	github.com/carlosframework/platform/internal/infraview	0.012s
ok  	github.com/carlosframework/platform/internal/instancectl	1.824s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.716s
ok  	github.com/carlosframework/platform/internal/instancefs	0.018s
ok  	github.com/carlosframework/platform/internal/instancekey	0.015s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.020s
ok  	github.com/carlosframework/platform/internal/layout	1.418s
ok  	github.com/carlosframework/platform/internal/lease	9.532s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.021s
ok  	github.com/carlosframework/platform/internal/ledger	0.010s
ok  	github.com/carlosframework/platform/internal/mail	0.158s
ok  	github.com/carlosframework/platform/internal/maintenance	0.075s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.563s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.075s
ok  	github.com/carlosframework/platform/internal/metrics	0.007s
ok  	github.com/carlosframework/platform/internal/moves	21.560s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.753s
ok  	github.com/carlosframework/platform/internal/objstore	0.174s
ok  	github.com/carlosframework/platform/internal/ops	0.156s
ok  	github.com/carlosframework/platform/internal/park	0.204s
ok  	github.com/carlosframework/platform/internal/privatefile	0.003s
ok  	github.com/carlosframework/platform/internal/provision	15.729s
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.236s
ok  	github.com/carlosframework/platform/internal/relay	7.288s
ok  	github.com/carlosframework/platform/internal/release	23.206s
ok  	github.com/carlosframework/platform/internal/rollout	3.937s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.003s
ok  	github.com/carlosframework/platform/internal/rootaws	0.007s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.009s
ok  	github.com/carlosframework/platform/internal/rootstate	0.026s
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.300s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	13.328s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.788s
ok  	github.com/carlosframework/platform/internal/status	0.042s
ok  	github.com/carlosframework/platform/internal/steering	0.005s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.227s
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.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.001s
ok  	github.com/carlosframework/platform/internal/vet	1.550s
=== 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.021s
=== RUN   TestT4StorageProbeMinio
2026/10/14 07:34:03 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE55134FC6EB9C, 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/internal/host/plan14_generation_kernel_linux_test.go b/internal/host/plan14_generation_kernel_linux_test.go
new file mode 100644
index 000000000..e390bfd8d
--- /dev/null
+++ b/internal/host/plan14_generation_kernel_linux_test.go
@@ -0,0 +1,236 @@
+//go:build linux
+
+package host
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/release"
+)
+
+type generationProcFixture struct {
+ proc, task, unrelated string
+ reservation release.GenerationReservation
+}
+
+func generationProc(t *testing.T) generationProcFixture {
+ t.Helper()
+ f, disk, _ := generationLaunchFixture(t)
+ if _, err := f.root.generations.reserve(t.Context(), f.root, f.peer, f.req); err != nil {
+ t.Fatal(err)
+ }
+ g := generationProcFixture{proc: t.TempDir(), unrelated: filepath.Join(t.TempDir(), "unrelated"), reservation: readGenerationLaunch(t, disk)}
+ if err := os.WriteFile(g.unrelated, []byte("other"), 0600); err != nil {
+ t.Fatal(err)
+ }
+ g.task = filepath.Join(g.proc, "41", "task", "41")
+ for _, p := range []string{g.task + "/fd", g.task + "/ns", g.proc + "/self/ns", g.proc + "/sys/kernel/random"} {
+ if err := os.MkdirAll(p, 0700); err != nil {
+ t.Fatal(err)
+ }
+ }
+ generationWrite(t, g.proc+"/sys/kernel/random/boot_id", "boot-fixture\n")
+ generationWrite(t, g.task+"/stat", "41 (test (child)) S 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 123 0\n")
+ generationWrite(t, g.task+"/status", "Uid:\t0\t0\t0\t0\nKthread:\t0\n")
+ generationWrite(t, g.task+"/maps", "")
+ generationWrite(t, g.task+"/mountinfo", fmt.Sprintf("1 0 %d:%d / / rw - tmpfs tmpfs rw\n", generationMajor(g.reservation.Binding.TreeIdentity.Device), generationMinor(g.reservation.Binding.TreeIdentity.Device)))
+ for _, ns := range []string{"mnt", "pid"} {
+ generationLink(t, "namespace-"+ns, g.proc+"/self/ns/"+ns)
+ generationLink(t, "namespace-"+ns, g.task+"/ns/"+ns)
+ }
+ for _, name := range []string{"exe", "cwd", "root"} {
+ generationLink(t, g.unrelated, g.task+"/"+name)
+ }
+ return g
+}
+func generationWrite(t *testing.T, p, s string) {
+ t.Helper()
+ if err := os.WriteFile(p, []byte(s), 0600); err != nil {
+ t.Fatal(err)
+ }
+}
+func generationLink(t *testing.T, target, path string) {
+ t.Helper()
+ if err := os.Symlink(target, path); err != nil {
+ t.Fatal(err)
+ }
+}
+func generationRelink(t *testing.T, target, path string) {
+ t.Helper()
+ if err := os.Remove(path); err != nil {
+ t.Fatal(err)
+ }
+ generationLink(t, target, path)
+}
+
+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) {
+ f := generationProc(t)
+ b := f.reservation.Binding
+ ctx := t.Context()
+ switch attack {
+ case "exe", "cwd", "root":
+ generationRelink(t, b.Path, f.task+"/"+attack)
+ case "fd":
+ generationLink(t, b.Path, f.task+"/fd/3")
+ case "deleted-fd":
+ file, err := os.Open(b.Path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer file.Close()
+ if err := os.Remove(b.Path); err != nil {
+ t.Fatal(err)
+ }
+ generationLink(t, fmt.Sprintf("/proc/%d/fd/%d", os.Getpid(), file.Fd()), f.task+"/fd/3")
+ case "mapped":
+ generationWrite(t, f.task+"/maps", fmt.Sprintf("1000-2000 r-xp 0000 %x:%x %d %s (deleted)\n", generationMajor(b.Executable.Device), generationMinor(b.Executable.Device), b.Executable.Inode, b.Path))
+ case "mount":
+ generationWrite(t, f.task+"/mountinfo", "2 1 0:1 "+b.Tree+" /bound ro - tmpfs tmpfs ro\n")
+ case "foreign-mount-ns":
+ generationRelink(t, "foreign", f.task+"/ns/mnt")
+ case "foreign-pid-ns":
+ generationRelink(t, "foreign", f.task+"/ns/pid")
+ case "namespace-fd":
+ generationLink(t, "mnt:[12345]", f.task+"/fd/3")
+ case "uncontained":
+ generationWrite(t, f.task+"/status", "Uid:\t1001\t1001\t1001\t1001\nKthread:\t0\n")
+ case "malformed-stat":
+ generationWrite(t, f.task+"/stat", "broken")
+ case "malformed-maps":
+ generationWrite(t, f.task+"/maps", "broken")
+ case "missing-status":
+ if err := os.Remove(f.task + "/status"); err != nil {
+ t.Fatal(err)
+ }
+ case "missing-fds":
+ if err := os.Remove(f.task + "/fd"); err != nil {
+ t.Fatal(err)
+ }
+ case "missing-mounts":
+ if err := os.Remove(f.task + "/mountinfo"); err != nil {
+ t.Fatal(err)
+ }
+ case "missing-boot":
+ if err := os.Remove(f.proc + "/sys/kernel/random/boot_id"); err != nil {
+ t.Fatal(err)
+ }
+ case "cancel":
+ var cancel context.CancelFunc
+ ctx, cancel = context.WithCancel(ctx)
+ cancel()
+ case "thread":
+ other := filepath.Join(filepath.Dir(f.task), "42")
+ if err := os.Mkdir(other, 0700); err != nil {
+ t.Fatal(err)
+ }
+ // A separate descriptor table on a nonleader must not be omitted.
+ for _, name := range []string{"stat", "status", "maps", "mountinfo", "ns", "exe", "cwd", "root"} {
+ generationLink(t, filepath.Join(f.task, name), filepath.Join(other, name))
+ }
+ if err := os.Mkdir(other+"/fd", 0700); err != nil {
+ t.Fatal(err)
+ }
+ generationLink(t, b.Path, other+"/fd/7")
+ }
+ evidence, err := (generationKernel{proc: f.proc}).absent(ctx, f.reservation)
+ if attack == "none" {
+ if err != nil || len(evidence) != 64 {
+ t.Fatalf("positive: %q %v", evidence, err)
+ }
+ } else if err == nil {
+ t.Fatalf("%s released kernel reference: %s", attack, evidence)
+ }
+ })
+ }
+}
+
+func TestPlan14GenerationKernelMappingIdentity(t *testing.T) {
+ f := generationProc(t)
+ b := f.reservation.Binding
+ // A renamed/deleted mapping is found by device/inode without its old path.
+ generationWrite(t, f.task+"/maps", fmt.Sprintf("1000-2000 r-xp 0000 %x:%x %d /renamed (deleted)\n", generationMajor(b.Executable.Device), generationMinor(b.Executable.Device), b.Executable.Inode))
+ if _, err := (generationKernel{proc: f.proc}).absent(t.Context(), f.reservation); err == nil {
+ t.Fatal("renamed mapping lost its pin")
+ }
+}
+
+func TestPlan14GenerationKernelControllerIdentity(t *testing.T) {
+ f := generationProc(t)
+ b := f.reservation.Binding
+ b.Target.BootID = "boot-fixture"
+ b.ControllerPID = 41
+ b.ControllerStart = "123"
+ generationLink(t, f.task+"/stat", f.proc+"/41/stat")
+ for _, test := range []struct {
+ name, start, boot string
+ allow bool
+ }{{"alive", "123", "boot-fixture", false}, {"pid-reused", "122", "boot-fixture", true}, {"reboot", "123", "old-boot", true}} {
+ t.Run(test.name, func(t *testing.T) {
+ b.ControllerStart, b.Target.BootID = test.start, test.boot
+ if err := (generationKernel{proc: f.proc}).controllerGone(t.Context(), b); (err == nil) != test.allow {
+ t.Fatalf("gone: %v", err)
+ }
+ })
+ }
+ b.ControllerPID = 99
+ if err := (generationKernel{proc: f.proc}).controllerGone(t.Context(), b); err != nil {
+ t.Fatal(err)
+ }
+ generationWrite(t, f.task+"/stat", strings.Repeat("x", 1<<20))
+ b.ControllerPID = 41
+ b.Target.BootID = "boot-fixture"
+ if err := (generationKernel{proc: f.proc}).controllerGone(t.Context(), b); err == nil {
+ t.Fatal("oversized stat accepted")
+ }
+}
+
+func TestPlan14GenerationKernelRetainedFiles(t *testing.T) {
+ for _, attack := range []string{"none", "path", "tree"} {
+ t.Run(attack, func(t *testing.T) {
+ f := generationProc(t)
+ b := f.reservation.Binding
+ switch attack {
+ case "path":
+ b.Executable.Inode++
+ case "tree":
+ b.TreeIdentity.Inode++
+ }
+ err := generationRetainedFiles(b, func(path string) (*os.File, error) {
+ file, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ info, err := file.Stat()
+ if err != nil || !info.Mode().IsRegular() {
+ file.Close()
+ return nil, os.ErrPermission
+ }
+ return file, nil
+ })
+ if (err == nil) != (attack == "none") {
+ t.Fatalf("retained files: %v", err)
+ }
+ })
+ }
+}
+
+func TestPlan14GenerationKernelSiblingAlias(t *testing.T) {
+ f := generationProc(t)
+ asset := filepath.Join(f.reservation.Binding.Tree, "asset")
+ generationWrite(t, asset, "sibling")
+ alias := filepath.Join(t.TempDir(), "aliased-asset")
+ if err := os.Link(asset, alias); err != nil {
+ t.Fatal(err)
+ }
+ generationLink(t, alias, f.task+"/fd/8")
+ if _, err := (generationKernel{proc: f.proc}).absent(t.Context(), f.reservation); err == nil {
+ t.Fatal("aliased sibling did not pin its tree")
+ }
+}
diff --git a/internal/host/plan14_generation_kernel_process_linux_test.go b/internal/host/plan14_generation_kernel_process_linux_test.go
new file mode 100644
index 000000000..df4823a88
--- /dev/null
+++ b/internal/host/plan14_generation_kernel_process_linux_test.go
@@ -0,0 +1,140 @@
+//go:build linux
+
+package host
+
+import (
+ "github.com/carlosframework/platform/internal/release"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strconv"
+ "testing"
+)
+
+func TestPlan14GenerationKernelRunningExecutable(t *testing.T) {
+ for _, change := range []string{"running", "deleted", "parent-renamed"} {
+ t.Run(change, func(t *testing.T) {
+ f := generationProc(t)
+ b := f.reservation.Binding
+ body, err := os.ReadFile("/bin/sleep")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(b.Path, body, 0700); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Chmod(b.Path, 0700); err != nil {
+ t.Fatal(err)
+ }
+ cmd := exec.Command(b.Path, "60")
+ cmd.Dir = t.TempDir()
+ if err := cmd.Start(); err != nil {
+ t.Fatal(err)
+ }
+ joined := false
+ defer func() {
+ if !joined {
+ _ = cmd.Process.Kill()
+ _ = cmd.Wait()
+ }
+ }()
+ proc := filepath.Join("/proc", strconv.Itoa(cmd.Process.Pid))
+ raw, err := generationRead(proc + "/stat")
+ if err != nil {
+ t.Fatal(err)
+ }
+ start, err := generationStart(raw)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // The copied binary kept the staged inode. Read the real proc link, not
+ // the fake launch fixture's claim that it observed a process.
+ generationRelink(t, proc+"/exe", f.task+"/exe")
+ switch change {
+ case "deleted":
+ if err := os.Remove(b.Path); err != nil {
+ t.Fatal(err)
+ }
+ case "parent-renamed":
+ if err := os.Rename(b.Tree, b.Tree+".old"); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if err := generationLinkRef(proc+"/exe", b); err == nil {
+ t.Fatal("running executable not found")
+ }
+ if _, err := (generationKernel{proc: f.proc}).absent(t.Context(), f.reservation); err == nil {
+ t.Fatal("running tenant released")
+ }
+ actual := b
+ actual.ControllerPID = cmd.Process.Pid
+ actual.ControllerStart = start
+ actual.Target.BootID, err = (generationKernel{proc: "/proc"}).boot()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := (generationKernel{proc: "/proc"}).controllerGone(t.Context(), actual); err == nil {
+ t.Fatal("live process reported joined")
+ }
+ if err := cmd.Process.Kill(); err != nil {
+ t.Fatal(err)
+ }
+ _ = cmd.Wait()
+ joined = true
+ if err := (generationKernel{proc: "/proc"}).controllerGone(t.Context(), actual); err != nil {
+ t.Fatal(err)
+ }
+ generationRelink(t, f.unrelated, f.task+"/exe")
+ _, err = (generationKernel{proc: f.proc}).absent(t.Context(), f.reservation)
+ if (err == nil) != (change != "parent-renamed") {
+ t.Fatalf("after join: %v", err)
+ }
+ })
+ }
+}
+
+func TestPlan14GenerationKernelCgroup(t *testing.T) {
+ for _, attack := range []string{"absent", "empty", "populated", "replaced", "unknown-child", "pending-present", "unreadable"} {
+ t.Run(attack, func(t *testing.T) {
+ f, d, _ := generationLaunchFixture(t)
+ if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil {
+ t.Fatal(err)
+ }
+ r := readGenerationLaunch(t, d)
+ root := t.TempDir()
+ group := filepath.Join(root, r.Process.Group)
+ if attack != "absent" {
+ if err := os.MkdirAll(group, 0700); err != nil {
+ t.Fatal(err)
+ }
+ id, err := stopIdentity(group)
+ if err != nil {
+ t.Fatal(err)
+ }
+ r.Process.GroupIdentity.Device, r.Process.GroupIdentity.Inode = id.Device, id.Inode
+ generationWrite(t, group+"/cgroup.events", "populated 0\n")
+ }
+ switch attack {
+ case "populated":
+ generationWrite(t, group+"/cgroup.events", "populated 1\n")
+ case "replaced":
+ r.Process.GroupIdentity.Inode++
+ case "unknown-child":
+ if err := os.Mkdir(group+"/child", 0700); err != nil {
+ t.Fatal(err)
+ }
+ case "pending-present":
+ r.Process = release.GenerationProcess{}
+ case "unreadable":
+ if err := os.Remove(group + "/cgroup.events"); err != nil {
+ t.Fatal(err)
+ }
+ }
+ err := generationGroupGone(t.Context(), root, r)
+ allow := attack == "absent" || attack == "empty"
+ if (err == nil) != allow {
+ t.Fatalf("group check=%v allow=%t", err, allow)
+ }
+ })
+ }
+}
diff --git a/internal/host/plan14_generation_retirement_binding_test.go b/internal/host/plan14_generation_retirement_binding_test.go
new file mode 100644
index 000000000..8a6ea95b8
--- /dev/null
+++ b/internal/host/plan14_generation_retirement_binding_test.go
@@ -0,0 +1,157 @@
+package host
+
+import (
+ "context"
+ "os"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/rootstate"
+)
+
+func TestPlan14GenerationRetirementRootBinding(t *testing.T) {
+ for _, attack := range []string{"none", "open-writers", "unsettled", "controller", "inputs", "invocation", "executable", "installation", "kernel", "no-kernel", "no-journal"} {
+ t.Run(attack, 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 attack != "unsettled" {
+ if err := f.root.stop(t.Context(), f.peer, *f.req.Reservation); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if attack != "open-writers" {
+ if err := f.journal.CloseWriters(t.Context(), f.target); err != nil {
+ t.Fatal(err)
+ }
+ }
+ b := r.Binding
+ switch attack {
+ case "controller":
+ b.ControllerPID++
+ case "inputs":
+ b.Inputs = "changed"
+ case "invocation":
+ b.Invocation = "changed"
+ case "executable":
+ b.Executable.Inode++
+ case "installation":
+ b.Installation = "changed"
+ }
+ if b != r.Binding {
+ r.Binding = b
+ }
+ calls := 0
+ verifier := generationRetirement{guard: f.root.Guard, journal: f.journal, kernel: func(context.Context, release.GenerationReservation) (string, error) {
+ calls++
+ if attack == "kernel" {
+ return "", os.ErrPermission
+ }
+ return r.Binding.Digest, nil
+ }}
+ if attack == "no-kernel" {
+ verifier.kernel = nil
+ }
+ if attack == "no-journal" {
+ verifier.journal = nil
+ }
+ err := verifier.closed(t.Context(), r)
+ if err == nil {
+ _, err = verifier.verify(t.Context(), r)
+ }
+ if attack == "none" {
+ if err != nil || calls != 1 {
+ t.Fatalf("positive: %v, kernel %d", err, calls)
+ }
+ } else if err == nil {
+ t.Fatal("unverified retirement accepted")
+ }
+ })
+ }
+}
+
+func TestPlan14GenerationRetirementAfterLostStarted(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)
+ }
+ if err := f.journal.IntendUnit(t.Context(), *f.req.Reservation); err != nil {
+ t.Fatal(err)
+ }
+ if err := f.journal.SettleUnit(t.Context(), f.target, f.req.Reservation.ID); err != nil {
+ t.Fatal(err)
+ }
+ if err := f.journal.CloseWriters(t.Context(), f.target); err != nil {
+ t.Fatal(err)
+ }
+ verifier := generationRetirement{guard: f.root.Guard, journal: f.journal, kernel: func(context.Context, release.GenerationReservation) (string, error) {
+ return permit.reservation.Binding.Digest, nil
+ }}
+ if err := verifier.retire(t.Context(), &release.RootGenerations{Store: disk}, permit.reservation); err != nil {
+ t.Fatal(err)
+ }
+ got, err := permit.ledger.Read(permit.reservation.Binding)
+ if err != nil || got.Retirement == "" {
+ t.Fatalf("pending not retired: %+v %v", got, err)
+ }
+ if err := f.journal.IntendUnit(t.Context(), *f.req.Reservation); err == nil {
+ t.Fatal("closed launch replayed")
+ }
+ if _, err := permit.ledger.Reserve(permit.reservation.Binding); err == nil {
+ t.Fatal("retired generation replayed")
+ }
+}
+
+func TestPlan14GenerationRetirementCensusGate(t *testing.T) {
+ for _, flag := range []string{"", "1"} {
+ for _, mode := range []string{"legacy", "census", "unreadable"} {
+ t.Run(flag+"/"+mode, func(t *testing.T) {
+ t.Setenv("CARLOS_HOST_CLAIM", flag)
+ 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 err := f.root.stop(t.Context(), f.peer, *f.req.Reservation); err != nil {
+ t.Fatal(err)
+ }
+ if err := f.journal.CloseWriters(t.Context(), f.target); err != nil {
+ t.Fatal(err)
+ }
+ opened, retired := 0, 0
+ source := productionRootGenerationPins().(generationPinSource)
+ source.legacy = func() (bool, error) {
+ if mode == "unreadable" {
+ return false, os.ErrPermission
+ }
+ return mode == "legacy", nil
+ }
+ source.open = func() (rootstate.Store, error) { opened++; return disk, nil }
+ source.retire = func(ctx context.Context, g *release.RootGenerations) error {
+ retired++
+ verifier := generationRetirement{guard: f.root.Guard, journal: f.journal, kernel: func(context.Context, release.GenerationReservation) (string, error) { return r.Binding.Digest, nil }}
+ return verifier.retire(ctx, g, r)
+ }
+ _, err := source.StagedPins(t.Context(), f.root.generations.optDir)
+ got := readGenerationLaunch(t, disk)
+ switch mode {
+ case "legacy":
+ if err != nil || opened != 0 || retired != 0 || got != r {
+ t.Fatalf("claims-off effects: %v %d %d %+v", err, opened, retired, got)
+ }
+ case "unreadable":
+ if err == nil || opened != 0 || retired != 0 || got != r {
+ t.Fatal("unknown census chose a path")
+ }
+ case "census":
+ if err == nil || opened != 1 || retired != 1 || got.Retirement == "" {
+ t.Fatalf("retirement substituted for completeness: %v %d %d %+v", err, opened, retired, got)
+ }
+ }
+ })
+ }
+ }
+}
diff --git a/internal/host/plan14_generation_retirement_process_test.go b/internal/host/plan14_generation_retirement_process_test.go
new file mode 100644
index 000000000..15b7b69d0
--- /dev/null
+++ b/internal/host/plan14_generation_retirement_process_test.go
@@ -0,0 +1,124 @@
+package host
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "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"
+)
+
+func TestPlan14GenerationRetirementBindingProcessCrash(t *testing.T) {
+ if dir := os.Getenv("PLAN14_E21_RETIRE_DIR"); dir != "" {
+ f, disk, rec := generationLaunchFixture(t)
+ state := &rollbackDisk{dir: filepath.Join(dir, "fences")}
+ disk.dir = filepath.Join(dir, "generations")
+ for _, path := range []string{state.dir, disk.dir} {
+ if err := os.Mkdir(path, 0700); err != nil {
+ t.Fatal(err)
+ }
+ }
+ f.root.Guard.Root = hostguard.NewRootAuthority(state)
+ installation := hostguard.RootInstallation{Host: rec.Host, Account: rec.Account, App: rec.App, Record: provision.RecordKey(rec.Account, rec.App, rec.Host), Incarnation: f.target.Incarnation, Stamp: f.target.Stamp, Box: f.target.Box, Enrollment: f.target.Enrollment, Migration: 1, InventoryRevision: 1}
+ if err := f.root.Guard.EstablishRoot(t.Context(), installation, func(*hostclaim.Claim) error { return nil }); err != nil {
+ t.Fatal(err)
+ }
+ in, err := f.journal.Read(t.Context(), f.target)
+ if err != nil {
+ t.Fatal(err)
+ }
+ journal, err := hostbox.OpenLaunchJournal(filepath.Join(dir, "launch.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer journal.Close()
+ if err := journal.Prelaunch(t.Context(), in); err != nil {
+ t.Fatal(err)
+ }
+ f.root.Journal = journal
+ if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil {
+ t.Fatal(err)
+ }
+ r := readGenerationLaunch(t, disk)
+ if err := f.root.stop(t.Context(), f.peer, *f.req.Reservation); err != nil {
+ t.Fatal(err)
+ }
+ if err := journal.CloseWriters(t.Context(), f.target); err != nil {
+ t.Fatal(err)
+ }
+ phase := os.Getenv("PLAN14_E21_RETIRE_PHASE")
+ disk.afterWrite = func(_ string, raw []byte) {
+ var record release.GenerationReservation
+ if err := json.Unmarshal(raw, &record); err != nil {
+ t.Fatal(err)
+ }
+ if phase == "closed" && record.Closed && record.Retirement == "" || phase == "retired" && record.Retirement != "" {
+ os.Exit(75)
+ }
+ }
+ verifier := generationRetirement{guard: f.root.Guard, journal: journal, kernel: func(context.Context, release.GenerationReservation) (string, error) {
+ if phase == "kernel" {
+ os.Exit(75)
+ }
+ return r.Binding.Digest, nil
+ }}
+ if err := verifier.retire(t.Context(), &release.RootGenerations{Store: disk}, r); err != nil {
+ t.Fatal(err)
+ }
+ t.Fatal("crash boundary not reached")
+ }
+ for _, phase := range []string{"closed", "kernel", "retired"} {
+ t.Run(phase, func(t *testing.T) {
+ dir := t.TempDir()
+ cmd := exec.Command(os.Args[0], "-test.run=^TestPlan14GenerationRetirementBindingProcessCrash$", "-test.count=1")
+ cmd.Dir = dir
+ cmd.Env = append(os.Environ(), "PLAN14_E21_RETIRE_DIR="+dir, "PLAN14_E21_RETIRE_PHASE="+phase)
+ out, err := cmd.CombinedOutput()
+ var exit *exec.ExitError
+ if !errors.As(err, &exit) || exit.ExitCode() != 75 {
+ t.Fatalf("child: %v %s", err, out)
+ }
+ disk := &generationDisk{rollbackDisk: &rollbackDisk{dir: filepath.Join(dir, "generations")}}
+ r := readGenerationLaunch(t, disk)
+ g := &release.RootGenerations{Store: disk}
+ got, err := g.Read(r.Binding)
+ if err != nil || !got.Closed || (got.Retirement != "") != (phase == "retired") {
+ t.Fatalf("lost restriction: %+v %v", got, err)
+ }
+ if _, err := g.Reserve(r.Binding); err == nil {
+ t.Fatal("crash reopened launch")
+ }
+ journal, err := hostbox.OpenLaunchJournal(filepath.Join(dir, "launch.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer journal.Close()
+ verifier := generationRetirement{guard: hostguard.Guard{DataDir: filepath.Join(dir, "data"), LockDir: t.TempDir(), Root: hostguard.NewRootAuthority(&rollbackDisk{dir: filepath.Join(dir, "fences")})}, journal: journal, kernel: func(context.Context, release.GenerationReservation) (string, error) { return "", os.ErrPermission }}
+ if phase != "retired" {
+ if err := verifier.retire(t.Context(), g, r); err == nil {
+ t.Fatal("reopened without kernel evidence")
+ }
+ got, err = g.Read(r.Binding)
+ if err != nil || got.Retirement != "" {
+ t.Fatal("failed reconciliation released pin", err)
+ }
+ }
+ verifier.kernel = func(context.Context, release.GenerationReservation) (string, error) { return r.Binding.Digest, nil }
+ if err := verifier.retire(t.Context(), g, r); err != nil {
+ t.Fatal(err)
+ }
+ got, err = g.Read(r.Binding)
+ if err != nil || got.Retirement == "" {
+ t.Fatal("verified recovery not retained", err)
+ }
+ })
+ }
+}
diff --git a/internal/host/plan14_recovery_pass_test.go b/internal/host/plan14_recovery_pass_test.go
index f18e73872..6316f0644 100644
--- a/internal/host/plan14_recovery_pass_test.go
+++ b/internal/host/plan14_recovery_pass_test.go
@@ -1,3 +1,5 @@
+//go:build linux
+
package host
import (
diff --git a/internal/host/rootgeneration_kernel_linux.go b/internal/host/rootgeneration_kernel_linux.go
new file mode 100644
index 000000000..dc99ca0a6
--- /dev/null
+++ b/internal/host/rootgeneration_kernel_linux.go
@@ -0,0 +1,556 @@
+//go:build linux
+
+package host
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "io"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/instancectl"
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/rootstate"
+ "golang.org/x/sys/unix"
+)
+
+// This reader can prove absence only with launch exclusion held by its caller.
+// Uncontained service processes and foreign namespaces remain unavailable until
+// their reference-creation boundary can be established independently.
+type generationKernel struct{ proc string }
+
+func generationMajor(dev uint64) uint32 { return unix.Major(dev) }
+func generationMinor(dev uint64) uint32 { return unix.Minor(dev) }
+
+func generationRead(path string) ([]byte, error) {
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+ raw, err := io.ReadAll(io.LimitReader(f, rootstate.MaxRecord+1))
+ if err != nil {
+ return nil, err
+ }
+ if len(raw) > rootstate.MaxRecord {
+ return nil, os.ErrPermission
+ }
+ return raw, nil
+}
+func generationStart(raw []byte) (string, error) {
+ end := strings.LastIndexByte(string(raw), ')')
+ if end < 0 {
+ return "", os.ErrPermission
+ }
+ fields := strings.Fields(string(raw[end+1:]))
+ if len(fields) < 20 {
+ return "", os.ErrPermission
+ }
+ start, err := strconv.ParseUint(fields[19], 10, 64)
+ if err != nil || start == 0 {
+ return "", os.ErrPermission
+ }
+ return fields[19], nil
+}
+func (k generationKernel) boot() (string, error) {
+ raw, err := generationRead(filepath.Join(k.proc, "sys/kernel/random/boot_id"))
+ if err != nil {
+ return "", err
+ }
+ boot := strings.TrimSpace(string(raw))
+ if boot == "" {
+ return "", os.ErrPermission
+ }
+ return boot, nil
+}
+func (k generationKernel) controllerGone(ctx context.Context, b release.GenerationBinding) error {
+ if err := ctx.Err(); err != nil {
+ return err
+ }
+ boot, err := k.boot()
+ if err != nil {
+ return err
+ }
+ if b.ControllerPID <= 0 || b.ControllerStart == "" || b.Target.BootID == "" {
+ return os.ErrPermission
+ }
+ if boot != b.Target.BootID {
+ return nil
+ }
+ raw, err := generationRead(filepath.Join(k.proc, strconv.Itoa(b.ControllerPID), "stat"))
+ if errors.Is(err, os.ErrNotExist) {
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+ start, err := generationStart(raw)
+ if err != nil {
+ return err
+ }
+ if start == b.ControllerStart {
+ return os.ErrPermission
+ }
+ return nil
+}
+func generationNames(path string) ([]string, error) {
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+ names, err := f.Readdirnames(8193)
+ if err != nil && err != io.EOF {
+ return nil, err
+ }
+ if len(names) > 8192 {
+ return nil, os.ErrPermission
+ }
+ return names, nil
+}
+func generationPathRef(path string, b release.GenerationBinding) bool {
+ path = strings.TrimSuffix(path, " (deleted)")
+ return path == b.Path || path == b.Tree || strings.HasPrefix(path, b.Tree+"/")
+}
+func generationIDRef(id rootstate.FileIdentity, b release.GenerationBinding) bool {
+ return id == b.Executable || id == b.TreeIdentity
+}
+func generationLinkRef(path string, b release.GenerationBinding, refs ...map[rootstate.FileIdentity]bool) error {
+ name, err := os.Readlink(path)
+ if err != nil {
+ return err
+ }
+ // A retained namespace descriptor can keep mounts alive after its last task.
+ if strings.HasPrefix(name, "mnt:[") || strings.HasPrefix(name, "pid:[") || generationPathRef(name, b) {
+ return os.ErrPermission
+ }
+ var st unix.Stat_t
+ if err := unix.Stat(path, &st); err != nil {
+ return err
+ }
+ id := rootstate.FileIdentity{Device: uint64(st.Dev), Inode: st.Ino}
+ for _, set := range refs {
+ if set[id] {
+ return os.ErrPermission
+ }
+ }
+ if generationIDRef(id, b) {
+ return os.ErrPermission
+ }
+ return nil
+}
+func generationMaps(raw []byte, b release.GenerationBinding, refs ...map[rootstate.FileIdentity]bool) error {
+ for _, line := range strings.Split(strings.TrimSpace(string(raw)), "\n") {
+ if line == "" {
+ continue
+ }
+ fields := strings.Fields(line)
+ if len(fields) < 5 {
+ return os.ErrPermission
+ }
+ major, minor, ok := strings.Cut(fields[3], ":")
+ if !ok {
+ return os.ErrPermission
+ }
+ maj, e1 := strconv.ParseUint(major, 16, 32)
+ min, e2 := strconv.ParseUint(minor, 16, 32)
+ ino, e3 := strconv.ParseUint(fields[4], 10, 64)
+ if e1 != nil || e2 != nil || e3 != nil {
+ return os.ErrPermission
+ }
+ id := rootstate.FileIdentity{Device: unix.Mkdev(uint32(maj), uint32(min)), Inode: ino}
+ for _, set := range refs {
+ if ino != 0 && set[id] {
+ return os.ErrPermission
+ }
+ }
+ if ino != 0 && generationIDRef(rootstate.FileIdentity{Device: unix.Mkdev(uint32(maj), uint32(min)), Inode: ino}, b) {
+ return os.ErrPermission
+ }
+ if len(fields) > 5 && generationPathRef(strings.Join(fields[5:], " "), b) {
+ return os.ErrPermission
+ }
+ }
+ return nil
+}
+func generationUnescape(s string) (string, error) {
+ var out strings.Builder
+ for i := 0; i < len(s); i++ {
+ if s[i] != '\\' {
+ out.WriteByte(s[i])
+ continue
+ }
+ if i+3 >= len(s) {
+ return "", os.ErrPermission
+ }
+ switch s[i : i+4] {
+ case "\\040":
+ out.WriteByte(' ')
+ case "\\011":
+ out.WriteByte('\t')
+ case "\\012":
+ out.WriteByte('\n')
+ case "\\134":
+ out.WriteByte('\\')
+ default:
+ return "", os.ErrPermission
+ }
+ i += 3
+ }
+ return out.String(), nil
+}
+
+type generationMount struct {
+ id, root, point string
+ device uint64
+}
+
+func generationMounts(raw []byte, b release.GenerationBinding) error {
+ var mounts []generationMount
+ for _, line := range strings.Split(strings.TrimSpace(string(raw)), "\n") {
+ f := strings.Fields(line)
+ if len(f) < 10 {
+ return os.ErrPermission
+ }
+ separator := -1
+ for i := 6; i < len(f); i++ {
+ if f[i] == "-" {
+ separator = i
+ break
+ }
+ }
+ if separator < 6 || len(f) != separator+4 {
+ return os.ErrPermission
+ }
+ major, minor, ok := strings.Cut(f[2], ":")
+ if !ok {
+ return os.ErrPermission
+ }
+ maj, e1 := strconv.ParseUint(major, 10, 32)
+ min, e2 := strconv.ParseUint(minor, 10, 32)
+ root, e3 := generationUnescape(f[3])
+ point, e4 := generationUnescape(f[4])
+ if e1 != nil || e2 != nil || e3 != nil || e4 != nil || !filepath.IsAbs(root) || !filepath.IsAbs(point) {
+ return os.ErrPermission
+ }
+ if generationPathRef(root, b) || generationPathRef(point, b) {
+ return os.ErrPermission
+ }
+ mounts = append(mounts, generationMount{f[0], root, point, unix.Mkdev(uint32(maj), uint32(min))})
+ }
+ // Translate the canonical tree into the containing filesystem's namespace.
+ // Every additional mount of that tree, its contents or an ancestor keeps it.
+ var base generationMount
+ for _, m := range mounts {
+ rel, err := filepath.Rel(m.point, b.Tree)
+ if err == nil && filepath.IsLocal(rel) && len(m.point) > len(base.point) {
+ base = m
+ }
+ }
+ if base.id == "" || base.device != b.TreeIdentity.Device {
+ return os.ErrPermission
+ }
+ rel, err := filepath.Rel(base.point, b.Tree)
+ if err != nil {
+ return err
+ }
+ tree := filepath.Join(base.root, rel)
+ for _, m := range mounts {
+ if m.id == base.id || m.device != base.device {
+ continue
+ }
+ 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)) {
+ return os.ErrPermission
+ }
+ }
+ return nil
+}
+func generationRootTask(raw []byte) (bool, error) {
+ uid, kernel := false, false
+ for _, line := range strings.Split(string(raw), "\n") {
+ f := strings.Fields(line)
+ if len(f) == 0 {
+ continue
+ }
+ switch f[0] {
+ case "Uid:":
+ if uid || len(f) != 5 {
+ return false, os.ErrPermission
+ }
+ uid = true
+ for _, id := range f[1:] {
+ if id != "0" {
+ return false, os.ErrPermission
+ }
+ }
+ case "Kthread:":
+ if len(f) != 2 || (f[1] != "0" && f[1] != "1") {
+ return false, os.ErrPermission
+ }
+ kernel = f[1] == "1"
+ }
+ }
+ if !uid {
+ return false, os.ErrPermission
+ }
+ return kernel, 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
+ }
+ raw, err := generationRead(path + "/stat")
+ if err != nil {
+ return "", err
+ }
+ start, err := generationStart(raw)
+ if err != nil {
+ return "", err
+ }
+ status, err := generationRead(path + "/status")
+ if err != nil {
+ return "", err
+ }
+ kernel, err := generationRootTask(status)
+ if err != nil {
+ return "", err
+ }
+ if !kernel {
+ for name, want := range ns {
+ got, err := os.Readlink(path + "/ns/" + name)
+ if err != nil {
+ return "", err
+ }
+ if got != want {
+ return "", os.ErrPermission
+ }
+ }
+ for _, name := range []string{"exe", "cwd", "root"} {
+ if err := generationLinkRef(path+"/"+name, b, refs); err != nil {
+ return "", err
+ }
+ }
+ fds, err := generationNames(path + "/fd")
+ if 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
+ }
+ if err := generationMaps(maps, b, refs); err != nil {
+ return "", err
+ }
+ mounts, err := generationRead(path + "/mountinfo")
+ if err != nil {
+ return "", err
+ }
+ if err := generationMounts(mounts, b); err != nil {
+ return "", err
+ }
+ }
+ raw, err = generationRead(path + "/stat")
+ if err != nil {
+ return "", err
+ }
+ again, err := generationStart(raw)
+ if err != nil || again != start {
+ return "", os.ErrPermission
+ }
+ return start, nil
+}
+func (k generationKernel) absent(ctx context.Context, r release.GenerationReservation) (string, error) {
+ if err := ctx.Err(); err != nil {
+ return "", err
+ }
+ boot, err := k.boot()
+ if err != nil {
+ return "", err
+ }
+ ns := map[string]string{}
+ for _, name := range []string{"mnt", "pid"} {
+ ns[name], err = os.Readlink(filepath.Join(k.proc, "self/ns", name))
+ if err != nil || ns[name] == "" {
+ return "", os.ErrPermission
+ }
+ }
+ refs, err := generationTree(ctx, r.Binding)
+ if err != nil {
+ return "", err
+ }
+ names, err := generationNames(k.proc)
+ if err != nil {
+ return "", err
+ }
+ observations := map[string]string{}
+ count := 0
+ for _, name := range names {
+ pid, err := strconv.ParseUint(name, 10, 32)
+ if err != nil {
+ continue
+ }
+ if pid == 0 {
+ return "", os.ErrPermission
+ }
+ tasks, err := generationNames(filepath.Join(k.proc, name, "task"))
+ if err != nil {
+ return "", err
+ }
+ if len(tasks) == 0 {
+ return "", os.ErrPermission
+ }
+ for _, task := range tasks {
+ if id, err := strconv.ParseUint(task, 10, 32); err != nil || id == 0 {
+ return "", os.ErrPermission
+ }
+ count++
+ if count > 8192 {
+ return "", os.ErrPermission
+ }
+ start, err := k.task(ctx, filepath.Join(k.proc, name, "task", task), r.Binding, ns, refs)
+ if err != nil {
+ return "", err
+ }
+ observations[name+"/"+task] = start
+ }
+ }
+ if count == 0 {
+ return "", os.ErrPermission
+ }
+ again, err := k.boot()
+ if err != nil || boot != again {
+ return "", os.ErrPermission
+ }
+ raw, err := json.Marshal([]any{1, r, boot, ns, observations})
+ if err != nil {
+ return "", err
+ }
+ sum := sha256.Sum256(raw)
+ return hex.EncodeToString(sum[:]), nil
+}
+
+func productionGenerationAbsence(ctx context.Context, r release.GenerationReservation) (string, error) {
+ if os.Geteuid() != 0 {
+ return "", os.ErrPermission
+ }
+ if err := verifyStopKernelMounts("/sys/fs/cgroup", "/proc"); err != nil {
+ return "", err
+ }
+ if r.Binding.Path != release.VersionPath(release.OptDir(), r.Binding.Binding.Account, r.Binding.Version, r.Binding.Binding.App) {
+ return "", os.ErrPermission
+ }
+ if err := generationRetainedFiles(r.Binding, rootstate.OpenTrustedFile); err != nil {
+ return "", err
+ }
+ if err := generationGroupGone(ctx, "/sys/fs/cgroup", r); err != nil {
+ return "", err
+ }
+ k := generationKernel{proc: "/proc"}
+ if err := k.controllerGone(ctx, r.Binding); err != nil {
+ return "", 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 {
+ pid int
+ start string
+ }{{r.Process.PID, r.Process.Start}, {r.Process.TenantPID, r.Process.TenantStart}} {
+ if p.pid == 0 {
+ continue
+ }
+ b := r.Binding
+ b.ControllerPID, b.ControllerStart = p.pid, p.start
+ if err := k.controllerGone(ctx, b); err != nil {
+ return "", err
+ }
+ }
+ return k.absent(ctx, r)
+}
+
+func generationGroupGone(ctx context.Context, root string, r release.GenerationReservation) error {
+ group := instancectl.ClaimUnitControlGroup(r.Binding.Binding.Host)
+ path := filepath.Join(root, group)
+ if _, err := os.Lstat(path); errors.Is(err, os.ErrNotExist) {
+ return nil
+ } else if err != nil {
+ return err
+ }
+ if r.Process == (release.GenerationProcess{}) || r.Process.Group != group {
+ return os.ErrPermission
+ }
+ _, err := scanStopCgroups(ctx, root, []hostbox.StopCgroupIdentity{{RelativePath: group, Device: r.Process.GroupIdentity.Device, Inode: r.Process.GroupIdentity.Inode}})
+ return err
+}
+
+func generationRetainedFiles(b release.GenerationBinding, open func(string) (*os.File, error)) error {
+ f, err := open(b.Path)
+ if err != nil {
+ return err
+ }
+ executable, err := rootstate.Identity(f)
+ f.Close()
+ if err != nil || executable != b.Executable {
+ return os.ErrPermission
+ }
+ // The file opener checked every ancestor. The caller holds release
+ // exclusion, so another root release writer cannot replace this directory.
+ d, err := os.Open(b.Tree)
+ if err != nil {
+ return err
+ }
+ tree, err := rootstate.Identity(d)
+ d.Close()
+ if err != nil || tree != b.TreeIdentity {
+ return os.ErrPermission
+ }
+ return nil
+}
+
+func generationTree(ctx context.Context, b release.GenerationBinding) (map[rootstate.FileIdentity]bool, error) {
+ refs := map[rootstate.FileIdentity]bool{b.Executable: true, b.TreeIdentity: true}
+ count := 0
+ err := filepath.WalkDir(b.Tree, func(path string, entry fs.DirEntry, walkErr error) error {
+ if walkErr != nil {
+ return walkErr
+ }
+ if err := ctx.Err(); err != nil {
+ return err
+ }
+ count++
+ if count > 8192 || entry.Type()&os.ModeSymlink != 0 {
+ return os.ErrPermission
+ }
+ var st unix.Stat_t
+ if err := unix.Lstat(path, &st); err != nil {
+ return err
+ }
+ if st.Mode&unix.S_IFMT != unix.S_IFDIR && st.Mode&unix.S_IFMT != unix.S_IFREG {
+ return os.ErrPermission
+ }
+ id := rootstate.FileIdentity{Device: uint64(st.Dev), Inode: st.Ino}
+ if path == b.Tree && id != b.TreeIdentity {
+ return os.ErrPermission
+ }
+ refs[id] = true
+ return nil
+ })
+ return refs, err
+}
diff --git a/internal/host/rootgeneration_kernel_other.go b/internal/host/rootgeneration_kernel_other.go
new file mode 100644
index 000000000..610ceb6f2
--- /dev/null
+++ b/internal/host/rootgeneration_kernel_other.go
@@ -0,0 +1,14 @@
+//go:build !linux
+
+package host
+
+import (
+ "context"
+ "os"
+
+ "github.com/carlosframework/platform/internal/release"
+)
+
+func productionGenerationAbsence(context.Context, release.GenerationReservation) (string, error) {
+ return "", os.ErrPermission
+}
diff --git a/internal/host/rootgeneration_retirement.go b/internal/host/rootgeneration_retirement.go
new file mode 100644
index 000000000..591dfc3fd
--- /dev/null
+++ b/internal/host/rootgeneration_retirement.go
@@ -0,0 +1,121 @@
+package host
+
+import (
+ "context"
+ "os"
+
+ "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"
+)
+
+type generationRetirement struct {
+ guard hostguard.Guard
+ journal *hostbox.LaunchJournal
+ 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 {
+ if v.guard.Root == nil || v.journal == nil || v.journal.Remote() || v.kernel == nil {
+ return 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
+ }
+ 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
+ }
+ launches, err := v.journal.HostLaunches(ctx, t.Host)
+ if err != nil {
+ return err
+ }
+ matches := 0
+ 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
+ }
+ 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
+ }
+ 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
+ }
+ matches++
+ }
+ }
+ if matches != 1 {
+ return os.ErrPermission
+ }
+ return nil
+}
+func (v generationRetirement) verify(ctx context.Context, r release.GenerationReservation) (string, error) {
+ if err := v.closed(ctx, r); err != nil {
+ return "", err
+ }
+ evidence, err := v.kernel(ctx, r)
+ if err != nil {
+ return "", err
+ }
+ if err := v.closed(ctx, r); err != nil {
+ return "", err
+ }
+ return evidence, nil
+}
+
+// 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) })
+}
+
+func productionRetireGenerations(ctx context.Context, g *release.RootGenerations) error {
+ records, err := g.Reservations(ctx)
+ if err != nil {
+ return err
+ }
+ // Absence is not completeness. The pin reader still requires its verifier.
+ if len(records) == 0 {
+ return nil
+ }
+ state, err := rootstate.Open("/var/lib/carlos-claims/authority/fences", false, false)
+ if err != nil {
+ 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")
+ if err != nil {
+ return err
+ }
+ defer journal.Close()
+ verifier := generationRetirement{guard: hostguard.Guard{DataDir: dataDir(), Root: hostguard.NewRootAuthority(state)}, journal: journal, kernel: productionGenerationAbsence}
+ for _, r := range records {
+ if r.Retirement != "" {
+ continue
+ }
+ if err := verifier.retire(ctx, g, r); err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/internal/host/rootgenerationpins.go b/internal/host/rootgenerationpins.go
index 5dd3d58e2..196dc3f29 100644
--- a/internal/host/rootgenerationpins.go
+++ b/internal/host/rootgenerationpins.go
@@ -12,10 +12,11 @@ type generationPinSource struct {
legacy func() (bool, error)
open func() (rootstate.Store, error)
verify func(context.Context, []release.GenerationReservation) error
+ retire func(context.Context, *release.RootGenerations) error
}
func productionRootGenerationPins() release.PinSource {
- s := generationPinSource{legacy: legacyRootRestart, open: func() (rootstate.Store, error) {
+ s := generationPinSource{legacy: legacyRootRestart, retire: productionRetireGenerations, open: func() (rootstate.Store, error) {
return rootstate.Open(release.RootGenerationsDir, false, false)
}}
// Until root reconciles installation, launch and kernel inventory, Verify
@@ -44,5 +45,11 @@ func (s generationPinSource) StagedPins(ctx context.Context, optDir string) (rel
return nil, os.ErrPermission
}
defer store.Close()
- return (release.RootGenerationPins{Ledger: &release.RootGenerations{Store: store}, Verify: s.verify}).StagedPins(ctx, optDir)
+ ledger := &release.RootGenerations{Store: store}
+ if s.retire != nil {
+ if err := s.retire(ctx, ledger); err != nil {
+ return nil, err
+ }
+ }
+ return (release.RootGenerationPins{Ledger: ledger, Verify: s.verify}).StagedPins(ctx, optDir)
}
diff --git a/internal/provision/testdata/plan14-root-reads-e.tsv b/internal/provision/testdata/plan14-root-reads-e.tsv
index 7362d1519..61fc894f5 100644
--- a/internal/provision/testdata/plan14-root-reads-e.tsv
+++ b/internal/provision/testdata/plan14-root-reads-e.tsv
@@ -54,8 +54,8 @@ 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 dbc936af7ac364d4e794d06ea27c95bc42e95ba7c177434f4249d4f712caeb15
-@coverage internal/release 93ee997f77af61acb63290480751458d379e27ebb79794357e07b0528d8f3ae0
+@coverage internal/host a42baa886227f073282129c57bba480a530022650acc1b0bb070aaf60206bc89
+@coverage internal/release 371a2ab81da755cce5c4e39eb7e250f68f0d644b2441a061d30ce6f366fefee9
@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
internal/host/legacyrestart.go restartEffectFailed authority B3 E8 merge ruling 5: optional identity failures preserve the same restart outcome; no second restart; real not-up takes main rollback. Fixtures: TestPlan14ERecoveryRestartOutcome, TestPlan14ERecoveryBindingRefusalKeepsPromotedFix, TestPlan14ERecoveryNotUpPrecedesBinding. b68bb65884a14fe0cf78a65f56ded4ea41584bba99af9195827d1ea191b340a9
@@ -211,15 +211,15 @@ internal/release/rootgenerations.go type GenerationProcess authority E2 E18: cla
internal/release/rootgenerations.go type GenerationReservation authority E2 E20: exact reservation closure precedes retirement verification; retained tombstones forbid replay and require sync recommit. Retired records release only their own pins; completeness remains a separate mandatory verifier, still unavailable in production. Fixtures: TestPlan14GenerationRetirementReleasesExactPin, TestPlan14GenerationRetirementRefusals, TestPlan14GenerationClosedReplayRefused, TestPlan14GenerationRetirementProcessCrash, TestPlan14GenerationRetirementKeepsIndependentReference, TestPlan14GenerationClaimsOffRestageMatchesMain. 1917db4836b3b18b45ce6bca1a55f6d5a8082e5de26af1710d0da25db0809551
internal/release/rootgenerations.go type RootGenerations authority E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. 240860014efc91f51ca7e3e920ffb5cfa98076340dc0c9e9972c35051e5eca84
internal/release/rootgenerations.go validGeneration authority E2 E20: exact reservation closure precedes retirement verification; retained tombstones forbid replay and require sync recommit. Retired records release only their own pins; completeness remains a separate mandatory verifier, still unavailable in production. Fixtures: TestPlan14GenerationRetirementReleasesExactPin, TestPlan14GenerationRetirementRefusals, TestPlan14GenerationClosedReplayRefused, TestPlan14GenerationRetirementProcessCrash, TestPlan14GenerationRetirementKeepsIndependentReference, TestPlan14GenerationClaimsOffRestageMatchesMain. 25b4b51c18290f5e169e4c8225710cf3d27078b7a89101ab069bc7a838e75335
-internal/host/rootgenerationpins.go generationPinSource.StagedPins authority E2 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree. 8484f0fc50fc55644ec8af81a3d89b6338cdbfb125ba40cd5cedf63a2b41a852
-internal/host/rootgenerationpins.go productionRootGenerationPins authority E2 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree. file:Open@158 fa0ccb940d808f9443e19aa4837cd36e9278add4210ee4b021b471b3bfb9dd04
-internal/host/rootgenerationpins.go type generationPinSource authority E2 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree. ccc17aa1e2436da6ae8518c3e2bce2d73a8b2a181d0de8bf33733ea49d12a974
+internal/host/rootgenerationpins.go generationPinSource.StagedPins authority E2 E21: production census-only pin reader calls exact retirement before the unchanged mandatory completeness verifier. No census performs no generation open or retirement. Fixtures: TestPlan14GenerationRetirementCensusGate, TestPlan14GenerationProductionModeGate, TestPlan14GenerationClaimsOffRestageMatchesMain. 25c90cc73e198e2a05c6e8069d35ddfbc72a5ac43ed6f5b90673bf015dce1800
+internal/host/rootgenerationpins.go productionRootGenerationPins authority E2 E21: production census-only pin reader calls exact retirement before the unchanged mandatory completeness verifier. No census performs no generation open or retirement. Fixtures: TestPlan14GenerationRetirementCensusGate, TestPlan14GenerationProductionModeGate, TestPlan14GenerationClaimsOffRestageMatchesMain. file:Open@195 8d0a7b9fc2bfd14808ec6ab8f52751fc563b4b1364f58ccf59f48ed1348b25f2
+internal/host/rootgenerationpins.go type generationPinSource authority E2 E21: production census-only pin reader calls exact retirement before the unchanged mandatory completeness verifier. No census performs no generation open or retirement. Fixtures: TestPlan14GenerationRetirementCensusGate, TestPlan14GenerationProductionModeGate, TestPlan14GenerationClaimsOffRestageMatchesMain. edbc03de4156cf4292a92928e190749d50b317041827b9b83123798882ee4481
internal/host/versionsweep.go sweepStagedVersions authority E2,J3,J4 root-route E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode. registry:GoodVersion@898,registry:GoodVersion@930,registry:PrevVersion@711,registry:PrevVersion@743,registry:Version@656,registry:Version@684,whole:Route@1231,whole:Route@435 e7fc68ed8c5ff2dc8a98daeb16c08f4d497b8016a3c459b6e4158721ba109843
internal/host/versionsweep.go sweepStagedVersionsPass authority E2 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree. efeae77306262f47556aeccb382409933995f80c52f989e469e088e714e7d888
internal/release/adopt.go stageFrom authority E2 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree. 2d40e6c3ad116e9ab3a97530f8d83404440c7095d5f2868044c2bd22f0190562
internal/release/adopt.go type passConfig authority E2 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree. 1726cb5cee66d8aa0b04c44ad758b05a6aec239bb6a963ec2cfec13431ab765c
internal/release/adopt.go type stageCheck authority E2 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree. b031290fdcac7eb495da71ffbe395b151504078d237ff279fe5fc2143e6b0f34
-internal/release/rootgenerationpins.go RootGenerationPins.StagedPins authority E2 E20: exact reservation closure precedes retirement verification; retained tombstones forbid replay and require sync recommit. Retired records release only their own pins; completeness remains a separate mandatory verifier, still unavailable in production. Fixtures: TestPlan14GenerationRetirementReleasesExactPin, TestPlan14GenerationRetirementRefusals, TestPlan14GenerationClosedReplayRefused, TestPlan14GenerationRetirementProcessCrash, TestPlan14GenerationRetirementKeepsIndependentReference, TestPlan14GenerationClaimsOffRestageMatchesMain. file:Read@486,file:Read@842 0ec35c923ccc30b86f2e8564b53a309a8ea93d23a49c7e78ed26a8cf7c35ce40
+internal/release/rootgenerationpins.go RootGenerationPins.StagedPins authority E2 E21: retirement and both destructive readers share strict root reservation enumeration and sync recommit. The independent completeness verifier remains mandatory and unset in production. Fixtures: TestPlan14GenerationRetirementCensusGate, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationRetirementReleasesExactPin, TestPlan14GenerationClaimsOffRestageMatchesMain. 5405788a77184059126c7dfbd2f2558e633aac6c0fe9e30daf48a7ea82aeb9f4
internal/release/rootgenerationpins.go WithRootGenerationPins authority E2 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree. befc123585ff625901fd8b0516b2fa73b51968db21a28fa0d64c417382d54f79
internal/release/rootgenerationpins.go generationFileIdentity authority E2 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree. file:Lstat@96,file:Open@222 20a8f6c7c1c20fc2007dd89f2244b7797faede8d43f2206cd534a8033183b6ff
internal/release/rootgenerationpins.go passConfig.generationWriteCheck authority E2 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree. file:Lstat@537 f32259e8d00c8a196c9326fdaec2de5727211bd61117a2e1620339f68791e6ee
@@ -228,3 +228,32 @@ internal/host/rootinstallation.go rootInstallationHostApp authority E2,J1 E20: r
internal/hostguard/rootview.go RootAuthority.Installation authority E2,J1 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. d819256332950ec26c1b541f8be21a6e8cea839708fcbd0f7b8367d118574f92
internal/release/rootgenerations.go RootGenerations.Retire authority E2 E20: exact reservation closure precedes retirement verification; retained tombstones forbid replay and require sync recommit. Retired records release only their own pins; completeness remains a separate mandatory verifier, still unavailable in production. Fixtures: TestPlan14GenerationRetirementReleasesExactPin, TestPlan14GenerationRetirementRefusals, TestPlan14GenerationClosedReplayRefused, TestPlan14GenerationRetirementProcessCrash, TestPlan14GenerationRetirementKeepsIndependentReference, TestPlan14GenerationClaimsOffRestageMatchesMain. file:Read@297 21bb00d11d6fd8474b4d0a556bd1af7e69dbf6bd75755daeb0f14481166f2a90
@coverage internal/hostguard c69ab7fe435f0f512d92538df6b505517a0b444f776c158d51fe16250edcde64
+internal/host/rootgeneration_kernel_linux.go generationGroupGone 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:Lstat@218 a4778a7342e51a58eb736c15463e75c93349690d1f07b0d840d201ae8f57ecc6
+internal/host/rootgeneration_kernel_linux.go generationIDRef 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. 949b4dddb141d7eafd4edc70b41f8dd337069f699ab30d86c46bc6fce986d40b
+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 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
+internal/host/rootgeneration_kernel_linux.go generationMinor 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. 91033cac8c511dd135d42ec7f319333b50f545cb5a72c4d667b9d41db64f85be
+internal/host/rootgeneration_kernel_linux.go generationMounts 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. 4e96c33e01ca5c38ed736d5c66ec7b1227b6eafeff45729b05abd507ad13a95d
+internal/host/rootgeneration_kernel_linux.go generationNames 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:Open@65,file:Readdirnames@149 1b3e2537c6d2f768d91517f2f7087bb2ff4a290f89c849e31b05de8b80ec9126
+internal/host/rootgeneration_kernel_linux.go generationPathRef 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. 1b980d5b7083aea4353446743ab723f8008963d6504d480c8898b7a0d0d94549
+internal/host/rootgeneration_kernel_linux.go generationRead 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:Open@62,file:ReadAll@144 6ccd7554fc1f5683c585eb4fd3613d8f2e834d5dfb6c77e4732cb003d1e53065
+internal/host/rootgeneration_kernel_linux.go generationRetainedFiles 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:Open@446 70869a008771c9b336b12c1a07915f196e2b78c4a12a9f61062bde8ce2f6f431
+internal/host/rootgeneration_kernel_linux.go generationRootTask 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. dc08dbd3f09c33fb0012566ee77d0c8541eb4eee418549120dd40488e793ed3b
+internal/host/rootgeneration_kernel_linux.go generationStart 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. 89515661c91a80144edf027064edf86878d9e2ec8f4ff313d172db26cffda38a
+internal/host/rootgeneration_kernel_linux.go generationTree 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:Lstat@529,file:WalkDir@216 be154099cae5ba8d363bf4ec43d7debe437867aff98fbbd7c335b8ac11e5e6ac
+internal/host/rootgeneration_kernel_linux.go generationUnescape 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. 4809282a7fbd2413b10e72886eb22d73abe245a5e267e43605982934e1cb6cc8
+internal/host/rootgeneration_kernel_linux.go productionGenerationAbsence 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. bb7efb2afc24229ad55129c4342bb1a6fc98fbadb42267d7039b5f549c4bdbf6
+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 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. e641da8055443fc03baae1a76548b76a863cd38f44c9c9b9f93e82f5b47749e0
+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.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 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 E21: retirement and both destructive readers share strict root reservation enumeration and sync recommit. The independent completeness verifier remains mandatory and unset in production. Fixtures: TestPlan14GenerationRetirementCensusGate, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationRetirementReleasesExactPin, TestPlan14GenerationClaimsOffRestageMatchesMain. file:Read@412,file:Read@761 773119e7f716096834b4e384b02e0f7672a2a27703d6760e745088bf62dbf709
diff --git a/internal/release/rootgenerationpins.go b/internal/release/rootgenerationpins.go
index 26c8d87bf..693866075 100644
--- a/internal/release/rootgenerationpins.go
+++ b/internal/release/rootgenerationpins.go
@@ -23,35 +23,16 @@ func (s RootGenerationPins) StagedPins(ctx context.Context, optDir string) (Stag
if s.Ledger == nil || s.Ledger.Store == nil || !filepath.IsAbs(optDir) {
return nil, os.ErrPermission
}
- names, err := s.Ledger.Store.Names()
+ records, err := s.Ledger.Reservations(ctx)
if err != nil {
return nil, err
}
- sort.Strings(names)
- records := make([]GenerationReservation, 0, len(names))
pins := StagedPaths{}
- for _, name := range names {
- if err := ctx.Err(); err != nil {
- return nil, err
- }
- raw, err := s.Ledger.Store.Read(name)
- if err != nil {
- return nil, err
- }
- var r GenerationReservation
- if len(raw) > rootstate.MaxRecord || strictCanonicalJSON(raw, &r) != nil || !validGeneration(r) || generationKey(r.Binding) != name {
- return nil, os.ErrPermission
- }
- // Read retries the directory sync before this record can inform a grant.
- r, err = s.Ledger.Read(r.Binding)
- if err != nil {
- return nil, err
- }
+ for _, r := range records {
b := r.Binding
if b.Path != VersionPath(optDir, b.Binding.Account, b.Version, b.Binding.App) {
return nil, os.ErrPermission
}
- records = append(records, r)
if r.Retirement != "" {
continue
}
@@ -139,3 +120,36 @@ func (c passConfig) generationWriteCheck(ctx context.Context, optDir, path strin
}
return nil
}
+
+// Reservations validates and recommits every retained record under release exclusion.
+func (g *RootGenerations) Reservations(ctx context.Context) ([]GenerationReservation, error) {
+ if g == nil || g.Store == nil {
+ return nil, os.ErrPermission
+ }
+ names, err := g.Store.Names()
+ if err != nil {
+ return nil, err
+ }
+ sort.Strings(names)
+ records := make([]GenerationReservation, 0, len(names))
+ for _, name := range names {
+ if err := ctx.Err(); err != nil {
+ return nil, err
+ }
+ raw, err := g.Store.Read(name)
+ if err != nil {
+ return nil, err
+ }
+ var r GenerationReservation
+ if len(raw) > rootstate.MaxRecord || strictCanonicalJSON(raw, &r) != nil || !validGeneration(r) || generationKey(r.Binding) != name {
+ return nil, os.ErrPermission
+ }
+ // Read retries the directory sync before this record can inform a grant.
+ r, err = g.Read(r.Binding)
+ if err != nil {
+ return nil, err
+ }
+ records = append(records, r)
+ }
+ return records, nil
+}