WIP claim Plan 14 track E checkpoint 43 (not for merge)
D3 launch verifier: release-exclusion token across all five VerifyLaunches transitions, registered at root startup; running-pin root witness.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
29 files changed,
+868
−141
internal/host/claimstop_run.go+5 −1internal/host/guardparticipation_test.go+10 −10internal/host/guardtransaction.go+25 −25internal/host/guardtransaction_test.go+7 −7internal/host/participation_flagoff_test.go+5 −5internal/host/plan14_fix2_live_linux_test.go+1 −1internal/host/plan14_generation_running_pin_root_linux_test.go+168 −0internal/host/plan14_launch_verifier_root_linux_test.go+120 −0internal/host/plan14_launch_verifier_test.go+198 −0internal/host/plan9_a_guard_test.go+11 −11internal/host/rootexecution_runtime.go+4 −2internal/host/rootexecution_update_test.go+6 −6internal/host/rootgeneration_inventory.go+6 −2internal/host/rootlaunchverifier.go+68 −0internal/host/systemapply.go+17 −13internal/host/systemparticipation.go+24 −22internal/hostfloor/controllers_linux_test.go+1 −1internal/hostfloor/disk.go+18 −11internal/hostfloor/floor_test.go+2 −2internal/hostfloor/participation.go+3 −3internal/hostfloor/participation_flagoff_test.go+2 −2internal/hostfloor/plan9_a_test.go+2 −2internal/hostfloor/rootparticipation_test.go+5 −5internal/provision/plan14_e_write_census_test.go+1 −1internal/provision/testdata/plan14-root-reads-e.tsv+24 −4internal/release/plan14_release_exclusion_test.go+53 −0internal/release/releaseexclusion.go+53 −0internal/rootparticipation/ledger.go+1 −0internal/rootparticipation/production.go+28 −5
CI passed — run details
CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok amadan.net/carlos/platform.git/launchctx 0.012s
? 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 325.668s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 4.972s
ok github.com/carlosframework/platform/cmd/moveprobe 93.453s
ok github.com/carlosframework/platform/cmd/zddprobe 0.843s
ok github.com/carlosframework/platform/internal/activator 252.407s
ok github.com/carlosframework/platform/internal/agentsock 0.032s
ok github.com/carlosframework/platform/internal/alerting 0.004s
ok github.com/carlosframework/platform/internal/analytics 0.022s
ok github.com/carlosframework/platform/internal/appconfig 3.145s
ok github.com/carlosframework/platform/internal/apperr 0.131s
ok github.com/carlosframework/platform/internal/applog 0.359s
ok github.com/carlosframework/platform/internal/awsinv 0.005s
ok github.com/carlosframework/platform/internal/boxchoice 0.008s
ok github.com/carlosframework/platform/internal/boxid 1.155s
? 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.010s
ok github.com/carlosframework/platform/internal/caps 0.041s
ok github.com/carlosframework/platform/internal/certwarm 0.004s
ok github.com/carlosframework/platform/internal/cgroup 0.876s
ok github.com/carlosframework/platform/internal/changelog 0.445s
ok github.com/carlosframework/platform/internal/clamd 0.059s
? github.com/carlosframework/platform/internal/cliflag [no test files]
ok github.com/carlosframework/platform/internal/console 401.648s
? github.com/carlosframework/platform/internal/controlname [no test files]
? github.com/carlosframework/platform/internal/controlsock [no test files]
ok github.com/carlosframework/platform/internal/dbdoor 6.569s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.026s
ok github.com/carlosframework/platform/internal/dbverify 0.005s
ok github.com/carlosframework/platform/internal/dnsrec 0.006s
ok github.com/carlosframework/platform/internal/docsite 0.008s
ok github.com/carlosframework/platform/internal/domains 2.809s
ok github.com/carlosframework/platform/internal/doorkey 0.008s
ok github.com/carlosframework/platform/internal/economics 0.008s
ok github.com/carlosframework/platform/internal/edge 28.769s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.010s
ok github.com/carlosframework/platform/internal/fleetapi 0.175s
ok github.com/carlosframework/platform/internal/gate 1.703s
ok github.com/carlosframework/platform/internal/genselect 0.023s
ok github.com/carlosframework/platform/internal/geo 0.017s
ok github.com/carlosframework/platform/internal/geoip 0.038s
ok github.com/carlosframework/platform/internal/historyreadback 0.219s
ok github.com/carlosframework/platform/internal/host 153.289s
ok github.com/carlosframework/platform/internal/hostactors 0.356s
ok github.com/carlosframework/platform/internal/hostbox 3.189s
ok github.com/carlosframework/platform/internal/hostclaim 1.030s
ok github.com/carlosframework/platform/internal/hostevidence 0.009s
ok github.com/carlosframework/platform/internal/hostexec 7.854s
ok github.com/carlosframework/platform/internal/hostfence 0.067s
ok github.com/carlosframework/platform/internal/hostfiles 11.457s
ok github.com/carlosframework/platform/internal/hostfloor 0.023s
ok github.com/carlosframework/platform/internal/hostguard 1.392s
ok github.com/carlosframework/platform/internal/hostops 2.805s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.004s
ok github.com/carlosframework/platform/internal/hostquarantine 0.002s
ok github.com/carlosframework/platform/internal/hostrelocate 0.952s
ok github.com/carlosframework/platform/internal/hostresolve 0.232s
ok github.com/carlosframework/platform/internal/hostwrite 0.003s
ok github.com/carlosframework/platform/internal/infraview 0.009s
ok github.com/carlosframework/platform/internal/instancectl 4.282s
ok github.com/carlosframework/platform/internal/instanceexec 2.923s
ok github.com/carlosframework/platform/internal/instancefs 0.016s
ok github.com/carlosframework/platform/internal/instancekey 0.006s
ok github.com/carlosframework/platform/internal/instanceupdates 0.031s
ok github.com/carlosframework/platform/internal/layout 2.264s
ok github.com/carlosframework/platform/internal/lease 10.005s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.008s
ok github.com/carlosframework/platform/internal/ledger 0.021s
ok github.com/carlosframework/platform/internal/mail 0.369s
ok github.com/carlosframework/platform/internal/maintenance 0.124s
ok github.com/carlosframework/platform/internal/maintenanceexec 6.038s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.152s
ok github.com/carlosframework/platform/internal/metrics 0.006s
ok github.com/carlosframework/platform/internal/moves 24.425s
ok github.com/carlosframework/platform/internal/moves/archive 1.400s
ok github.com/carlosframework/platform/internal/objstore 0.452s
ok github.com/carlosframework/platform/internal/ops 0.236s
ok github.com/carlosframework/platform/internal/park 0.302s
ok github.com/carlosframework/platform/internal/privatefile 0.005s
ok github.com/carlosframework/platform/internal/provision 25.762s
ok github.com/carlosframework/platform/internal/publicrelease 0.013s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 6.479s
ok github.com/carlosframework/platform/internal/relay 4.315s
ok github.com/carlosframework/platform/internal/release 26.267s
ok github.com/carlosframework/platform/internal/rollout 5.212s
ok github.com/carlosframework/platform/internal/rolloutwords 0.019s
ok github.com/carlosframework/platform/internal/rootaws 0.011s
ok github.com/carlosframework/platform/internal/rootparticipation 0.010s
ok github.com/carlosframework/platform/internal/rootstate 0.029s
ok github.com/carlosframework/platform/internal/runs 0.565s
ok github.com/carlosframework/platform/internal/s3compat 0.006s
ok github.com/carlosframework/platform/internal/safefile 0.006s
ok github.com/carlosframework/platform/internal/schedule 2.979s
ok github.com/carlosframework/platform/internal/seeds 0.012s
ok github.com/carlosframework/platform/internal/sim/hostlife 21.844s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 3.697s
ok github.com/carlosframework/platform/internal/status 0.064s
ok github.com/carlosframework/platform/internal/steering 0.027s
ok github.com/carlosframework/platform/internal/stopevidence 0.538s
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.005s
ok github.com/carlosframework/platform/internal/tunnelclient 0.007s
ok github.com/carlosframework/platform/internal/unixdial 0.004s
ok github.com/carlosframework/platform/internal/updatewords 0.006s
ok github.com/carlosframework/platform/internal/usage 0.006s
ok github.com/carlosframework/platform/internal/version 0.003s
ok github.com/carlosframework/platform/internal/vet 1.991s
=== 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
linux/arm64 carlos binary: 47055010 bytes (ceiling 52000000)
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
minio: cached, digest verified
mc: cached, digest verified
minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
buckets and scoped user ready
=== RUN TestDeliveredContractMinio
=== RUN TestDeliveredContractMinio/put
=== RUN TestDeliveredContractMinio/get_round-trips_the_body
=== RUN TestDeliveredContractMinio/list_shows_the_key
=== RUN TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN TestDeliveredContractMinio/delete_removes_it
=== RUN TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
--- PASS: TestDeliveredContractMinio/put (0.00s)
--- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
--- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
--- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
--- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok github.com/carlosframework/platform/internal/objstore 0.023s
=== RUN TestT4StorageProbeMinio
2026/10/15 13:24:44 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DEB6CAE35DCEDD, 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.022s
minio contract: PASS
diff --git a/internal/host/claimstop_run.go b/internal/host/claimstop_run.go| index 51ea563d9..8c55ad5e4 100644 |
| --- a/internal/host/claimstop_run.go |
| +++ b/internal/host/claimstop_run.go |
| @@ -19,8 +19,12 @@ import ( |
| func RunInstanceControl(args []string) { |
| ctx, cancel := context.WithCancel(context.Background()) |
| defer cancel() |
| + if err := registerProductionLaunchVerifier(); err != nil { |
| + log.Print(err) |
| + return |
| + } |
| if hostwrite.Enabled(os.Getenv) { |
| - if err := rootparticipation.ActivationReady(ctx); err != nil { |
| + if err := rootparticipation.WithLaunchExclusion(ctx, rootparticipation.ActivationReady); err != nil { |
| log.Print(&ClaimDependencyError{Contracts: []string{"census", "registered verifier", "flag must stay off until Part 3 (G)"}}) |
| return |
| } |
diff --git a/internal/host/guardparticipation_test.go b/internal/host/guardparticipation_test.go| index 79175d84b..13746e1b9 100644 |
| --- a/internal/host/guardparticipation_test.go |
| +++ b/internal/host/guardparticipation_test.go |
| @@ -26,17 +26,17 @@ func sharedGuardFixture(t *testing.T) (*applyFixture, string) { |
| func TestSystemGuardRegionHAPreservesReservationThroughRecovery(t *testing.T) { |
| f, id := sharedGuardFixture(t) |
| a := f.a |
| - if err := a.floor.Begin(id, a.dataDir); err != nil { |
| + if err := a.floor.Begin(t.Context(), id, a.dataDir); err != nil { |
| t.Fatal(err) |
| } |
| before, err := os.ReadFile(updateBegunPath(a.optDir)) |
| if err != nil { |
| t.Fatal(err) |
| } |
| - if err := a.beginExecutableChange("new-build", f.now); err != nil { |
| + if err := a.beginExecutableChange(t.Context(), "new-build", f.now); err != nil { |
| t.Fatal(err) |
| } |
| - a.settleUpdateBegun() |
| + a.settleUpdateBegun(t.Context()) |
| after, err := os.ReadFile(updateBegunPath(a.optDir)) |
| if err != nil || string(after) != string(before) { |
| t.Fatalf("region HA changed the floor reservation: %s, %v", after, err) |
| @@ -61,7 +61,7 @@ func TestSystemGuardRegionHAKeepsFailedCompletionEvidence(t *testing.T) { |
| t.Run(failure, func(t *testing.T) { |
| f, id := sharedGuardFixture(t) |
| a := f.a |
| - if err := a.floor.Begin(id, a.dataDir); err != nil { |
| + if err := a.floor.Begin(t.Context(), id, a.dataDir); err != nil { |
| t.Fatal(err) |
| } |
| if err := os.MkdirAll(a.stagingDir(), 0755); err != nil { |
| @@ -80,18 +80,18 @@ func TestSystemGuardRegionHAKeepsFailedCompletionEvidence(t *testing.T) { |
| if err := os.WriteFile(filepath.Join(a.dataDir, hostexecDir, "pending.json"), []byte(`{"state":"pending"}`), 0600); err != nil { |
| t.Fatal(err) |
| } |
| - if err := a.beginExecutableChange("new-build", f.now); !errors.Is(err, errSystemDeferred) { |
| + if err := a.beginExecutableChange(t.Context(), "new-build", f.now); !errors.Is(err, errSystemDeferred) { |
| t.Fatalf("late participation = %v", err) |
| } |
| case "marker": |
| if err := os.WriteFile(updateBegunPath(a.optDir), []byte(`{"target":"new-build"}`), 0600); err != nil { |
| t.Fatal(err) |
| } |
| - if err := a.beginExecutableChange("new-build", f.now); err == nil { |
| + if err := a.beginExecutableChange(t.Context(), "new-build", f.now); err == nil { |
| t.Fatal("region marker accepted for an armed transaction") |
| } |
| } |
| - a.settleUpdateBegun() |
| + a.settleUpdateBegun(t.Context()) |
| if err := a.Run(t.Context(), true); err == nil { |
| t.Fatal("unsafe recovery succeeded") |
| } |
| @@ -119,7 +119,7 @@ func TestSystemGuardUnpublishedKeepsRegionHAParticipation(t *testing.T) { |
| if err := os.WriteFile(filepath.Join(hostexec(t, f), "pending.json"), []byte(`{}`), 0600); err != nil { |
| t.Fatal(err) |
| } |
| - if err := a.beginExecutableChange("v2", f.now); !errors.Is(err, errSystemDeferred) { |
| + if err := a.beginExecutableChange(t.Context(), "v2", f.now); !errors.Is(err, errSystemDeferred) { |
| t.Fatalf("unpublished floor bypassed participation: %v", err) |
| } |
| if _, err := os.Stat(updateBegunPath(a.optDir)); !os.IsNotExist(err) { |
| @@ -128,7 +128,7 @@ func TestSystemGuardUnpublishedKeepsRegionHAParticipation(t *testing.T) { |
| if err := os.Remove(filepath.Join(a.dataDir, hostexecDir, "pending.json")); err != nil { |
| t.Fatal(err) |
| } |
| - if err := a.beginExecutableChange("v2", f.now); err != nil { |
| + if err := a.beginExecutableChange(t.Context(), "v2", f.now); err != nil { |
| t.Fatal(err) |
| } |
| raw, err := os.ReadFile(updateBegunPath(a.optDir)) |
| @@ -136,7 +136,7 @@ func TestSystemGuardUnpublishedKeepsRegionHAParticipation(t *testing.T) { |
| if err != nil || json.Unmarshal(raw, &begun) != nil || begun.Target != "v2" { |
| t.Fatalf("unpublished marker = %s, %v", raw, err) |
| } |
| - a.settleUpdateBegun() |
| + a.settleUpdateBegun(t.Context()) |
| if _, err := os.Stat(updateBegunPath(a.optDir)); !os.IsNotExist(err) { |
| t.Fatalf("completed unarmed update retained marker: %v", err) |
| } |
diff --git a/internal/host/guardtransaction.go b/internal/host/guardtransaction.go| index 30ef1013d..ce9b43a45 100644 |
| --- a/internal/host/guardtransaction.go |
| +++ b/internal/host/guardtransaction.go |
| @@ -131,7 +131,7 @@ func (a *systemApplier) guardFinished(tx guardTransaction) (bool, error) { |
| // result, and the console's reservation stays held for the agent's retry |
| // or an operator. The withdrawal does not wait for controllers: it gives up |
| // only root's intent, on a box it did not change. |
| -func (a *systemApplier) settleGuard(tx guardTransaction, beforeCompletion func() error) error { |
| +func (a *systemApplier) settleGuard(ctx context.Context, tx guardTransaction, beforeCompletion func() error) error { |
| finished, err := a.guardFinished(tx) |
| if err != nil { |
| return err |
| @@ -140,7 +140,7 @@ func (a *systemApplier) settleGuard(tx guardTransaction, beforeCompletion func() |
| if err := hostfloor.RemoveDurable(filepath.Join(a.floor.Dir, "update-begun.json")); err != nil { |
| return err |
| } |
| - return a.floor.EndParticipation(context.Background(), tx.ID, func() error { return hostfloor.RemoveDurable(filepath.Join(a.floor.Dir, "activation.json")) }) |
| + return a.floor.EndParticipation(ctx, tx.ID, func() error { return hostfloor.RemoveDurable(filepath.Join(a.floor.Dir, "activation.json")) }) |
| } |
| if beforeCompletion != nil { |
| if err := beforeCompletion(); err != nil { |
| @@ -152,7 +152,7 @@ func (a *systemApplier) settleGuard(tx guardTransaction, beforeCompletion func() |
| if err = hostfloor.Durable(filepath.Join(a.floor.Dir, "completed.json"), []byte(tx.ID), 0644); err != nil { |
| return err |
| } |
| - return a.floor.EndParticipation(context.Background(), tx.ID, func() error { return hostfloor.RemoveDurable(filepath.Join(a.floor.Dir, "update-begun.json")) }) |
| + return a.floor.EndParticipation(ctx, tx.ID, func() error { return hostfloor.RemoveDurable(filepath.Join(a.floor.Dir, "update-begun.json")) }) |
| } |
| func (a *systemApplier) guardTransaction() (guardTransaction, error) { |
| raw, err := os.ReadFile(filepath.Join(a.floor.Dir, "activation.json")) |
| @@ -215,22 +215,22 @@ func (a *systemApplier) beginGuard(ctx context.Context, target *release.SystemPo |
| if string(x) != string(y) { |
| return hostfloor.ErrFloor |
| } |
| - return a.floor.Begin(tx.ID, a.dataDir) |
| + return a.floor.Begin(ctx, tx.ID, a.dataDir) |
| } |
| // checkGuardRecovery runs with the apply lock held. No-journal markers can |
| // only precede the first destructive step or follow durable journal removal. |
| // Both are terminal after the selected current image has been verified. |
| -func (a *systemApplier) checkGuardRecovery() error { |
| +func (a *systemApplier) checkGuardRecovery(ctx context.Context) error { |
| if !a.guarded() { |
| - return a.checkUnarmedParticipationRecovery() |
| + return a.checkUnarmedParticipationRecovery(ctx) |
| } |
| p, _, err := a.floor.Load() |
| if err != nil { |
| return err |
| } |
| if p.Mode == "unpublished" { |
| - return a.checkUnarmedParticipationRecovery() |
| + return a.checkUnarmedParticipationRecovery(ctx) |
| } |
| j, err := readSystemJournal(a.optDir) |
| if err != nil { |
| @@ -248,12 +248,12 @@ func (a *systemApplier) checkGuardRecovery() error { |
| } |
| completed, readErr := os.ReadFile(filepath.Join(a.floor.Dir, "completed.json")) |
| if readErr == nil && string(completed) == tx.ID { |
| - return a.floor.EndParticipation(context.Background(), tx.ID, nil) |
| + return a.floor.EndParticipation(ctx, tx.ID, nil) |
| } |
| if readErr != nil && !errors.Is(readErr, os.ErrNotExist) { |
| return readErr |
| } |
| - return a.settleGuard(tx, a.controllersRecovered) |
| + return a.settleGuard(ctx, tx, func() error { return a.controllersRecovered(ctx) }) |
| } |
| if err != nil { |
| return err |
| @@ -269,9 +269,9 @@ func (a *systemApplier) checkGuardRecovery() error { |
| return hostfloor.ErrFloor |
| } |
| if j == nil { |
| - return a.settleGuard(tx, a.controllersRecovered) |
| + return a.settleGuard(ctx, tx, func() error { return a.controllersRecovered(ctx) }) |
| } |
| - if err := a.controllersRecovered(); err != nil { |
| + if err := a.controllersRecovered(ctx); err != nil { |
| return err |
| } |
| if j.Phase == systemPhaseBackingUp { |
| @@ -279,7 +279,7 @@ func (a *systemApplier) checkGuardRecovery() error { |
| } |
| return a.guardImage(systemPrevPath(a.optDir)) |
| } |
| -func (a *systemApplier) endGuard() error { |
| +func (a *systemApplier) endGuard(ctx context.Context) error { |
| if !a.guarded() { |
| return nil |
| } |
| @@ -296,11 +296,11 @@ func (a *systemApplier) endGuard() error { |
| } else if j != nil { |
| return hostfloor.ErrFloor |
| } |
| - return a.settleGuard(tx, nil) |
| + return a.settleGuard(ctx, tx, nil) |
| } |
| -func (a *systemApplier) controllersIdle() error { |
| - return a.participationDisk().ControllersIdle(context.Background(), a.dataDir) |
| +func (a *systemApplier) controllersIdle(ctx context.Context) error { |
| + return a.participationDisk().ControllersIdle(ctx, a.dataDir) |
| } |
| var recoverPendingControllers = recoverRootExecution |
| @@ -308,8 +308,8 @@ var recoverPendingControllers = recoverRootExecution |
| // controllersRecovered lets root recover a provably dead pending controller |
| // before refusing, whether or not a journal is open; a live or unreadable one |
| // still refuses. |
| -func (a *systemApplier) controllersRecovered() error { |
| - err := a.controllersIdle() |
| +func (a *systemApplier) controllersRecovered(ctx context.Context) error { |
| + err := a.controllersIdle(ctx) |
| if err == nil { |
| return nil |
| } |
| @@ -317,7 +317,7 @@ func (a *systemApplier) controllersRecovered() error { |
| if a.floor != nil { |
| guardDir = a.floor.Dir |
| } |
| - required, checkErr := a.participationDisk().ParticipationRequired(context.Background()) |
| + required, checkErr := a.participationDisk().ParticipationRequired(ctx) |
| if checkErr != nil { |
| return errors.Join(err, checkErr) |
| } |
| @@ -325,13 +325,13 @@ func (a *systemApplier) controllersRecovered() error { |
| if !required { |
| recover = execlease.RecoverPending |
| } |
| - if recoveryErr := recover(context.Background(), a.dataDir, guardDir); recoveryErr != nil { |
| + if recoveryErr := recover(ctx, a.dataDir, guardDir); recoveryErr != nil { |
| return errors.Join(err, recoveryErr) |
| } |
| - return a.controllersIdle() |
| + return a.controllersIdle(ctx) |
| } |
| -func (a *systemApplier) closeGuardJournal() error { |
| - required, err := a.participationDisk().ParticipationRequired(context.Background()) |
| +func (a *systemApplier) closeGuardJournal(ctx context.Context) error { |
| + required, err := a.participationDisk().ParticipationRequired(ctx) |
| if err != nil { |
| return err |
| } |
| @@ -343,7 +343,7 @@ func (a *systemApplier) closeGuardJournal() error { |
| if err := hostfloor.RemoveDurable(a.file("journal.json")); err != nil { |
| return err |
| } |
| - return a.endGuard() |
| + return a.endGuard(ctx) |
| } |
| active, err := a.activeFloor() |
| if err != nil { |
| @@ -353,9 +353,9 @@ func (a *systemApplier) closeGuardJournal() error { |
| return err |
| } |
| if !active { |
| - return a.clearUpdateBegun() |
| + return a.clearUpdateBegun(ctx) |
| } |
| - return a.endGuard() |
| + return a.endGuard(ctx) |
| } |
| func (a *systemApplier) guardRun(ctx context.Context, wait time.Duration, name string, args ...string) (string, error) { |
| if !a.guarded() { |
diff --git a/internal/host/guardtransaction_test.go b/internal/host/guardtransaction_test.go| index fcf5989bf..310d0e3aa 100644 |
| --- a/internal/host/guardtransaction_test.go |
| +++ b/internal/host/guardtransaction_test.go |
| @@ -44,10 +44,10 @@ func TestUnpublishedCompletionAndRecovery(t *testing.T) { |
| if err := f.a.beginGuard(t.Context(), nil); err != nil { |
| t.Fatal(err) |
| } |
| - if err := f.a.endGuard(); err != nil { |
| + if err := f.a.endGuard(t.Context()); err != nil { |
| t.Fatal(err) |
| } |
| - if err := f.a.checkGuardRecovery(); err != nil { |
| + if err := f.a.checkGuardRecovery(t.Context()); err != nil { |
| t.Fatal(err) |
| } |
| if _, err := os.Stat(filepath.Join(f.a.floor.Dir, "activation.json")); !os.IsNotExist(err) { |
| @@ -119,7 +119,7 @@ func TestGuardRecoveryEveryTerminalCrashState(t *testing.T) { |
| marker := filepath.Join(a.floor.Dir, "update-begun.json") |
| completed := filepath.Join(a.floor.Dir, "completed.json") |
| if state != "prepared" { |
| - if err := a.floor.Begin(id, a.dataDir); err != nil { |
| + if err := a.floor.Begin(t.Context(), id, a.dataDir); err != nil { |
| t.Fatal(err) |
| } |
| } |
| @@ -157,7 +157,7 @@ func TestGuardRecoveryEveryTerminalCrashState(t *testing.T) { |
| if len(f.calls) != 0 { |
| t.Fatal("recovery executed a binary", f.calls) |
| } |
| - if err := a.floor.Begin("next", a.dataDir); err != nil { |
| + if err := a.floor.Begin(t.Context(), "next", a.dataDir); err != nil { |
| t.Fatal("subsequent update blocked", err) |
| } |
| }) |
| @@ -168,7 +168,7 @@ func TestGuardRecoveryRetainsMarkerOnInvalidImageOrMarker(t *testing.T) { |
| t.Run(bad, func(t *testing.T) { |
| f, id := guardRecoveryFixture(t) |
| a := f.a |
| - if err := a.floor.Begin(id, a.dataDir); err != nil { |
| + if err := a.floor.Begin(t.Context(), id, a.dataDir); err != nil { |
| t.Fatal(err) |
| } |
| marker := filepath.Join(a.floor.Dir, "update-begun.json") |
| @@ -230,7 +230,7 @@ func TestGuardRecoveryRefusesControllersAfterBegin(t *testing.T) { |
| t.Run(state.name+"-"+path.name, func(t *testing.T) { |
| f, id := guardRecoveryFixture(t) |
| a := f.a |
| - if err := a.floor.Begin(id, a.dataDir); err != nil { |
| + if err := a.floor.Begin(t.Context(), id, a.dataDir); err != nil { |
| t.Fatal(err) |
| } |
| marker := filepath.Join(a.floor.Dir, "update-begun.json") |
| @@ -381,7 +381,7 @@ func TestPlan9M8GuardRecoveryRecoversPendingWithoutJournal(t *testing.T) { |
| t.Run(name, func(t *testing.T) { |
| f, id := guardRecoveryFixture(t) |
| a := f.a |
| - if err := a.floor.Begin(id, a.dataDir); err != nil { |
| + if err := a.floor.Begin(t.Context(), id, a.dataDir); err != nil { |
| t.Fatal(err) |
| } |
| marker := filepath.Join(a.floor.Dir, "update-begun.json") |
diff --git a/internal/host/participation_flagoff_test.go b/internal/host/participation_flagoff_test.go| index b223e42dc..e178f7e37 100644 |
| --- a/internal/host/participation_flagoff_test.go |
| +++ b/internal/host/participation_flagoff_test.go |
| @@ -99,7 +99,7 @@ func TestPlan14ParticipationFlagAndCensusExclusion(t *testing.T) { |
| } |
| } |
| f.a.participation.VerifyLaunches = nil |
| - err := f.a.beginExecutableChange("new", f.now) |
| + err := f.a.beginExecutableChange(t.Context(), "new", f.now) |
| if (err != nil) != (flag != "" || census) { |
| t.Fatalf("admission error=%v flag=%q census=%t", err, flag, census) |
| } |
| @@ -121,13 +121,13 @@ func TestPlan14ParticipationFlagOffControllersRecovered(t *testing.T) { |
| return nil |
| } |
| dir := hostexec(t, f) |
| - if err := f.a.controllersRecovered(); err != nil { |
| + if err := f.a.controllersRecovered(t.Context()); err != nil { |
| t.Fatal(err) |
| } |
| if err := os.WriteFile(filepath.Join(dir, "pending.json"), []byte(`{}`), 0600); err != nil { |
| t.Fatal(err) |
| } |
| - if err := f.a.controllersRecovered(); err == nil { |
| + if err := f.a.controllersRecovered(t.Context()); err == nil { |
| t.Fatal("malformed legacy controller admitted") |
| } |
| } |
| @@ -140,10 +140,10 @@ func TestPlan14ParticipationFlagOffGuardRecovery(t *testing.T) { |
| t.Fatal(err) |
| } |
| a.participation.VerifyLaunches = nil |
| - if err := a.floor.Begin(id, a.dataDir); err != nil { |
| + if err := a.floor.Begin(t.Context(), id, a.dataDir); err != nil { |
| t.Fatal(err) |
| } |
| - if err := a.beginExecutableChange("new-build", f.now); err != nil { |
| + if err := a.beginExecutableChange(t.Context(), "new-build", f.now); err != nil { |
| t.Fatal(err) |
| } |
| if err := a.journal(systemJournal{Phase: systemPhaseBackingUp, Target: "new-build", PrevBuild: "old-build", StartedAt: f.now}); err != nil { |
diff --git a/internal/host/plan14_fix2_live_linux_test.go b/internal/host/plan14_fix2_live_linux_test.go| index 2ab363b71..5dd70b0d2 100644 |
| --- a/internal/host/plan14_fix2_live_linux_test.go |
| +++ b/internal/host/plan14_fix2_live_linux_test.go |
| @@ -183,7 +183,7 @@ func TestPlan14Fix2PlatformUpdateClaimsOffNoCensus(t *testing.T) { |
| if err := os.Mkdir(filepath.Join(data, ".hostexec"), 0700); err != nil { |
| t.Fatal(err) |
| } |
| - if err := guard.Begin("update", data); err != nil { |
| + if err := guard.Begin(t.Context(), "update", data); err != nil { |
| t.Fatal("real update path", err) |
| } |
| if _, err := os.Stat(filepath.Join(guard.Dir, "update-begun.json")); err != nil { |
diff --git a/internal/host/plan14_generation_running_pin_root_linux_test.go b/internal/host/plan14_generation_running_pin_root_linux_test.go| new file mode 100644 |
| index 000000000..790a7ea67 |
| --- /dev/null |
| +++ b/internal/host/plan14_generation_running_pin_root_linux_test.go |
| @@ -0,0 +1,168 @@ |
| +//go:build linux && rootrunner |
| + |
| +package host |
| + |
| +import ( |
| + "context" |
| + "crypto/sha256" |
| + "encoding/hex" |
| + "os" |
| + "os/exec" |
| + "path/filepath" |
| + "strconv" |
| + "strings" |
| + "syscall" |
| + "testing" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/registry" |
| + "github.com/carlosframework/platform/internal/release" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| +) |
| + |
| +func TestPlan14GenerationRunningTenantPinRootRunner(t *testing.T) { |
| + dir := os.Getenv("PLAN14_E43_RUNNING_CHILD") |
| + if dir == "" { |
| + dir = generationRootRunnerDir(t) |
| + binary, err := os.Executable() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + cmd := exec.CommandContext(t.Context(), "unshare", "--mount", "--pid", "--fork", "--mount-proc", binary, "-test.run=^TestPlan14GenerationRunningTenantPinRootRunner$", "-test.v", "-test.timeout=2m") |
| + cmd.Dir = dir |
| + cmd.Env = append(os.Environ(), "PLAN14_E43_RUNNING_CHILD="+dir) |
| + if out, err := cmd.CombinedOutput(); err != nil { |
| + t.Fatalf("running tenant pin: %v\n%s", err, out) |
| + } |
| + return |
| + } |
| + t.Setenv("CARLOS_OPT_DIR", dir) |
| + if err := os.Chmod(dir, 0755); err != nil { |
| + t.Fatal(err) |
| + } |
| + body, err := os.ReadFile("/bin/sleep") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, v := range []string{"v1", "v2", "v3"} { |
| + if err := release.StageArtifact(dir, "bab", v, "shop", body); err != nil { |
| + t.Fatal(err) |
| + } |
| + old := time.Now().Add(-48 * time.Hour) |
| + if err := os.Chtimes(release.VersionPath(dir, "bab", v, "shop"), old, old); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + identity := func(path string) rootstate.FileIdentity { |
| + t.Helper() |
| + f, err := os.Open(path) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer f.Close() |
| + id, err := rootstate.Identity(f) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + return id |
| + } |
| + store, err := rootstate.Open(filepath.Join(dir, "generations"), true, false) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer store.Close() |
| + ledger := &release.RootGenerations{Store: store} |
| + x := &release.ReleaseExclusion{Open: func() (rootstate.Store, error) { return rootstate.Open(filepath.Join(dir, "release"), true, false) }} |
| + boot, err := (generationKernel{proc: "/proc"}).boot() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + start, err := claimLaunchProcessStart("/proc", os.Getpid()) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + sum := sha256.Sum256(body) |
| + reserve := func(v string) release.GenerationReservation { |
| + t.Helper() |
| + path := release.VersionPath(dir, "bab", v, "shop") |
| + b := release.GenerationBinding{Binding: release.HistoryBinding{Host: "shop.example", Account: "bab", App: "shop"}, Target: release.GenerationOwner{Host: "shop.example", Incarnation: "fixture", Stamp: 1, Session: 1, Box: "box", Enrollment: "en", BootID: boot, ControllerID: "controller"}, Installation: "fixture", Record: "apps/bab/shop/instances/shop.example.json", RecordETag: "fixture", Attempt: v, Inputs: "fixture", Invocation: v, ControllerPID: os.Getpid(), ControllerStart: start, Version: v, Digest: hex.EncodeToString(sum[:]), Path: path, Tree: filepath.Dir(path), Executable: identity(path), TreeIdentity: identity(filepath.Dir(path))} |
| + var r release.GenerationReservation |
| + if err := x.With(t.Context(), func(context.Context) error { var err error; r, err = ledger.Reserve(b); return err }); err != nil { |
| + t.Fatal(err) |
| + } |
| + return r |
| + } |
| + v1, v3 := reserve("v1"), reserve("v3") |
| + child := exec.Command(v1.Binding.Path, "120") |
| + child.SysProcAttr = &syscall.SysProcAttr{Credential: &syscall.Credential{Uid: 65534, Gid: 65534}} |
| + child.Dir = "/" |
| + if err := child.Start(); err != nil { |
| + t.Fatal(err) |
| + } |
| + defer func() { child.Process.Kill(); child.Wait() }() |
| + pid := strconv.Itoa(child.Process.Pid) |
| + deadline := time.Now().Add(5 * time.Second) |
| + for { |
| + actual, err := os.Readlink("/proc/" + pid + "/exe") |
| + if err == nil && actual == v1.Binding.Path { |
| + break |
| + } |
| + if time.Now().After(deadline) { |
| + t.Fatalf("tenant never executed staged build: %s %v", actual, err) |
| + } |
| + time.Sleep(time.Millisecond) |
| + } |
| + status, err := os.ReadFile("/proc/" + pid + "/status") |
| + if err != nil || !strings.Contains(string(status), "Uid:\t65534\t65534\t65534\t65534") { |
| + t.Fatalf("tenant uid: %s %v", status, err) |
| + } |
| + source := generationPinSource{open: func() (rootstate.Store, error) { |
| + return rootstate.Open(filepath.Join(dir, "generations"), false, false) |
| + }, verify: func(ctx context.Context, _ []release.GenerationReservation) error { |
| + _, _, err := productionGenerationKernelCensus(ctx) |
| + return err |
| + }} |
| + rows := fakeLister{routes: []registry.Route{{Host: "shop.example", Account: "bab", App: "shop", Version: "v2"}}} |
| + legacy := filepath.Join(dir, "zdd") |
| + if err := os.Mkdir(legacy, 0755); err != nil { |
| + t.Fatal(err) |
| + } |
| + check := func(label string) { |
| + t.Helper() |
| + if err := x.With(t.Context(), func(ctx context.Context) error { |
| + if pins, err := source.StagedPins(ctx, dir); err == nil || pins != nil { |
| + t.Fatalf("%s: uncontained tenant certified", label) |
| + } |
| + sweepStagedVersions(rows, dir, filepath.Join(legacy, "generations.json"), time.Hour, time.Now(), source) |
| + return nil |
| + }); err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, r := range []release.GenerationReservation{v1, v3} { |
| + got, err := ledger.Read(r.Binding) |
| + if err != nil || got != r { |
| + t.Fatalf("%s: reservation changed: %v", label, err) |
| + } |
| + raw, err := os.ReadFile(r.Binding.Path) |
| + if err != nil || sha256.Sum256(raw) != sum || identity(r.Binding.Path) != r.Binding.Executable { |
| + t.Fatalf("%s: pinned build changed: %v", label, err) |
| + } |
| + } |
| + } |
| + for _, file := range []string{"generations.json", "generations.seq"} { |
| + if err := os.WriteFile(filepath.Join(legacy, file), []byte("{}"), 0644); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + check("empty service publication") |
| + for _, file := range []string{"generations.json", "generations.seq"} { |
| + if err := os.Remove(filepath.Join(legacy, file)); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + check("deleted service publication") |
| + if err := os.Rename(legacy, legacy+".old"); err != nil { |
| + t.Fatal(err) |
| + } |
| + check("renamed service publication parent") |
| +} |
diff --git a/internal/host/plan14_launch_verifier_root_linux_test.go b/internal/host/plan14_launch_verifier_root_linux_test.go| new file mode 100644 |
| index 000000000..d155e89ee |
| --- /dev/null |
| +++ b/internal/host/plan14_launch_verifier_root_linux_test.go |
| @@ -0,0 +1,120 @@ |
| +//go:build linux && rootrunner |
| + |
| +package host |
| + |
| +import ( |
| + "context" |
| + "encoding/json" |
| + "errors" |
| + "os" |
| + "os/exec" |
| + "path/filepath" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/release" |
| + "github.com/carlosframework/platform/internal/rootparticipation" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| +) |
| + |
| +func TestPlan14LaunchVerifierRootRunner(t *testing.T) { |
| + dir := os.Getenv("PLAN14_E43_VERIFIER_CHILD") |
| + if dir == "" { |
| + dir = generationRootRunnerDir(t) |
| + binary, err := filepath.Abs(os.Args[0]) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + cmd := exec.CommandContext(t.Context(), "unshare", "--mount", "--pid", "--fork", "--mount-proc", binary, "-test.run=^TestPlan14LaunchVerifierRootRunner$", "-test.v", "-test.timeout=2m") |
| + cmd.Dir = dir |
| + cmd.Env = append(os.Environ(), "PLAN14_E43_VERIFIER_CHILD="+dir) |
| + if out, err := cmd.CombinedOutput(); err != nil { |
| + t.Fatalf("root verifier: %v\n%s", err, out) |
| + } |
| + return |
| + } |
| + t.Setenv("CARLOS_OPT_DIR", dir) |
| + if err := os.MkdirAll(filepath.Join(dir, "versions"), 0755); err != nil { |
| + t.Fatal(err) |
| + } |
| + state, err := rootstate.Open(filepath.Join(dir, "participation"), true, false) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer state.Close() |
| + generations, err := rootstate.Open(filepath.Join(dir, "generations"), true, false) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + generations.Close() |
| + x := &release.ReleaseExclusion{Open: func() (rootstate.Store, error) { return rootstate.Open(filepath.Join(dir, "release"), true, false) }} |
| + l := rootparticipation.Ledger{State: state, ExcludeLaunches: x.With} |
| + c := rootparticipation.Census{Schema: 1, Box: "box", Enrollment: "en", Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true} |
| + write := func() { |
| + raw, _ := json.Marshal(c) |
| + if err := state.Write("census.json", raw); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + write() |
| + v := rootLaunchVerifier{exclude: x, open: func() (rootstate.Store, error) { |
| + return rootstate.Open(filepath.Join(dir, "generations"), false, false) |
| + }, reconcile: generationReconciler{ |
| + read: func(context.Context) (generationInventory, error) { |
| + c, err := l.RecoveryCensus() |
| + return generationInventory{Census: c}, err |
| + }, kernel: productionGenerationKernelCensus, |
| + }} |
| + l.VerifyLaunches = v.verify |
| + if err := rootparticipation.RegisterLaunchVerifier(v.verify, x.With); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := rootparticipation.RegisterLaunchVerifier(v.verify, x.With); !errors.Is(err, rootparticipation.ErrUnavailable) { |
| + t.Fatal("registered twice", err) |
| + } |
| + if err := l.Idle(t.Context()); !errors.Is(err, rootparticipation.ErrUnavailable) { |
| + t.Fatal("missing token", err) |
| + } |
| + if err := l.ActivationReady(t.Context()); !errors.Is(err, rootparticipation.ErrUnavailable) { |
| + t.Fatal("activation accepted missing token", err) |
| + } |
| + for _, name := range []string{"idle", "begin", "end", "recover", "activation"} { |
| + t.Run(name, func(t *testing.T) { |
| + if name == "recover" { |
| + c.Pending = "acquisition" |
| + write() |
| + } |
| + if err := x.With(t.Context(), func(ctx context.Context) error { |
| + switch name { |
| + case "idle": |
| + return l.Idle(ctx) |
| + case "begin": |
| + return l.BeginUpdate(ctx, "update") |
| + case "end": |
| + return l.EndUpdate(ctx, "update") |
| + case "recover": |
| + return l.RecoverAcquisition(ctx) |
| + default: |
| + return l.ActivationReady(ctx) |
| + } |
| + }); err != nil { |
| + t.Fatal(err) |
| + } |
| + }) |
| + } |
| + path := filepath.Join(dir, "versions", "unreserved") |
| + if err := os.WriteFile(path, []byte("unreserved staged object"), 0644); err != nil { |
| + t.Fatal(err) |
| + } |
| + file, err := os.Open(path) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := x.With(t.Context(), l.Idle); err == nil { |
| + file.Close() |
| + t.Fatal("kernel reference escaped verifier") |
| + } |
| + file.Close() |
| + if err := x.With(t.Context(), l.Idle); err != nil { |
| + t.Fatal("released reference refused", err) |
| + } |
| +} |
diff --git a/internal/host/plan14_launch_verifier_test.go b/internal/host/plan14_launch_verifier_test.go| new file mode 100644 |
| index 000000000..181a0d21e |
| --- /dev/null |
| +++ b/internal/host/plan14_launch_verifier_test.go |
| @@ -0,0 +1,198 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "encoding/json" |
| + "errors" |
| + "os" |
| + "path/filepath" |
| + "strings" |
| + "sync" |
| + "testing" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/hostfloor" |
| + "github.com/carlosframework/platform/internal/release" |
| + "github.com/carlosframework/platform/internal/rootparticipation" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| +) |
| + |
| +func TestPlan14LaunchVerifierToken(t *testing.T) { |
| + lock := &plan14ReleaseStore{} |
| + x := &release.ReleaseExclusion{Open: func() (rootstate.Store, error) { return lock, nil }} |
| + disk := &rollbackDisk{dir: t.TempDir()} |
| + reads := 0 |
| + v := rootLaunchVerifier{exclude: x, open: func() (rootstate.Store, error) { reads++; return disk, nil }, reconcile: generationReconciler{ |
| + read: func(context.Context) (generationInventory, error) { |
| + return generationInventory{Census: rootparticipation.Census{Schema: 1, Box: "box", Enrollment: "en", Migration: 1, Revision: 1, LaunchRevision: 7, Complete: true}}, nil |
| + }, |
| + kernel: func(context.Context) (string, string, error) { return "boot", strings.Repeat("a", 64), nil }, |
| + }} |
| + if !errors.Is(v.verify(t.Context(), 7), rootparticipation.ErrUnavailable) || reads != 0 { |
| + t.Fatal("missing token reached authority") |
| + } |
| + foreign := &release.ReleaseExclusion{Open: x.Open} |
| + if err := foreign.With(t.Context(), func(ctx context.Context) error { return v.verify(ctx, 7) }); !errors.Is(err, rootparticipation.ErrUnavailable) || reads != 0 { |
| + t.Fatal("foreign token reached authority", err) |
| + } |
| + var stale context.Context |
| + if err := x.With(t.Context(), func(ctx context.Context) error { stale = ctx; return v.verify(ctx, 7) }); err != nil { |
| + t.Fatal(err) |
| + } |
| + if !errors.Is(v.verify(stale, 7), rootparticipation.ErrUnavailable) { |
| + t.Fatal("stale token accepted") |
| + } |
| + if err := x.With(t.Context(), func(ctx context.Context) error { return v.verify(ctx, 8) }); err == nil { |
| + t.Fatal("wrong launch revision accepted") |
| + } |
| +} |
| + |
| +func TestPlan14LaunchVerifierCallerOrder(t *testing.T) { |
| + for _, name := range []string{"idle", "begin", "end", "recover", "activation", "floor-begin", "floor-end", "system-begin", "system-clear", "system-recovery"} { |
| + t.Run(name, func(t *testing.T) { |
| + lock := &plan14ReleaseStore{} |
| + x := &release.ReleaseExclusion{Open: func() (rootstate.Store, error) { return lock, nil }} |
| + l := updateParticipationFixture(t) |
| + l.State = &d3ParticipationDisk{Store: l.State} |
| + l.ExcludeLaunches = x.With |
| + verified := 0 |
| + l.VerifyLaunches = func(ctx context.Context, _ uint64) error { |
| + verified++ |
| + if err := x.Check(ctx); err != nil { |
| + return rootparticipation.ErrUnavailable |
| + } |
| + return nil |
| + } |
| + if name == "end" || name == "floor-end" || name == "system-clear" { |
| + if err := x.With(t.Context(), func(ctx context.Context) error { return l.BeginUpdate(ctx, "update") }); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + if name == "recover" { |
| + c, err := l.Census() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + c.Pending = "acquisition" |
| + raw, _ := json.Marshal(c) |
| + if err := l.State.Write("census.json", raw); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + floor := hostfloor.Disk{Dir: t.TempDir(), Participation: l} |
| + a := &systemApplier{participation: l, optDir: t.TempDir(), dataDir: t.TempDir()} |
| + run := func(ctx context.Context) error { |
| + switch name { |
| + case "idle": |
| + return floor.ControllersIdle(ctx) |
| + case "begin": |
| + return floor.BeginParticipation(ctx, "update", a.dataDir, nil, func() error { t.Error("legacy selected"); return nil }) |
| + case "end": |
| + return floor.EndParticipation(ctx, "update", nil) |
| + case "recover": |
| + return x.With(ctx, l.RecoverAcquisition) |
| + case "activation": |
| + return l.WithLaunchExclusion(ctx, func(ctx context.Context) error { return l.ActivationReady(ctx) }) |
| + case "floor-begin": |
| + return floor.Begin(ctx, "update", a.dataDir) |
| + case "floor-end": |
| + return floor.EndParticipation(ctx, "update", nil) |
| + case "system-begin": |
| + return a.beginExecutableChange(ctx, "new", time.Now()) |
| + case "system-clear": |
| + return a.clearUpdateBegun(ctx) |
| + case "system-recovery": |
| + return a.controllersRecovered(ctx) |
| + } |
| + return nil |
| + } |
| + held, leave, holderDone := make(chan struct{}), make(chan struct{}), make(chan error, 1) |
| + go func() { |
| + holderDone <- x.With(t.Context(), func(ctx context.Context) error { |
| + close(held) |
| + <-leave |
| + unlock, err := l.State.Lock(ctx, "transaction.lock") |
| + if err == nil { |
| + unlock() |
| + } |
| + return err |
| + }) |
| + }() |
| + <-held |
| + done := make(chan error, 1) |
| + go func() { done <- run(t.Context()) }() |
| + select { |
| + case err := <-done: |
| + t.Fatal("caller escaped held release", err) |
| + case <-time.After(15 * time.Millisecond): |
| + } |
| + close(leave) |
| + select { |
| + case err := <-holderDone: |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + case <-time.After(2 * time.Second): |
| + t.Fatal("release holder deadlocked") |
| + } |
| + select { |
| + case err := <-done: |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + case <-time.After(2 * time.Second): |
| + t.Fatal("caller deadlocked") |
| + } |
| + if verified == 0 { |
| + t.Fatal("verifier not reached") |
| + } |
| + }) |
| + } |
| +} |
| + |
| +type d3ParticipationDisk struct { |
| + rootstate.Store |
| + mu sync.Mutex |
| +} |
| + |
| +func (d *d3ParticipationDisk) Lock(ctx context.Context, name string) (func(), error) { |
| + d.mu.Lock() |
| + return d.mu.Unlock, nil |
| +} |
| + |
| +func TestPlan14LaunchVerifierNoCensus(t *testing.T) { |
| + t.Setenv("CARLOS_HOST_CLAIM", "") |
| + l := updateParticipationFixture(t) |
| + if err := l.State.Remove("census.json"); err != nil { |
| + t.Fatal(err) |
| + } |
| + l.ExcludeLaunches = func(context.Context, func(context.Context) error) error { |
| + t.Fatal("no-census box acquired release exclusion") |
| + return nil |
| + } |
| + l.VerifyLaunches = func(context.Context, uint64) error { t.Fatal("no-census box consulted verifier"); return nil } |
| + floor := hostfloor.Disk{Dir: t.TempDir(), Participation: l} |
| + data := t.TempDir() |
| + if err := os.Mkdir(filepath.Join(data, ".hostexec"), 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := floor.ControllersIdle(t.Context(), data); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := floor.Begin(t.Context(), "legacy", data); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := floor.EndParticipation(t.Context(), "legacy", nil); err != nil { |
| + t.Fatal(err) |
| + } |
| + a := &systemApplier{participation: l, optDir: t.TempDir(), dataDir: data} |
| + if err := a.beginExecutableChange(t.Context(), "new", time.Now()); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := a.clearUpdateBegun(t.Context()); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := a.checkUnarmedParticipationRecovery(t.Context()); err != nil { |
| + t.Fatal(err) |
| + } |
| +} |
diff --git a/internal/host/plan9_a_guard_test.go b/internal/host/plan9_a_guard_test.go| index 08f378207..66381082b 100644 |
| --- a/internal/host/plan9_a_guard_test.go |
| +++ b/internal/host/plan9_a_guard_test.go |
| @@ -87,7 +87,7 @@ func TestPlan9AUnchangedTransactionRecordsNoCompletion(t *testing.T) { |
| marker := filepath.Join(a.floor.Dir, updateBegunFile) |
| record := filepath.Join(a.dataDir, hostexecDir, "pending.json") |
| if state != "prepared" { |
| - if err := a.floor.Begin(id, a.dataDir); err != nil { |
| + if err := a.floor.Begin(t.Context(), id, a.dataDir); err != nil { |
| t.Fatal(err) |
| } |
| } |
| @@ -99,7 +99,7 @@ func TestPlan9AUnchangedTransactionRecordsNoCompletion(t *testing.T) { |
| if err := os.WriteFile(record, []byte(`{"state":"pending"}`), 0600); err != nil { |
| t.Fatal(err) |
| } |
| - if err := a.beginExecutableChange("new-build", f.now); !errors.Is(err, errSystemDeferred) { |
| + if err := a.beginExecutableChange(t.Context(), "new-build", f.now); !errors.Is(err, errSystemDeferred) { |
| t.Fatalf("late participation = %v", err) |
| } |
| case "abandoned-install": |
| @@ -133,7 +133,7 @@ func TestPlan9AUnchangedTransactionRecordsNoCompletion(t *testing.T) { |
| } |
| } |
| // The retry begins the same reservation again. |
| - if err := a.floor.Begin(id, a.dataDir); err != nil { |
| + if err := a.floor.Begin(t.Context(), id, a.dataDir); err != nil { |
| t.Fatalf("retry blocked: %v", err) |
| } |
| }) |
| @@ -144,7 +144,7 @@ func TestPlan9AUnchangedTransactionRecordsNoCompletion(t *testing.T) { |
| func TestPlan9AForwardFinishRecordsCompletion(t *testing.T) { |
| f, id := plan9aTargetFixture(t) |
| a := f.a |
| - if err := a.floor.Begin(id, a.dataDir); err != nil { |
| + if err := a.floor.Begin(t.Context(), id, a.dataDir); err != nil { |
| t.Fatal(err) |
| } |
| if err := a.journal(systemJournal{Phase: systemPhaseGated, Target: "new-build", PrevBuild: "old-build", StartedAt: f.now}); err != nil { |
| @@ -153,7 +153,7 @@ func TestPlan9AForwardFinishRecordsCompletion(t *testing.T) { |
| if err := os.WriteFile(systemBinaryPath(a.optDir), plan9aNewBody, 0755); err != nil { |
| t.Fatal(err) |
| } |
| - if err := a.closeGuardJournal(); err != nil { |
| + if err := a.closeGuardJournal(t.Context()); err != nil { |
| t.Fatal(err) |
| } |
| if raw, err := os.ReadFile(filepath.Join(a.floor.Dir, "completed.json")); err != nil || string(raw) != id { |
| @@ -171,7 +171,7 @@ func TestPlan9ABackwardRecoveryRecordsCompletion(t *testing.T) { |
| if err != nil { |
| t.Fatal(err) |
| } |
| - if err := a.floor.Begin(id, a.dataDir); err != nil { |
| + if err := a.floor.Begin(t.Context(), id, a.dataDir); err != nil { |
| t.Fatal(err) |
| } |
| if err := os.WriteFile(systemPrevPath(a.optDir), old, 0755); err != nil { |
| @@ -223,7 +223,7 @@ func TestPlan9ABackwardRecoveryRecordsCompletion(t *testing.T) { |
| func TestPlan9ARolledBackMarkerCompletesAfterCrash(t *testing.T) { |
| f, id := plan9aTargetFixture(t) |
| a := f.a |
| - if err := a.floor.Begin(id, a.dataDir); err != nil { |
| + if err := a.floor.Begin(t.Context(), id, a.dataDir); err != nil { |
| t.Fatal(err) |
| } |
| if err := hostfloor.MarkRolledBack(a.floor.Dir, id); err != nil { |
| @@ -249,7 +249,7 @@ func TestPlan9ARollbackCloseRefusesForeignMarker(t *testing.T) { |
| if err := a.journal(systemJournal{Phase: systemPhaseUnits, Target: "new-build", PrevBuild: "old-build", StartedAt: f.now}); err != nil { |
| t.Fatal(err) |
| } |
| - if err := a.closeGuardRollback(); err == nil { |
| + if err := a.closeGuardRollback(t.Context()); err == nil { |
| t.Fatal("foreign marker accepted") |
| } |
| if _, err := os.Stat(a.file("journal.json")); err != nil { |
| @@ -303,7 +303,7 @@ func TestPlan9AGuardArmCreatesControllerDir(t *testing.T) { |
| } |
| verify := a.floor.Participation.VerifyLaunches |
| a.floor.Participation.VerifyLaunches = nil |
| - if err := a.floor.Begin(id, a.dataDir); err == nil { |
| + if err := a.floor.Begin(t.Context(), id, a.dataDir); err == nil { |
| t.Fatal("precondition: update began without root completeness") |
| } |
| me, err := user.Current() |
| @@ -324,11 +324,11 @@ func TestPlan9AGuardArmCreatesControllerDir(t *testing.T) { |
| if err != nil || !fi.IsDir() || fi.Mode().Perm() != 0o770 { |
| t.Fatalf("controller dir = %v, %v", fi, err) |
| } |
| - if err := a.floor.Begin(id, a.dataDir); err == nil { |
| + if err := a.floor.Begin(t.Context(), id, a.dataDir); err == nil { |
| t.Fatal("agent directory replaced root completeness") |
| } |
| a.floor.Participation.VerifyLaunches = verify |
| - if err := a.floor.Begin(id, a.dataDir); err != nil { |
| + if err := a.floor.Begin(t.Context(), id, a.dataDir); err != nil { |
| t.Fatalf("armed update still refused: %v", err) |
| } |
| lookupServiceUser = func(string) (*user.User, error) { return nil, user.UnknownUserError(serviceUser) } |
diff --git a/internal/host/rootexecution_runtime.go b/internal/host/rootexecution_runtime.go| index bc5958ebf..f8aa9b4fc 100644 |
| --- a/internal/host/rootexecution_runtime.go |
| +++ b/internal/host/rootexecution_runtime.go |
| @@ -58,8 +58,10 @@ func newClaimExecutionRuntime(h ClaimRootHTTP, state rootstate.Store) (*claimExe |
| Transition: func(ctx context.Context, r execlease.ExecutionRecord, q execlease.ExecutionRequest, a execlease.ExecutionAuthority) error { |
| return runtime.root.transitionExecution(ctx, r, q, a) |
| }, |
| - Exclude: ledger.Acquire, |
| - Recovered: ledger.RecoverAcquisition, |
| + Exclude: ledger.Acquire, |
| + Recovered: func(ctx context.Context) error { |
| + return rootparticipation.WithLaunchExclusion(ctx, ledger.RecoverAcquisition) |
| + }, |
| }) |
| if err != nil { |
| return nil, err |
diff --git a/internal/host/rootexecution_update_test.go b/internal/host/rootexecution_update_test.go| index 37d0fda7b..049a4b16f 100644 |
| --- a/internal/host/rootexecution_update_test.go |
| +++ b/internal/host/rootexecution_update_test.go |
| @@ -16,7 +16,7 @@ func updateParticipationFixture(t *testing.T) *rootparticipation.Ledger { |
| if err := disk.Write("census.json", raw); err != nil { |
| t.Fatal(err) |
| } |
| - return &rootparticipation.Ledger{State: disk, VerifyLaunches: func(context.Context, uint64) error { return nil }} |
| + return &rootparticipation.Ledger{State: disk, VerifyLaunches: func(context.Context, uint64) error { return nil }, ExcludeLaunches: func(ctx context.Context, fn func(context.Context) error) error { return fn(ctx) }} |
| } |
| func TestPlan14ExecutionSystemUpdateExclusion(t *testing.T) { |
| @@ -37,7 +37,7 @@ func TestPlan14ExecutionSystemUpdateExclusion(t *testing.T) { |
| if err := os.Remove(mirror); err != nil { |
| t.Fatal(err) |
| } |
| - err := f.a.beginExecutableChange("new", f.now) |
| + err := f.a.beginExecutableChange(t.Context(), "new", f.now) |
| if pending { |
| if err == nil { |
| t.Fatal("system update trusted deleted agent state") |
| @@ -50,7 +50,7 @@ func TestPlan14ExecutionSystemUpdateExclusion(t *testing.T) { |
| if err := f.a.participation.Acquire(t.Context(), func() error { return nil }); err == nil { |
| t.Fatal("acquisition crossed durable system update") |
| } |
| - f.a.settleUpdateBegun() |
| + f.a.settleUpdateBegun(t.Context()) |
| if err := f.a.participation.Acquire(t.Context(), func() error { return nil }); err != nil { |
| t.Fatal("settled update still excludes", err) |
| } |
| @@ -65,7 +65,7 @@ func TestPlan14ExecutionRecoveryWithoutArmedFloor(t *testing.T) { |
| if f.a.guarded() { |
| t.Fatal("fixture has an armed floor") |
| } |
| - if err := f.a.beginExecutableChange("new", f.now); err != nil { |
| + if err := f.a.beginExecutableChange(t.Context(), "new", f.now); err != nil { |
| t.Fatal(err) |
| } |
| if err := f.a.participation.State.Write("shop.example.json", []byte(`{"Schema":1,"Status":"lost"}`)); err != nil { |
| @@ -84,7 +84,7 @@ func TestPlan14ExecutionRecoveryWithoutArmedFloor(t *testing.T) { |
| } |
| return f.a.participation.State.Remove("shop.example.json") |
| } |
| - err := f.a.checkGuardRecovery() |
| + err := f.a.checkGuardRecovery(t.Context()) |
| if (err == nil) != recoverable || calls != 1 { |
| t.Fatalf("recovery=%t calls=%d err=%v", recoverable, calls, err) |
| } |
| @@ -110,7 +110,7 @@ func TestPlan14ExecutionLegacyJournalRecoversBeforeReservation(t *testing.T) { |
| calls++ |
| return f.a.participation.State.Remove("shop.example.json") |
| } |
| - if err := f.a.checkGuardRecovery(); err != nil { |
| + if err := f.a.checkGuardRecovery(t.Context()); err != nil { |
| t.Fatal(err) |
| } |
| if calls != 1 { |
diff --git a/internal/host/rootgeneration_inventory.go b/internal/host/rootgeneration_inventory.go| index 191abe5e9..d84df1024 100644 |
| --- a/internal/host/rootgeneration_inventory.go |
| +++ b/internal/host/rootgeneration_inventory.go |
| @@ -65,7 +65,7 @@ func (v generationReconciler) reconcile(ctx context.Context, records []release.G |
| return zero, err |
| } |
| c := before.Census |
| - if c.Schema != 1 || !c.Complete || c.NeverEnrolled || c.Pending != "" || c.Migration == 0 || c.Revision == 0 || c.LaunchRevision == 0 || c.Box == "" || c.Enrollment == "" { |
| + if c.Schema != 1 || !c.Complete || c.NeverEnrolled || (c.Pending != "" && !(c.Pending == "acquisition" && ctx.Value(launchRecoveryCensusKey{}) == true)) || c.Migration == 0 || c.Revision == 0 || c.LaunchRevision == 0 || c.Box == "" || c.Enrollment == "" { |
| return zero, os.ErrPermission |
| } |
| installs := map[string]hostguard.FenceView{} |
| @@ -269,7 +269,11 @@ func generationInventoryAt(ctx context.Context, censusPath, fencesPath, journalP |
| return out, err |
| } |
| defer census.State.Close() |
| - out.Census, err = census.Census() |
| + if ctx.Value(launchRecoveryCensusKey{}) == true { |
| + out.Census, err = census.RecoveryCensus() |
| + } else { |
| + out.Census, err = census.Census() |
| + } |
| if err != nil { |
| return out, err |
| } |
diff --git a/internal/host/rootlaunchverifier.go b/internal/host/rootlaunchverifier.go| new file mode 100644 |
| index 000000000..39b757009 |
| --- /dev/null |
| +++ b/internal/host/rootlaunchverifier.go |
| @@ -0,0 +1,68 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "sync" |
| + |
| + "github.com/carlosframework/platform/internal/release" |
| + "github.com/carlosframework/platform/internal/rootparticipation" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| +) |
| + |
| +type rootLaunchVerifier struct { |
| + exclude *release.ReleaseExclusion |
| + open func() (rootstate.Store, error) |
| + reconcile generationReconciler |
| +} |
| + |
| +type launchRecoveryCensusKey struct{} |
| + |
| +func (v rootLaunchVerifier) verify(ctx context.Context, revision uint64) error { |
| + if v.exclude.Check(ctx) != nil || v.open == nil || v.reconcile.read == nil || revision == 0 { |
| + return rootparticipation.ErrUnavailable |
| + } |
| + store, err := v.open() |
| + if err != nil { |
| + return err |
| + } |
| + if store == nil { |
| + return rootparticipation.ErrUnavailable |
| + } |
| + defer store.Close() |
| + ledger := &release.RootGenerations{Store: store} |
| + records, err := ledger.Reservations(ctx) |
| + if err != nil { |
| + return err |
| + } |
| + verifier := v.reconcile |
| + read := verifier.read |
| + verifier.read = func(ctx context.Context) (generationInventory, error) { |
| + snapshot, err := read(ctx) |
| + if err == nil && snapshot.Census.LaunchRevision != revision { |
| + return generationInventory{}, rootparticipation.ErrUnavailable |
| + } |
| + return snapshot, err |
| + } |
| + ctx = context.WithValue(ctx, launchRecoveryCensusKey{}, true) |
| + _, err = ledger.Certify(ctx, records, verifier.reconcile) |
| + return err |
| +} |
| + |
| +var rootLaunchStartup struct { |
| + sync.Once |
| + err error |
| +} |
| +var participationRelease = &release.ReleaseExclusion{Open: func() (rootstate.Store, error) { |
| + return rootstate.Open(rootReleasePrivate, true, false) |
| +}} |
| + |
| +func registerProductionLaunchVerifier() error { |
| + rootLaunchStartup.Do(func() { |
| + v := rootLaunchVerifier{exclude: participationRelease, |
| + open: func() (rootstate.Store, error) { return rootstate.Open(release.RootGenerationsDir, false, false) }, |
| + reconcile: generationReconciler{read: productionGenerationInventory, kernel: productionGenerationKernelCensus}, |
| + } |
| + rootLaunchStartup.err = rootparticipation.RegisterLaunchVerifier(v.verify, participationRelease.With) |
| + }) |
| + return rootLaunchStartup.err |
| +} |
diff --git a/internal/host/systemapply.go b/internal/host/systemapply.go| index ad80a4729..74145dfbb 100644 |
| --- a/internal/host/systemapply.go |
| +++ b/internal/host/systemapply.go |
| @@ -108,6 +108,10 @@ func RunSystemApply(args []string) { |
| if os.Geteuid() != 0 { |
| log.Fatal("carlos system apply is the root oneshot behind carlos-system-update.service; it is not for operators") |
| } |
| + if err := registerProductionLaunchVerifier(); err != nil { |
| + log.Print(err) |
| + return |
| + } |
| ctx := context.Background() |
| channel, invalidChannel := systemChannel(os.Getenv) |
| if invalidChannel != "" { |
| @@ -290,7 +294,7 @@ func (a *systemApplier) Run(ctx context.Context, recover bool) (retErr error) { |
| } |
| } |
| // Rejected floor recovery must retain staging and the journal. |
| - if err := a.checkGuardRecovery(); err != nil { |
| + if err := a.checkGuardRecovery(ctx); err != nil { |
| return err |
| } |
| defer func() { |
| @@ -300,16 +304,16 @@ func (a *systemApplier) Run(ctx context.Context, recover bool) (retErr error) { |
| return |
| } |
| if !active { |
| - a.settleUpdateBegun() |
| + a.settleUpdateBegun(ctx) |
| _ = os.RemoveAll(a.stagingDir()) |
| return |
| } |
| if j, e := readSystemJournal(a.optDir); e == nil && j == nil { |
| - if err := a.checkGuardRecovery(); err != nil { |
| + if err := a.checkGuardRecovery(ctx); err != nil { |
| retErr = errors.Join(retErr, err) |
| return |
| } |
| - a.settleUpdateBegun() |
| + a.settleUpdateBegun(ctx) |
| _ = os.RemoveAll(a.stagingDir()) |
| } |
| }() |
| @@ -482,7 +486,7 @@ func (a *systemApplier) install(ctx context.Context, req systemRequest, p *relea |
| // Region HA (spec 1 §5.10): the marker, then the participation check, |
| // before the first step that changes the box. Either failing defers the |
| // update, which the agent retries; the box is left exactly as it was. |
| - if err := a.beginExecutableChange(target, req.PromotedAt); err != nil { |
| + if err := a.beginExecutableChange(ctx, target, req.PromotedAt); err != nil { |
| if !errors.Is(err, errSystemDeferred) { |
| err = fmt.Errorf("%w: update-begun marker: %v", errSystemDeferred, err) |
| } |
| @@ -559,7 +563,7 @@ func (a *systemApplier) install(ctx context.Context, req systemRequest, p *relea |
| // raised since applies to the next one. (Recovery's forward finish is |
| // different: it has no rename of its own, so the finish is its |
| // activation, and it is guarded.) |
| - return a.finish(req, target, newPlan) |
| + return a.finish(ctx, req, target, newPlan) |
| } |
| // abandonInstall undoes an install that the activation guard refused at the |
| @@ -683,7 +687,7 @@ func (a *systemApplier) rearm(units []string) error { |
| } |
| // finish is steps 11–12, and what recovery runs forward from phase gated. |
| -func (a *systemApplier) finish(req systemRequest, target string, plan bootstrapPlan) error { |
| +func (a *systemApplier) finish(ctx context.Context, req systemRequest, target string, plan bootstrapPlan) error { |
| reason := "" |
| if err := a.arm(plan); err != nil { |
| // The gate passed, so this run is an apply and rolling back now |
| @@ -700,7 +704,7 @@ func (a *systemApplier) finish(req systemRequest, target string, plan bootstrapP |
| if err := a.writeResult(req, systemOutcomeApplied, reason); err != nil { |
| return err |
| } |
| - return a.closeGuardJournal() |
| + return a.closeGuardJournal(ctx) |
| } |
| // removeJournal closes the transaction, durably: an unsynced removal can |
| @@ -1563,7 +1567,7 @@ func (a *systemApplier) rollback(ctx context.Context, req systemRequest, reason |
| // floor's completion of a rolled-back transaction) inside it. |
| var closeErr error |
| if gerr := a.activationGuard(restored, restored, func() error { |
| - closeErr = a.closeGuardRollback() |
| + closeErr = a.closeGuardRollback(ctx) |
| return nil |
| }); gerr != nil { |
| return fmt.Errorf("%v; the journal stays: %v", err, gerr) |
| @@ -1584,7 +1588,7 @@ func (a *systemApplier) rollback(ctx context.Context, req systemRequest, reason |
| if err := a.writeResult(req, systemOutcomeRolledBack, reason); err != nil { |
| return err |
| } |
| - closeErr = a.closeGuardRollback() |
| + closeErr = a.closeGuardRollback(ctx) |
| return nil |
| }); err != nil { |
| return failed("finish", err) // journal kept |
| @@ -1758,7 +1762,7 @@ func (a *systemApplier) recoverIfNeeded(ctx context.Context) (bool, error) { |
| // and it is not the file anything reads. The run carries on to the |
| // request it was started for. |
| os.Remove(systemPrevPath(a.optDir) + ".tmp") |
| - return false, a.closeGuardJournal() |
| + return false, a.closeGuardJournal(ctx) |
| } |
| // Forward from gated — but only while the build that passed the gate is |
| // still the one on disk. A binary that has moved since means this is a |
| @@ -1783,7 +1787,7 @@ func (a *systemApplier) recoverIfNeeded(ctx context.Context) (bool, error) { |
| finished := false |
| err = a.activationGuard(j.Target, j.Target, func() error { |
| finished = true |
| - return a.finish(req, j.Target, plan) |
| + return a.finish(ctx, req, j.Target, plan) |
| }) |
| if finished { |
| return true, err |
| @@ -1836,7 +1840,7 @@ func (a *systemApplier) recoverIfNeeded(ctx context.Context) (bool, error) { |
| if err := a.writeResult(req, systemOutcomeRolledBack, "interrupted at "+j.Phase); err != nil { |
| return err |
| } |
| - closeErr = a.closeGuardRollback() |
| + closeErr = a.closeGuardRollback(ctx) |
| return nil |
| }); err != nil { |
| return false, fmt.Errorf("system apply: recovery: %w", err) // journal kept |
diff --git a/internal/host/systemparticipation.go b/internal/host/systemparticipation.go| index 5041b7add..740f9721c 100644 |
| --- a/internal/host/systemparticipation.go |
| +++ b/internal/host/systemparticipation.go |
| @@ -36,8 +36,8 @@ type updateBegun struct { |
| } |
| // hostexecParticipation preserves the legacy region-HA check until root owns a census. |
| -func hostexecParticipation(dataDir string) error { |
| - return hostfloor.CheckParticipation(dataDir, false) |
| +func hostexecParticipation(ctx context.Context, dataDir string) error { |
| + return hostfloor.CheckParticipation(ctx, dataDir, false) |
| } |
| // beginExecutableChange is §5.10's root side, run under the apply lock |
| @@ -48,8 +48,8 @@ func hostexecParticipation(dataDir string) error { |
| // this one must be retried once the operation ends (spec §5.10: the |
| // update is refused and retried later). The agent's leg re-requests a |
| // deferred apply on its backoff ladder, at most every five minutes. |
| -func (a *systemApplier) beginExecutableChange(target string, promotedAt time.Time) error { |
| - required, err := a.participationDisk().ParticipationRequired(context.Background()) |
| +func (a *systemApplier) beginExecutableChange(ctx context.Context, target string, promotedAt time.Time) error { |
| + required, err := a.participationDisk().ParticipationRequired(ctx) |
| if err != nil { |
| return err |
| } |
| @@ -77,14 +77,14 @@ func (a *systemApplier) beginExecutableChange(target string, promotedAt time.Tim |
| if json.Unmarshal(raw, &begun) != nil || begun.ID != tx.ID { |
| return hostfloor.ErrFloor |
| } |
| - if err := a.controllersIdle(); err != nil { |
| + if err := a.controllersIdle(ctx); err != nil { |
| return fmt.Errorf("%w: %v", errSystemDeferred, err) |
| } |
| return nil |
| } |
| if !a.regionHA { |
| - return a.participationDisk().WithParticipation(context.Background(), func(l rootparticipation.Ledger) error { |
| - return l.BeginUpdate(context.Background(), "system-"+target) |
| + return a.participationDisk().WithParticipation(ctx, func(ctx context.Context, l rootparticipation.Ledger) error { |
| + return l.BeginUpdate(ctx, "system-"+target) |
| }, func() error { return nil }) |
| } |
| writeMarker := func() error { |
| @@ -93,11 +93,11 @@ func (a *systemApplier) beginExecutableChange(target string, promotedAt time.Tim |
| } |
| return writeJSONDurable(updateBegunPath(a.optDir), updateBegun{Target: target, PromotedAt: promotedAt, At: a.ops.now().UTC()}, 0o644) |
| } |
| - err = a.participationDisk().BeginParticipation(context.Background(), "system-"+target, a.dataDir, writeMarker, func() error { |
| + err = a.participationDisk().BeginParticipation(ctx, "system-"+target, a.dataDir, writeMarker, func() error { |
| if err := writeMarker(); err != nil { |
| return err |
| } |
| - if reason := a.participationDisk().CheckParticipation(context.Background(), a.dataDir, false); reason != nil { |
| + if reason := a.participationDisk().CheckParticipation(ctx, a.dataDir, false); reason != nil { |
| if err := a.clearLegacyUpdateBegun(); err != nil { |
| a.ops.logf("system apply: %v", err) |
| } |
| @@ -112,20 +112,20 @@ func (a *systemApplier) beginExecutableChange(target string, promotedAt time.Tim |
| } |
| // clearUpdateBegun removes both markers only after positive root settlement. |
| -func (a *systemApplier) clearUpdateBegun() error { |
| - return a.participationDisk().WithParticipation(context.Background(), func(l rootparticipation.Ledger) error { |
| +func (a *systemApplier) clearUpdateBegun(ctx context.Context) error { |
| + return a.participationDisk().WithParticipation(ctx, func(ctx context.Context, l rootparticipation.Ledger) error { |
| id, err := l.UpdateID() |
| if errors.Is(err, os.ErrNotExist) { |
| if _, e := os.Lstat(updateBegunPath(a.optDir)); errors.Is(e, os.ErrNotExist) { |
| return nil |
| } |
| id = "recover-legacy" |
| - err = l.BeginUpdate(context.Background(), id) |
| + err = l.BeginUpdate(ctx, id) |
| } |
| if err != nil { |
| return err |
| } |
| - return l.EndUpdateWith(context.Background(), id, func() error { |
| + return l.EndUpdateWith(ctx, id, func() error { |
| if _, e := os.Lstat(updateBegunPath(a.optDir)); errors.Is(e, os.ErrNotExist) { |
| return nil |
| } |
| @@ -150,14 +150,14 @@ func (a *systemApplier) clearLegacyUpdateBegun() error { |
| // got past its first step. A kept journal (a rollback that could not |
| // finish) keeps the marker with it. Run with or without CARLOS_REGION_HA, |
| // so a marker left behind after the flag is turned off still goes. |
| -func (a *systemApplier) settleUpdateBegun() { |
| +func (a *systemApplier) settleUpdateBegun(ctx context.Context) { |
| if active, err := a.activeFloor(); err != nil || active { |
| return |
| } |
| if j, err := readSystemJournal(a.optDir); err != nil || j != nil { |
| return |
| } |
| - if err := a.clearUpdateBegun(); err != nil { |
| + if err := a.clearUpdateBegun(ctx); err != nil { |
| a.ops.logf("system apply: %v", err) |
| } |
| } |
| @@ -168,7 +168,7 @@ func (a *systemApplier) settleUpdateBegun() { |
| // completion that follows is recorded for an image that is not the target |
| // (Opus re-review M1-4). A marker for another transaction refuses and keeps |
| // the journal. With no armed floor this is closeGuardJournal, as on main. |
| -func (a *systemApplier) closeGuardRollback() error { |
| +func (a *systemApplier) closeGuardRollback(ctx context.Context) error { |
| active, err := a.activeFloor() |
| if err != nil { |
| return err |
| @@ -184,7 +184,7 @@ func (a *systemApplier) closeGuardRollback() error { |
| return err |
| } |
| } |
| - return a.closeGuardJournal() |
| + return a.closeGuardJournal(ctx) |
| } |
| func (a *systemApplier) participationDisk() hostfloor.Disk { |
| @@ -194,8 +194,8 @@ func (a *systemApplier) participationDisk() hostfloor.Disk { |
| return hostfloor.Disk{Participation: a.participation} |
| } |
| -func (a *systemApplier) checkUnarmedParticipationRecovery() error { |
| - required, err := a.participationDisk().ParticipationRequired(context.Background()) |
| +func (a *systemApplier) checkUnarmedParticipationRecovery(ctx context.Context) error { |
| + required, err := a.participationDisk().ParticipationRequired(ctx) |
| if err != nil || !required { |
| return err |
| } |
| @@ -204,7 +204,7 @@ func (a *systemApplier) checkUnarmedParticipationRecovery() error { |
| return err |
| } |
| recover, reserve := false, false |
| - err = a.participationDisk().WithParticipation(context.Background(), func(l rootparticipation.Ledger) error { |
| + err = a.participationDisk().WithParticipation(ctx, func(ctx context.Context, l rootparticipation.Ledger) error { |
| _, err := l.UpdateID() |
| if errors.Is(err, os.ErrNotExist) { |
| if j == nil { |
| @@ -221,12 +221,14 @@ func (a *systemApplier) checkUnarmedParticipationRecovery() error { |
| return err |
| } |
| if recover { |
| - if err := a.controllersRecovered(); err != nil { |
| + if err := a.controllersRecovered(ctx); err != nil { |
| return err |
| } |
| } |
| if reserve { |
| - return a.participationDisk().WithParticipation(context.Background(), func(l rootparticipation.Ledger) error { return l.BeginUpdate(context.Background(), "recover-system") }, func() error { return nil }) |
| + return a.participationDisk().WithParticipation(ctx, func(ctx context.Context, l rootparticipation.Ledger) error { |
| + return l.BeginUpdate(ctx, "recover-system") |
| + }, func() error { return nil }) |
| } |
| return nil |
| } |
diff --git a/internal/hostfloor/controllers_linux_test.go b/internal/hostfloor/controllers_linux_test.go| index 539bc7964..2c8c6f033 100644 |
| --- a/internal/hostfloor/controllers_linux_test.go |
| +++ b/internal/hostfloor/controllers_linux_test.go |
| @@ -28,7 +28,7 @@ func TestControllerDirectoryRefusesLinkedAncestor(t *testing.T) { |
| t.Fatal(err) |
| } |
| for _, strict := range []bool{false, true} { |
| - if err := CheckParticipation(data, strict); err == nil { |
| + if err := CheckParticipation(t.Context(), data, strict); err == nil { |
| t.Fatalf("participation accepted linked ancestor with requireDirectory=%v", strict) |
| } |
| } |
diff --git a/internal/hostfloor/disk.go b/internal/hostfloor/disk.go| index a4a9862a8..e05625e9a 100644 |
| --- a/internal/hostfloor/disk.go |
| +++ b/internal/hostfloor/disk.go |
| @@ -136,7 +136,7 @@ func (d Disk) VerifyFile(f *os.File, platform string, roles ...string) (string, |
| } |
| return "", ErrFloor |
| } |
| -func (d Disk) Begin(id, dataDir string) error { |
| +func (d Disk) Begin(ctx context.Context, id, dataDir string) error { |
| if id == "" { |
| return ErrFloor |
| } |
| @@ -149,11 +149,11 @@ func (d Disk) Begin(id, dataDir string) error { |
| raw, _ := json.Marshal(struct { |
| ID string `json:"id"` |
| }{id}) |
| - return d.WithParticipation(context.Background(), func(l rootparticipation.Ledger) error { |
| + return d.WithParticipation(ctx, func(ctx context.Context, l rootparticipation.Ledger) error { |
| if err := checkLegacyParticipation(dataDir, false); err != nil { |
| return err |
| } |
| - return l.BeginUpdateWith(context.Background(), id, func() error { return Durable(marker, raw, 0644) }) |
| + return l.BeginUpdateWith(ctx, id, func() error { return Durable(marker, raw, 0644) }) |
| }, func() error { |
| if err := Durable(marker, raw, 0644); err != nil { |
| return err |
| @@ -168,8 +168,8 @@ func (d Disk) Begin(id, dataDir string) error { |
| // CheckControllersIdle refuses while any controller state is present. The |
| // directory must also be readable: a missing or unreadable directory cannot |
| // prove that no controller is active. |
| -func CheckControllersIdle(dataDir string) error { |
| - return (Disk{}).ControllersIdle(context.Background(), dataDir) |
| +func CheckControllersIdle(ctx context.Context, dataDir string) error { |
| + return (Disk{}).ControllersIdle(ctx, dataDir) |
| } |
| func RemoveDurable(path string) error { |
| @@ -194,7 +194,11 @@ func UpdateIdle(guardDir, journal string) error { |
| return nil |
| } |
| -func (d Disk) WithParticipation(ctx context.Context, fn func(rootparticipation.Ledger) error, legacy func() error) error { |
| +func (d Disk) WithParticipation(ctx context.Context, fn func(context.Context, rootparticipation.Ledger) error, legacy func() error) error { |
| + return d.withParticipation(ctx, fn, legacy, true) |
| +} |
| + |
| +func (d Disk) withParticipation(ctx context.Context, fn func(context.Context, rootparticipation.Ledger) error, legacy func() error, exclude bool) error { |
| l := d.Participation |
| var required bool |
| var err error |
| @@ -214,17 +218,20 @@ func (d Disk) WithParticipation(ctx context.Context, fn func(rootparticipation.L |
| if !required { |
| return legacy() |
| } |
| - return fn(*l) |
| + if !exclude { |
| + return fn(ctx, *l) |
| + } |
| + return l.WithLaunchExclusion(ctx, func(ctx context.Context) error { return fn(ctx, *l) }) |
| } |
| func (d Disk) ParticipationRequired(ctx context.Context) (bool, error) { |
| required := false |
| - err := d.WithParticipation(ctx, func(rootparticipation.Ledger) error { required = true; return nil }, func() error { return nil }) |
| + err := d.withParticipation(ctx, func(context.Context, rootparticipation.Ledger) error { required = true; return nil }, func() error { return nil }, false) |
| return required, err |
| } |
| func (d Disk) ControllersIdle(ctx context.Context, dataDir ...string) error { |
| - return d.WithParticipation(ctx, func(l rootparticipation.Ledger) error { |
| + return d.WithParticipation(ctx, func(ctx context.Context, l rootparticipation.Ledger) error { |
| if err := l.Idle(ctx); err != nil { |
| return err |
| } |
| @@ -241,7 +248,7 @@ func (d Disk) ControllersIdle(ctx context.Context, dataDir ...string) error { |
| } |
| func (d Disk) BeginParticipation(ctx context.Context, id, dataDir string, commit, legacy func() error) error { |
| - return d.WithParticipation(ctx, func(l rootparticipation.Ledger) error { |
| + return d.WithParticipation(ctx, func(ctx context.Context, l rootparticipation.Ledger) error { |
| if err := checkLegacyParticipation(dataDir, false); err != nil { |
| return err |
| } |
| @@ -250,7 +257,7 @@ func (d Disk) BeginParticipation(ctx context.Context, id, dataDir string, commit |
| } |
| func (d Disk) EndParticipation(ctx context.Context, id string, commit func() error) error { |
| - return d.WithParticipation(ctx, func(l rootparticipation.Ledger) error { |
| + return d.WithParticipation(ctx, func(ctx context.Context, l rootparticipation.Ledger) error { |
| if _, err := l.UpdateID(); errors.Is(err, os.ErrNotExist) { |
| if err := l.BeginUpdate(ctx, id); err != nil { |
| return err |
diff --git a/internal/hostfloor/floor_test.go b/internal/hostfloor/floor_test.go| index ebd642437..4660057be 100644 |
| --- a/internal/hostfloor/floor_test.go |
| +++ b/internal/hostfloor/floor_test.go |
| @@ -57,7 +57,7 @@ func TestPendingAndLostControllersBlockUpdate(t *testing.T) { |
| data := t.TempDir() |
| _ = os.Mkdir(filepath.Join(data, ".hostexec"), 0700) |
| _ = os.WriteFile(filepath.Join(data, ".hostexec", "host.json"), []byte(state), 0600) |
| - if err := d.Begin("update", data); err == nil { |
| + if err := d.Begin(t.Context(), "update", data); err == nil { |
| t.Fatal("controller did not block") |
| } |
| if _, err := os.Stat(filepath.Join(d.Dir, "update-begun.json")); !os.IsNotExist(err) { |
| @@ -68,7 +68,7 @@ func TestPendingAndLostControllersBlockUpdate(t *testing.T) { |
| d := Disk{Dir: t.TempDir(), Participation: floorParticipationFixture(t)} |
| data := t.TempDir() |
| _ = os.Mkdir(filepath.Join(data, ".hostexec"), 0700) |
| - if err := d.Begin("update", data); err != nil { |
| + if err := d.Begin(t.Context(), "update", data); err != nil { |
| t.Fatal(err) |
| } |
| if err := UpdateIdle(d.Dir, filepath.Join(data, "journal.json")); err == nil { |
diff --git a/internal/hostfloor/participation.go b/internal/hostfloor/participation.go| index 9d24596ec..c4f6d40d3 100644 |
| --- a/internal/hostfloor/participation.go |
| +++ b/internal/hostfloor/participation.go |
| @@ -15,12 +15,12 @@ import ( |
| // CheckParticipation reads only directory entries. Floor-armed boxes require |
| // the controller directory; legacy region HA boxes may not have one yet. |
| -func CheckParticipation(dataDir string, requireDirectory bool) error { |
| - return (Disk{}).CheckParticipation(context.Background(), dataDir, requireDirectory) |
| +func CheckParticipation(ctx context.Context, dataDir string, requireDirectory bool) error { |
| + return (Disk{}).CheckParticipation(ctx, dataDir, requireDirectory) |
| } |
| func (d Disk) CheckParticipation(ctx context.Context, dataDir string, requireDirectory bool) error { |
| - return d.WithParticipation(ctx, func(l rootparticipation.Ledger) error { |
| + return d.WithParticipation(ctx, func(ctx context.Context, l rootparticipation.Ledger) error { |
| if err := l.Idle(ctx); err != nil { |
| return err |
| } |
diff --git a/internal/hostfloor/participation_flagoff_test.go b/internal/hostfloor/participation_flagoff_test.go| index 2e72bc0c8..4204e473d 100644 |
| --- a/internal/hostfloor/participation_flagoff_test.go |
| +++ b/internal/hostfloor/participation_flagoff_test.go |
| @@ -42,7 +42,7 @@ func TestPlan14ParticipationFlagOffFloor(t *testing.T) { |
| if err := d.ControllersIdle(t.Context(), data); (err == nil) != want { |
| t.Fatalf("idle=%v want admitted=%t", err, want) |
| } |
| - if err := d.Begin("legacy-update", data); (err == nil) != want { |
| + if err := d.Begin(t.Context(), "legacy-update", data); (err == nil) != want { |
| t.Fatalf("begin=%v want admitted=%t", err, want) |
| } |
| marker := filepath.Join(d.Dir, "update-begun.json") |
| @@ -94,7 +94,7 @@ func TestPlan14ParticipationRetainedCensusFloor(t *testing.T) { |
| if err := d.ControllersIdle(t.Context(), data); (err == nil) != want { |
| t.Fatalf("idle=%v want admitted=%t", err, want) |
| } |
| - if err := d.Begin("update", data); (err == nil) != want { |
| + if err := d.Begin(t.Context(), "update", data); (err == nil) != want { |
| t.Fatalf("begin=%v want admitted=%t", err, want) |
| } |
| if state == "idle" { |
diff --git a/internal/hostfloor/plan9_a_test.go b/internal/hostfloor/plan9_a_test.go| index 67c60da9e..bd44896af 100644 |
| --- a/internal/hostfloor/plan9_a_test.go |
| +++ b/internal/hostfloor/plan9_a_test.go |
| @@ -161,7 +161,7 @@ func TestPlan9AControllerDirCreatedWithoutFollowingLinks(t *testing.T) { |
| } |
| t.Run("absent", func(t *testing.T) { |
| data := t.TempDir() |
| - if err := CheckControllersIdle(data); err == nil { |
| + if err := CheckControllersIdle(t.Context(), data); err == nil { |
| t.Fatal("precondition: armed path accepted a missing directory") |
| } |
| if err := EnsureControllerDir(data, uid, gid); err != nil { |
| @@ -256,7 +256,7 @@ func TestPlan9AMarkerRecordsRollback(t *testing.T) { |
| if _, err := os.Stat(filepath.Join(d.Dir, "update-begun.json")); !errors.Is(err, os.ErrNotExist) { |
| t.Fatal("marking created a marker", err) |
| } |
| - if err := d.Begin("tx", data); err != nil { |
| + if err := d.Begin(t.Context(), "tx", data); err != nil { |
| t.Fatal(err) |
| } |
| if b, err := ReadBegun(d.Dir); err != nil || b.ID != "tx" || b.RolledBack { |
diff --git a/internal/hostfloor/rootparticipation_test.go b/internal/hostfloor/rootparticipation_test.go| index 6795aca10..2c294f050 100644 |
| --- a/internal/hostfloor/rootparticipation_test.go |
| +++ b/internal/hostfloor/rootparticipation_test.go |
| @@ -49,7 +49,7 @@ func floorParticipationFixture(t *testing.T) *rootparticipation.Ledger { |
| if err := d.Write("census.json", raw); err != nil { |
| t.Fatal(err) |
| } |
| - return &rootparticipation.Ledger{State: d, VerifyLaunches: func(context.Context, uint64) error { return nil }} |
| + return &rootparticipation.Ledger{State: d, VerifyLaunches: func(context.Context, uint64) error { return nil }, ExcludeLaunches: func(ctx context.Context, fn func(context.Context) error) error { return fn(ctx) }} |
| } |
| func TestPlan14ExecutionFloorUsesRootCensus(t *testing.T) { |
| @@ -58,18 +58,18 @@ func TestPlan14ExecutionFloorUsesRootCensus(t *testing.T) { |
| if err := d.Participation.State.Write("shop.example.json", []byte("{\"Schema\":1,\"Status\":\"lost\"}")); err != nil { |
| t.Fatal(err) |
| } |
| - if err := d.Begin("update", data); err == nil { |
| + if err := d.Begin(t.Context(), "update", data); err == nil { |
| t.Fatal("missing agent directory cleared root obligation") |
| } |
| if err := d.Participation.State.Remove("shop.example.json"); err != nil { |
| t.Fatal(err) |
| } |
| d.Participation.VerifyLaunches = nil |
| - if err := d.Begin("update", data); err == nil { |
| + if err := d.Begin(t.Context(), "update", data); err == nil { |
| t.Fatal("missing kernel completeness permitted update") |
| } |
| d.Participation.VerifyLaunches = func(context.Context, uint64) error { return nil } |
| - if err := d.Begin("update", data); err != nil { |
| + if err := d.Begin(t.Context(), "update", data); err != nil { |
| t.Fatal(err) |
| } |
| if err := d.Participation.Acquire(t.Context(), func() error { return nil }); err == nil { |
| @@ -97,7 +97,7 @@ func TestPlan14ExecutionFloorUpdateRace(t *testing.T) { |
| return d.Participation.Acquire(t.Context(), func() error { return state.Write("shop.example.json", []byte(`{"Schema":1,"Status":"pending"}`)) }) |
| } |
| data := t.TempDir() |
| - update := func() error { return d.Begin("update", data) } |
| + update := func() error { return d.Begin(t.Context(), "update", data) } |
| a, b := acquisition, update |
| if first == "update" { |
| a, b = update, acquisition |
diff --git a/internal/provision/plan14_e_write_census_test.go b/internal/provision/plan14_e_write_census_test.go| index 6e4b2c8e3..1f9a0a00b 100644 |
| --- a/internal/provision/plan14_e_write_census_test.go |
| +++ b/internal/provision/plan14_e_write_census_test.go |
| @@ -3,7 +3,7 @@ package provision |
| // Track E overlays the reviewed Part 1 write census without changing its baseline. |
| var plan14EWriteCensusOverlay = []censusEntry{ |
| {"internal/host/rootgeneration_requirements.go", "generationRequiredInventory", "key-site", "key:RecordKey", "0ceac8b867717d959ccc6a66a44505a177ad56d09bd79d826c89a506eb8d00a8"}, |
| - {"internal/host/rootgeneration_inventory.go", "generationReconciler.reconcile", "key-site", "key:RecordKey", "790671d424afb68f27e6ac404f4709443ce7b9a5af0df030d5a4784a20e61441"}, |
| + {"internal/host/rootgeneration_inventory.go", "generationReconciler.reconcile", "key-site", "key:RecordKey", "f3bcd466fa3bc92b50f93f6986b4c566db198247ef0e37fe6daf3e2fee760a92"}, |
| {"internal/host/rootgenerations_launch.go", "rootGenerationSource.resolve", "key-site", "key:RecordKey", "d69a3c0c86b03d5469708ac8bf084f4f516c0b61a18519d46cc3c0a0616f5381"}, |
| {"internal/historyreadback/workflow.go", "Operation.deliverGrant", "nonrecord", "sel:Put", "fb410ef6274ef7de0997a804c837ad1ecf29d8826ba1c4dcda00c2ae986fc434"}, |
| {"internal/host/rootinventory.go", "ClaimRootHTTP.commitRootInventoryPending", "key-site", "key:RecordKey", "12b08da25b91b0827412cfe0119bdcc9a98bda5a60c56a987e0d44f7016d3bab"}, |
diff --git a/internal/provision/testdata/plan14-root-reads-e.tsv b/internal/provision/testdata/plan14-root-reads-e.tsv| index c01ba436e..1a33c0ac4 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 ba2c661788e1f0e9daf8e145410848b7d801bf8fd76ebe1969631cbe399ec457 |
| -@coverage internal/release 056a53d2a1183fa21ebf5a506c278572aee6d1c9e89f19a9a6535fa705c11e76 |
| +@coverage internal/host 826ec7eaf12152b9a877635e7a73892edaf102da8f09b0cd155b441a52d2e544 |
| +@coverage internal/release 23bf042522655b27434395d03f05f6ab5e0d86025f58431b842d286a58bda0b7 |
| @coverage internal/rootstate a773d6dbc0e4150e0054f7a94d0e1493c88d0d57f4f4530138b3427e2c217bc5 |
| 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 |
| @@ -292,7 +292,7 @@ internal/release/materializeheld.go serveHeldStageRequest authority E2 E23: old |
| 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 |
| internal/host/rootgeneration_census_linux.go checkedGenerationTree authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel witnesses passed on the E27 root runner. E29 physical-payload and installed-unit witnesses passed on the controller runner; E30 required-artifact witness pending. file:Lstat@467,file:Open@84,file:WalkDir@192 2a9f530f41143ce64723797ef4c5628b536caa6fefb11bcbe57adbec38f9a012 |
| internal/host/rootgeneration_census_linux.go generationKernel.census authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel witnesses passed on the E27 root runner. E29 physical-payload and installed-unit witnesses passed on the controller runner; E30 required-artifact witness pending. file:Open@170 5c43de2497824076701fb6275b86d629f0a1035be212d41754d90ba39c431140 |
| -internal/host/rootgeneration_inventory.go generationInventoryAt authority E2 E26: checked read-only payload enumeration includes inventory and revision orphans; completeness binds current and retained predecessor inventories, exact current revision and both source reads. Fixtures: TestPlan14GenerationPayloadCompleteness, TestPlan14GenerationPayloadRetainedPredecessor, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationPayloadInventory, TestPlan14GenerationPayloadReaderDoesNotInitialize. Checked root reader acceptance passed at E27; running-tenant containment remains unfinished. file:Open@173,file:Open@424 094f659671b83f647aa57b613b9b2d51a723cc6d00e49a6e19c71ff9f63c2087 |
| +internal/host/rootgeneration_inventory.go generationInventoryAt authority E2 E26: checked read-only payload enumeration includes inventory and revision orphans; completeness binds current and retained predecessor inventories, exact current revision and both source reads. Fixtures: TestPlan14GenerationPayloadCompleteness, TestPlan14GenerationPayloadRetainedPredecessor, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationPayloadInventory, TestPlan14GenerationPayloadReaderDoesNotInitialize. Checked root reader acceptance passed at E27; running-tenant containment remains unfinished. PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. file:Open@173,file:Open@533 57f4c32cd65afcefad93a41d5266e2b12acc624ff808f47d40e31a79bbc7a827 |
| internal/hostbox/generationinventory.go LaunchJournal.GenerationLaunches authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel witnesses passed on the E27 root runner. E29 physical-payload and installed-unit witnesses passed on the controller runner; E30 required-artifact witness pending. file:QueryContext@270 6694bd3a35b8d91f53e67134145c44854086f08593110ed1b905bca9587be290 |
| internal/hostbox/generationinventory.go OpenGenerationLaunchReader authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel witnesses passed on the E27 root runner. E29 physical-payload and installed-unit witnesses passed on the controller runner; E30 required-artifact witness pending. file:Lstat@218,file:Open@283 552d8629a7932c4ae5dca9d2beef4559f7e8a2864259d11e2ff98c699fdcd999 |
| @coverage internal/hostbox 3b30e41161008d1ae3f1176da62f07c2f770da3e717e437b2f0bdc44fca7869d |
| @@ -301,7 +301,7 @@ internal/host/rootgeneration_census_other.go productionGenerationKernelCensus au |
| internal/host/rootgeneration_inventory.go generationInstallation authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel witnesses passed on the E27 root runner. E29 physical-payload and installed-unit witnesses passed on the controller runner; E30 required-artifact witness pending. 3e5e15d50752510acb44b10eb29621757a87bff54ceae015f704dd7f3a848ecb |
| internal/host/rootgeneration_inventory.go generationInventoryDigest authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel witnesses passed on the E27 root runner. E29 physical-payload and installed-unit witnesses passed on the controller runner; E30 required-artifact witness pending. 36ff5a31c6a013ac907d85e59b754cf4088df4600646ba7df177cf14e990177a |
| internal/host/rootgeneration_inventory.go generationOwner authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel witnesses passed on the E27 root runner. E29 physical-payload and installed-unit witnesses passed on the controller runner; E30 required-artifact witness pending. 69f89153837761f13116937b330ffbf406891299d5580296ea4aade905aa02ce |
| -internal/host/rootgeneration_inventory.go generationReconciler.reconcile authority E2 E38: reconcile completed root retirement against the retained stop receipt and exact inventory; inspect parked identities, require retired rows absent or an independently inventoried successor, and retain generation pins. Fixtures: TestPlan14GenerationParkedPayload, TestPlan14GenerationParkedCrash, TestPlan14GenerationParkedKeepsReservation, TestPlan14GenerationParkedReadback; TestPlan14GenerationParkedRootRunner is runner-pending. Timer producer matches retained entries once and preserves successors on replay: TestPlan14GenerationTimerRetirement. No claims-off path changes. 790671d424afb68f27e6ac404f4709443ce7b9a5af0df030d5a4784a20e61441 |
| +internal/host/rootgeneration_inventory.go generationReconciler.reconcile authority E2 E38: reconcile completed root retirement against the retained stop receipt and exact inventory; inspect parked identities, require retired rows absent or an independently inventoried successor, and retain generation pins. Fixtures: TestPlan14GenerationParkedPayload, TestPlan14GenerationParkedCrash, TestPlan14GenerationParkedKeepsReservation, TestPlan14GenerationParkedReadback; TestPlan14GenerationParkedRootRunner is runner-pending. Timer producer matches retained entries once and preserves successors on replay: TestPlan14GenerationTimerRetirement. No claims-off path changes. PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. f3bcd466fa3bc92b50f93f6986b4c566db198247ef0e37fe6daf3e2fee760a92 |
| internal/host/rootgeneration_inventory.go openGenerationLaunchInventory authority E2 E33: census-only generation journal reads accept private root-owned ancestry without chmod or repair. Every ancestor remains descriptor-checked, root-owned and non-writable by group/other; leaves remain single-link regular root-owned files, checked for DB/WAL/SHM. Public-file callers remain unchanged. Fixtures: TestPlan14AuthorityFilePrivateAncestors, TestPlan14AuthorityFileAbsoluteBoundary; TestPlan14GenerationPrivateJournalRootRunner and TestPlan14GenerationRequiredArtifactsRootRunner are runner-pending. Pre-flight 76, 78, 82 and 89. 2b54531e16282caeb0ddbc1dc92b18d6cb2db652589eb3757daefe10daa63a53 |
| internal/host/rootgeneration_inventory.go productionGenerationInventory authority E2 E38: reconcile completed root retirement against the retained stop receipt and exact inventory; inspect parked identities, require retired rows absent or an independently inventoried successor, and retain generation pins. Fixtures: TestPlan14GenerationParkedPayload, TestPlan14GenerationParkedCrash, TestPlan14GenerationParkedKeepsReservation, TestPlan14GenerationParkedReadback; TestPlan14GenerationParkedRootRunner is runner-pending. Timer producer matches retained entries once and preserves successors on replay: TestPlan14GenerationTimerRetirement. No claims-off path changes. 32fe8188ec5a22297f4e0aa68f2741839461637a2ef2e7a282f9c5115141db6d |
| internal/host/rootgeneration_inventory.go type generationInventory authority E2 E38: reconcile completed root retirement against the retained stop receipt and exact inventory; inspect parked identities, require retired rows absent or an independently inventoried successor, and retain generation pins. Fixtures: TestPlan14GenerationParkedPayload, TestPlan14GenerationParkedCrash, TestPlan14GenerationParkedKeepsReservation, TestPlan14GenerationParkedReadback; TestPlan14GenerationParkedRootRunner is runner-pending. Timer producer matches retained entries once and preserves successors on replay: TestPlan14GenerationTimerRetirement. No claims-off path changes. 7a1e16370c99823e83e11e7824162082f85abf82e5dd5e18f029c82ab9e26f08 |
| @@ -353,3 +353,23 @@ internal/activator/backend_exec.go ExecBackend.startSidecarUnitFrom authority E2 |
| internal/activator/containment.go ExecBackend.spawnInCgroupAtTier authority E2 E40: claim-enabled raw backend refuses unsupported direct, placed and legacy-control launches before spawn. Claims-off body unchanged. TestPlan14GenerationBackendDirectBoundary, TestPlan14GenerationBackendControlBoundary, TestPlan14GenerationBackendAuxiliaryBoundary; each guard has an independent behavioral mutation. Agent-side refusal is not root reservation or containment evidence. registry:Host@1098,registry:Host@543,registry:Host@625,registry:Host@756,registry:Host@812,registry:Host@911,whole:Route@312,whole:Route@441 99fa73504a1418a92bb3cfb2d89a34b2de4346ec7635ce5bee9044343d7b2eca |
| internal/activator/containment.go ExecBackend.startRole authority E2 E40: claim-enabled raw backend refuses unsupported direct, placed and legacy-control launches before spawn. Claims-off body unchanged. TestPlan14GenerationBackendDirectBoundary, TestPlan14GenerationBackendControlBoundary, TestPlan14GenerationBackendAuxiliaryBoundary; each guard has an independent behavioral mutation. Agent-side refusal is not root reservation or containment evidence. registry:Host@312,registry:Host@459,registry:Host@573,registry:Host@835,registry:Host@899,whole:Route@334,whole:Route@539,whole:Route@724 cd68c5f7705f207659124dc3302d9977987a70cfbdec5a92ffe24262caf2256f |
| internal/host/relocationinstall.go ClaimAssembly.relocationInstall authority B2,C,E2 Existing root inventory and installation obligations remain B2,C; E41: default relocation materializer refuses claim-mode direct stage/publication until a root-reserved relay exists. Claims-off body unchanged; already-live verified releases remain readable. TestPlan14GenerationRelocationDefaultMaterializer and TestPlan14GenerationRelocationRelayRefusal. Positive root relocation remains open. file:Read@6045,registry:DBPath@6324,registry:HoldOp@9706,registry:Status@9693,whole:Route@13446,whole:Route@13571,whole:Route@14300,whole:Route@3422,whole:Route@3471,whole:Route@815,whole:Route@9688,whole:Route@9812 f4eca0dac7b1b0dd43b701114e30f177cc21350851a50e95b3cc2c336e2d3c1c |
| +internal/host/guardtransaction.go systemApplier.beginGuard authority D Claims-on or retained root census uses root exclusion; claims-off with no census preserves main participation and recovery behavior. PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. file:Read@679,file:Read@903 7d3e508615e4469231a856637aa1d1a38d8a4c52ed5a65cd4334ebb914385ec1 |
| +internal/host/guardtransaction.go systemApplier.checkGuardRecovery authority D Claims-on or retained root census uses root exclusion; claims-off with no census preserves main participation and recovery behavior. PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. file:ReadFile@433,file:ReadFile@660 25070bd97c1b3c9ce9fbdc8f44cec9680db72abf4ab9f50e603aad9b3a1ea4e2 |
| +internal/host/rootexecution_runtime.go newClaimExecutionRuntime authority D Positive claim or fence mismatches are terminal; transport failures retain bounded retry eligibility. PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. file:Read@205 91514737432c25a2256b449c97d70fc61f9deb32726b9aea90e7ffdeb962a9db |
| +internal/host/rootlaunchverifier.go registerProductionLaunchVerifier authority E2 PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. file:Open@191 20e6bdeb66c580f64b9a315a65bb647c17dde0fe49c4f2bc45adfcd2bdc6989f |
| +internal/host/rootlaunchverifier.go var participationRelease authority E2 PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. file:Open@101,file:Open@53 03650562dfb07973650f2ee6f1797353fac444893f36180654ab0707f6738fc4 |
| +internal/host/systemapply.go systemApplier.Run authority D,F,J4 root-input;kept=D,F;scope=protected-decision-only PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. file:OpenFile@229 8151becbeaaf35a6eb8a03fcc30faae5016124e55566dd75a37f193acd7fee7e |
| +internal/host/systemparticipation.go systemApplier.beginExecutableChange authority D Claims-on or retained root census uses root exclusion; claims-off with no census preserves main participation and recovery behavior. PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. file:ReadFile@565 f4cacc520429d698655295d87a14326a4a3f3d11e70fa16536e2c3f554d6c307 |
| +internal/host/systemparticipation.go systemApplier.clearUpdateBegun authority D Claims-on or retained root census uses root exclusion; claims-off with no census preserves main participation and recovery behavior. PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. file:Lstat@263,file:Lstat@511 6ac36ce598723cf4af81fbe36c6c53408e8c34a1202173796cc95e5e8bef0c3f |
| +internal/hostfloor/disk.go Disk.Begin authority D Claims-on or retained root census uses root exclusion; claims-off with no census preserves main participation and recovery behavior. PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. file:Stat@172 e298c9bc261cc9240341987c2604dd86ecbf7e3590c4926f6f47d69821d60ac8 |
| +internal/release/releaseexclusion.go ReleaseExclusion.With authority E2 PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. file:Open@109,file:Open@300 aa1a252eb198065d42e97014bd416257756337819b2788f94f8823ee26a749f3 |
| +internal/release/releaseexclusion.go type ReleaseExclusion authority E2 PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. file:Open@32 831e83a39af9484c139388aac8e0c39ee7566f19dd56384ccad73da5f17f3e5c |
| +internal/rootparticipation/production.go ActivationReady authority D,G Refuse claims activation without complete census and registered verifier; Part 3 G remains mandatory. PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. file:Open@61 0103a8635bfc3a4ab17998aab202eebcc9ed8dc1a9d7c41ac8fabad435ff5c05 |
| +internal/rootparticipation/production.go Open authority D Claims-on or retained root census uses root exclusion; claims-off with no census preserves main participation and recovery behavior. PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. file:Open@45,file:Open@5 e7feda4757ec1a808e42729cc91ba57c37a34f48640f72dcdb9de706d8a55a24 |
| +internal/host/claimstop_run.go RunInstanceControl authority C,D,G Refuse premature claim activation and continue serving recovery after per-host startup failures. PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. 9cb1257df4cf5800111b0d8828acbe131886bdc5642febe0ad6dcdd811b8cc36 |
| +internal/hostfloor/disk.go Disk.WithParticipation authority D Claims-on or retained root census uses root exclusion; claims-off with no census preserves main participation and recovery behavior. PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. f06b9c85d99dc0fe071f4b2bf0eced7ec8f118c7df5c1871040df3ea7323b3a3 |
| +internal/hostfloor/participation.go CheckParticipation authority D Claims-on or retained root census uses root exclusion; claims-off with no census preserves main participation and recovery behavior. PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. d5390731acd74a7ce0ef5e89a1cb1af769d35433c1bf4cf4bd196a2b4da650d2 |
| +internal/host/rootlaunchverifier.go rootLaunchVerifier.verify authority E2 PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. 5183759149e649b5dd61a5b833a3b9dafcfa59878d0220882aad3c8c55e0daf1 |
| +internal/rootparticipation/production.go Ledger.ActivationReady authority E2 PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. 99a3a8f61a8b769da5bce734d6d8a937b861ed38ef1e4f9cc1eb2b4d473286fb |
| +@coverage internal/hostfloor 37459f6d8d47ed5ec3892e22483e34abde2dabc0775cf0846b6fe00fb66dcd75 |
| +@coverage internal/rootparticipation 24ff9d273269e9be6c54794c23f1183ce0535c253e714158e34a9bca9e3fdd81 |
diff --git a/internal/release/plan14_release_exclusion_test.go b/internal/release/plan14_release_exclusion_test.go| new file mode 100644 |
| index 000000000..0f8f00cf6 |
| --- /dev/null |
| +++ b/internal/release/plan14_release_exclusion_test.go |
| @@ -0,0 +1,53 @@ |
| +package release |
| + |
| +import ( |
| + "context" |
| + "errors" |
| + "os" |
| + "sync" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/rootstate" |
| +) |
| + |
| +type exclusionDisk struct { |
| + rootstate.Store |
| + mu sync.Mutex |
| +} |
| + |
| +func (d *exclusionDisk) Lock(ctx context.Context, name string) (func(), error) { |
| + d.mu.Lock() |
| + return d.mu.Unlock, nil |
| +} |
| +func (d *exclusionDisk) Close() error { return nil } |
| + |
| +func TestPlan14ReleaseExclusionToken(t *testing.T) { |
| + disk := &exclusionDisk{} |
| + a := &ReleaseExclusion{Open: func() (rootstate.Store, error) { return disk, nil }} |
| + b := &ReleaseExclusion{Open: a.Open} |
| + if !errors.Is(a.Check(t.Context()), os.ErrPermission) { |
| + t.Fatal("missing token accepted") |
| + } |
| + var stale context.Context |
| + if err := a.With(t.Context(), func(ctx context.Context) error { |
| + stale = ctx |
| + if err := a.Check(ctx); err != nil { |
| + t.Fatal(err) |
| + } |
| + if !errors.Is(b.Check(ctx), os.ErrPermission) { |
| + t.Fatal("foreign token accepted") |
| + } |
| + if !errors.Is(b.With(ctx, func(context.Context) error { t.Fatal("foreign nested lock"); return nil }), os.ErrPermission) { |
| + t.Fatal("foreign nesting accepted") |
| + } |
| + return a.With(ctx, func(nested context.Context) error { return a.Check(nested) }) |
| + }); err != nil { |
| + t.Fatal(err) |
| + } |
| + if !errors.Is(a.Check(stale), os.ErrPermission) { |
| + t.Fatal("stale token accepted") |
| + } |
| + if !errors.Is(a.With(stale, func(context.Context) error { t.Fatal("stale reused"); return nil }), os.ErrPermission) { |
| + t.Fatal("stale context reacquired") |
| + } |
| +} |
diff --git a/internal/release/releaseexclusion.go b/internal/release/releaseexclusion.go| new file mode 100644 |
| index 000000000..40f3c527b |
| --- /dev/null |
| +++ b/internal/release/releaseexclusion.go |
| @@ -0,0 +1,53 @@ |
| +package release |
| + |
| +import ( |
| + "context" |
| + "os" |
| + "sync/atomic" |
| + |
| + "github.com/carlosframework/platform/internal/rootstate" |
| +) |
| + |
| +type ReleaseExclusion struct { |
| + Open func() (rootstate.Store, error) |
| +} |
| + |
| +type releaseHeldKey struct{} |
| +type releaseHeld struct { |
| + owner *ReleaseExclusion |
| + active atomic.Bool |
| +} |
| + |
| +func (x *ReleaseExclusion) Check(ctx context.Context) error { |
| + held, _ := ctx.Value(releaseHeldKey{}).(*releaseHeld) |
| + if x == nil || held == nil || held.owner != x || !held.active.Load() { |
| + return os.ErrPermission |
| + } |
| + return ctx.Err() |
| +} |
| + |
| +func (x *ReleaseExclusion) With(ctx context.Context, fn func(context.Context) error) error { |
| + if x == nil || x.Open == nil || fn == nil { |
| + return os.ErrPermission |
| + } |
| + if ctx.Value(releaseHeldKey{}) != nil { |
| + if err := x.Check(ctx); err != nil { |
| + return err |
| + } |
| + return fn(ctx) |
| + } |
| + store, err := x.Open() |
| + if err != nil { |
| + return err |
| + } |
| + if store == nil { |
| + return os.ErrPermission |
| + } |
| + defer store.Close() |
| + return rootstate.WithRelease(ctx, store, func() error { |
| + held := &releaseHeld{owner: x} |
| + held.active.Store(true) |
| + defer held.active.Store(false) |
| + return fn(context.WithValue(ctx, releaseHeldKey{}, held)) |
| + }) |
| +} |
diff --git a/internal/rootparticipation/ledger.go b/internal/rootparticipation/ledger.go| index 36a6cb222..6016ec4da 100644 |
| --- a/internal/rootparticipation/ledger.go |
| +++ b/internal/rootparticipation/ledger.go |
| @@ -28,6 +28,7 @@ type Ledger struct { |
| State rootstate.Store |
| Box, Enrollment string |
| VerifyLaunches func(context.Context, uint64) error |
| + ExcludeLaunches func(context.Context, func(context.Context) error) error |
| } |
| func Decode(raw []byte, v any) error { |
diff --git a/internal/rootparticipation/production.go b/internal/rootparticipation/production.go| index 810d55942..7508ae92c 100644 |
| --- a/internal/rootparticipation/production.go |
| +++ b/internal/rootparticipation/production.go |
| @@ -13,18 +13,20 @@ import ( |
| var launchVerifier struct { |
| sync.RWMutex |
| - verify func(context.Context, uint64) error |
| + verify func(context.Context, uint64) error |
| + exclude func(context.Context, func(context.Context) error) error |
| } |
| // RegisterLaunchVerifier binds E2's root reservation and kernel census reader. |
| // It runs under participation exclusion and must not call the agent or broker. |
| -func RegisterLaunchVerifier(verify func(context.Context, uint64) error) error { |
| +func RegisterLaunchVerifier(verify func(context.Context, uint64) error, exclude func(context.Context, func(context.Context) error) error) error { |
| launchVerifier.Lock() |
| defer launchVerifier.Unlock() |
| - if verify == nil || launchVerifier.verify != nil { |
| + if verify == nil || exclude == nil || launchVerifier.verify != nil { |
| return ErrUnavailable |
| } |
| launchVerifier.verify = verify |
| + launchVerifier.exclude = exclude |
| return nil |
| } |
| @@ -34,9 +36,9 @@ func Open() (Ledger, error) { |
| return Ledger{}, err |
| } |
| launchVerifier.RLock() |
| - verify := launchVerifier.verify |
| + verify, exclude := launchVerifier.verify, launchVerifier.exclude |
| launchVerifier.RUnlock() |
| - return Ledger{State: state, VerifyLaunches: verify}, nil |
| + return Ledger{State: state, VerifyLaunches: verify, ExcludeLaunches: exclude}, nil |
| } |
| func VerifyLaunchCensus(ctx context.Context, revision uint64) error { |
| @@ -92,6 +94,10 @@ func ActivationReady(ctx context.Context) error { |
| return err |
| } |
| defer l.State.Close() |
| + return l.ActivationReady(ctx) |
| +} |
| + |
| +func (l Ledger) ActivationReady(ctx context.Context) error { |
| c, err := l.Census() |
| if err != nil { |
| return err |
| @@ -101,3 +107,20 @@ func ActivationReady(ctx context.Context) error { |
| } |
| return l.VerifyLaunches(ctx, c.LaunchRevision) |
| } |
| + |
| +func WithLaunchExclusion(ctx context.Context, fn func(context.Context) error) error { |
| + launchVerifier.RLock() |
| + exclude := launchVerifier.exclude |
| + launchVerifier.RUnlock() |
| + if exclude == nil { |
| + return ErrUnavailable |
| + } |
| + return exclude(ctx, fn) |
| +} |
| + |
| +func (l Ledger) WithLaunchExclusion(ctx context.Context, fn func(context.Context) error) error { |
| + if l.ExcludeLaunches == nil { |
| + return ErrUnavailable |
| + } |
| + return l.ExcludeLaunches(ctx, fn) |
| +} |