WIP claim Plan 14 track E checkpoint 23 (not for merge): E2 versioned generation-completeness record and pin reader, retained-predecessor retirement after rebind, held-stage relay pinned
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 files changed,
+918
−14
internal/host/heldstage.go+1 −1internal/host/plan14_generation_rebind_retirement_test.go+82 −0internal/host/rootgeneration_retirement.go+7 −0internal/provision/testdata/plan14-root-reads-e.tsv+24 −7internal/release/materializeheld.go+14 −4internal/release/plan14_generation_completeness_process_test.go+94 −0internal/release/plan14_generation_completeness_root_test.go+122 −0internal/release/plan14_generation_completeness_test.go+317 −0internal/release/plan14_generation_legacy_relay_test.go+97 −0internal/release/rootgenerationcompleteness.go+142 −0internal/release/rootgenerationpins.go+15 −2internal/release/rootgenerations.go+3 −0
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 346.952s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 3.164s
ok github.com/carlosframework/platform/cmd/moveprobe 92.259s
ok github.com/carlosframework/platform/cmd/zddprobe 0.727s
ok github.com/carlosframework/platform/internal/activator 233.511s
ok github.com/carlosframework/platform/internal/agentsock 0.016s
ok github.com/carlosframework/platform/internal/alerting 0.004s
ok github.com/carlosframework/platform/internal/analytics 0.020s
ok github.com/carlosframework/platform/internal/appconfig 2.353s
ok github.com/carlosframework/platform/internal/apperr 0.075s
ok github.com/carlosframework/platform/internal/applog 0.170s
ok github.com/carlosframework/platform/internal/awsinv 0.003s
ok github.com/carlosframework/platform/internal/boxchoice 0.004s
ok github.com/carlosframework/platform/internal/boxid 1.050s
? 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.718s
ok github.com/carlosframework/platform/internal/changelog 0.309s
ok github.com/carlosframework/platform/internal/clamd 0.054s
? github.com/carlosframework/platform/internal/cliflag [no test files]
ok github.com/carlosframework/platform/internal/console 363.627s
? github.com/carlosframework/platform/internal/controlname [no test files]
? github.com/carlosframework/platform/internal/controlsock [no test files]
ok github.com/carlosframework/platform/internal/dbdoor 5.564s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.010s
ok github.com/carlosframework/platform/internal/dbverify 0.004s
ok github.com/carlosframework/platform/internal/dnsrec 0.004s
ok github.com/carlosframework/platform/internal/docsite 0.005s
ok github.com/carlosframework/platform/internal/domains 2.130s
ok github.com/carlosframework/platform/internal/doorkey 0.006s
ok github.com/carlosframework/platform/internal/economics 0.004s
ok github.com/carlosframework/platform/internal/edge 26.069s
? 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.124s
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.016s
ok github.com/carlosframework/platform/internal/geoip 0.030s
ok github.com/carlosframework/platform/internal/historyreadback 0.152s
ok github.com/carlosframework/platform/internal/host 132.499s
ok github.com/carlosframework/platform/internal/hostactors 0.424s
ok github.com/carlosframework/platform/internal/hostbox 2.151s
ok github.com/carlosframework/platform/internal/hostclaim 0.491s
ok github.com/carlosframework/platform/internal/hostevidence 0.006s
ok github.com/carlosframework/platform/internal/hostexec 4.906s
ok github.com/carlosframework/platform/internal/hostfence 0.036s
ok github.com/carlosframework/platform/internal/hostfiles 10.510s
ok github.com/carlosframework/platform/internal/hostfloor 0.012s
ok github.com/carlosframework/platform/internal/hostguard 1.005s
ok github.com/carlosframework/platform/internal/hostops 1.411s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.002s
ok github.com/carlosframework/platform/internal/hostquarantine 0.002s
ok github.com/carlosframework/platform/internal/hostrelocate 0.446s
ok github.com/carlosframework/platform/internal/hostresolve 0.133s
ok github.com/carlosframework/platform/internal/hostwrite 0.004s
ok github.com/carlosframework/platform/internal/infraview 0.002s
ok github.com/carlosframework/platform/internal/instancectl 1.823s
ok github.com/carlosframework/platform/internal/instanceexec 2.728s
ok github.com/carlosframework/platform/internal/instancefs 0.004s
ok github.com/carlosframework/platform/internal/instancekey 0.014s
ok github.com/carlosframework/platform/internal/instanceupdates 0.019s
ok github.com/carlosframework/platform/internal/layout 1.436s
ok github.com/carlosframework/platform/internal/lease 9.465s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.018s
ok github.com/carlosframework/platform/internal/ledger 0.011s
ok github.com/carlosframework/platform/internal/mail 0.162s
ok github.com/carlosframework/platform/internal/maintenance 0.076s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.507s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.081s
ok github.com/carlosframework/platform/internal/metrics 0.004s
ok github.com/carlosframework/platform/internal/moves 20.988s
ok github.com/carlosframework/platform/internal/moves/archive 0.781s
ok github.com/carlosframework/platform/internal/objstore 0.177s
ok github.com/carlosframework/platform/internal/ops 0.157s
ok github.com/carlosframework/platform/internal/park 0.200s
ok github.com/carlosframework/platform/internal/privatefile 0.004s
ok github.com/carlosframework/platform/internal/provision 13.700s
ok github.com/carlosframework/platform/internal/publicrelease 0.007s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 3.992s
ok github.com/carlosframework/platform/internal/relay 7.272s
ok github.com/carlosframework/platform/internal/release 21.605s
ok github.com/carlosframework/platform/internal/rollout 3.747s
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.025s
ok github.com/carlosframework/platform/internal/s3compat 0.006s
ok github.com/carlosframework/platform/internal/safefile 0.004s
ok github.com/carlosframework/platform/internal/schedule 2.283s
ok github.com/carlosframework/platform/internal/sim/hostlife 9.639s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 4.176s
ok github.com/carlosframework/platform/internal/status 0.040s
ok github.com/carlosframework/platform/internal/steering 0.006s
ok github.com/carlosframework/platform/internal/stopevidence 0.228s
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.004s
ok github.com/carlosframework/platform/internal/version 0.001s
ok github.com/carlosframework/platform/internal/vet 1.530s
=== 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.022s
=== RUN TestT4StorageProbeMinio
2026/10/14 08:34:28 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE585F59B6209C, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok github.com/carlosframework/platform/internal/host 0.021s
minio contract: PASS
diff --git a/internal/host/heldstage.go b/internal/host/heldstage.go| index df347fb34..8adc33b80 100644 |
| --- a/internal/host/heldstage.go |
| +++ b/internal/host/heldstage.go |
| @@ -202,7 +202,7 @@ func heldStageClaim(obs hostclaim.Observed, req release.HeldStageRequest) error |
| // adoption uses). Anything else is dropped before a byte is downloaded. |
| func heldStagePass(ctx context.Context, dataDir, optDir string, pub ed25519.PublicKey, r *release.DigestRatchet, reg *registry.Registry, store release.Store, box string) { |
| proof := release.NewRootProofGate(release.ProofPolicyPath, dataDir) |
| - if err := release.ServeHeldStageRequests(ctx, store, pub, r, optDir, dataDir, proof, heldStageAuthority(reg, store, box, dataDir)); err != nil { |
| + if err := release.ServeHeldStageRequestsWithPins(ctx, store, pub, r, optDir, dataDir, proof, heldStageAuthority(reg, store, box, dataDir), productionRootGenerationPins()); err != nil { |
| log.Printf("adopt: held stage requests: %v", err) |
| } |
| retireEndedHeldRefusals(ctx, store, dataDir, box) |
diff --git a/internal/host/plan14_generation_rebind_retirement_test.go b/internal/host/plan14_generation_rebind_retirement_test.go| new file mode 100644 |
| index 000000000..f7a2169cd |
| --- /dev/null |
| +++ b/internal/host/plan14_generation_rebind_retirement_test.go |
| @@ -0,0 +1,82 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| + "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/release" |
| +) |
| + |
| +func TestPlan14GenerationRetirementRetainedRebind(t *testing.T) { |
| + for _, change := range []string{"none", "open-writers", "unsettled", "wrong-installation", "wrong-incarnation", "wrong-stamp", "wrong-account", "wrong-enrollment"} { |
| + t.Run(change, func(t *testing.T) { |
| + f, disk, _ := generationLaunchFixture(t) |
| + if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil { |
| + t.Fatal(err) |
| + } |
| + r := readGenerationLaunch(t, disk) |
| + if change != "unsettled" { |
| + if err := f.root.stop(t.Context(), f.peer, *f.req.Reservation); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + if change != "open-writers" { |
| + if err := f.journal.CloseWriters(t.Context(), f.target); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + prior := hostguard.Captured{Incarnation: f.target.Incarnation, Stamp: f.target.Stamp} |
| + in, err := f.root.Guard.Root.Installation(t.Context(), f.target.Host, prior) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + next := in |
| + next.Stamp += 3 |
| + next.InventoryRevision++ |
| + f.claims.set(func(c *hostclaim.Claim) { c.Holder.Stamp = next.Stamp }) |
| + if err := f.root.Guard.RebindRootInventory(t.Context(), prior, in.InventoryRevision, next, []byte("root-verified-rebind"), func(*hostclaim.Claim) error { return nil }, func() error { return nil }); err != nil { |
| + t.Fatal(err) |
| + } |
| + switch change { |
| + case "wrong-installation": |
| + r.Binding.Installation = "other" |
| + case "wrong-incarnation": |
| + r.Binding.Target.Incarnation = "other" |
| + case "wrong-stamp": |
| + r.Binding.Target.Stamp++ |
| + case "wrong-account": |
| + r.Binding.Binding.Account = "other" |
| + case "wrong-enrollment": |
| + r.Binding.Target.Enrollment = "other" |
| + } |
| + calls := 0 |
| + verifier := generationRetirement{guard: f.root.Guard, journal: f.journal, kernel: func(context.Context, release.GenerationReservation) (string, error) { |
| + calls++ |
| + return r.Binding.Digest, nil |
| + }} |
| + _, err = verifier.verify(t.Context(), r) |
| + if change != "none" { |
| + if err == nil || calls != 0 { |
| + t.Fatalf("rebind erased old obligation: %v kernel=%d", err, calls) |
| + } |
| + return |
| + } |
| + if err != nil || calls != 1 { |
| + t.Fatalf("retained predecessor refused: %v kernel=%d", err, calls) |
| + } |
| + if err := verifier.retire(t.Context(), &release.RootGenerations{Store: disk}, r); err != nil { |
| + t.Fatal(err) |
| + } |
| + got := readGenerationLaunch(t, disk) |
| + if got.Retirement == "" { |
| + t.Fatal("exact old reservation retained after verified retirement") |
| + } |
| + current, err := f.root.Guard.Root.Installation(t.Context(), next.Host, hostguard.Captured{Incarnation: next.Incarnation, Stamp: next.Stamp}) |
| + if err != nil || current != next { |
| + t.Fatal("retirement changed successor installation", err) |
| + } |
| + }) |
| + } |
| +} |
diff --git a/internal/host/rootgeneration_retirement.go b/internal/host/rootgeneration_retirement.go| index 591dfc3fd..fe09845f9 100644 |
| --- a/internal/host/rootgeneration_retirement.go |
| +++ b/internal/host/rootgeneration_retirement.go |
| @@ -29,6 +29,13 @@ func (v generationRetirement) closed(ctx context.Context, r release.GenerationRe |
| if err != nil { |
| return err |
| } |
| + // Installation admits an older capture only when root retained it as |
| + // retired. Rebind preserves this identity's account, enrollment and migration. |
| + // Its old launch journal and kernel obligations still have to settle below. |
| + if in.Incarnation != t.Incarnation || in.Stamp < t.Stamp { |
| + return os.ErrPermission |
| + } |
| + in.Stamp = t.Stamp |
| if in.Host != b.Binding.Host || in.Account != b.Binding.Account || in.App != b.Binding.App || in.Box != t.Box || in.Enrollment != t.Enrollment || in.InventoryRevision == 0 || in.Record != b.Record || rootHistoryInstallation(in) != b.Installation { |
| return os.ErrPermission |
| } |
diff --git a/internal/provision/testdata/plan14-root-reads-e.tsv b/internal/provision/testdata/plan14-root-reads-e.tsv| index 4ee88057d..59ad98868 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 fc312f5ee0e7725d5c4949b40a79a97d185cd52ba90232c79eb2f4ae6ba7c301 |
| -@coverage internal/release d29fc60f16dd714f5b0521654666f33bf04165181d813583dc010bde6d863dd2 |
| +@coverage internal/host 8e78fc36197a3e57ec73d46b824b93a87c4a5031a6f0fd11d6e86f86d188260d |
| +@coverage internal/release 2bb546a86c1ab49b02e424eb0b366b46776857a1249baa2d6fb61ead4209d6bd |
| @coverage internal/rootstate 6338f74a6b2059cf77cf478bd6111b37b441eb9d65a309464c378359d4bdac05 |
| internal/release/roothistory_status.go RootHistory.MigrationStatus authority E1 Exact completion binds the accepted authorization digest from durable private history to enrolled-root signed readback. Pending originals, changed evidence and canonical sources cannot supply retirement completion. Fixtures: TestPlan14HistoryReconstructionCompletion, TestPlan14HistoryRetirementCompletion, TestPlan14ReadReconstructionCompletion, TestPlan14ReadRetirementCompletion, TestPlan14RecoveryRetirementProductionCallback, TestPlan14RootHistoryReconstructionCrash, TestPlan14HistoryRetirementProcessCrash. file:Read@1062,file:Read@3891 f0ab7ad2d2d70f8c22ebfd575f5a1c6931f7eba30ccdba80ad43d0efffc88154 |
| 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 |
| @@ -202,7 +202,7 @@ internal/release/rootgenerations.go RootGenerations.Check authority E2 E20: exac |
| 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 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 RootGenerations.write authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. 3b7ee54d0f46efb1507f9a5f421b979047d8c0eb654ac5f31b1c3c42f67763c3 |
| 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 |
| @@ -219,11 +219,11 @@ 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. E22: root generation pins guard existing-tree artifact and manifest writes in adoption and moves-held staging/publication. Held snapshot is read under release exclusion before the leaf held-stage lock; failures remain restrictive for writes. Claims-off source returns no root pins. Fresh wholly absent version trees retain signed staging. Fixtures: TestPlan14GenerationSiblingWrites, TestPlan14GenerationVerifiedSiblingWrites, TestPlan14GenerationHeldStage (production source assembly, both flag values without census, lock order, pin/error/fresh cases). Production completeness remains unavailable; no new completeness grant. 30efb69f9a7ee5aca5a1909d94d3c6669065c7880207430e7e0d2a23ae90a8b6 |
| 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 E21: retirement and both destructive readers share strict root reservation enumeration and sync recommit. The independent completeness verifier remains mandatory and unset in production. Fixtures: TestPlan14GenerationRetirementCensusGate, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationRetirementReleasesExactPin, TestPlan14GenerationClaimsOffRestageMatchesMain. 5405788a77184059126c7dfbd2f2558e633aac6c0fe9e30daf48a7ea82aeb9f4 |
| +internal/release/rootgenerationpins.go RootGenerationPins.StagedPins authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. dff534cec0d4bea04750c4ea0774e71c0fbd3bf2d10dda7806df5bf797a68b36 |
| 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/release/rootgenerationpins.go type RootGenerationPins authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. b10f7631296398380745d742ab966bbb1e39fe0768a054589fee7c1ac8c999e3 |
| 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 |
| @@ -251,12 +251,12 @@ internal/host/rootgeneration_kernel_linux.go productionGenerationAbsence authori |
| internal/host/rootgeneration_kernel_linux.go type generationKernel authority E2 E21: bounded kernel reader retains executable, sibling, descriptor, mapping and mount references; checks controller boot/start and cgroup emptiness. Unknown nonroot tasks, foreign namespaces, missing kernel state or renamed trees refuse. Fixtures: TestPlan14GenerationKernelReferences, TestPlan14GenerationKernelMappingIdentity, TestPlan14GenerationKernelControllerIdentity, TestPlan14GenerationKernelRunningExecutable, TestPlan14GenerationKernelCgroup, TestPlan14GenerationKernelRetainedFiles, TestPlan14GenerationKernelSiblingAlias. Real proc executable tests are not full privileged namespace acceptance. 5a8673c8cf4c7b2e660688c4cd0a1043271159cebb11835da2b00bdfef798f69 |
| internal/host/rootgeneration_kernel_linux.go type generationMount authority E2 E21: bounded kernel reader retains executable, sibling, descriptor, mapping and mount references; checks controller boot/start and cgroup emptiness. Unknown nonroot tasks, foreign namespaces, missing kernel state or renamed trees refuse. Fixtures: TestPlan14GenerationKernelReferences, TestPlan14GenerationKernelMappingIdentity, TestPlan14GenerationKernelControllerIdentity, TestPlan14GenerationKernelRunningExecutable, TestPlan14GenerationKernelCgroup, TestPlan14GenerationKernelRetainedFiles, TestPlan14GenerationKernelSiblingAlias. Real proc executable tests are not full privileged namespace acceptance. b4cd115a225b26a7906357404f8d04692a8d1b062662f80672280972049a8acb |
| internal/host/rootgeneration_kernel_other.go productionGenerationAbsence authority E2 E21: non-Linux production generation retirement refuses. Darwin, Windows and Linux ARM64 compile gates; no non-Linux retirement authority. de47d997019075326e1d5513d7576390d085566fd69edc35efd00f067fb1db24 |
| -internal/host/rootgeneration_retirement.go generationRetirement.closed authority E2 E21: release then host exclusion; root installation and exact settled launch with irreversibly closed writers bind retirement before and after independent kernel verification. Production invokes it from the census-only pin source. Fixtures: TestPlan14GenerationRetirementRootBinding, TestPlan14GenerationRetirementAfterLostStarted, TestPlan14GenerationRetirementBindingProcessCrash, TestPlan14GenerationRetirementCensusGate. Privileged production opener acceptance remains pending. e641da8055443fc03baae1a76548b76a863cd38f44c9c9b9f93e82f5b47749e0 |
| +internal/host/rootgeneration_retirement.go generationRetirement.closed authority E2 E23: the root installation reader admits an old capture only from its retained retired set; restore that same-incarnation stamp solely to check the original installation identity. Require exact closed launch and independent kernel absence afterward. Fixtures: TestPlan14GenerationRetirementRetainedRebind, TestPlan14GenerationRetirementRootBinding. Running-tenant kernel acceptance remains pending. f871fd29171851a2300b68c947124d5d014ed97cdef45eef496645411df76107 |
| internal/host/rootgeneration_retirement.go generationRetirement.retire authority E2 E21: release then host exclusion; root installation and exact settled launch with irreversibly closed writers bind retirement before and after independent kernel verification. Production invokes it from the census-only pin source. Fixtures: TestPlan14GenerationRetirementRootBinding, TestPlan14GenerationRetirementAfterLostStarted, TestPlan14GenerationRetirementBindingProcessCrash, TestPlan14GenerationRetirementCensusGate. Privileged production opener acceptance remains pending. e4caafad8e42625aa21927f447f5f50a82f0890778635b74c3498c1d86efe1e2 |
| internal/host/rootgeneration_retirement.go generationRetirement.verify authority E2 E21: release then host exclusion; root installation and exact settled launch with irreversibly closed writers bind retirement before and after independent kernel verification. Production invokes it from the census-only pin source. Fixtures: TestPlan14GenerationRetirementRootBinding, TestPlan14GenerationRetirementAfterLostStarted, TestPlan14GenerationRetirementBindingProcessCrash, TestPlan14GenerationRetirementCensusGate. Privileged production opener acceptance remains pending. 7bf7540ad8bac41c5ea23d7bbf2f09c870b9bed78cf912fb9645c1a621289f70 |
| internal/host/rootgeneration_retirement.go productionRetireGenerations authority E2 E21: release then host exclusion; root installation and exact settled launch with irreversibly closed writers bind retirement before and after independent kernel verification. Production invokes it from the census-only pin source. Fixtures: TestPlan14GenerationRetirementRootBinding, TestPlan14GenerationRetirementAfterLostStarted, TestPlan14GenerationRetirementBindingProcessCrash, TestPlan14GenerationRetirementCensusGate. Privileged production opener acceptance remains pending. file:Open@292 fbe2431f88b9f2bd904f046855b63f619d20d5bdde6bcb0dc6d962388046a3c7 |
| internal/host/rootgeneration_retirement.go type generationRetirement authority E2 E21: release then host exclusion; root installation and exact settled launch with irreversibly closed writers bind retirement before and after independent kernel verification. Production invokes it from the census-only pin source. Fixtures: TestPlan14GenerationRetirementRootBinding, TestPlan14GenerationRetirementAfterLostStarted, TestPlan14GenerationRetirementBindingProcessCrash, TestPlan14GenerationRetirementCensusGate. Privileged production opener acceptance remains pending. 942978edbce75390dd3fcbfee5ec8b1d86102053f99da7915b7afdd89a503b8e |
| -internal/release/rootgenerationpins.go RootGenerations.Reservations authority E2 E21: retirement and both destructive readers share strict root reservation enumeration and sync recommit. The independent completeness verifier remains mandatory and unset in production. Fixtures: TestPlan14GenerationRetirementCensusGate, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationRetirementReleasesExactPin, TestPlan14GenerationClaimsOffRestageMatchesMain. file:Read@412,file:Read@761 773119e7f716096834b4e384b02e0f7672a2a27703d6760e745088bf62dbf709 |
| +internal/release/rootgenerationpins.go RootGenerations.Reservations authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. file:Read@541,file:Read@890 87f58596d5807a0e9eaed6dbd0cf5ba4e21a0906fba5959a2d974a68e9365d8c |
| internal/host/movesheldrelease_production.go WithProductionMovesHeldRelease authority B2,C,E1,E2 Root held-release constructor requires root guard and private release store, authenticates peer and reads root health history. E22: root generation pins guard existing-tree artifact and manifest writes in adoption and moves-held staging/publication. Held snapshot is read under release exclusion before the leaf held-stage lock; failures remain restrictive for writes. Claims-off source returns no root pins. Fresh wholly absent version trees retain signed staging. Fixtures: TestPlan14GenerationSiblingWrites, TestPlan14GenerationVerifiedSiblingWrites, TestPlan14GenerationHeldStage (production source assembly, both flag values without census, lock order, pin/error/fresh cases). Production completeness remains unavailable; no new completeness grant. file:Open@1365,file:Read@1612 9981c473733dd5214ab5baf79fa4402e8112ec3ca9095d1ff742fcde10119d5f |
| internal/host/movesheldrelease_root.go type movesHeldReleaseRoot authority E2 E22: root generation pins guard existing-tree artifact and manifest writes in adoption and moves-held staging/publication. Held snapshot is read under release exclusion before the leaf held-stage lock; failures remain restrictive for writes. Claims-off source returns no root pins. Fresh wholly absent version trees retain signed staging. Fixtures: TestPlan14GenerationSiblingWrites, TestPlan14GenerationVerifiedSiblingWrites, TestPlan14GenerationHeldStage (production source assembly, both flag values without census, lock order, pin/error/fresh cases). Production completeness remains unavailable; no new completeness grant. b42ce49488270ad07fd3f6b3607ed3f1d3d36ee23f60f55cc7c01c3fb8928233 |
| internal/host/movesheldrelease_root.go movesHeldReleaseRoot.stageRelease authority E2 E22: root generation pins guard existing-tree artifact and manifest writes in adoption and moves-held staging/publication. Held snapshot is read under release exclusion before the leaf held-stage lock; failures remain restrictive for writes. Claims-off source returns no root pins. Fresh wholly absent version trees retain signed staging. Fixtures: TestPlan14GenerationSiblingWrites, TestPlan14GenerationVerifiedSiblingWrites, TestPlan14GenerationHeldStage (production source assembly, both flag values without census, lock order, pin/error/fresh cases). Production completeness remains unavailable; no new completeness grant. whole:Route@1777 4ed5a64b1dd790e214c2e6b32b82804f40b40ceb1698325aec752d05eb9a6a19 |
| @@ -273,3 +273,20 @@ internal/release/materializeheld.go StageHeldVerified authority C,E1,J1,E2 root- |
| internal/release/materializeheld.go StageHeldVerifiedWithPins authority E2 E22: root generation pins guard existing-tree artifact and manifest writes in adoption and moves-held staging/publication. Held snapshot is read under release exclusion before the leaf held-stage lock; failures remain restrictive for writes. Claims-off source returns no root pins. Fresh wholly absent version trees retain signed staging. Fixtures: TestPlan14GenerationSiblingWrites, TestPlan14GenerationVerifiedSiblingWrites, TestPlan14GenerationHeldStage (production source assembly, both flag values without census, lock order, pin/error/fresh cases). Production completeness remains unavailable; no new completeness grant. registry:HoldOp@739,registry:Host@596,registry:Host@854,whole:Route@231,whole:Route@400,whole:Route@503,whole:Route@676 32ba175691ea06a141bdb4bb8bba8218354878927afd287704cc584cff6e0589 |
| internal/release/materializeheld.go stageHeld authority C,E1,J1,E2 root-route;kept=C,E1;scope=protected-decision-only E22: root generation pins guard existing-tree artifact and manifest writes in adoption and moves-held staging/publication. Held snapshot is read under release exclusion before the leaf held-stage lock; failures remain restrictive for writes. Claims-off source returns no root pins. Fresh wholly absent version trees retain signed staging. Fixtures: TestPlan14GenerationSiblingWrites, TestPlan14GenerationVerifiedSiblingWrites, TestPlan14GenerationHeldStage (production source assembly, both flag values without census, lock order, pin/error/fresh cases). Production completeness remains unavailable; no new completeness grant. registry:App@308,registry:App@467,registry:App@818,registry:Host@484,registry:Host@835,whole:Route@197,whole:Route@303,whole:Route@462,whole:Route@813 4a7f555f1f30e626d447cacf79541747e75d3ff90ea101c46c982f4e591f024b |
| internal/release/rootgenerationpins.go stageGenerationSibling authority E2 E22: root generation pins guard existing-tree artifact and manifest writes in adoption and moves-held staging/publication. Held snapshot is read under release exclusion before the leaf held-stage lock; failures remain restrictive for writes. Claims-off source returns no root pins. Fresh wholly absent version trees retain signed staging. Fixtures: TestPlan14GenerationSiblingWrites, TestPlan14GenerationVerifiedSiblingWrites, TestPlan14GenerationHeldStage (production source assembly, both flag values without census, lock order, pin/error/fresh cases). Production completeness remains unavailable; no new completeness grant. 572860919099c7d09c63350b88754950fa95dd67c21566fa9d84758928cf84b3 |
| +internal/release/rootgenerationcompleteness.go GenerationEvidence.valid authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. c75845d27ace65dff1fee22b4e9d0174ae7e1769c1c967d9af08d39429169c24 |
| +internal/release/rootgenerationcompleteness.go RootGenerations.Certify authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. 0de395b25205d34db23511d4c2cbe6fabbbc1ac55efec15a548a65db57515a3e |
| +internal/release/rootgenerationcompleteness.go RootGenerations.Completeness authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. file:Read@183 01d502f20c896188722ead35de66dea745e34a4b37fa14736b4ae620c88621df |
| +internal/release/rootgenerationcompleteness.go RootGenerations.invalidateCompleteness authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. 2ca32eef40bcb1ac6d44327615725b526b60aa45d88cfb90c0b55e18c3dcea55 |
| +internal/release/rootgenerationcompleteness.go RootGenerations.writeCompleteness authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. d0a679b392995290ac8de99773aba981c27afef41d2ce4bf98fd0f8935a7aaa7 |
| +internal/release/rootgenerationcompleteness.go const generationCompletenessKey authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. 77774aa68284ad4f21124b957e1bf7562cb6e68b427bb8445a307f188a07dfc6 |
| +internal/release/rootgenerationcompleteness.go const rootGenerationCompletenessLimit authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. e32dc27303a30317ac81a686cbcf258402163f33001dece4a1a5e09410c98766 |
| +internal/release/rootgenerationcompleteness.go generationDigest authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. 535b6a9639f825c40541a6b5002e8cf9452d5470fb09607f7473d115544dae61 |
| +internal/release/rootgenerationcompleteness.go generationHash authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. bb7bd4705e38dbf222ad1791c7d833ddbf6a505dd023ac94cfbe58643a612def |
| +internal/release/rootgenerationcompleteness.go type GenerationCompleteness authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. 7ef6167e481c979a5e6f5f14edf5428bfb5c7c96bded22e7487484ddc6c9167b |
| +internal/release/rootgenerationcompleteness.go type GenerationEvidence authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. 7b56092e5735237424e046507ca24e3df8f4c3f32bab4822389c55703cff2366 |
| +internal/release/materializeheld.go MaterializeHeld authority E2 E23: old held-stage relay forwards the root generation source before artifact writes and outside publication exclusion. Request fields retain their prior hint/authorization checks; only root pins authorize destructive staging on census boxes. No-census source yields empty pins. Fixtures: TestPlan14GenerationLegacyRelay, TestHeldStagePassIsANoOpWithoutRequests, TestHeldStagePassDropsAnUnauthorizedBuild, TestServeHeldStageRequestsRefusesStaleAndTraversal. Production source wiring is inspected; privileged completeness is not claimed. whole:Route@276 91f97547e14dccf71ee408979b47f641f8f3594775e7482f8437b7d1ed0509fc |
| +internal/release/materializeheld.go materializeHeldChecked authority E2 E23: old held-stage relay forwards the root generation source before artifact writes and outside publication exclusion. Request fields retain their prior hint/authorization checks; only root pins authorize destructive staging on census boxes. No-census source yields empty pins. Fixtures: TestPlan14GenerationLegacyRelay, TestHeldStagePassIsANoOpWithoutRequests, TestHeldStagePassDropsAnUnauthorizedBuild, TestServeHeldStageRequestsRefusesStaleAndTraversal. Production source wiring is inspected; privileged completeness is not claimed. whole:Route@272,whole:Route@368 8d751e8dc8e8d768a74b601f088dba9fe81bcfe32ec32025e30579f9f29a8f82 |
| +internal/release/materializeheld.go ServeHeldStageRequests authority E2 E23: old held-stage relay forwards the root generation source before artifact writes and outside publication exclusion. Request fields retain their prior hint/authorization checks; only root pins authorize destructive staging on census boxes. No-census source yields empty pins. Fixtures: TestPlan14GenerationLegacyRelay, TestHeldStagePassIsANoOpWithoutRequests, TestHeldStagePassDropsAnUnauthorizedBuild, TestServeHeldStageRequestsRefusesStaleAndTraversal. Production source wiring is inspected; privileged completeness is not claimed. 3ada05f6519d2a3e9ba95bb8a1238098ee84994c1dc9f1e0f4b0dfed52c7c0aa |
| +internal/release/materializeheld.go ServeHeldStageRequestsWithPins authority E2 E23: old held-stage relay forwards the root generation source before artifact writes and outside publication exclusion. Request fields retain their prior hint/authorization checks; only root pins authorize destructive staging on census boxes. No-census source yields empty pins. Fixtures: TestPlan14GenerationLegacyRelay, TestHeldStagePassIsANoOpWithoutRequests, TestHeldStagePassDropsAnUnauthorizedBuild, TestServeHeldStageRequestsRefusesStaleAndTraversal. Production source wiring is inspected; privileged completeness is not claimed. file:ReadDir@429 edb9927c53af09311b4dd3e277a34d5eba7ab048d343b9b05e25f0b24a48e79d |
| +internal/release/materializeheld.go serveHeldStageRequest authority E2 E23: old held-stage relay forwards the root generation source before artifact writes and outside publication exclusion. Request fields retain their prior hint/authorization checks; only root pins authorize destructive staging on census boxes. No-census source yields empty pins. Fixtures: TestPlan14GenerationLegacyRelay, TestHeldStagePassIsANoOpWithoutRequests, TestHeldStagePassDropsAnUnauthorizedBuild, TestServeHeldStageRequestsRefusesStaleAndTraversal. Production source wiring is inspected; privileged completeness is not claimed. file:OpenFile@615,file:ReadAll@812,whole:Route@1676 bbda896335311f5e09a75542d82c57fb993da72cbc27875b74bda79bf0239280 |
| +internal/host/heldstage.go heldStagePass authority E2 E23: old held-stage relay forwards the root generation source before artifact writes and outside publication exclusion. Request fields retain their prior hint/authorization checks; only root pins authorize destructive staging on census boxes. No-census source yields empty pins. Fixtures: TestPlan14GenerationLegacyRelay, TestHeldStagePassIsANoOpWithoutRequests, TestHeldStagePassDropsAnUnauthorizedBuild, TestServeHeldStageRequestsRefusesStaleAndTraversal. Production source wiring is inspected; privileged completeness is not claimed. 759f041ad504375f3000acccc10b5fba80aaa44ab574c3491d1df2cb49a35087 |
diff --git a/internal/release/materializeheld.go b/internal/release/materializeheld.go| index 9e98503d0..b389e3c97 100644 |
| --- a/internal/release/materializeheld.go |
| +++ b/internal/release/materializeheld.go |
| @@ -258,10 +258,14 @@ func verifyHeldLive(ctx context.Context, s Store, optDir string, rt registry.Rou |
| // signed promotion record and no ratchet-admitted pointer of the app |
| // authorizes is ErrHeldNotAuthorized, before anything is downloaded. |
| func MaterializeHeld(ctx context.Context, s Store, pub ed25519.PublicKey, r *DigestRatchet, optDir string, rt registry.Route, version string, proof *ProofGate, guard func(publish func() error) error) (*Manifest, error) { |
| + return materializeHeldChecked(ctx, s, pub, r, optDir, rt, version, proof, guard) |
| +} |
| + |
| +func materializeHeldChecked(ctx context.Context, s Store, pub ed25519.PublicKey, r *DigestRatchet, optDir string, rt registry.Route, version string, proof *ProofGate, guard func(publish func() error) error, pins ...PinSource) (*Manifest, error) { |
| if err := heldStageable(rt, version); err != nil { |
| return nil, err |
| } |
| - return materializeHeld(ctx, s, pub, r, optDir, rt, version, proof, guard) |
| + return materializeHeld(ctx, s, pub, r, optDir, rt, version, proof, guard, pins...) |
| } |
| // MaterializeHeldUnder is MaterializeHeld for a held row under the host |
| @@ -717,6 +721,12 @@ const heldStageRequestMax = 4096 |
| // and removal goes through safefile: no symlink is followed at any |
| // component, and only a regular file with one link is read. |
| func ServeHeldStageRequests(ctx context.Context, s Store, pub ed25519.PublicKey, r *DigestRatchet, optDir, dataDir string, proof *ProofGate, authorize func(req HeldStageRequest, then func() error) error) error { |
| + return ServeHeldStageRequestsWithPins(ctx, s, pub, r, optDir, dataDir, proof, authorize) |
| +} |
| + |
| +// ServeHeldStageRequestsWithPins protects the old relay on census boxes, including |
| +// after claims are disabled. The caller holds release exclusion through the pass. |
| +func ServeHeldStageRequestsWithPins(ctx context.Context, s Store, pub ed25519.PublicKey, r *DigestRatchet, optDir, dataDir string, proof *ProofGate, authorize func(req HeldStageRequest, then func() error) error, pins ...PinSource) error { |
| reqDir := HeldStageDir(dataDir) |
| dir, err := safefile.OpenDir(reqDir) |
| if errors.Is(err, os.ErrNotExist) { |
| @@ -736,7 +746,7 @@ func ServeHeldStageRequests(ctx context.Context, s Store, pub ed25519.PublicKey, |
| if strings.HasPrefix(name, ".") || !strings.HasSuffix(name, ".json") { |
| continue // a writer's temp file, or not a request |
| } |
| - if err := serveHeldStageRequest(ctx, s, pub, r, optDir, dataDir, reqDir, name, proof, authorize); err != nil { |
| + if err := serveHeldStageRequest(ctx, s, pub, r, optDir, dataDir, reqDir, name, proof, authorize, pins...); err != nil { |
| return err |
| } |
| } |
| @@ -745,7 +755,7 @@ func ServeHeldStageRequests(ctx context.Context, s Store, pub ed25519.PublicKey, |
| // serveHeldStageRequest serves one request file. It returns an error only |
| // when this build cannot do the work at all. |
| -func serveHeldStageRequest(ctx context.Context, s Store, pub ed25519.PublicKey, r *DigestRatchet, optDir, dataDir, reqDir, name string, proof *ProofGate, authorize func(HeldStageRequest, func() error) error) error { |
| +func serveHeldStageRequest(ctx context.Context, s Store, pub ed25519.PublicKey, r *DigestRatchet, optDir, dataDir, reqDir, name string, proof *ProofGate, authorize func(HeldStageRequest, func() error) error, pins ...PinSource) error { |
| drop := func(format string, args ...any) error { |
| fmt.Printf(" held stage request %q: "+format+"; removed\n", append([]any{name}, args...)...) |
| if err := safefile.Remove(filepath.Join(reqDir, name)); err != nil { |
| @@ -786,7 +796,7 @@ func serveHeldStageRequest(ctx context.Context, s Store, pub ed25519.PublicKey, |
| } |
| rt := registry.Route{Host: req.Host, Account: req.Account, App: req.App, HoldOp: req.Op} |
| guard := func(publish func() error) error { return authorize(req, publish) } |
| - if _, err := MaterializeHeld(ctx, s, pub, r, optDir, rt, req.Version, proof, guard); err != nil { |
| + if _, err := materializeHeldChecked(ctx, s, pub, r, optDir, rt, req.Version, proof, guard, pins...); err != nil { |
| if errors.Is(err, ErrStaleHeldStage) { |
| return drop("refused at publication: %v", err) |
| } |
diff --git a/internal/release/plan14_generation_completeness_process_test.go b/internal/release/plan14_generation_completeness_process_test.go| new file mode 100644 |
| index 000000000..cc75f7270 |
| --- /dev/null |
| +++ b/internal/release/plan14_generation_completeness_process_test.go |
| @@ -0,0 +1,94 @@ |
| +package release |
| + |
| +import ( |
| + "context" |
| + "encoding/json" |
| + "os" |
| + "os/exec" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/rootstate" |
| +) |
| + |
| +type generationCertificateCrash struct { |
| + rootstate.Store |
| + phase string |
| +} |
| + |
| +func (s generationCertificateCrash) Write(k string, b []byte) error { |
| + if err := s.Store.Write(k, b); err != nil { |
| + return err |
| + } |
| + var c GenerationCompleteness |
| + if k == generationCompletenessKey && json.Unmarshal(b, &c) == nil { |
| + if s.phase == "invalidate" && !c.Complete { |
| + os.Exit(74) |
| + } |
| + if s.phase == "certified" && c.Complete && c.Revision > 1 { |
| + os.Exit(74) |
| + } |
| + } |
| + return nil |
| +} |
| + |
| +func TestPlan14GenerationCompletenessProcessCrash(t *testing.T) { |
| + if dir := os.Getenv("PLAN14_GENERATION_CERT_CHILD"); dir != "" { |
| + phase := os.Getenv("PLAN14_GENERATION_CERT_PHASE") |
| + disk := &healthyRetryStore{dir: dir} |
| + g := &RootGenerations{Store: generationCertificateCrash{disk, phase}} |
| + records, err := g.Reservations(t.Context()) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + _, err = g.Certify(t.Context(), records, func(context.Context, []GenerationReservation) (GenerationEvidence, error) { |
| + if phase == "verify" { |
| + os.Exit(74) |
| + } |
| + return generationEvidence(), nil |
| + }) |
| + t.Fatal("crash not reached", err) |
| + } |
| + for _, phase := range []string{"invalidate", "verify", "certified"} { |
| + t.Run(phase, func(t *testing.T) { |
| + g, s, b := generationFixture(t) |
| + r, err := g.Reserve(b) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + records, err := g.Reservations(t.Context()) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + verify := func(context.Context, []GenerationReservation) (GenerationEvidence, error) { |
| + return generationEvidence(), nil |
| + } |
| + if _, err = g.Certify(t.Context(), records, verify); err != nil { |
| + t.Fatal(err) |
| + } |
| + cmd := exec.Command(os.Args[0], "-test.run=^TestPlan14GenerationCompletenessProcessCrash$", "-test.count=1") |
| + cmd.Dir = t.TempDir() |
| + cmd.Env = append(os.Environ(), "PLAN14_GENERATION_CERT_CHILD="+s.dir, "PLAN14_GENERATION_CERT_PHASE="+phase) |
| + out, err := cmd.CombinedOutput() |
| + if code, ok := err.(*exec.ExitError); !ok || code.ExitCode() != 74 { |
| + t.Fatalf("wrong crash %v %s", err, out) |
| + } |
| + reopened := &RootGenerations{Store: s} |
| + c, err := reopened.Completeness() |
| + if err != nil || c.Complete != (phase == "certified") { |
| + t.Fatalf("crash state %+v %v", c, err) |
| + } |
| + if got, err := reopened.Read(b); err != nil || got != r { |
| + t.Fatal("crash lost pin", err) |
| + } |
| + // Even the positive record cannot replace fresh kernel reconciliation. |
| + if _, err := reopened.Certify(t.Context(), records, func(context.Context, []GenerationReservation) (GenerationEvidence, error) { |
| + return GenerationEvidence{}, os.ErrPermission |
| + }); err == nil { |
| + t.Fatal("crashed certificate supplied authority") |
| + } |
| + if _, err := reopened.Certify(t.Context(), records, verify); err != nil { |
| + t.Fatal(err) |
| + } |
| + }) |
| + } |
| +} |
diff --git a/internal/release/plan14_generation_completeness_root_test.go b/internal/release/plan14_generation_completeness_root_test.go| new file mode 100644 |
| index 000000000..65b0e230c |
| --- /dev/null |
| +++ b/internal/release/plan14_generation_completeness_root_test.go |
| @@ -0,0 +1,122 @@ |
| +package release |
| + |
| +import ( |
| + "context" |
| + "os" |
| + "path/filepath" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/rootstate" |
| +) |
| + |
| +func TestPlan14GenerationCompletenessCheckedRootStore(t *testing.T) { |
| + parent := os.Getenv("PLAN14_GENERATION_ROOT_RUNNER") |
| + if parent == "" { |
| + t.Skip("requires a designated disposable root runner and its private scratch parent") |
| + } |
| + if os.Geteuid() != 0 { |
| + t.Fatal("designated runner must execute as root") |
| + } |
| + // The runner supplies a root-owned private parent, outside production state. |
| + check, err := rootstate.Open(parent, false, false) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + check.Close() |
| + dir, err := os.MkdirTemp(parent, "generation-certificate-") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + t.Cleanup(func() { |
| + if err := os.RemoveAll(dir); err != nil { |
| + t.Error(err) |
| + } |
| + }) |
| + state := filepath.Join(dir, "state") |
| + disk, err := rootstate.Open(state, true, false) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer disk.Close() |
| + g := &RootGenerations{Store: disk} |
| + _, _, b := generationFixture(t) |
| + if _, err := g.Reserve(b); err != nil { |
| + t.Fatal(err) |
| + } |
| + records, err := g.Reservations(t.Context()) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + verify := func(context.Context, []GenerationReservation) (GenerationEvidence, error) { |
| + return generationEvidence(), nil |
| + } |
| + if _, err := g.Certify(t.Context(), records, verify); err != nil { |
| + t.Fatal(err) |
| + } |
| + file := filepath.Join(state, generationCompletenessKey) |
| + for _, mode := range []os.FileMode{0666, 0620} { |
| + if err := os.Chmod(file, mode); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := g.Completeness(); err == nil { |
| + t.Fatal("accepted writable certificate") |
| + } |
| + if err := os.Chmod(file, 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + if err := os.Chown(file, 65534, 65534); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := g.Completeness(); err == nil { |
| + t.Fatal("accepted service-owned certificate") |
| + } |
| + if err := os.Chown(file, 0, 0); err != nil { |
| + t.Fatal(err) |
| + } |
| + saved := filepath.Join(state, "saved") |
| + if err := os.Rename(file, saved); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Symlink(saved, file); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := g.Completeness(); err == nil { |
| + t.Fatal("accepted symlink certificate") |
| + } |
| + if err := os.Remove(file); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Rename(saved, file); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := g.Certify(t.Context(), records, verify); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Rename(state, state+".retained"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Mkdir(state, 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := g.Completeness(); err != nil { |
| + t.Fatal("retained descriptor lost its checked store", err) |
| + } |
| + if _, err := os.Stat(filepath.Join(state, generationCompletenessKey)); !os.IsNotExist(err) { |
| + t.Fatal("retained descriptor wrote to replacement directory", err) |
| + } |
| + replacement, err := rootstate.Open(state, false, false) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer replacement.Close() |
| + if _, err := (&RootGenerations{Store: replacement}).Completeness(); !os.IsNotExist(err) { |
| + t.Fatal("replacement directory inherited completeness", err) |
| + } |
| + if err := os.Chmod(state+".retained", 0777); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := g.Completeness(); err == nil { |
| + t.Fatal("accepted writable store directory") |
| + } |
| +} |
diff --git a/internal/release/plan14_generation_completeness_test.go b/internal/release/plan14_generation_completeness_test.go| new file mode 100644 |
| index 000000000..81703d53e |
| --- /dev/null |
| +++ b/internal/release/plan14_generation_completeness_test.go |
| @@ -0,0 +1,317 @@ |
| +package release |
| + |
| +import ( |
| + "context" |
| + "encoding/json" |
| + "errors" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| + "os" |
| + "strings" |
| + "testing" |
| +) |
| + |
| +func generationEvidence() GenerationEvidence { |
| + return GenerationEvidence{Box: "box", Enrollment: "enrollment", Boot: "boot", Migration: 1, Installations: strings.Repeat("a", 64), Launches: strings.Repeat("b", 64), Kernel: strings.Repeat("c", 64)} |
| +} |
| + |
| +func TestPlan14GenerationCompleteness(t *testing.T) { |
| + for _, attack := range []string{"none", "missing-verifier", "kernel-unknown", "empty-evidence", "wrong-box", "wrong-enrollment", "changed-reservations", "corrupt-marker", "sync"} { |
| + t.Run(attack, func(t *testing.T) { |
| + g, s, b := generationFixture(t) |
| + if _, err := g.Reserve(b); err != nil { |
| + t.Fatal(err) |
| + } |
| + records, err := g.Reservations(t.Context()) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + verify := func(context.Context, []GenerationReservation) (GenerationEvidence, error) { |
| + e := generationEvidence() |
| + switch attack { |
| + case "kernel-unknown": |
| + return e, os.ErrPermission |
| + case "empty-evidence": |
| + e.Kernel = "" |
| + case "wrong-box": |
| + e.Box = "other" |
| + case "wrong-enrollment": |
| + e.Enrollment = "other" |
| + case "changed-reservations": |
| + b.Attempt = "another" |
| + if _, err := g.Reserve(b); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "sync": |
| + s.fail = 2 |
| + } |
| + return e, nil |
| + } |
| + if attack == "missing-verifier" { |
| + verify = nil |
| + } |
| + if attack == "corrupt-marker" { |
| + if err := s.Write(generationCompletenessKey, []byte(`{}`)); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + _, err = g.Certify(t.Context(), records, verify) |
| + if attack != "none" { |
| + if err == nil { |
| + t.Fatal("incomplete sources certified") |
| + } |
| + return |
| + } |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + c, err := g.Completeness() |
| + if err != nil || !c.Complete || c.Revision != 1 { |
| + t.Fatalf("certificate: %+v %v", c, err) |
| + } |
| + again, err := g.Certify(t.Context(), records, verify) |
| + if err != nil || !again.Complete || again.Revision <= c.Revision || again.Reservations != c.Reservations { |
| + t.Fatalf("recheck did not recertify: %+v %v", again, err) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestPlan14GenerationCompletenessInvalidation(t *testing.T) { |
| + for _, change := range []string{"reserve", "started", "closed", "retired"} { |
| + t.Run(change, func(t *testing.T) { |
| + g, _, b := generationFixture(t) |
| + r, err := g.Reserve(b) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + verify := func(context.Context, []GenerationReservation) (GenerationEvidence, error) { |
| + return generationEvidence(), nil |
| + } |
| + records, err := g.Reservations(t.Context()) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + first, err := g.Certify(t.Context(), records, verify) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + switch change { |
| + case "reserve": |
| + b.Attempt = "new" |
| + _, err = g.Reserve(b) |
| + case "started": |
| + err = g.Started(r, GenerationProcess{PID: 100, TenantPID: 101, Start: "1", TenantStart: "2", Group: "system.slice/unit", Invocation: "invoke", GroupIdentity: b.Executable}) |
| + default: |
| + err = g.Retire(t.Context(), r, func(context.Context, GenerationReservation) error { return nil }, func(context.Context, GenerationReservation) (string, error) { |
| + if change == "closed" { |
| + return "", os.ErrPermission |
| + } |
| + return strings.Repeat("d", 64), nil |
| + }) |
| + } |
| + if err != nil && change != "closed" { |
| + t.Fatal(err) |
| + } |
| + c, err := g.Completeness() |
| + if err != nil || c.Complete || c.Revision <= first.Revision { |
| + t.Fatalf("mutation left certificate usable: %+v %v", c, err) |
| + } |
| + records, err = g.Reservations(t.Context()) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + next, err := g.Certify(t.Context(), records, verify) |
| + if err != nil || !next.Complete || next.Reservations == first.Reservations || next.Revision <= c.Revision { |
| + t.Fatalf("new revision: %+v %v", next, err) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestPlan14GenerationCompletenessDurability(t *testing.T) { |
| + g, s, b := generationFixture(t) |
| + r, err := g.Reserve(b) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + records, err := g.Reservations(t.Context()) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + verify := func(context.Context, []GenerationReservation) (GenerationEvidence, error) { |
| + return generationEvidence(), nil |
| + } |
| + c, err := g.Certify(t.Context(), records, verify) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + s.fail = 1 |
| + if _, err = g.Completeness(); err == nil { |
| + t.Fatal("visible certificate bypassed sync") |
| + } |
| + g.Store = &generationCompletenessFault{Store: s} |
| + b.Attempt = "new" |
| + if _, err = g.Reserve(b); err == nil { |
| + t.Fatal("unsynced invalidation allowed reserve") |
| + } |
| + if _, err = g.Read(b); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatal("reservation written before invalidation", err) |
| + } |
| + raw, err := s.Read(generationCompletenessKey) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + var pending GenerationCompleteness |
| + if err = json.Unmarshal(raw, &pending); err != nil || pending.Complete { |
| + t.Fatal("invalidation not visible", err) |
| + } |
| + reopened := &RootGenerations{Store: s} |
| + if _, err = reopened.Reserve(b); err != nil { |
| + t.Fatal(err) |
| + } |
| + if got, err := reopened.Read(r.Binding); err != nil || got != r { |
| + t.Fatal("old obligation changed", err) |
| + } |
| + records, err = reopened.Reservations(t.Context()) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + next, err := reopened.Certify(t.Context(), records, verify) |
| + if err != nil || next.Revision <= c.Revision { |
| + t.Fatal("recovery did not advance revision", err) |
| + } |
| +} |
| + |
| +type generationCompletenessFault struct{ rootstate.Store } |
| + |
| +func (s *generationCompletenessFault) Write(k string, b []byte) error { |
| + if err := s.Store.Write(k, b); err != nil { |
| + return err |
| + } |
| + var c GenerationCompleteness |
| + if k == generationCompletenessKey && json.Unmarshal(b, &c) == nil && !c.Complete { |
| + return os.ErrPermission |
| + } |
| + return nil |
| +} |
| + |
| +func TestPlan14GenerationCompletenessPinConsumer(t *testing.T) { |
| + f := newReuseFixture(t) |
| + g, r := generationPinFixture(t, f.opt, "bab", "myapp", "v2") |
| + fail := false |
| + calls := 0 |
| + source := RootGenerationPins{Ledger: g, Reconcile: func(context.Context, []GenerationReservation) (GenerationEvidence, error) { |
| + calls++ |
| + if fail { |
| + return GenerationEvidence{}, os.ErrPermission |
| + } |
| + return generationEvidence(), nil |
| + }} |
| + pins, err := source.StagedPins(t.Context(), f.opt) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if pinned, err := pins.Refers(r.Binding.Path); err != nil || !pinned { |
| + t.Fatal("complete snapshot dropped pending pin", err) |
| + } |
| + c, err := g.Completeness() |
| + if err != nil || !c.Complete || calls != 1 { |
| + t.Fatalf("pin reader omitted certificate: %+v %v calls=%d", c, err, calls) |
| + } |
| + fail = true |
| + if _, err := source.StagedPins(t.Context(), f.opt); err == nil { |
| + t.Fatal("cached completeness survived unknown kernel") |
| + } |
| + c, err = g.Completeness() |
| + if err != nil || c.Complete || calls != 2 { |
| + t.Fatalf("failed scan retained certificate: %+v %v calls=%d", c, err, calls) |
| + } |
| + fail = false |
| + if _, err := source.StagedPins(t.Context(), f.opt); err != nil { |
| + t.Fatal(err) |
| + } |
| +} |
| + |
| +func TestPlan14GenerationCompletenessCertificateSyncFailure(t *testing.T) { |
| + g, s, b := generationFixture(t) |
| + if _, err := g.Reserve(b); err != nil { |
| + t.Fatal(err) |
| + } |
| + records, err := g.Reservations(t.Context()) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + g.Store = generationPositiveCertificateFault{Store: s} |
| + verify := func(context.Context, []GenerationReservation) (GenerationEvidence, error) { |
| + return generationEvidence(), nil |
| + } |
| + if _, err := g.Certify(t.Context(), records, verify); err == nil { |
| + t.Fatal("unsynced certificate returned success") |
| + } |
| + raw, err := s.Read(generationCompletenessKey) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + var visible GenerationCompleteness |
| + if err := json.Unmarshal(raw, &visible); err != nil || !visible.Complete { |
| + t.Fatal("positive rename was not visible", err) |
| + } |
| + if _, err := g.Completeness(); err == nil { |
| + t.Fatal("unsynced visible record bypassed durability retry") |
| + } |
| + g.Store = s |
| + checked := false |
| + if _, err := g.Certify(t.Context(), records, func(context.Context, []GenerationReservation) (GenerationEvidence, error) { |
| + checked = true |
| + return GenerationEvidence{}, os.ErrPermission |
| + }); err == nil || !checked { |
| + t.Fatal("reopen reused old evidence") |
| + } |
| + if _, err := g.Certify(t.Context(), records, verify); err != nil { |
| + t.Fatal(err) |
| + } |
| +} |
| + |
| +type generationPositiveCertificateFault struct{ rootstate.Store } |
| + |
| +func (s generationPositiveCertificateFault) Write(k string, b []byte) error { |
| + if err := s.Store.Write(k, b); err != nil { |
| + return err |
| + } |
| + var c GenerationCompleteness |
| + if k == generationCompletenessKey && json.Unmarshal(b, &c) == nil && c.Complete { |
| + return os.ErrPermission |
| + } |
| + return nil |
| +} |
| + |
| +func TestPlan14GenerationCompletenessUnknownRevisionAdvances(t *testing.T) { |
| + g, _, b := generationFixture(t) |
| + r, err := g.Reserve(b) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + records, err := g.Reservations(t.Context()) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := g.Certify(t.Context(), records, func(context.Context, []GenerationReservation) (GenerationEvidence, error) { |
| + return generationEvidence(), nil |
| + }); 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 "", os.ErrPermission }); err == nil { |
| + t.Fatal("unknown kernel retired") |
| + } |
| + before, err := g.Completeness() |
| + if err != nil || before.Complete { |
| + t.Fatal("missing incomplete state", err) |
| + } |
| + 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) |
| + } |
| + after, err := g.Completeness() |
| + if err != nil || after.Complete || after.Revision <= before.Revision { |
| + t.Fatalf("retirement failed to advance unknown revision: before=%+v after=%+v err=%v", before, after, err) |
| + } |
| +} |
diff --git a/internal/release/plan14_generation_legacy_relay_test.go b/internal/release/plan14_generation_legacy_relay_test.go| new file mode 100644 |
| index 000000000..7d06c9dbd |
| --- /dev/null |
| +++ b/internal/release/plan14_generation_legacy_relay_test.go |
| @@ -0,0 +1,97 @@ |
| +package release |
| + |
| +import ( |
| + "context" |
| + "os" |
| + "path/filepath" |
| + "testing" |
| +) |
| + |
| +func TestPlan14GenerationLegacyRelay(t *testing.T) { |
| + for _, mode := range []string{"pinned", "unknown", "unreserved", "legacy", "fresh", "claims-enabled"} { |
| + t.Run(mode, func(t *testing.T) { |
| + t.Setenv("CARLOS_HOST_CLAIM", "") |
| + ctx, s, opt := heldFixture(t) |
| + data := t.TempDir() |
| + request := writeHeldRequest(t, data, heldTestHost+".json", goodHeldRequest()) |
| + path := VersionPath(opt, "bab", "v1", "console") |
| + if mode != "fresh" { |
| + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + calls, published := 0, false |
| + source := PinSourceFunc(func(context.Context, string) (StagedPaths, error) { |
| + calls++ |
| + if published { |
| + t.Fatal("pin read inside publication") |
| + } |
| + switch mode { |
| + case "unknown", "fresh": |
| + return nil, os.ErrPermission |
| + case "pinned": |
| + return StagedPaths{filepath.Dir(path): true}, nil |
| + } |
| + return StagedPaths{}, nil |
| + }) |
| + auth := func(_ HeldStageRequest, then func() error) error { |
| + if then != nil { |
| + published = true |
| + return then() |
| + } |
| + return nil |
| + } |
| + var err error |
| + if mode == "claims-enabled" { |
| + t.Setenv("CARLOS_HOST_CLAIM", "1") |
| + } |
| + if mode == "legacy" { |
| + err = ServeHeldStageRequests(ctx, s, heldPub, nr(t), opt, data, offGate(t), auth) |
| + } else { |
| + err = ServeHeldStageRequestsWithPins(ctx, s, heldPub, nr(t), opt, data, offGate(t), auth, source) |
| + } |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + refused := mode == "pinned" || mode == "unknown" || mode == "claims-enabled" |
| + if refused { |
| + if published { |
| + t.Fatal("refused stage published") |
| + } |
| + if _, err := os.Stat(request); err != nil { |
| + t.Fatal("refused request not retained", err) |
| + } |
| + for _, p := range []string{path, LiveLink(opt, heldTestHost), filepath.Join(HeldStagedDir(data), heldTestHost+".json")} { |
| + if _, err := os.Lstat(p); !os.IsNotExist(err) { |
| + t.Fatal("refused stage wrote artifact/link/ack", p, err) |
| + } |
| + } |
| + if mode != "claims-enabled" && calls == 0 { |
| + t.Fatal("relay omitted pin reader") |
| + } |
| + if mode == "claims-enabled" && calls != 0 { |
| + t.Fatal("claims refusal read pins") |
| + } |
| + } else { |
| + if !published { |
| + t.Fatal("permitted stage not published") |
| + } |
| + if _, err := os.Stat(request); !os.IsNotExist(err) { |
| + t.Fatal("served request retained", err) |
| + } |
| + if raw, err := os.ReadFile(path); err != nil || string(raw) != "pinned build" { |
| + t.Fatal("permitted artifact missing", err) |
| + } |
| + if v, err := LiveVersion(opt, heldTestHost); err != nil || v != "v1" { |
| + t.Fatal("wrong live target", v, err) |
| + } |
| + if _, err := os.Stat(filepath.Join(HeldStagedDir(data), heldTestHost+".json")); err != nil { |
| + t.Fatal("missing acknowledgement", err) |
| + } |
| + if mode == "fresh" && calls != 0 { |
| + t.Fatal("fresh tree required completeness") |
| + } |
| + } |
| + }) |
| + } |
| +} |
diff --git a/internal/release/rootgenerationcompleteness.go b/internal/release/rootgenerationcompleteness.go| new file mode 100644 |
| index 000000000..0600f3f0a |
| --- /dev/null |
| +++ b/internal/release/rootgenerationcompleteness.go |
| @@ -0,0 +1,142 @@ |
| +package release |
| + |
| +import ( |
| + "context" |
| + "crypto/sha256" |
| + "encoding/hex" |
| + "encoding/json" |
| + "errors" |
| + "os" |
| +) |
| + |
| +const generationCompletenessKey = "completeness.json" |
| + |
| +type GenerationEvidence struct { |
| + Box, Enrollment, Boot string |
| + Migration uint64 |
| + Installations, Launches, Kernel string |
| +} |
| + |
| +type GenerationCompleteness struct { |
| + Schema int |
| + Revision uint64 |
| + Complete bool |
| + Reservations string |
| + Evidence GenerationEvidence |
| +} |
| + |
| +func generationDigest(v any) string { |
| + raw, _ := json.Marshal(v) |
| + sum := sha256.Sum256(raw) |
| + return hex.EncodeToString(sum[:]) |
| +} |
| +func generationHash(s string) bool { |
| + b, err := hex.DecodeString(s) |
| + return err == nil && len(b) == sha256.Size && hex.EncodeToString(b) == s |
| +} |
| +func (e GenerationEvidence) valid() bool { |
| + return e.Box != "" && e.Enrollment != "" && e.Boot != "" && e.Migration > 0 && generationHash(e.Installations) && generationHash(e.Launches) && generationHash(e.Kernel) |
| +} |
| + |
| +// Completeness is evidence, not a cached permission. A destructive reader must |
| +// Certify again under release exclusion and retain that exclusion through use. |
| +func (g *RootGenerations) Completeness() (GenerationCompleteness, error) { |
| + var c GenerationCompleteness |
| + if g == nil || g.Store == nil { |
| + return c, os.ErrPermission |
| + } |
| + raw, err := g.Store.Read(generationCompletenessKey) |
| + if err != nil { |
| + return c, err |
| + } |
| + if len(raw) > rootGenerationCompletenessLimit || strictCanonicalJSON(raw, &c) != nil || c.Schema != 1 || c.Revision == 0 || !generationHash(c.Reservations) || !c.Evidence.valid() { |
| + return GenerationCompleteness{}, os.ErrPermission |
| + } |
| + if err := g.Store.Write(generationCompletenessKey, raw); err != nil { |
| + return GenerationCompleteness{}, err |
| + } |
| + return c, nil |
| +} |
| + |
| +const rootGenerationCompletenessLimit = 4096 |
| + |
| +func (g *RootGenerations) writeCompleteness(c GenerationCompleteness) error { |
| + if c.Revision == 0 { |
| + return os.ErrPermission |
| + } |
| + raw, err := json.Marshal(c) |
| + if err != nil { |
| + return err |
| + } |
| + if len(raw) > rootGenerationCompletenessLimit { |
| + return os.ErrPermission |
| + } |
| + return g.Store.Write(generationCompletenessKey, raw) |
| +} |
| + |
| +// Invalidation precedes each reservation transition. A crash between the two |
| +// writes leaves the old obligation with an unusable certificate. |
| +func (g *RootGenerations) invalidateCompleteness() error { |
| + c, err := g.Completeness() |
| + if errors.Is(err, os.ErrNotExist) { |
| + return nil |
| + } |
| + if err != nil { |
| + return err |
| + } |
| + c.Complete = false |
| + c.Revision++ |
| + return g.writeCompleteness(c) |
| +} |
| + |
| +// Certify accepts only independently reconciled installation, launch and kernel |
| +// evidence. The verifier must not acquire release exclusion: its caller holds it. |
| +func (g *RootGenerations) Certify(ctx context.Context, expected []GenerationReservation, verify func(context.Context, []GenerationReservation) (GenerationEvidence, error)) (GenerationCompleteness, error) { |
| + var zero GenerationCompleteness |
| + if verify == nil { |
| + return zero, os.ErrPermission |
| + } |
| + before, err := g.Reservations(ctx) |
| + if err != nil { |
| + return zero, err |
| + } |
| + digest := generationDigest(before) |
| + if digest != generationDigest(expected) { |
| + return zero, os.ErrPermission |
| + } |
| + // A failed reconciliation must not leave the previous positive record live. |
| + if err := g.invalidateCompleteness(); err != nil { |
| + return zero, err |
| + } |
| + e, err := verify(ctx, before) |
| + if err != nil { |
| + return zero, err |
| + } |
| + if !e.valid() { |
| + return zero, os.ErrPermission |
| + } |
| + for _, r := range before { |
| + if r.Binding.Target.Box != e.Box || r.Binding.Target.Enrollment != e.Enrollment { |
| + return zero, os.ErrPermission |
| + } |
| + } |
| + after, err := g.Reservations(ctx) |
| + if err != nil { |
| + return zero, err |
| + } |
| + if generationDigest(after) != digest { |
| + return zero, os.ErrPermission |
| + } |
| + old, err := g.Completeness() |
| + if err != nil && !errors.Is(err, os.ErrNotExist) { |
| + return zero, err |
| + } |
| + c := GenerationCompleteness{Schema: 1, Revision: old.Revision + 1, Complete: true, Reservations: digest, Evidence: e} |
| + if err := ctx.Err(); err != nil { |
| + return zero, err |
| + } |
| + if err := g.writeCompleteness(c); err != nil { |
| + return zero, err |
| + } |
| + return c, nil |
| +} |
diff --git a/internal/release/rootgenerationpins.go b/internal/release/rootgenerationpins.go| index fab245112..6c2617d5d 100644 |
| --- a/internal/release/rootgenerationpins.go |
| +++ b/internal/release/rootgenerationpins.go |
| @@ -15,8 +15,9 @@ import ( |
| // Verify must reconcile the records with root installation/launch inventory and |
| // kernel references. An empty directory alone never establishes completeness. |
| type RootGenerationPins struct { |
| - Ledger *RootGenerations |
| - Verify func(context.Context, []GenerationReservation) error |
| + Ledger *RootGenerations |
| + Verify func(context.Context, []GenerationReservation) error |
| + Reconcile func(context.Context, []GenerationReservation) (GenerationEvidence, error) |
| } |
| func (s RootGenerationPins) StagedPins(ctx context.Context, optDir string) (StagedPaths, error) { |
| @@ -44,6 +45,12 @@ func (s RootGenerationPins) StagedPins(ctx context.Context, optDir string) (Stag |
| pins.add(b.Path) |
| pins.add(b.Tree) |
| } |
| + if s.Reconcile != nil { |
| + if _, err := s.Ledger.Certify(ctx, records, s.Reconcile); err != nil { |
| + return nil, err |
| + } |
| + return pins, nil |
| + } |
| if s.Verify == nil { |
| return nil, os.ErrPermission |
| } |
| @@ -146,6 +153,12 @@ func (g *RootGenerations) Reservations(ctx context.Context) ([]GenerationReserva |
| sort.Strings(names) |
| records := make([]GenerationReservation, 0, len(names)) |
| for _, name := range names { |
| + if name == generationCompletenessKey { |
| + if _, err := g.Completeness(); err != nil { |
| + return nil, err |
| + } |
| + continue |
| + } |
| if err := ctx.Err(); err != nil { |
| return nil, err |
| } |
diff --git a/internal/release/rootgenerations.go b/internal/release/rootgenerations.go| index 0a9112d08..3b1b07464 100644 |
| --- a/internal/release/rootgenerations.go |
| +++ b/internal/release/rootgenerations.go |
| @@ -125,6 +125,9 @@ func (g *RootGenerations) write(r GenerationReservation) error { |
| if len(raw) > rootstate.MaxRecord { |
| return os.ErrPermission |
| } |
| + if err := g.invalidateCompleteness(); err != nil { |
| + return err |
| + } |
| return g.Store.Write(generationKey(r.Binding), raw) |
| } |