carlos / platform Public

WIP claim Plan 14 track E checkpoint 20 (not for merge): E2 launch lock fixes, generation-retirement ledger transition

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev c677a0431e31697db04f30e92a489c5862471659 parent 6a1a25e
11 files changed, +587 −23
  • internal/host/plan14_generation_lock_test.go +59 −0
  • internal/host/plan14_generation_retirement_test.go +90 −0
  • internal/host/rootgenerations_launch.go +1 −2
  • internal/host/rootinstallation.go +1 −2
  • internal/hostguard/rootview.go +11 −0
  • internal/provision/plan14_e_write_census_test.go +1 −0
  • internal/provision/testdata/plan14-root-reads-e.tsv +13 −9
  • internal/release/plan14_generation_retirement_process_test.go +99 −0
  • internal/release/plan14_generation_retirement_test.go +235 −0
  • internal/release/rootgenerationpins.go +4 −1
  • internal/release/rootgenerations.go +73 −9

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	353.729s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.162s
ok  	github.com/carlosframework/platform/cmd/moveprobe	92.247s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.708s
ok  	github.com/carlosframework/platform/internal/activator	235.150s
ok  	github.com/carlosframework/platform/internal/agentsock	0.015s
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.387s
ok  	github.com/carlosframework/platform/internal/apperr	0.071s
ok  	github.com/carlosframework/platform/internal/applog	0.148s
ok  	github.com/carlosframework/platform/internal/awsinv	0.004s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.004s
ok  	github.com/carlosframework/platform/internal/boxid	0.854s
?   	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.011s
ok  	github.com/carlosframework/platform/internal/certwarm	0.002s
ok  	github.com/carlosframework/platform/internal/cgroup	0.707s
ok  	github.com/carlosframework/platform/internal/changelog	0.304s
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	365.538s
?   	github.com/carlosframework/platform/internal/controlname	[no test files]
?   	github.com/carlosframework/platform/internal/controlsock	[no test files]
ok  	github.com/carlosframework/platform/internal/dbdoor	6.146s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.010s
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.120s
ok  	github.com/carlosframework/platform/internal/doorkey	0.006s
ok  	github.com/carlosframework/platform/internal/economics	0.005s
ok  	github.com/carlosframework/platform/internal/edge	26.218s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.008s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.126s
ok  	github.com/carlosframework/platform/internal/gate	1.655s
ok  	github.com/carlosframework/platform/internal/genselect	0.015s
ok  	github.com/carlosframework/platform/internal/geo	0.010s
ok  	github.com/carlosframework/platform/internal/geoip	0.030s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.153s
ok  	github.com/carlosframework/platform/internal/host	134.240s
ok  	github.com/carlosframework/platform/internal/hostactors	0.578s
ok  	github.com/carlosframework/platform/internal/hostbox	2.222s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.509s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.012s
ok  	github.com/carlosframework/platform/internal/hostexec	4.944s
ok  	github.com/carlosframework/platform/internal/hostfence	0.037s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.562s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.013s
ok  	github.com/carlosframework/platform/internal/hostguard	1.007s
ok  	github.com/carlosframework/platform/internal/hostops	1.459s
?   	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.012s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.453s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.126s
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.815s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.714s
ok  	github.com/carlosframework/platform/internal/instancefs	0.016s
ok  	github.com/carlosframework/platform/internal/instancekey	0.013s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.020s
ok  	github.com/carlosframework/platform/internal/layout	1.436s
ok  	github.com/carlosframework/platform/internal/lease	9.455s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.017s
ok  	github.com/carlosframework/platform/internal/ledger	0.010s
ok  	github.com/carlosframework/platform/internal/mail	0.170s
ok  	github.com/carlosframework/platform/internal/maintenance	0.092s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.535s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.067s
ok  	github.com/carlosframework/platform/internal/metrics	0.005s
ok  	github.com/carlosframework/platform/internal/moves	21.040s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.759s
ok  	github.com/carlosframework/platform/internal/objstore	0.181s
ok  	github.com/carlosframework/platform/internal/ops	0.160s
ok  	github.com/carlosframework/platform/internal/park	0.192s
ok  	github.com/carlosframework/platform/internal/privatefile	0.003s
ok  	github.com/carlosframework/platform/internal/provision	13.964s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.008s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	4.055s
ok  	github.com/carlosframework/platform/internal/relay	7.236s
ok  	github.com/carlosframework/platform/internal/release	21.457s
ok  	github.com/carlosframework/platform/internal/rollout	3.798s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.004s
ok  	github.com/carlosframework/platform/internal/rootaws	0.007s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.007s
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.318s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	9.938s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.833s
ok  	github.com/carlosframework/platform/internal/status	0.038s
ok  	github.com/carlosframework/platform/internal/steering	0.006s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.154s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.004s
ok  	github.com/carlosframework/platform/internal/termfence	0.004s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.002s
ok  	github.com/carlosframework/platform/internal/updatewords	0.004s
ok  	github.com/carlosframework/platform/internal/usage	0.005s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.564s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
  minio: cached, digest verified
  mc: cached, digest verified
  minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
  buckets and scoped user ready
=== RUN   TestDeliveredContractMinio
=== RUN   TestDeliveredContractMinio/put
=== RUN   TestDeliveredContractMinio/get_round-trips_the_body
=== RUN   TestDeliveredContractMinio/list_shows_the_key
=== RUN   TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN   TestDeliveredContractMinio/delete_removes_it
=== RUN   TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
    --- PASS: TestDeliveredContractMinio/put (0.00s)
    --- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
    --- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
    --- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
    --- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
    --- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok  	github.com/carlosframework/platform/internal/objstore	0.023s
=== RUN   TestT4StorageProbeMinio
2026/10/14 06:54:12 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE52E6B5CA1A5B, 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.023s
minio contract: PASS
diff --git a/internal/host/plan14_generation_lock_test.go b/internal/host/plan14_generation_lock_test.go
new file mode 100644
index 000000000..f5a5b3001
--- /dev/null
+++ b/internal/host/plan14_generation_lock_test.go
@@ -0,0 +1,59 @@
+package host
+
+import (
+ "context"
+ "os"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/instancectl"
+ "github.com/carlosframework/platform/internal/rootstate"
+)
+
+type generationExclusiveStore struct {
+ rootstate.Store
+ held bool
+ nested int
+}
+
+func (s *generationExclusiveStore) Lock(ctx context.Context, name string) (func(), error) {
+ if s.held {
+ s.nested++
+ return nil, os.ErrPermission
+ }
+ unlock, err := s.Store.Lock(ctx, name)
+ if err != nil {
+ return nil, err
+ }
+ s.held = true
+ return func() { s.held = false; unlock() }, nil
+}
+
+func TestPlan14GenerationLaunchDoesNotReenterRootLock(t *testing.T) {
+ for _, validator := range []bool{false, true} {
+ t.Run(map[bool]string{false: "reservation", true: "production-validator"}[validator], func(t *testing.T) {
+ f, disk, rec := generationLaunchFixture(t)
+ rec.Schema, rec.Channel = 1, "canary/console"
+ plan14PutRecord(t, f.root.generations.store, rec)
+ store := &generationExclusiveStore{Store: &rollbackDisk{dir: t.TempDir()}}
+ f.root.Guard.Root = hostguard.NewRootAuthority(store)
+ in := hostguard.RootInstallation{Host: rec.Host, Account: rec.Account, App: rec.App, Record: "apps/" + rec.Account + "/" + rec.App + "/instances/" + rec.Host + ".json", 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(), in, func(*hostclaim.Claim) error { return nil }); err != nil {
+ t.Fatal(err)
+ }
+ if validator {
+ f.root.ValidateExecutable = claimStagedExecutable(&instancectl.Server{OptDir: f.root.generations.optDir, HostApp: rootInstallationHostApp(f.root.Guard, f.root.generations.store)})
+ }
+ if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil {
+ t.Fatalf("launch refused: %v; recursive locks=%d", err, store.nested)
+ }
+ if store.nested != 0 {
+ t.Fatalf("recursive root locks: %d", store.nested)
+ }
+ if r := readGenerationLaunch(t, disk); r.Process.PID == 0 {
+ t.Fatal("launch was not recorded")
+ }
+ })
+ }
+}
diff --git a/internal/host/plan14_generation_retirement_test.go b/internal/host/plan14_generation_retirement_test.go
new file mode 100644
index 000000000..f99ea4b88
--- /dev/null
+++ b/internal/host/plan14_generation_retirement_test.go
@@ -0,0 +1,90 @@
+package host
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/rootstate"
+)
+
+func TestPlan14GenerationRetirementReleasesExactPin(t *testing.T) {
+ f, disk, _ := generationLaunchFixture(t)
+ permit, err := f.root.generations.reserve(t.Context(), f.root, f.peer, f.req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ r := permit.reservation
+ opt := f.root.generations.optDir
+ stageSweepVersion(t, opt, "bap", "v2", "other", 48*time.Hour)
+ stageSweepVersion(t, opt, "bap", "v3", "pending", 48*time.Hour)
+ pending := r.Binding
+ pending.Attempt, pending.Invocation = "pending-v3", "pending-v3"
+ pending.Binding.Account, pending.Binding.App, pending.Version = "bap", "pending", "v3"
+ pending.Path = release.VersionPath(opt, "bap", "v3", "pending")
+ pending.Tree = filepath.Dir(pending.Path)
+ for path, dst := range map[string]*rootstate.FileIdentity{pending.Path: &pending.Executable, pending.Tree: &pending.TreeIdentity} {
+ file, err := os.Open(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ *dst, err = rootstate.Identity(file)
+ file.Close()
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ p, err := permit.ledger.Reserve(pending)
+ if err != nil {
+ t.Fatal(err)
+ }
+ when := time.Now().Add(-48 * time.Hour)
+ if err := os.Chtimes(r.Binding.Path, when, when); err != nil {
+ t.Fatal(err)
+ }
+ source := generationPinSource{open: func() (rootstate.Store, error) { return disk, nil }, verify: func(context.Context, []release.GenerationReservation) error { return nil }}
+ // The injected verifier exercises the destructive consumer contract; this
+ // fixture does not establish production installation/kernel completeness.
+ rows := fakeLister{routes: []registry.Route{{Host: "other.example", Account: "bap", App: "other", Version: "v2"}}}
+ gen := noGenerations(t)
+ sweepStagedVersions(rows, opt, gen, 6*time.Hour, time.Now(), source)
+ if !staged(t, opt, "v1") {
+ t.Fatal("pending generation lost pin")
+ }
+ if err := permit.ledger.Retire(t.Context(), r, func(context.Context, release.GenerationReservation) error { return nil }, func(context.Context, release.GenerationReservation) (string, error) { return "", os.ErrPermission }); err == nil {
+ t.Fatal("retired without kernel witness")
+ }
+ sweepStagedVersions(rows, opt, gen, 6*time.Hour, time.Now(), source)
+ if !staged(t, opt, "v1") {
+ t.Fatal("incomplete retirement lost pin")
+ }
+ if err := permit.ledger.Retire(t.Context(), r, func(context.Context, release.GenerationReservation) error { return nil }, func(context.Context, release.GenerationReservation) (string, error) {
+ return strings.Repeat("a", 64), nil
+ }); err != nil {
+ t.Fatal(err)
+ }
+ sweepStagedVersions(rows, opt, gen, 72*time.Hour, time.Now(), source)
+ if !staged(t, opt, "v1") {
+ t.Fatal("retirement bypassed sweep grace")
+ }
+ sweepStagedVersions(rows, opt, gen, 6*time.Hour, time.Now(), source)
+ if staged(t, opt, "v1") || !staged(t, opt, "v2") || !staged(t, opt, "v3") {
+ t.Fatal("sweep did not release only the retired version")
+ }
+ if got, err := permit.ledger.Read(p.Binding); err != nil || got != p {
+ t.Fatal("pending V3 changed", err)
+ }
+ // Once swept, the retirement tombstone must still read without the old
+ // executable or tree, and must never permit the original launch again.
+ if _, err := source.StagedPins(t.Context(), opt); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := permit.ledger.Reserve(r.Binding); err == nil {
+ t.Fatal("swept reservation replayed")
+ }
+}
diff --git a/internal/host/rootgenerations_launch.go b/internal/host/rootgenerations_launch.go
index 3750ee2ec..b2e7d082a 100644
--- a/internal/host/rootgenerations_launch.go
+++ b/internal/host/rootgenerations_launch.go
@@ -43,11 +43,10 @@ func (s *rootGenerationSource) resolve(ctx context.Context, h *ClaimLaunchRoot,
return b, os.ErrPermission
}
t := q.Reservation.Target
- v, err := h.Guard.Root.View(ctx, t.Host, hostguard.Captured{Incarnation: t.Incarnation, Stamp: t.Stamp})
+ in, err := h.Guard.Root.Installation(ctx, t.Host, hostguard.Captured{Incarnation: t.Incarnation, Stamp: t.Stamp})
if err != nil {
return b, err
}
- in := v.Installation
if in.Host != t.Host || in.Incarnation != t.Incarnation || in.Stamp != t.Stamp || in.Box != t.Box || in.Enrollment != t.Enrollment || in.InventoryRevision == 0 || in.Record != provision.RecordKey(in.Account, in.App, in.Host) {
return b, os.ErrPermission
}
diff --git a/internal/host/rootinstallation.go b/internal/host/rootinstallation.go
index 2ee6f8685..52acf7f0c 100644
--- a/internal/host/rootinstallation.go
+++ b/internal/host/rootinstallation.go
@@ -17,7 +17,7 @@ func rootInstallationHostApp(guard hostguard.Guard, store release.Store) func(st
if err != nil {
return "", "", err
}
- view, err := guard.Root.View(context.Background(), host, captured)
+ in, err := guard.Root.Installation(context.Background(), host, captured)
if err != nil {
return "", "", err
}
@@ -29,7 +29,6 @@ func rootInstallationHostApp(guard hostguard.Guard, store release.Store) func(st
return "", "", os.ErrPermission
}
h := observed.Claim.Holder
- in := view.Installation
if h.Incarnation != in.Incarnation || h.Stamp != in.Stamp || h.Account != in.Account || h.App != in.App || h.Key != in.Record {
return "", "", os.ErrPermission
}
diff --git a/internal/hostguard/rootview.go b/internal/hostguard/rootview.go
index 5e33329a3..8b47f1877 100644
--- a/internal/hostguard/rootview.go
+++ b/internal/hostguard/rootview.go
@@ -14,6 +14,17 @@ type FenceView struct {
}
type FenceViewProvider func(context.Context, string, Captured) (FenceView, error)
+// Installation reads one atomic root record without acquiring the host lock.
+// A caller using it to authorize an effect must hold Guard.With's exclusion.
+// It does not confer admission or replace the guard's fresh claim checks.
+func (a *RootAuthority) Installation(ctx context.Context, host string, captured Captured) (RootInstallation, error) {
+ if err := ctx.Err(); err != nil {
+ return RootInstallation{}, err
+ }
+ v, err := a.viewLocked(host, captured)
+ return v.Installation, err
+}
+
func (a *RootAuthority) View(ctx context.Context, host string, captured Captured) (FenceView, error) {
if err := ctx.Err(); err != nil {
return FenceView{}, err
diff --git a/internal/provision/plan14_e_write_census_test.go b/internal/provision/plan14_e_write_census_test.go
index b99e6c4f8..340ad96b1 100644
--- a/internal/provision/plan14_e_write_census_test.go
+++ b/internal/provision/plan14_e_write_census_test.go
@@ -2,6 +2,7 @@ package provision
// Track E overlays the reviewed Part 1 write census without changing its baseline.
var plan14EWriteCensusOverlay = []censusEntry{
+ {"internal/host/rootgenerations_launch.go", "rootGenerationSource.resolve", "key-site", "key:RecordKey", "d69a3c0c86b03d5469708ac8bf084f4f516c0b61a18519d46cc3c0a0616f5381"},
{"internal/historyreadback/workflow.go", "Operation.deliverGrant", "nonrecord", "sel:Put", "fb410ef6274ef7de0997a804c837ad1ecf29d8826ba1c4dcda00c2ae986fc434"},
{"internal/host/rootinventory.go", "ClaimRootHTTP.commitRootInventoryPending", "key-site", "key:RecordKey", "cbc0c1fa8be968c6dd18a42ff200fe5765f25d35c07ed3ece3b18ad4d619d64c"},
{"internal/host/rootinventory.go", "ClaimRootHTTP.recoverRootInventory", "key-site", "key:RecordKey", "a828684ad31a409d3e1fc306216d907681f514cb813302eb965a040e5084feda"},
diff --git a/internal/provision/testdata/plan14-root-reads-e.tsv b/internal/provision/testdata/plan14-root-reads-e.tsv
index 068f646fb..7362d1519 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 255d2b64782c34c73f12965a1738eefcef8387ea769670d475efcd802821905e
-@coverage internal/release 07038b66266e63a9be3ce85b954d4955223ce280e1c2e3f93ae508ae8ffeb1dc
+@coverage internal/host dbc936af7ac364d4e794d06ea27c95bc42e95ba7c177434f4249d4f712caeb15
+@coverage internal/release 93ee997f77af61acb63290480751458d379e27ebb79794357e07b0528d8f3ae0
@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
@@ -193,24 +193,24 @@ internal/host/rootgenerations_launch.go generationLaunchPermit.check authority E
internal/host/rootgenerations_launch.go generationLaunchPermit.started 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. 8f8b0ab8295f39fd54dd8a7d78ef2653eb7bc3b2ac581c47f8657ed59de84326
internal/host/rootgenerations_launch.go productionRootGenerations 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. file:Open@346 a1132403ace7213de9e12720d5a393a3bb85045d1d2c53f0c37ea2ae8e933b04
internal/host/rootgenerations_launch.go rootGenerationSource.reserve 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. c4269e65bcac751cd1a51a439e48eda77b055320554b3489d605fea47fa7f3a8
-internal/host/rootgenerations_launch.go rootGenerationSource.resolve 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. file:Open@2514 1d66e9ffa6aca3ea4a2d370c04f782b01c8a15d9ae6ca93a9356632d2b032977
+internal/host/rootgenerations_launch.go rootGenerationSource.resolve authority E2 E20: read the atomic root installation under the existing host exclusion without recursive lock acquisition; retain fresh claim, canonical record, signed artifact and final reservation checks. Fixtures: TestPlan14GenerationLaunchDoesNotReenterRootLock, TestPlan14GenerationLaunchRefusals, TestPlan14RootDispatchRejectsRegistryMutation. file:Open@2501 d69a3c0c86b03d5469708ac8bf084f4f516c0b61a18519d46cc3c0a0616f5381
internal/host/rootgenerations_launch.go type generationLaunchPermit 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. 52b30a79d7ced7875f435eff76d3c924ff4e460d3a2360cda5a9f12c407a2442
internal/host/rootgenerations_launch.go type rootGenerationSource 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. 59a336027ef7750d36f1a36a27cf3d0449395493c05789291c4ed9bc16724e5f
internal/release/rootgenerations.go GenerationBinding.valid 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. 636d5eb63f80ec3f3f2b87b859c96ce09700948ea3f3a5d58f04e5b1cf5ac3d3
internal/release/rootgenerations.go GenerationProcess.valid 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. 6032100296677f5397c44226c3158eb75f336f0317af6994c4f3ebbfb2894861
-internal/release/rootgenerations.go RootGenerations.Check 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. file:Read@78 5bb55d8607353739eb9a8c96a7795ba0f9a0bcf6fc59974657620a313e26ce01
+internal/release/rootgenerations.go RootGenerations.Check 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@78 8ee72f46a5a45879f0625b90e11057d758b73586e5913067d2cd9493705fff83
internal/release/rootgenerations.go RootGenerations.Read 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. file:Read@206,file:Read@26 d77a3e6d7f8bdc2c16973910b3089aa7fb7c3c40b2d0b94f2ca7cdf32f9463b6
-internal/release/rootgenerations.go RootGenerations.Reserve 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. file:Read@397,file:Read@99 be082a4f3529a2dffc21dd5110db36581e0e1e4853ec63c6176a877bfbfaefae
-internal/release/rootgenerations.go RootGenerations.Started 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. file:Read@147 c1e96eedb87a4baf5e87e65cf0b63511e7e4cc1b8a71d882cd2cd2a880c370b8
+internal/release/rootgenerations.go RootGenerations.Reserve 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@409,file:Read@99 b6d4fe53dbd011641d69789f8f7926a6234fd819b9c455670b57e3385ffb0e58
+internal/release/rootgenerations.go RootGenerations.Started 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@147 932a9a6497e8d64c7d9366c504acdb94f012f899a32b4ae4ce8080371627c2b7
internal/release/rootgenerations.go RootGenerations.write 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. 72f8b4aa9f0487ee8b26843b5cbdc76aa5447d60535766cd4e4ce32f4baa5a2a
internal/release/rootgenerations.go const RootGenerationsDir 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. e235f24f29b1f8511349cae97fbbc3f3472b49fdc6781c781d5b6f93ff3d45ff
internal/release/rootgenerations.go generationKey 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. cdd7e0d8cdff508772ab54e421a4ab914dbb15ea5d97847f35a93544f0a9259a
internal/release/rootgenerations.go type GenerationBinding 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. 9bd76e1f6ef4d809df1a59719a1880215c910b0cab78449ad8cd79a678da401a
internal/release/rootgenerations.go type GenerationOwner 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. 6a6c3576622656c2bc4b65075c8f5d70b29968203101a1916926adc48b20bce5
internal/release/rootgenerations.go type GenerationProcess 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. eeaafe23c23e4c95e58c45797065baa9ca6c784fab593e361e0d647ddaaa2957
-internal/release/rootgenerations.go type GenerationReservation 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. 4d1b20a5f99117b6bc2e16c6a60033496f99796c47e682ab19bbddbdb6cc35e4
+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 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. 7fa482be0515f5b94acd6fe5e3072b2a45e89c043961e30ab764d0f719b9a7cd
+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
@@ -219,8 +219,12 @@ internal/host/versionsweep.go sweepStagedVersionsPass authority E2 E19: both des
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 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:Read@486,file:Read@842 3d76e660f2a5302f01d31f608b9d145e2b891794cde97eee586f9a30afb2d688
+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 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
internal/release/rootgenerationpins.go type RootGenerationPins 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. c680202be840bd6e420cc0c9ae192f6f73d259498e564a1c4dbaf413e3e944a1
+internal/host/rootinstallation.go rootInstallationHostApp 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. file:Read@488,registry:Account@1062,registry:App@1091 0bbe17aff8c82841ae47fb01e060e1e1a80865e635d4a5c95dc5a638d2754d1c
+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
diff --git a/internal/release/plan14_generation_retirement_process_test.go b/internal/release/plan14_generation_retirement_process_test.go
new file mode 100644
index 000000000..c4bb12024
--- /dev/null
+++ b/internal/release/plan14_generation_retirement_process_test.go
@@ -0,0 +1,99 @@
+package release
+
+import (
+ "context"
+ "encoding/json"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+type generationRetirementCrashStore struct {
+ generationListingStore
+ phase string
+}
+
+func (s generationRetirementCrashStore) Write(name string, body []byte) error {
+ if err := s.generationListingStore.Write(name, body); err != nil {
+ return err
+ }
+ var r GenerationReservation
+ if json.Unmarshal(body, &r) != nil {
+ return os.ErrPermission
+ }
+ if s.phase == "closed" && r.Closed && r.Retirement == "" {
+ os.Exit(74)
+ }
+ if s.phase == "retired" && r.Retirement != "" {
+ os.Exit(74)
+ }
+ return nil
+}
+
+func TestPlan14GenerationRetirementProcessCrash(t *testing.T) {
+ if dir := os.Getenv("PLAN14_GENERATION_RETIRE_CHILD"); dir != "" {
+ raw, err := os.ReadFile(filepath.Join(dir, "expected"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ var expected GenerationReservation
+ if json.Unmarshal(raw, &expected) != nil {
+ t.Fatal("bad expected record")
+ }
+ store := generationRetirementCrashStore{generationListingStore: generationListingStore{&healthyRetryStore{dir: filepath.Join(dir, "state")}}, phase: os.Getenv("PLAN14_GENERATION_RETIRE_PHASE")}
+ g := &RootGenerations{Store: store}
+ err = g.Retire(t.Context(), expected, func(context.Context, GenerationReservation) error { return nil }, func(context.Context, GenerationReservation) (string, error) {
+ if store.phase == "verify" {
+ os.Exit(74)
+ }
+ return strings.Repeat("d", 64), nil
+ })
+ t.Fatalf("crash hook not reached: %v", err)
+ }
+ for _, phase := range []string{"closed", "verify", "retired"} {
+ t.Run(phase, func(t *testing.T) {
+ g, store, b := generationFixture(t)
+ r, err := g.Reserve(b)
+ if err != nil {
+ t.Fatal(err)
+ }
+ dir := t.TempDir()
+ if err := os.Rename(store.dir, filepath.Join(dir, "state")); err != nil {
+ t.Fatal(err)
+ }
+ store.dir = filepath.Join(dir, "state")
+ raw, _ := json.Marshal(r)
+ if err := os.WriteFile(filepath.Join(dir, "expected"), raw, 0600); err != nil {
+ t.Fatal(err)
+ }
+ cmd := exec.Command(os.Args[0], "-test.run=^TestPlan14GenerationRetirementProcessCrash$", "-test.count=1")
+ cmd.Dir = dir
+ cmd.Env = append(os.Environ(), "PLAN14_GENERATION_RETIRE_CHILD="+dir, "PLAN14_GENERATION_RETIRE_PHASE="+phase)
+ if output, err := cmd.CombinedOutput(); err == nil {
+ t.Fatalf("no crash: %s", output)
+ } else if code, ok := err.(*exec.ExitError); !ok || code.ExitCode() != 74 {
+ t.Fatalf("wrong crash: %v %s", err, output)
+ }
+ got, err := g.Read(b)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !got.Closed || (got.Retirement != "") != (phase == "retired") {
+ t.Fatalf("crash lost restriction: %+v", got)
+ }
+ if _, err := g.Reserve(b); err == nil {
+ t.Fatal("crashed retirement revived launch")
+ }
+ if phase != "retired" {
+ if err := g.Retire(t.Context(), r, func(context.Context, GenerationReservation) error { t.Fatal("closed authority reopened"); return nil }, func(context.Context, GenerationReservation) (string, error) { return "", os.ErrPermission }); err == nil {
+ t.Fatal("recovery released pin without evidence")
+ }
+ }
+ if err := g.Retire(t.Context(), r, func(context.Context, GenerationReservation) error { return nil }, func(context.Context, GenerationReservation) (string, error) { return strings.Repeat("d", 64), nil }); err != nil {
+ t.Fatal(err)
+ }
+ })
+ }
+}
diff --git a/internal/release/plan14_generation_retirement_test.go b/internal/release/plan14_generation_retirement_test.go
new file mode 100644
index 000000000..9677a8094
--- /dev/null
+++ b/internal/release/plan14_generation_retirement_test.go
@@ -0,0 +1,235 @@
+package release
+
+import (
+ "context"
+ "errors"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/rootstate"
+)
+
+func TestPlan14GenerationRetirementReleasesExactPin(t *testing.T) {
+ f := newReuseFixture(t)
+ g, r := generationPinFixture(t, f.opt, "bab", "myapp", "v2")
+ pending := r.Binding
+ pending.Attempt, pending.Invocation = "pending-v3", "pending-v3"
+ pending.Version = "v3"
+ pending.Path = VersionPath(f.opt, "bap", "v3", "other")
+ pending.Binding.Account, pending.Binding.App = "bap", "other"
+ pending.Tree = filepath.Dir(pending.Path)
+ if err := StageArtifact(f.opt, "bap", "v3", "other", []byte("pending")); err != nil {
+ t.Fatal(err)
+ }
+ for path, dst := range map[string]*rootstate.FileIdentity{pending.Path: &pending.Executable, pending.Tree: &pending.TreeIdentity} {
+ file, err := os.Open(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ *dst, err = rootstate.Identity(file)
+ file.Close()
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ p, err := g.Reserve(pending)
+ if err != nil {
+ t.Fatal(err)
+ }
+ closed, verified := 0, 0
+ err = g.Retire(t.Context(), r, func(context.Context, GenerationReservation) error { closed++; return nil }, func(_ context.Context, got GenerationReservation) (string, error) {
+ verified++
+ if !got.Closed || got.ID != r.ID {
+ t.Fatal("kernel checked before durable close")
+ }
+ if err := g.Check(r); err == nil {
+ t.Fatal("closed authority can launch")
+ }
+ return strings.Repeat("a", 64), nil
+ })
+ if err != nil || closed != 1 || verified != 1 {
+ t.Fatalf("retire: %v %d %d", err, closed, verified)
+ }
+ source := RootGenerationPins{Ledger: g, Verify: func(context.Context, []GenerationReservation) error { return nil }}
+ pins, err := source.StagedPins(t.Context(), f.opt)
+ if err != nil || pins.Has(r.Binding.Path) || !pins.Has(p.Binding.Path) {
+ t.Fatalf("wrong pins: %v %v", pins, err)
+ }
+ if got, err := g.Read(p.Binding); err != nil || got != p {
+ t.Fatalf("pending reservation changed: %+v %v", got, err)
+ }
+ if _, err := g.Reserve(r.Binding); err == nil {
+ t.Fatal("retired request replay authorized launch")
+ }
+ if err := g.Started(r, GenerationProcess{PID: 51, TenantPID: 52, Start: "51", TenantStart: "52", Group: "system.slice/run.service", Invocation: "unit", GroupIdentity: rootstate.FileIdentity{Device: 1, Inode: 2}}); err == nil {
+ t.Fatal("retired reservation recorded a late launch")
+ }
+ f.pass(t, WithRootGenerationPins(source))
+ if got := f.stagedV2(t); got != "new v2" {
+ t.Fatalf("exact retirement failed to release restage: %q", got)
+ }
+}
+
+func TestPlan14GenerationRetirementRefusals(t *testing.T) {
+ for _, attack := range []string{"join", "kernel", "close-sync", "retire-sync", "wrong-id", "wrong-binding", "no-close", "no-kernel", "bad-evidence"} {
+ t.Run(attack, func(t *testing.T) {
+ g, s, b := generationFixture(t)
+ r, err := g.Reserve(b)
+ if err != nil {
+ t.Fatal(err)
+ }
+ expected := r
+ close := func(context.Context, GenerationReservation) error { return nil }
+ verify := func(context.Context, GenerationReservation) (string, error) { return strings.Repeat("b", 64), nil }
+ switch attack {
+ case "join":
+ close = func(context.Context, GenerationReservation) error { return os.ErrPermission }
+ case "kernel":
+ verify = func(context.Context, GenerationReservation) (string, error) { return "", os.ErrPermission }
+ case "close-sync":
+ close = func(context.Context, GenerationReservation) error { s.fail = 1; return nil }
+ case "retire-sync":
+ verify = func(context.Context, GenerationReservation) (string, error) {
+ s.fail = 1
+ return strings.Repeat("b", 64), nil
+ }
+ case "wrong-id":
+ expected.ID = strings.Repeat("c", 64)
+ case "wrong-binding":
+ expected.Binding.ControllerStart = "other"
+ case "no-close":
+ close = nil
+ case "no-kernel":
+ verify = nil
+ case "bad-evidence":
+ verify = func(context.Context, GenerationReservation) (string, error) { return "complete", nil }
+ }
+ if err := g.Retire(t.Context(), expected, close, verify); err == nil {
+ t.Fatal("retirement accepted incomplete evidence")
+ }
+ // A failed final sync may have made the evidence visible. It must be
+ // recommitted before a reader can release its pin.
+ if attack == "retire-sync" {
+ s.fail = 1
+ if _, err := g.Read(b); err == nil {
+ t.Fatal("uncommitted retirement granted read")
+ }
+ }
+ got, err := g.Read(b)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if attack != "retire-sync" && got.Retirement != "" {
+ t.Fatal("unverified retirement released pin")
+ }
+ if got.ID != r.ID || got.Binding != r.Binding {
+ t.Fatal("retirement changed identity")
+ }
+ if got.Closed && !errors.Is(g.Check(r), os.ErrPermission) {
+ t.Fatal("closed request remained launchable")
+ }
+ })
+ }
+}
+
+func TestPlan14GenerationRetirementKeepsIndependentReference(t *testing.T) {
+ f := newReuseFixture(t)
+ g, r := generationPinFixture(t, f.opt, "bab", "myapp", "v2")
+ b := r.Binding
+ b.Attempt = "independent"
+ b.Invocation = "independent"
+ other, err := g.Reserve(b)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := g.Retire(t.Context(), r, func(context.Context, GenerationReservation) error { return nil }, func(context.Context, GenerationReservation) (string, error) { return strings.Repeat("a", 64), nil }); err != nil {
+ t.Fatal(err)
+ }
+ source := RootGenerationPins{Ledger: g, Verify: func(context.Context, []GenerationReservation) error { return nil }}
+ f.pass(t, WithRootGenerationPins(source))
+ if got := f.stagedV2(t); got != "old v2" {
+ t.Fatalf("independent reference overwritten: %q", got)
+ }
+ if got, err := g.Read(other.Binding); err != nil || got != other {
+ t.Fatal("other reference changed", err)
+ }
+ source.Verify = nil
+ if _, err := source.StagedPins(t.Context(), f.opt); err == nil {
+ t.Fatal("retirement supplied completeness")
+ }
+}
+
+func TestPlan14GenerationClaimsOffRestageMatchesMain(t *testing.T) {
+ for _, pinned := range []bool{false, true} {
+ type result struct {
+ bytes, live, version, previous string
+ restarts int
+ }
+ run := func(withRoot bool) result {
+ f := newReuseFixture(t)
+ if pinned {
+ if err := f.reg.Upsert(registry.Route{Host: "pending.example", Account: "bab", App: "myapp", Version: "v2", Status: "creating"}); err != nil {
+ t.Fatal(err)
+ }
+ }
+ var opts []PassOption
+ if withRoot {
+ opts = append(opts, WithRootGenerationPins(PinSourceFunc(func(context.Context, string) (StagedPaths, error) { return StagedPaths{}, nil })))
+ }
+ restarts := 0
+ if err := Pass(t.Context(), f.reg, f.s, f.pub, f.opt, func(string) error { restarts++; return nil }, opts...); err != nil {
+ t.Fatal(err)
+ }
+ live, err := LiveVersion(f.opt, "h1.example.com")
+ if err != nil {
+ t.Fatal(err)
+ }
+ row, err := f.reg.Get("h1.example.com")
+ if err != nil || row == nil {
+ t.Fatal(err)
+ }
+ return result{f.stagedV2(t), live, row.Version, row.PrevVersion, restarts}
+ }
+ main, branch := run(false), run(true)
+ if main != branch {
+ t.Fatalf("claims-off changed pinned=%v: main=%+v branch=%+v", pinned, main, branch)
+ }
+ if !pinned && (branch.bytes != "new v2" || branch.live != "v2" || branch.restarts != 1) {
+ t.Fatalf("positive restage refused: %+v", branch)
+ }
+ }
+}
+
+func TestPlan14GenerationClosedReplayRefused(t *testing.T) {
+ for _, started := range []bool{false, true} {
+ g, _, b := generationFixture(t)
+ r, err := g.Reserve(b)
+ if err != nil {
+ t.Fatal(err)
+ }
+ process := GenerationProcess{PID: 51, TenantPID: 52, Start: "51", TenantStart: "52", Group: "system.slice/run.service", Invocation: "unit", GroupIdentity: rootstate.FileIdentity{Device: 1, Inode: 2}}
+ if started {
+ if err := g.Started(r, process); err != nil {
+ t.Fatal(err)
+ }
+ r, err = g.Read(b)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ if err := g.Retire(t.Context(), r, func(context.Context, GenerationReservation) error { return nil }, func(_ context.Context, closed GenerationReservation) (string, error) {
+ if err := g.Check(closed); err == nil {
+ t.Fatal("closed reservation granted a spawn")
+ }
+ if err := g.Started(r, process); err == nil {
+ t.Fatal("closed reservation accepted a started replay")
+ }
+ return strings.Repeat("a", 64), nil
+ }); err != nil {
+ t.Fatal(err)
+ }
+ }
+}
diff --git a/internal/release/rootgenerationpins.go b/internal/release/rootgenerationpins.go
index 7f1635e44..26c8d87bf 100644
--- a/internal/release/rootgenerationpins.go
+++ b/internal/release/rootgenerationpins.go
@@ -51,6 +51,10 @@ func (s RootGenerationPins) StagedPins(ctx context.Context, optDir string) (Stag
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
+ }
for path, expected := range map[string]rootstate.FileIdentity{b.Path: b.Executable, b.Tree: b.TreeIdentity} {
if err := generationFileIdentity(path, expected); err != nil {
return nil, err
@@ -58,7 +62,6 @@ func (s RootGenerationPins) StagedPins(ctx context.Context, optDir string) (Stag
}
pins.add(b.Path)
pins.add(b.Tree)
- records = append(records, r)
}
if s.Verify == nil {
return nil, os.ErrPermission
diff --git a/internal/release/rootgenerations.go b/internal/release/rootgenerations.go
index 1d8d081a8..0a9112d08 100644
--- a/internal/release/rootgenerations.go
+++ b/internal/release/rootgenerations.go
@@ -1,6 +1,7 @@
package release
import (
+ "context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
@@ -37,11 +38,13 @@ type GenerationProcess struct {
}
type GenerationReservation struct {
- Schema int
- Revision uint64
- ID string
- Binding GenerationBinding
- Process GenerationProcess
+ Schema int
+ Revision uint64
+ ID string
+ Binding GenerationBinding
+ Process GenerationProcess
+ Closed bool `json:",omitempty"`
+ Retirement string `json:",omitempty"`
}
// RootGenerations records additional pins under the caller's release exclusion.
@@ -72,8 +75,24 @@ func (p GenerationProcess) valid() bool {
func validGeneration(r GenerationReservation) bool {
id, err := hex.DecodeString(r.ID)
+ if r.Retirement != "" {
+ evidence, err := hex.DecodeString(r.Retirement)
+ if err != nil || len(evidence) != sha256.Size || hex.EncodeToString(evidence) != r.Retirement || !r.Closed {
+ return false
+ }
+ }
+ base := uint64(1)
+ if r.Process != (GenerationProcess{}) {
+ base = 2
+ }
+ if r.Closed {
+ base += 2
+ }
+ if r.Retirement != "" {
+ base += 2
+ }
return r.Schema == 1 && err == nil && len(id) == 32 && hex.EncodeToString(id) == r.ID && r.Binding.valid() &&
- ((r.Revision == 1 && r.Process == (GenerationProcess{})) || (r.Revision == 2 && r.Process.valid()))
+ r.Revision == base && (r.Process == (GenerationProcess{}) || r.Process.valid())
}
func (g *RootGenerations) Read(b GenerationBinding) (GenerationReservation, error) {
@@ -114,7 +133,7 @@ func (g *RootGenerations) write(r GenerationReservation) error {
func (g *RootGenerations) Reserve(b GenerationBinding) (GenerationReservation, error) {
r, err := g.Read(b)
if err == nil {
- if r.Process != (GenerationProcess{}) {
+ if r.Closed || r.Process != (GenerationProcess{}) {
return GenerationReservation{}, os.ErrPermission
}
return r, nil
@@ -138,7 +157,7 @@ func (g *RootGenerations) Check(r GenerationReservation) error {
if err != nil {
return err
}
- if got != r || r.Process != (GenerationProcess{}) {
+ if got != r || r.Closed || r.Process != (GenerationProcess{}) {
return os.ErrPermission
}
return nil
@@ -152,7 +171,7 @@ func (g *RootGenerations) Started(r GenerationReservation, p GenerationProcess)
if err != nil {
return err
}
- if got.ID != r.ID {
+ if got.ID != r.ID || got.Closed {
return os.ErrPermission
}
if got.Process == p {
@@ -164,3 +183,48 @@ func (g *RootGenerations) Started(r GenerationReservation, p GenerationProcess)
got.Process, got.Revision = p, 2
return g.write(got)
}
+
+// Retire runs under release exclusion. close verifies that the exact launch
+// authority has ended; verify independently checks controller join and kernel
+// references after the durable close. Interrupted retirement retains its pin.
+func (g *RootGenerations) Retire(ctx context.Context, expected GenerationReservation, close func(context.Context, GenerationReservation) error, verify func(context.Context, GenerationReservation) (string, error)) error {
+ if close == nil || verify == nil {
+ return os.ErrPermission
+ }
+ r, err := g.Read(expected.Binding)
+ if err != nil {
+ return err
+ }
+ if r.ID != expected.ID || r.Process != expected.Process {
+ return os.ErrPermission
+ }
+ if r.Retirement != "" {
+ return nil
+ }
+ if err := ctx.Err(); err != nil {
+ return err
+ }
+ if !r.Closed {
+ if err := close(ctx, r); err != nil {
+ return err
+ }
+ r.Closed = true
+ r.Revision += 2
+ if err := g.write(r); err != nil {
+ return err
+ }
+ }
+ evidence, err := verify(ctx, r)
+ if err != nil {
+ return err
+ }
+ if evidence == "" {
+ return os.ErrPermission
+ }
+ if err := ctx.Err(); err != nil {
+ return err
+ }
+ r.Retirement = evidence
+ r.Revision += 2
+ return g.write(r)
+}