carlos / platform Public

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>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 46a13cb3a73d31bfb1c57ac088feaac2a3595410 parent bd87f35
29 files changed, +868 −141
  • internal/host/claimstop_run.go +5 −1
  • internal/host/guardparticipation_test.go +10 −10
  • internal/host/guardtransaction.go +25 −25
  • internal/host/guardtransaction_test.go +7 −7
  • internal/host/participation_flagoff_test.go +5 −5
  • internal/host/plan14_fix2_live_linux_test.go +1 −1
  • internal/host/plan14_generation_running_pin_root_linux_test.go +168 −0
  • internal/host/plan14_launch_verifier_root_linux_test.go +120 −0
  • internal/host/plan14_launch_verifier_test.go +198 −0
  • internal/host/plan9_a_guard_test.go +11 −11
  • internal/host/rootexecution_runtime.go +4 −2
  • internal/host/rootexecution_update_test.go +6 −6
  • internal/host/rootgeneration_inventory.go +6 −2
  • internal/host/rootlaunchverifier.go +68 −0
  • internal/host/systemapply.go +17 −13
  • internal/host/systemparticipation.go +24 −22
  • internal/hostfloor/controllers_linux_test.go +1 −1
  • internal/hostfloor/disk.go +18 −11
  • internal/hostfloor/floor_test.go +2 −2
  • internal/hostfloor/participation.go +3 −3
  • internal/hostfloor/participation_flagoff_test.go +2 −2
  • internal/hostfloor/plan9_a_test.go +2 −2
  • internal/hostfloor/rootparticipation_test.go +5 −5
  • internal/provision/plan14_e_write_census_test.go +1 −1
  • internal/provision/testdata/plan14-root-reads-e.tsv +24 −4
  • internal/release/plan14_release_exclusion_test.go +53 −0
  • internal/release/releaseexclusion.go +53 −0
  • internal/rootparticipation/ledger.go +1 −0
  • internal/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)
+}