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>
11 files changed,
+587
−23
internal/host/plan14_generation_lock_test.go+59 −0internal/host/plan14_generation_retirement_test.go+90 −0internal/host/rootgenerations_launch.go+1 −2internal/host/rootinstallation.go+1 −2internal/hostguard/rootview.go+11 −0internal/provision/plan14_e_write_census_test.go+1 −0internal/provision/testdata/plan14-root-reads-e.tsv+13 −9internal/release/plan14_generation_retirement_process_test.go+99 −0internal/release/plan14_generation_retirement_test.go+235 −0internal/release/rootgenerationpins.go+4 −1internal/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) |
| +} |