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>
11 files changed,
+1429
−29
internal/host/plan14_generation_kernel_linux_test.go+236 −0internal/host/plan14_generation_kernel_process_linux_test.go+140 −0internal/host/plan14_generation_retirement_binding_test.go+157 −0internal/host/plan14_generation_retirement_process_test.go+124 −0internal/host/plan14_recovery_pass_test.go+2 −0internal/host/rootgeneration_kernel_linux.go+556 −0internal/host/rootgeneration_kernel_other.go+14 −0internal/host/rootgeneration_retirement.go+121 −0internal/host/rootgenerationpins.go+9 −2internal/provision/testdata/plan14-root-reads-e.tsv+35 −6internal/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 |
| +} |