carlos / platform Public

WIP files seed checkpoint 26 (not for merge)

Reboot-pair regression fixed (private fixture binds); FS-R2 combined 200-record recovery and update; claims-on instance-control construction; killed-executor settlement; direct real-origin launch and readiness.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev a107047cdcfa0ccebd3cdf27fe4184902e22fa65 parent 22fb2d1
13 files changed, +1027 −20
  • internal/host/claimconstruction_rootrunner_linux_test.go +136 −0
  • internal/host/claimfilesseed_crash_rootrunner_linux_test.go +249 −0
  • internal/host/claimfilesseed_integration_linux_test.go +51 −3
  • internal/host/claimfilesseed_isolated_rootrunner_linux_test.go +299 −0
  • internal/host/claimfilesseed_live_rootrunner_linux_test.go +8 −0
  • internal/host/claimrecovery_combined_linux_test.go +149 −0
  • internal/host/claimrecovery_combined_rootrunner_linux_test.go +37 −0
  • internal/host/claimservingpark_rootrunner_linux_test.go +8 −1
  • internal/host/claimservingpeer_rootrunner_linux_test.go +40 −12
  • internal/host/rootexecution_recovery.go +5 −1
  • internal/lease/leasetest/s3server.go +10 −0
  • internal/lease/leasetest/s3server_test.go +29 −0
  • internal/provision/testdata/plan14-root-reads-seed.tsv +6 −3

CI passed — run details

CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok  	amadan.net/carlos/platform.git/launchctx	0.011s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok  	github.com/carlosframework/platform	280.254s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	2.776s
ok  	github.com/carlosframework/platform/cmd/moveprobe	93.195s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.702s
ok  	github.com/carlosframework/platform/internal/activator	249.735s
ok  	github.com/carlosframework/platform/internal/agentsock	0.014s
ok  	github.com/carlosframework/platform/internal/alerting	0.003s
ok  	github.com/carlosframework/platform/internal/analytics	0.021s
ok  	github.com/carlosframework/platform/internal/appconfig	2.457s
ok  	github.com/carlosframework/platform/internal/apperr	0.071s
ok  	github.com/carlosframework/platform/internal/applog	0.122s
ok  	github.com/carlosframework/platform/internal/awsinv	0.003s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.004s
ok  	github.com/carlosframework/platform/internal/boxid	0.898s
?   	github.com/carlosframework/platform/internal/boxid/boxidtest	[no test files]
?   	github.com/carlosframework/platform/internal/boxid/internal/signtest	[no test files]
ok  	github.com/carlosframework/platform/internal/boxvolumes	0.008s
ok  	github.com/carlosframework/platform/internal/caps	0.011s
ok  	github.com/carlosframework/platform/internal/certwarm	0.002s
ok  	github.com/carlosframework/platform/internal/cgroup	0.687s
ok  	github.com/carlosframework/platform/internal/changelog	0.302s
ok  	github.com/carlosframework/platform/internal/clamd	0.054s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	385.560s
?   	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.045s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.008s
ok  	github.com/carlosframework/platform/internal/dbverify	0.004s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.002s
ok  	github.com/carlosframework/platform/internal/docsite	0.004s
ok  	github.com/carlosframework/platform/internal/domains	2.314s
ok  	github.com/carlosframework/platform/internal/doorkey	0.006s
ok  	github.com/carlosframework/platform/internal/economics	0.004s
ok  	github.com/carlosframework/platform/internal/edge	26.529s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.007s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.128s
ok  	github.com/carlosframework/platform/internal/gate	1.654s
ok  	github.com/carlosframework/platform/internal/genselect	0.014s
ok  	github.com/carlosframework/platform/internal/geo	0.010s
ok  	github.com/carlosframework/platform/internal/geoip	0.029s
ok  	github.com/carlosframework/platform/internal/host	158.880s
ok  	github.com/carlosframework/platform/internal/hostactors	0.383s
ok  	github.com/carlosframework/platform/internal/hostbox	2.375s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.495s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.008s
ok  	github.com/carlosframework/platform/internal/hostexec	4.667s
ok  	github.com/carlosframework/platform/internal/hostfence	0.035s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.469s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.016s
ok  	github.com/carlosframework/platform/internal/hostguard	1.017s
ok  	github.com/carlosframework/platform/internal/hostops	1.510s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.006s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.002s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.440s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.151s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.002s
ok  	github.com/carlosframework/platform/internal/infraview	0.002s
ok  	github.com/carlosframework/platform/internal/instancectl	4.173s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.703s
ok  	github.com/carlosframework/platform/internal/instancefs	0.005s
ok  	github.com/carlosframework/platform/internal/instancekey	0.003s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.008s
ok  	github.com/carlosframework/platform/internal/layout	1.476s
ok  	github.com/carlosframework/platform/internal/lease	9.791s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.007s
ok  	github.com/carlosframework/platform/internal/ledger	0.022s
ok  	github.com/carlosframework/platform/internal/mail	0.162s
ok  	github.com/carlosframework/platform/internal/maintenance	0.082s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.488s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.068s
ok  	github.com/carlosframework/platform/internal/metrics	0.016s
ok  	github.com/carlosframework/platform/internal/moves	20.905s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.746s
ok  	github.com/carlosframework/platform/internal/objstore	0.165s
ok  	github.com/carlosframework/platform/internal/ops	0.169s
ok  	github.com/carlosframework/platform/internal/park	0.205s
ok  	github.com/carlosframework/platform/internal/privatefile	0.004s
ok  	github.com/carlosframework/platform/internal/provision	14.495s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.011s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	4.877s
ok  	github.com/carlosframework/platform/internal/relay	7.230s
ok  	github.com/carlosframework/platform/internal/release	19.478s
ok  	github.com/carlosframework/platform/internal/rollout	3.602s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.003s
ok  	github.com/carlosframework/platform/internal/rootaws	0.008s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.006s
ok  	github.com/carlosframework/platform/internal/rootstate	0.024s
ok  	github.com/carlosframework/platform/internal/runs	0.561s
ok  	github.com/carlosframework/platform/internal/s3compat	0.005s
ok  	github.com/carlosframework/platform/internal/safefile	0.003s
ok  	github.com/carlosframework/platform/internal/schedule	2.443s
ok  	github.com/carlosframework/platform/internal/seeds	0.011s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	8.611s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.951s
ok  	github.com/carlosframework/platform/internal/status	0.039s
ok  	github.com/carlosframework/platform/internal/steering	0.005s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.213s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.003s
ok  	github.com/carlosframework/platform/internal/termfence	0.003s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.003s
ok  	github.com/carlosframework/platform/internal/unixdial	0.003s
ok  	github.com/carlosframework/platform/internal/updatewords	0.004s
ok  	github.com/carlosframework/platform/internal/usage	0.007s
ok  	github.com/carlosframework/platform/internal/version	0.001s
ok  	github.com/carlosframework/platform/internal/vet	1.526s
=== 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: 46989474 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.021s
=== RUN   TestT4StorageProbeMinio
2026/10/15 16:56:05 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DEC25371102410, 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.00s)
PASS
ok  	github.com/carlosframework/platform/internal/host	0.020s
minio contract: PASS
diff --git a/internal/host/claimconstruction_rootrunner_linux_test.go b/internal/host/claimconstruction_rootrunner_linux_test.go
new file mode 100644
index 000000000..9c669fe11
--- /dev/null
+++ b/internal/host/claimconstruction_rootrunner_linux_test.go
@@ -0,0 +1,136 @@
+//go:build linux && rootrunner
+
+package host
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net"
+ "os"
+ "os/exec"
+ "os/user"
+ "path/filepath"
+ "strconv"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/hostactors"
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/rootparticipation"
+ "github.com/carlosframework/platform/internal/rootstate"
+)
+
+func init() {
+ if len(os.Args) > 1 && os.Args[1] == registryChildCommand {
+ if err := RunRegistryChild(os.Args[2:]); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+ os.Exit(0)
+ }
+}
+
+func TestRootFilesSeedInstanceControlConstruction(t *testing.T) {
+ runServingParkPeerNamespace(t, false, false, "seed-construction")
+}
+
+func filesSeedInstanceControlChild(t *testing.T, binary, root string, listener *net.UnixListener) {
+ t.Helper()
+ for _, path := range []string{"/data", "/var/lib/carlos-claims"} {
+ if err := os.Mkdir(path, 0755); err != nil {
+ t.Fatal("refuse existing fixture path", path, err)
+ }
+ t.Cleanup(func() { os.RemoveAll(path) })
+ }
+ account, err := user.Lookup("carlos")
+ if err != nil {
+ t.Fatal(err)
+ }
+ uid, _ := strconv.Atoi(account.Uid)
+ gid, _ := strconv.Atoi(account.Gid)
+ if err := os.Chown("/data", uid, gid); err != nil {
+ t.Fatal(err)
+ }
+ file, err := listener.File()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer file.Close()
+ listener.SetUnlinkOnClose(false)
+ if err := listener.Close(); err != nil {
+ t.Fatal(err)
+ }
+ output, err := os.Create(filepath.Join(root, "control.log"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ cmd := exec.Command(binary, "-test.run=^TestRootFilesSeedInstanceControlPayload$", "-test.v", "-test.timeout=90s")
+ cmd.Env = append(os.Environ(), "FILES_SEED_CONTROL=1", "TMPDIR="+root)
+ cmd.ExtraFiles = []*os.File{file}
+ cmd.Stdout, cmd.Stderr = output, output
+ if err := cmd.Start(); err != nil {
+ output.Close()
+ t.Fatal(err)
+ }
+ t.Cleanup(func() {
+ cmd.Process.Kill()
+ cmd.Wait()
+ output.Close()
+ body, _ := os.ReadFile(filepath.Join(root, "control.log"))
+ t.Log(string(body))
+ })
+ // The inherited listener stays open while RunInstanceControl constructs its owners.
+}
+
+func TestRootFilesSeedInstanceControlPayload(t *testing.T) {
+ if os.Getenv("FILES_SEED_CONTROL") != "1" {
+ t.Fatal("invoke through construction witness")
+ }
+ t.Setenv("CARLOS_HOST_CLAIM", "1")
+ t.Setenv("CARLOS_HOST_CLAIM_ORIGINS", "1")
+ t.Setenv("CARLOS_MOVES_PHASE1", "")
+ t.Setenv("CARLOS_REGION_HA", "")
+ t.Setenv("CARLOS_DEPLOYMENT_BUCKET", "")
+ t.Setenv("CARLOS_DATA_DIR", "/data")
+ t.Setenv("LISTEN_FDS", "1")
+ t.Setenv("LISTEN_PID", strconv.Itoa(os.Getpid()))
+ store, resources, owners, _ := fix4JournalFixture(t)
+ dir := t.TempDir()
+ t.Setenv("CARLOS_DEPLOYMENT_DIR", dir)
+ t.Setenv("CARLOS_OPT_DIR", resources.OptDir)
+ body, _, err := store.Get(t.Context(), hostactors.Key)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := release.NewDirStore(dir).Put(t.Context(), hostactors.Key, body, ""); err != nil {
+ t.Fatal(err)
+ }
+ owners.Serving = plan9D5Fixture(t).owners.Sources
+ owners.Controllers = productionStopControllers{}
+ claimOwnerRegistration.Provider = seedConstructionOwners{owners}
+ state, err := rootstate.Open(rootparticipation.Namespace, true, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ raw, _ := json.Marshal(rootparticipation.Census{Schema: 1, Box: "box", Enrollment: "en", Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true})
+ if err := state.Write("census.json", raw); err != nil {
+ t.Fatal(err)
+ }
+ state.Close()
+ if err := rootparticipation.RegisterLaunchVerifier(func(context.Context, uint64) error { return nil }); err != nil {
+ t.Fatal(err)
+ }
+ RunInstanceControl(nil)
+ t.Fatal("claims-on instance-control returned before serving")
+}
+
+type seedConstructionOwners struct{ fix4Owners }
+
+func (o seedConstructionOwners) OpenClaimOwners(_ context.Context, store release.Store, _ ClaimRuntimeResources) (ClaimOwnerBindings, error) {
+ owners := o.ClaimOwnerBindings
+ authority := *owners.RetirementAuthority
+ authority.Store = store
+ authority.Directory.Store = store
+ owners.RetirementAuthority = &authority
+ return owners, nil
+}
diff --git a/internal/host/claimfilesseed_crash_rootrunner_linux_test.go b/internal/host/claimfilesseed_crash_rootrunner_linux_test.go
new file mode 100644
index 000000000..c46a85a9a
--- /dev/null
+++ b/internal/host/claimfilesseed_crash_rootrunner_linux_test.go
@@ -0,0 +1,249 @@
+//go:build linux && rootrunner
+
+package host
+
+import (
+ "bytes"
+ "context"
+ "crypto/ed25519"
+ "encoding/json"
+ "fmt"
+ "net"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "syscall"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/activator"
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostfiles"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/lease"
+ "github.com/carlosframework/platform/internal/lease/leasetest"
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/rootstate"
+)
+
+type seedCrashFixture struct {
+ Claims, Data, Locks, Root, Parks, Journal, Registry, Retirement, Signal string
+ Actor hostbox.Actor
+ Now time.Time
+}
+
+func rootFilesSeedWitnessFor(t *testing.T, binary string) filesSeedLiveWitness {
+ if strings.Contains(t.Name(), "WholeExecutorKilledRerun") {
+ return filesSeedLiveWitness{seed: func(t *testing.T, p *servingPreparation, e *filesSeedExecutor) { rootFilesSeedCrash(t, binary, p, e) }}
+ }
+ return rootFilesSeedLiveWitness(t, binary)
+}
+
+func rootFilesSeedCrash(t *testing.T, binary string, p *servingPreparation, e *filesSeedExecutor) {
+ f := p.f
+ root := t.TempDir()
+ saved := seedCrashFixture{Claims: filepath.Join(root, "claims"), Data: f.f.guard.DataDir, Locks: f.f.guard.LockDir, Root: f.rootDisk.dir, Parks: "/var/lib/carlos-claims/authority/serving-parks", Journal: f.f.journalPath, Registry: f.p.root.Reconcile.Registry.Path(), Retirement: f.p.root.Reconcile.Retirement.JournalDir, Signal: filepath.Join(root, "precommit"), Actor: f.actor, Now: f.f.now}
+ claims := release.NewDirStore(saved.Claims)
+ keys, err := f.f.claims.List(t.Context(), "")
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, key := range keys {
+ body, _, err := f.f.claims.Get(t.Context(), key)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err = claims.Put(t.Context(), key, body, ""); err != nil {
+ t.Fatal(err)
+ }
+ }
+ // The seed selection registry and the handover registry share the same route here.
+ rt, err := e.registry.Get(recoveryHost)
+ if err != nil || rt == nil {
+ t.Fatal(err)
+ }
+ savedBody, _ := json.Marshal(saved)
+ config := filepath.Join(root, "fixture.json")
+ if err := os.WriteFile(config, savedBody, 0600); err != nil {
+ t.Fatal(err)
+ }
+ run := func(crash bool) error {
+ cmd := exec.Command(binary, "-test.run=^TestRootFilesSeedWholeExecutorPayload$", "-test.v", "-test.timeout=40s")
+ cmd.Env = append(os.Environ(), "FILES_SEED_CRASH_CONFIG="+config, fmt.Sprintf("FILES_SEED_CRASH=%v", crash))
+ cmd.Stdin = os.Stdin
+ var out bytes.Buffer
+ cmd.Stdout, cmd.Stderr = &out, &out
+ err := cmd.Run()
+ t.Log(out.String())
+ return err
+ }
+ err = run(true)
+ var status syscall.WaitStatus
+ if exit, ok := err.(*exec.ExitError); ok {
+ status = exit.Sys().(syscall.WaitStatus)
+ }
+ if !status.Signaled() || status.Signal() != syscall.SIGKILL {
+ t.Fatal("seed did not die at precommit", err)
+ }
+ if _, err := os.Stat(saved.Signal); err != nil {
+ t.Fatal("never reached materialization", err)
+ }
+ before, err := hostclaim.Read(t.Context(), claims, recoveryHost)
+ if err != nil || before.Claim.Holder.WakeRetry.State != "attempting" || before.Claim.Holder.WakeRetry.AttemptsReserved != 1 {
+ t.Fatal("crash lost retry debt", err)
+ }
+ data := f.f.data.(*leasetest.VersionedBucket)
+ first := movesFilesPrefix(before.Claim.Holder.Location, before.Claim.Holder.Generation)
+ if data.LatestID(f.actor.Bucket, hostfiles.ManifestKey(first)) != "" {
+ t.Fatal("crashed seed committed")
+ }
+ // Model expiry of the dead owner's remote lease; no production TTL is changed.
+ versions, err := data.ListVersions(t.Context(), f.actor.Bucket, before.Claim.Holder.Location+"/")
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, v := range versions {
+ if strings.HasSuffix(v.Key, "/lock.json") && v.IsLatest {
+ body, err := data.GetVersion(t.Context(), f.actor.Bucket, v.Key, v.VersionID)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var lock map[string]any
+ if err := json.Unmarshal(body, &lock); err != nil {
+ t.Fatal(err)
+ }
+ lock["expires_at"] = time.Now().Add(-time.Minute).UTC().Format(time.RFC3339Nano)
+ expired, _ := json.Marshal(lock)
+ data.Put(f.actor.Bucket, v.Key, expired)
+ }
+ }
+ if err := run(false); err == nil {
+ t.Fatal("rerun bypassed settlement backoff")
+ }
+ settled, err := hostclaim.Read(t.Context(), claims, recoveryHost)
+ if err != nil || settled.Claim.Holder.WakeRetry.State != "open" || settled.Claim.Holder.WakeRetry.AttemptsReserved != 1 || settled.Claim.Holder.WakeRetry.Attempt != nil {
+ t.Fatal("rerun did not settle crashed attempt before backoff", err)
+ }
+ saved.Now = saved.Now.Add(time.Minute)
+ savedBody, _ = json.Marshal(saved)
+ if err := os.WriteFile(config, savedBody, 0600); err != nil {
+ t.Fatal(err)
+ }
+ if err := run(false); err != nil {
+ t.Fatal("rerun after backoff", err)
+ }
+ after, err := hostclaim.Read(t.Context(), claims, recoveryHost)
+ if err != nil || after.Claim.Holder.WakeRetry.State != "open" || after.Claim.Holder.WakeRetry.AttemptsReserved != 2 || after.Claim.Holder.Generation == before.Claim.Holder.Generation {
+ t.Fatal("rerun did not settle and reserve once", err)
+ }
+ final := movesFilesPrefix(after.Claim.Holder.Location, after.Claim.Holder.Generation)
+ if data.LatestID(f.actor.Bucket, hostfiles.ManifestKey(final)) == "" {
+ t.Fatal("rerun did not commit real admitted prefix")
+ }
+ etag := after.ETag
+ if err := run(false); err != nil {
+ t.Fatal(err)
+ }
+ after, err = hostclaim.Read(t.Context(), claims, recoveryHost)
+ if err != nil || after.ETag != etag {
+ t.Fatal("established rerun changed claim", err)
+ }
+}
+
+func TestRootFilesSeedWholeExecutorPayload(t *testing.T) {
+ path := os.Getenv("FILES_SEED_CRASH_CONFIG")
+ if path == "" {
+ t.Fatal("invoke through killed executor witness")
+ }
+ body, err := os.ReadFile(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var saved seedCrashFixture
+ if err := json.Unmarshal(body, &saved); err != nil {
+ t.Fatal(err)
+ }
+ t.Setenv("AWS_REGION", "eu-west-1")
+ t.Setenv("AWS_DEFAULT_REGION", "eu-west-1")
+ claims := release.NewDirStore(saved.Claims)
+ data, err := lease.NewVersionedS3(t.Context(), map[string]string{saved.Actor.Bucket: "eu-west-1"})
+ if err != nil {
+ t.Fatal(err)
+ }
+ journal, err := hostbox.OpenLaunchJournal(saved.Journal)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer journal.Close()
+ roots := &servingAuthorityDisk{rollbackDisk: &rollbackDisk{dir: saved.Root}}
+ rf := &recoveryFixture{t: t, claims: claims, evidence: claims, journal: journal, journalPath: saved.Journal, data: data, bucket: saved.Actor.Bucket, signer: ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)), now: saved.Now, seq: os.Getpid() * 100000}
+ rf.guard = hostguard.Guard{DataDir: saved.Data, LockDir: saved.Locks, Claims: hostguard.StoreReader{Store: claims}, Root: hostguard.NewRootAuthority(roots)}
+ rf.kernel = &recoveryKernel{journal: journal}
+ crash := os.Getenv("FILES_SEED_CRASH") == "true"
+ if !crash {
+ rf.now = rf.now.Add(time.Minute)
+ }
+ reg, err := registry.Open(saved.Registry)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer reg.Close()
+ row, err := reg.Get(recoveryHost)
+ if err != nil || row == nil {
+ t.Fatal(err)
+ }
+ state, err := rootstate.Open(saved.Parks, false, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer state.Close()
+ journals, err := hostbox.NewJournalService(journal)
+ if err != nil {
+ t.Fatal(err)
+ }
+ f := &servingRootFixture{t: t, f: rf, rootDisk: roots, db: row.DBPath, actor: saved.Actor}
+ f.p = &servingParkRoot{ledger: &servingParkLedger{state: state, fs: newServingParkFS(saved.Data, state), journal: journal, now: func() time.Time { return rf.now }}, empty: plan9D5Fixture(t).owners}
+ coordinator := rf.root(nil)
+ coordinator.Kernel = &noProcessKernelFake{}
+ coordinator.Inventory = journals.Inventory
+ f.p.root = ClaimRootHTTP{Roots: map[string]string{"data": saved.Data}, Journals: journals, Launches: journal, Stop: ClaimStopHTTP{Coordinator: coordinator, Authenticate: func(context.Context, net.Conn) (StopPeer, error) { return rf.peer, nil }}, InventoryActor: func(context.Context) (hostbox.Actor, error) { return f.actor, nil }, RequireNewWork: func(context.Context) error { return nil }}
+ f.p.root.Reconcile.Registry = reg
+ f.p.root.Reconcile.Inventory = journals.Inventory
+ f.p.root.Reconcile.Box, f.p.root.Reconcile.Enrollment = f.actor.Box, f.actor.Enrollment
+ f.p.root.Reconcile.Retirement.JournalDir = saved.Retirement
+ p := newServingPreparation(t, f, "boot", fmt.Sprintf("seed-%d", os.Getpid()))
+ a := p.assembly
+ a.Data = data
+ a.Identity = handoverRecoveryIdentity(t, saved.Actor)
+ source := &movesFilesSource{DataDir: saved.Data, Routes: reg, newFiles: func(ctx context.Context, _, _ string) (release.Store, error) {
+ return release.NewS3Store(ctx, saved.Actor.Bucket)
+ }}
+ a.Wake.seedFactory = func(ctx context.Context, op *filesSeedOperation, token *activator.SeedExclusion) (*activator.ClaimRuntime, *ClaimWakeAssembly, error) {
+ runtime, wake, err := newFilesSeedRuntime(ctx, a, p.source, p.writer, op, token)
+ if err != nil {
+ return runtime, wake, err
+ }
+ prepare := runtime.Prepare
+ runtime.Prepare = func(ctx context.Context, rt registry.Route, o hostclaim.Observed, policy activator.StartPolicy) (activator.ClaimStart, error) {
+ start, err := prepare(ctx, rt, o, policy)
+ if err == nil && crash {
+ start.Materialize = func(context.Context) error {
+ if err := os.WriteFile(saved.Signal, []byte(o.Claim.Holder.Generation), 0600); err != nil {
+ return err
+ }
+ syscall.Kill(os.Getpid(), syscall.SIGKILL)
+ select {}
+ }
+ }
+ return start, err
+ }
+ return runtime, wake, nil
+ }
+ executor := &filesSeedExecutor{assembly: &a, service: &activator.Service{}, registry: reg, source: source}
+ if err := executor.run(t.Context(), recoveryHost, os.Stdout); err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/internal/host/claimfilesseed_integration_linux_test.go b/internal/host/claimfilesseed_integration_linux_test.go
index aad01e295..0d16d5265 100644
--- a/internal/host/claimfilesseed_integration_linux_test.go
+++ b/internal/host/claimfilesseed_integration_linux_test.go
@@ -99,6 +99,12 @@ func runFilesSeedRealPreparation(t *testing.T, p *servingPreparation, live ...fi
t.Fatal("input selection", err)
}
executor := &filesSeedExecutor{assembly: &a, service: &activator.Service{}, registry: reg, source: source}
+ for _, witness := range live {
+ if witness.seed != nil {
+ witness.seed(t, p, executor)
+ return
+ }
+ }
var output bytes.Buffer
if err := executor.run(t.Context(), rt.Host, &output); err != nil {
t.Fatalf("seed: %v\n%s", err, output.String())
@@ -131,6 +137,11 @@ func runFilesSeedRealPreparation(t *testing.T, p *servingPreparation, live ...fi
witness.configure(w)
}
})
+ for _, witness := range live {
+ if witness.prepare != nil {
+ witness.prepare(normal)
+ }
+ }
admitted, start := normal.prepare(t)
if err := start.Restore(t.Context()); err != nil {
t.Fatal("cold restore", wake, err)
@@ -156,8 +167,14 @@ func runFilesSeedRealPreparation(t *testing.T, p *servingPreparation, live ...fi
for _, witness := range live {
witness.serve(t, normal, admitted, start)
}
- if err := start.Failed(t.Context(), 0); err != nil {
- t.Fatal(err)
+ healthy := false
+ for _, witness := range live {
+ healthy = healthy || witness.healthy
+ }
+ if !healthy {
+ if err := start.Failed(t.Context(), 0); err != nil {
+ t.Fatal(err)
+ }
}
if err := normal.runtime.FailedAdmission(t.Context(), admitted, hostclaim.ErrWrongState); err != nil {
t.Fatal(err)
@@ -166,13 +183,24 @@ func runFilesSeedRealPreparation(t *testing.T, p *servingPreparation, live ...fi
t.Fatal("ordinary provider armed")
}
}
- if f.f.read().Claim.Holder.WakeRetry.AttemptsReserved != 3 {
+ healthy := false
+ for _, witness := range live {
+ healthy = healthy || witness.healthy
+ }
+ if healthy {
+ if f.f.read().Claim.Holder.WakeRetry != nil {
+ t.Fatal("health did not clear retry debt")
+ }
+ } else if f.f.read().Claim.Holder.WakeRetry.AttemptsReserved != 3 {
t.Fatal("attempt budget changed")
}
t.Log(output.String())
}
type filesSeedLiveWitness struct {
+ healthy bool
+ prepare func(*servingPreparation)
+ seed func(*testing.T, *servingPreparation, *filesSeedExecutor)
configure func(*ClaimWakeAssembly)
serve func(*testing.T, *servingPreparation, hostclaim.Observed, activator.ClaimStart)
}
@@ -180,3 +208,23 @@ type filesSeedLiveWitness struct {
func TestFilesSeedRealOriginPreparation(t *testing.T) {
runFilesSeedOrdinaryPreparation(t, "real-seed", "same-controller")
}
+
+func TestFilesSeedPreparationChangedController(t *testing.T) {
+ witness := filesSeedLiveWitness{configure: func(*ClaimWakeAssembly) {}, serve: func(*testing.T, *servingPreparation, hostclaim.Observed, activator.ClaimStart) {}}
+ witness.prepare = func(p *servingPreparation) {
+ p.local.BootID, p.local.ControllerID, p.local.PID, p.local.ProcessStart = "new-boot", "new-controller", 88, "new-start"
+ p.f.f.peer = StopPeer{UID: 2001, PID: 88, ProcessStart: "new-start", BootID: "new-boot", ControllerID: "new-controller"}
+ p.f.p.root.Stop.Coordinator.CarlosUID = 2001
+ p.wake.Retries.BootID, p.wake.Retries.ControllerID = "new-boot", "new-controller"
+ if len(p.wake.pending) != 0 {
+ t.Fatal("rebuilding active fixture")
+ }
+ p.wake.pending = nil
+ var err error
+ p.runtime, err = p.wake.Build(t.Context(), p.source, p.writer)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ runFilesSeedPreparationWithLive(t, "real-seed", "same-controller", nil, []filesSeedLiveWitness{witness})
+}
diff --git a/internal/host/claimfilesseed_isolated_rootrunner_linux_test.go b/internal/host/claimfilesseed_isolated_rootrunner_linux_test.go
new file mode 100644
index 000000000..c420d79b2
--- /dev/null
+++ b/internal/host/claimfilesseed_isolated_rootrunner_linux_test.go
@@ -0,0 +1,299 @@
+//go:build linux && rootrunner
+
+package host
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "os"
+ "os/exec"
+ "os/user"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/activator"
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ execlease "github.com/carlosframework/platform/internal/hostexec"
+ "github.com/carlosframework/platform/internal/hostwrite"
+ "github.com/carlosframework/platform/internal/instancectl"
+ "github.com/carlosframework/platform/internal/instanceexec"
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/rootparticipation"
+ "github.com/carlosframework/platform/internal/rootstate"
+ "golang.org/x/sys/unix"
+)
+
+func init() {
+ if len(os.Args) > 1 {
+ switch os.Args[1] {
+ case "instance-exec":
+ instanceexec.RunExec(os.Args[2:])
+ os.Exit(0)
+ case "instance-exit":
+ instanceexec.RunExit(os.Args[2:])
+ os.Exit(0)
+ }
+ }
+}
+
+func TestRootFilesSeedIsolatedTenant(t *testing.T) {
+ runServingParkPeerNamespace(t, false, false, "seed-isolated")
+}
+
+type seedIsolatedClient struct {
+ root *ClaimLaunchRoot
+ peer StopPeer
+}
+
+func (c seedIsolatedClient) Capability(context.Context) (string, error) {
+ return hostbox.RoleLaunchProtocol, nil
+}
+func (c seedIsolatedClient) Launch(ctx context.Context, q hostbox.RoleLaunchRequest) (hostbox.RoleLaunchGrant, error) {
+ out, err := c.root.Serve(ctx, c.peer, q)
+ if err != nil {
+ return hostbox.RoleLaunchGrant{}, err
+ }
+ return *out.Grant, nil
+}
+func (c seedIsolatedClient) Stop(ctx context.Context, q hostbox.RoleLaunchRequest) error {
+ _, err := c.root.Serve(ctx, c.peer, q)
+ return err
+}
+
+func rootFilesSeedIsolated(t *testing.T, binary string) {
+ for _, dir := range []string{"/data", "/var/lib/carlos-claims", "/opt/carlos", "/run/carlos", "/run/carlos-control", "/etc/carlos"} {
+ if err := os.Mkdir(dir, 0755); err != nil {
+ t.Fatal("refuse existing fixture path", dir, err)
+ }
+ t.Cleanup(func() { os.RemoveAll(dir) })
+ }
+ account, err := user.Lookup("carlos")
+ if err != nil {
+ t.Fatal(err)
+ }
+ uid, _ := strconv.Atoi(account.Uid)
+ gid, _ := strconv.Atoi(account.Gid)
+ for _, dir := range []string{"/opt/carlos/versions/fixture", "/run/carlos/instances", "/run/carlos/logs"} {
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ t.Fatal(err)
+ }
+ }
+ image, err := os.ReadFile(binary)
+ if err != nil {
+ t.Fatal(err)
+ }
+ app := "/opt/carlos/versions/fixture/app"
+ for _, path := range []string{"/opt/carlos/carlos", app} {
+ if err := os.WriteFile(path, image, 0755); err != nil {
+ t.Fatal(err)
+ }
+ }
+ raw, err := unitsFS.ReadFile("units/carlos-instance@.service")
+ if err != nil {
+ t.Fatal(err)
+ }
+ lines := []string{}
+ for _, line := range strings.Split(string(raw), "\n") {
+ if strings.HasPrefix(line, "BindsTo=") || strings.HasPrefix(line, "Requires=") || strings.HasPrefix(line, "After=") {
+ continue
+ }
+ lines = append(lines, line)
+ }
+ unit := "/run/systemd/system/carlos-instance@.service"
+ file, err := os.OpenFile(unit, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := file.WriteString(strings.Join(lines, "\n") + "\nIPAddressDeny=any\n"); err != nil {
+ t.Fatal(err)
+ }
+ file.Close()
+ t.Cleanup(func() {
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+ if out, err := exec.CommandContext(ctx, "systemctl", "stop", instancectl.UnitName(instancectl.RoleRun, recoveryHost)).CombinedOutput(); err != nil {
+ t.Errorf("stop isolated tenant: %v %s", err, out)
+ }
+ os.Remove(unit)
+ os.RemoveAll("/run/systemd/system/" + instancectl.UnitName(instancectl.RoleRun, recoveryHost) + ".d")
+ if out, err := exec.CommandContext(ctx, "systemctl", "daemon-reload").CombinedOutput(); err != nil {
+ t.Errorf("reload isolated unit: %v %s", err, out)
+ }
+ })
+ servingRunnerSystemctl(t, "daemon-reload")
+ denied := []string{"/etc/carlos/fixture-secret", "/var/lib/carlos-claims/fixture-secret", "/data/instances/foreign.example/fixture-secret"}
+ for _, path := range denied {
+ if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(path, []byte("foreign sentinel"), 0644); err != nil {
+ t.Fatal(err)
+ }
+ }
+ agent := "/sys/fs/cgroup/files-seed-controller"
+ if err := os.Mkdir(agent, 0755); err != nil {
+ t.Fatal("refuse existing controller cgroup", err)
+ }
+ t.Cleanup(func() {
+ if err := os.Remove(agent); err != nil {
+ t.Error(err)
+ }
+ })
+ witness := rootFilesSeedLiveWitness(t, app)
+ configure := witness.configure
+ witness.configure = func(w *ClaimWakeAssembly) {
+ configure(w)
+ owners := w.Inputs.(*claimServingOwners)
+ selected := owners.Sources.Launch
+ owners.Sources.Launch = func(ctx context.Context, rt registry.Route) (ClaimServingLaunch, error) {
+ in, err := selected(ctx, rt)
+ in.Env = append(in.Env, "FILES_SEED_DENIED="+strings.Join(denied, ":"))
+ return in, err
+ }
+ }
+
+ witness.healthy = true
+ witness.prepare = func(p *servingPreparation) {
+ boot, err := execlease.BootID()
+ if err != nil {
+ t.Fatal(err)
+ }
+ start, err := claimLaunchProcessStart("/proc", os.Getpid())
+ if err != nil {
+ t.Fatal(err)
+ }
+ p.local.BootID, p.local.ControllerID, p.local.PID, p.local.ProcessStart = boot, "isolated-controller", os.Getpid(), start
+ p.f.f.peer = StopPeer{UID: uint32(uid), PID: os.Getpid(), ProcessStart: start, BootID: boot, ControllerID: p.local.ControllerID}
+ p.wake.Retries.BootID, p.wake.Retries.ControllerID = boot, p.local.ControllerID
+ p.f.p.root.Stop.Coordinator.CarlosUID = uint32(uid)
+ prior := claimContainmentAgentRoot
+ claimContainmentAgentRoot = func(context.Context) (string, error) { return agent, nil }
+ t.Cleanup(func() { claimContainmentAgentRoot = prior })
+ p.wake.Prelaunch = func(ctx context.Context, o hostclaim.Observed, work hostwrite.Work, in ClaimLaunchInputs) (*activator.RoleLaunch, error) {
+ target := recoveryTuple(o, p.local)
+ group, err := claimContainmentParent(agent, target)
+ if err != nil {
+ return nil, err
+ }
+ path := filepath.Join("/sys/fs/cgroup", group)
+ if err := os.Mkdir(path, 0755); err != nil {
+ return nil, err
+ }
+ t.Cleanup(func() { os.Remove(path) })
+ var st unix.Stat_t
+ if err := unix.Stat(path, &st); err != nil {
+ return nil, err
+ }
+ err = work.With(ctx, in.Identity, func() error {
+ return p.f.f.journal.Prelaunch(ctx, hostbox.LaunchInventory{Target: target, ControllerPID: p.local.PID, ProcessStart: start, NoState: true, Groups: []hostbox.LaunchGroup{{RelativePath: group, Device: uint64(st.Dev), Inode: st.Ino}}})
+ })
+ return &activator.RoleLaunch{Target: target, Journal: p.f.f.journal}, err
+ }
+ state, err := rootstate.Open(filepath.Join(t.TempDir(), "execution"), true, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { state.Close() })
+ census, _ := json.Marshal(rootparticipation.Census{Schema: 1, Box: p.f.actor.Box, Enrollment: p.f.actor.Enrollment, Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true})
+ if err := state.Write("census.json", census); err != nil {
+ t.Fatal(err)
+ }
+ units := instancectl.ClaimUnits{Systemctl: func(ctx context.Context, args ...string) (string, error) {
+ out, err := exec.CommandContext(ctx, "systemctl", args...).CombinedOutput()
+ return string(out), err
+ }, CgroupRoot: "/sys/fs/cgroup", ExeRoot: "/run/carlos-control/exe", UnitRoot: "/run/systemd/system", DataRoot: "/data"}
+ root := &ClaimLaunchRoot{Guard: p.f.f.guard, Journal: p.f.f.journal, DataRoot: "/data", ExecLockDir: t.TempDir(), Local: &rev2Local{}, Clock: execlease.SystemClock{}, Actor: func(context.Context) (hostbox.Actor, error) { return p.f.actor, nil }, Units: units, Kernel: linuxClaimLaunchKernel{OptRoot: "/opt/carlos", CgroupRoot: "/sys/fs/cgroup", ProcRoot: "/proc"}, ValidateExecutable: func(string, string, string) error { return nil }, executionState: state}
+ client := seedIsolatedClient{root: root, peer: p.f.f.peer}
+ p.wake.Launch = func(ctx context.Context, o hostclaim.Observed, work hostwrite.Work, in ClaimLaunchInputs, pre *activator.RoleLaunch) (*activator.RoleLaunch, error) {
+ paths, err := claimIsolatedPaths(map[string]string{"data": "/data"}, o.Host, in)
+ if err != nil {
+ return nil, err
+ }
+ l := *pre
+ l.Work, l.Identity, l.Root, l.CgroupRoot = work, in.Identity, client, "/sys/fs/cgroup"
+ l.Spec = &hostbox.LaunchSpec{Identity: in.Identity, Executable: in.Executable, Digest: in.ExecutableSHA256, Args: in.Args, Env: in.Env}
+ l.LaunchDir, l.RunDir, l.LogSocket, l.MemoryMax = paths.LaunchDir, paths.RunDir, paths.LogSocket, paths.MemoryMax
+ l.Executable = func(context.Context, *exec.Cmd) (*os.File, error) { return os.Open(app) }
+ l.Initialize = func(context.Context) error {
+ if err := filepath.Walk(l.LaunchDir, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ return os.Chown(path, uid, gid)
+ }); err != nil {
+ return err
+ }
+ if err := os.MkdirAll(l.RunDir, 0750); err != nil {
+ return err
+ }
+ return os.Chown(l.RunDir, uid, gid)
+ }
+ return &l, nil
+ }
+ if len(p.wake.pending) != 0 {
+ t.Fatal("fixture rebuilt active wake assembly")
+ }
+ p.wake.pending = nil
+ p.runtime, err = p.wake.Build(t.Context(), p.source, p.writer)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ witness.serve = func(t *testing.T, p *servingPreparation, o hostclaim.Observed, start activator.ClaimStart) {
+ launch, err := start.PrepareLaunch(t.Context())
+ if err != nil {
+ t.Fatal("prepare isolated launch", err)
+ }
+ cmd, _, settle, _, err := launch.Start(t.Context(), "main", start.CurrentAttempt(), start.Build, func() error { return nil })
+ if err != nil {
+ out, _ := exec.Command("journalctl", "-u", instancectl.UnitName(instancectl.RoleRun, o.Host), "--no-pager", "-n", "30").CombinedOutput()
+ t.Fatalf("isolated launch: %v\n%s", err, out)
+ }
+ defer func() {
+ if err := settle(t.Context()); err != nil {
+ t.Error(err)
+ }
+ }()
+ ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
+ defer cancel()
+ if err := start.Ready(ctx, cmd); err != nil {
+ t.Fatal("isolated readiness", err)
+ }
+ wrong, _ := os.FindProcess(os.Getpid())
+ if err := start.Healthy(ctx, 0, &exec.Cmd{Process: wrong}); err == nil {
+ t.Fatal("controller accepted as healthy tenant")
+ }
+ if err := start.Healthy(ctx, 0, cmd); err != nil {
+ t.Fatal("isolated exact health", err)
+ }
+ transport := &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
+ return (&net.Dialer{}).DialContext(ctx, "unix", provision.NestedSocket(o.Host))
+ }}
+ defer transport.CloseIdleConnections()
+ response, err := (&http.Client{Transport: transport, Timeout: time.Second}).Get("http://tenant/")
+ if err != nil {
+ t.Fatal(err)
+ }
+ body, err := io.ReadAll(response.Body)
+ response.Body.Close()
+ if err != nil || response.StatusCode != 200 || string(body) != "real seeded bytes" {
+ t.Fatal("isolated payload", string(body), err)
+ }
+ for _, path := range denied {
+ if body, err := os.ReadFile(path); err != nil || string(body) != "foreign sentinel" {
+ t.Fatal("foreign sentinel changed", path, err)
+ }
+ }
+ t.Log(fmt.Sprintf("healthy isolated wake: host=%s session=%d pid=%d", o.Host, o.Claim.Holder.Session, cmd.Process.Pid))
+ }
+ runFilesSeedPreparationWithLive(t, "real-seed", "same-controller", []bool{true}, []filesSeedLiveWitness{witness})
+}
diff --git a/internal/host/claimfilesseed_live_rootrunner_linux_test.go b/internal/host/claimfilesseed_live_rootrunner_linux_test.go
index 0e69299b7..f777eab11 100644
--- a/internal/host/claimfilesseed_live_rootrunner_linux_test.go
+++ b/internal/host/claimfilesseed_live_rootrunner_linux_test.go
@@ -15,6 +15,7 @@ import (
"os"
"os/exec"
"path/filepath"
+ "strings"
"testing"
"time"
@@ -41,6 +42,13 @@ func TestRootFilesSeedTenantPayload(t *testing.T) {
if os.Getenv("FILES_SEED_TENANT") != "1" {
t.Fatal("invoke through real-origin tenant witness")
}
+ for _, path := range strings.Split(os.Getenv("FILES_SEED_DENIED"), ":") {
+ if path != "" {
+ if body, err := os.ReadFile(path); err == nil {
+ t.Fatalf("tenant read foreign path %s: %q", path, body)
+ }
+ }
+ }
db, err := sql.Open("sqlite", *seedTenantDB)
if err != nil {
t.Fatal(err)
diff --git a/internal/host/claimrecovery_combined_linux_test.go b/internal/host/claimrecovery_combined_linux_test.go
new file mode 100644
index 000000000..580134366
--- /dev/null
+++ b/internal/host/claimrecovery_combined_linux_test.go
@@ -0,0 +1,149 @@
+//go:build linux
+
+package host
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "testing"
+ "time"
+
+ execlease "github.com/carlosframework/platform/internal/hostexec"
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/rootparticipation"
+ "github.com/carlosframework/platform/internal/rootstate"
+)
+
+func filesSeedCombinedRecovery(t *testing.T, install func(*servingParkDiskFixture) rootstate.Store) {
+ t.Helper()
+ f := newServingParkDiskFixture(t)
+ state := install(f)
+ var parks []servingParkRecord
+ for i := 0; i < 200; i++ {
+ f.base.Host = fmt.Sprintf("park-%03d.example.com", i)
+ f.target.Host = f.base.Host
+ if err := os.MkdirAll(filepath.Dir(provision.NestedDBPath(f.data, f.base.Host)), 0700); err != nil {
+ t.Fatal(err)
+ }
+ f.put("", fmt.Sprintf("retained-%03d", i))
+ auth, receipts := f.evidence(10, true, "missing")
+ r, err := f.park(auth, receipts)
+ if err != nil {
+ t.Fatal(i, err)
+ }
+ if i != 0 {
+ f.complete(auth)
+ }
+ parks = append(parks, r)
+ }
+ ctx, cancel := context.WithTimeout(t.Context(), 20*time.Second)
+ defer cancel()
+ maintenance := &servingParkRuntime{authority: &servingParkRoot{ledger: f.ledger}, ctx: ctx, cancel: cancel, idle: make(chan struct{})}
+ root := ClaimRootHTTP{servingParks: maintenance}
+ ledger := rootparticipation.Ledger{State: state, VerifyLaunches: func(context.Context, uint64) error { return nil }}
+ raw, _ := json.Marshal(rootparticipation.Census{Schema: 1, Box: "box", Enrollment: "en", Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true})
+ if err := state.Write("census.json", raw); err != nil {
+ t.Fatal(err)
+ }
+ child := exec.Command("sleep", "60")
+ if err := child.Start(); err != nil {
+ t.Fatal(err)
+ }
+ defer func() { child.Process.Kill(); child.Wait() }()
+ boot, err := execlease.BootID()
+ if err != nil {
+ t.Fatal(err)
+ }
+ start, err := claimLaunchProcessStart("/proc", child.Process.Pid)
+ if err != nil {
+ t.Fatal(err)
+ }
+ peer := execlease.ControllerProcess{KernelBootID: boot, PID: child.Process.Pid, Start: start}
+ q := execlease.ExecutionRequest{Schema: 1, ID: "begin", Operation: "executionBegin", Host: "crashed.example.com", Incarnation: "inc", Stamp: 1, Session: 1, Op: "move"}
+ cleaned, settled := 0, 0
+ cfg := execlease.RootBrokerConfig{State: state, Remote: release.NewDirStore(t.TempDir()), Clock: execlease.SystemClock{}, BootID: boot, Identity: "old-root", Alive: execlease.ControllerAlive,
+ Authority: func(context.Context, execlease.ExecutionRequest, execlease.ControllerProcess) (execlease.ExecutionAuthority, error) {
+ return execlease.ExecutionAuthority{Box: "box", Enrollment: "en", Account: "bab", App: "jam", Installation: "inc/1/box/en", Migration: 1, ClaimRevision: "claim", RecordRevision: "record", DirectoryRevision: "1", Kind: "landing", Phase: "checking"}, nil
+ },
+ Cleanup: func(ctx context.Context, r execlease.ExecutionRecord) error {
+ if execlease.ControllerAlive(ctx, r.Peer) == nil {
+ return execlease.ErrState
+ }
+ cleaned++
+ return nil
+ },
+ Settle: func(context.Context, execlease.ExecutionRecord) error { settled++; return nil }, Exclude: ledger.Acquire, Recovered: ledger.RecoverAcquisition}
+ broker, err := execlease.NewRootBroker(cfg)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := broker.Execute(ctx, peer, q); err != nil {
+ t.Fatal(err)
+ }
+ if err := ledger.BeginUpdate(ctx, "update"); err == nil {
+ t.Fatal("live participation allowed update")
+ }
+ if err := child.Process.Kill(); err != nil {
+ t.Fatal(err)
+ }
+ child.Wait()
+ if err := broker.Close(ctx); err != nil {
+ t.Fatal(err)
+ }
+ cleaned = 0
+ cfg.Identity = "replacement-root"
+ broker, err = execlease.NewRootBroker(cfg)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer broker.Close(context.Background())
+ root.Execution = broker
+ update, _ := applyHappy(t)
+ update.a.participation = &ledger
+ originalRecovery := recoverPendingControllers
+ t.Cleanup(func() { recoverPendingControllers = originalRecovery })
+ recoverPendingControllers = func(call context.Context, _, _ string) error {
+ return recoverRootExecutionBeforeUpdate(call, broker, root)
+ }
+ if err := update.a.controllersRecovered(); err != nil {
+ t.Fatal(err)
+ }
+ if cleaned != 1 || settled != 1 {
+ t.Fatal("controller debt not settled exactly once", cleaned, settled)
+ }
+ if maintenance.recoveryAfter == "" {
+ t.Fatal("first page lost its cursor")
+ }
+ if err := maintenance.recoverPending(ctx); err != nil {
+ t.Fatal(err)
+ }
+ if maintenance.recoveryAfter != "" {
+ t.Fatal("second page did not complete")
+ }
+ if err := ledger.BeginUpdate(ctx, "update"); err != nil {
+ t.Fatal("retained parks blocked update", err)
+ }
+ if err := ledger.EndUpdate(ctx, "update"); err != nil {
+ t.Fatal(err)
+ }
+ if err := update.a.Run(ctx, false); err != nil {
+ t.Fatal(err)
+ }
+ if r := update.result(); r == nil || r.Outcome != systemOutcomeApplied {
+ t.Fatal("update did not apply", r)
+ }
+ for i, r := range parks {
+ f.retained(r.Authorization, fmt.Sprintf("retained-%03d", i), r.ReapAfter)
+ }
+}
+
+func TestFilesSeedCombinedRecoveryAllowsUpdate(t *testing.T) {
+ filesSeedCombinedRecovery(t, func(*servingParkDiskFixture) rootstate.Store {
+ return &servingAuthorityDisk{rollbackDisk: &rollbackDisk{dir: t.TempDir()}}
+ })
+}
diff --git a/internal/host/claimrecovery_combined_rootrunner_linux_test.go b/internal/host/claimrecovery_combined_rootrunner_linux_test.go
new file mode 100644
index 000000000..fccb84cf1
--- /dev/null
+++ b/internal/host/claimrecovery_combined_rootrunner_linux_test.go
@@ -0,0 +1,37 @@
+//go:build linux && rootrunner
+
+package host
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/rootstate"
+)
+
+func TestRootFilesSeedCombinedRecoveryAllowsUpdate(t *testing.T) {
+ servingRootRunner(t)
+ root, err := os.MkdirTemp("/var/lib", "carlos-rootrunner-combined-")
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { os.RemoveAll(root) })
+ t.Setenv("TMPDIR", root)
+ filesSeedCombinedRecovery(t, func(f *servingParkDiskFixture) rootstate.Store {
+ servingRunnerMount(t, f.data)
+ parks, err := rootstate.Open(filepath.Join(root, "parks"), true, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { parks.Close() })
+ f.fs = newServingParkFS(f.data, parks)
+ f.ledger.fs, f.ledger.state = f.fs, parks
+ state, err := rootstate.Open(filepath.Join(root, "execution"), true, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { state.Close() })
+ return state
+ })
+}
diff --git a/internal/host/claimservingpark_rootrunner_linux_test.go b/internal/host/claimservingpark_rootrunner_linux_test.go
index e348c8fc0..1d5e2df4b 100644
--- a/internal/host/claimservingpark_rootrunner_linux_test.go
+++ b/internal/host/claimservingpark_rootrunner_linux_test.go
@@ -97,6 +97,10 @@ func servingRunnerMount(t *testing.T, data string) {
if err := unix.Mount(instances, instances, "", unix.MS_BIND, ""); err != nil {
t.Fatal(err)
}
+ if err := unix.Mount("", instances, "", unix.MS_PRIVATE|unix.MS_REC, ""); err != nil {
+ _ = unix.Unmount(instances, 0)
+ t.Fatal(err)
+ }
t.Cleanup(func() {
if err := unix.Unmount(instances, 0); err != nil {
t.Error(err)
@@ -685,6 +689,9 @@ func TestRootFilesSeedRealOriginPreparation(t *testing.T) {
runRootFilesSeedRealOriginPreparation(t, false)
}
func TestRootFilesSeedRealOriginTenant(t *testing.T) { runRootFilesSeedRealOriginPreparation(t, true) }
+func TestRootFilesSeedWholeExecutorKilledRerun(t *testing.T) {
+ runRootFilesSeedRealOriginPreparation(t, true)
+}
func runRootFilesSeedRealOriginPreparation(t *testing.T, live bool) {
servingRootRunner(t)
if os.Getenv("PLAN14_ROOT_FIXTURE") == t.Name() {
@@ -695,7 +702,7 @@ func runRootFilesSeedRealOriginPreparation(t *testing.T, live bool) {
t.Fatal("root child did not enter its scratch root")
}
if live {
- runFilesSeedPreparationWithLive(t, "real-seed", "same-controller", []bool{true}, []filesSeedLiveWitness{rootFilesSeedLiveWitness(t, "/tenant.test")})
+ runFilesSeedPreparationWithLive(t, "real-seed", "same-controller", []bool{true}, []filesSeedLiveWitness{rootFilesSeedWitnessFor(t, "/tenant.test")})
} else {
runFilesSeedOrdinaryPreparation(t, "real-seed", "same-controller", true)
}
diff --git a/internal/host/claimservingpeer_rootrunner_linux_test.go b/internal/host/claimservingpeer_rootrunner_linux_test.go
index 6d6883a2c..a731b71e3 100644
--- a/internal/host/claimservingpeer_rootrunner_linux_test.go
+++ b/internal/host/claimservingpeer_rootrunner_linux_test.go
@@ -115,6 +115,20 @@ func TestRootServingParkPeerPayload(t *testing.T) {
var reply servingPeerReply
var callErr error
switch q.Action {
+ case "control-request":
+ transport := &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
+ return (&net.Dialer{}).DialContext(ctx, "unix", socket)
+ }}
+ response, err := (&http.Client{Transport: transport, Timeout: 3 * time.Second}).Get("http://root/claim/retire")
+ if err != nil {
+ callErr = err
+ } else {
+ response.Body.Close()
+ if response.StatusCode != http.StatusMethodNotAllowed {
+ callErr = fmt.Errorf("control status %d", response.StatusCode)
+ }
+ }
+ transport.CloseIdleConnections()
case "seed-listen":
ready := make(chan struct{})
close(ready)
@@ -310,6 +324,10 @@ func runServingParkPeerNamespace(t *testing.T, handover, writer bool, namespace
if err := os.WriteFile(installed, body, 0755); err != nil {
t.Fatal(err)
}
+ if namespace == "seed-isolated" {
+ rootFilesSeedIsolated(t, installed)
+ return
+ }
f := newServingRootFixture(t, true)
servingRunnerMount(t, f.f.guard.DataDir)
for name, body := range map[string]string{"files/upload": "tenant upload", "env": "tenant configuration"} {
@@ -380,19 +398,23 @@ func runServingParkPeerNamespace(t *testing.T, handover, writer bool, namespace
capability.SeedController = verifyFilesSeedDelegation
handler = ClaimRootWithLaunch{Root: h, Launch: capability}
}
- server := &instancectl.Server{ClaimStop: handler, PeerOK: func(net.Conn) error { return nil }, Logf: t.Logf}
- ctx, cancel := context.WithCancel(context.Background())
- if lifecycle, ok := handler.(interface{ Start(context.Context) }); ok {
- lifecycle.Start(ctx)
- }
- done := make(chan error, 1)
- go func() { done <- server.Serve(ctx, listener) }()
- t.Cleanup(func() {
- cancel()
- if err := <-done; err != nil {
- t.Error(err)
+ if namespace == "seed-construction" {
+ filesSeedInstanceControlChild(t, installed, root, listener.(*net.UnixListener))
+ } else {
+ server := &instancectl.Server{ClaimStop: handler, PeerOK: func(net.Conn) error { return nil }, Logf: t.Logf}
+ ctx, cancel := context.WithCancel(context.Background())
+ if lifecycle, ok := handler.(interface{ Start(context.Context) }); ok {
+ lifecycle.Start(ctx)
}
- })
+ done := make(chan error, 1)
+ go func() { done <- server.Serve(ctx, listener) }()
+ t.Cleanup(func() {
+ cancel()
+ if err := <-done; err != nil {
+ t.Error(err)
+ }
+ })
+ }
controlPath := filepath.Join(root, "control.sock")
control, err := net.ListenUnix("unix", &net.UnixAddr{Name: controlPath, Net: "unix"})
if err != nil {
@@ -499,6 +521,12 @@ func runServingParkPeerNamespace(t *testing.T, handover, writer bool, namespace
}
return reply
}
+ if namespace == "seed-construction" {
+ command(servingPeerCommand{Action: "control-request"}, false)
+ command(servingPeerCommand{Action: "seed-manager"}, false)
+ command(servingPeerCommand{Action: "seed-capability"}, false)
+ return
+ }
if strings.HasPrefix(namespace, "seed-") {
command(servingPeerCommand{Action: "seed-capability"}, true)
command(servingPeerCommand{Action: "seed-manager"}, false)
diff --git a/internal/host/rootexecution_recovery.go b/internal/host/rootexecution_recovery.go
index 3b87f649d..501ef0302 100644
--- a/internal/host/rootexecution_recovery.go
+++ b/internal/host/rootexecution_recovery.go
@@ -72,7 +72,11 @@ func recoverRootExecution(ctx context.Context, dataDir, guardDir string) (result
return err
}
root := handler.(ClaimRootWithLaunch).Root
- if err := runtime.broker.Recover(ctx); err != nil {
+ return recoverRootExecutionBeforeUpdate(ctx, runtime.broker, root)
+}
+
+func recoverRootExecutionBeforeUpdate(ctx context.Context, broker *execlease.RootBroker, root ClaimRootHTTP) error {
+ if err := broker.Recover(ctx); err != nil {
return err
}
if err := root.RecoverPending(ctx); err != nil {
diff --git a/internal/lease/leasetest/s3server.go b/internal/lease/leasetest/s3server.go
index 9ccf76364..0b07ae29d 100644
--- a/internal/lease/leasetest/s3server.go
+++ b/internal/lease/leasetest/s3server.go
@@ -160,6 +160,9 @@ func NewS3Server(f *VersionedBucket, serves ...string) (http.Handler, func() int
writeErr(w, err)
return
}
+ if v, ok := f.find(bucket, key, versionID); ok {
+ w.Header().Set("ETag", `"`+v.etag+`"`)
+ }
if hdr := r.Header.Get("Range"); hdr != "" {
span, ok := strings.CutPrefix(hdr, "bytes=")
start, end, dash := strings.Cut(span, "-")
@@ -187,6 +190,13 @@ func NewS3Server(f *VersionedBucket, serves ...string) (http.Handler, func() int
case r.Method == http.MethodPut && r.Header.Get("x-amz-copy-source") != "":
f.serveCopy(w, r, bucket, key)
case r.Method == http.MethodPut:
+ if match := r.Header.Get("If-Match"); match != "" {
+ current := f.buckets[bucket][key]
+ if len(current) == 0 || current[0].deleteMarker || current[0].etag != strings.Trim(match, `"`) {
+ s3err(w, http.StatusPreconditionFailed, "PreconditionFailed")
+ return
+ }
+ }
var id string
var err error
if r.Header.Get("If-None-Match") == "*" {
diff --git a/internal/lease/leasetest/s3server_test.go b/internal/lease/leasetest/s3server_test.go
index 5defbec0e..b4546f938 100644
--- a/internal/lease/leasetest/s3server_test.go
+++ b/internal/lease/leasetest/s3server_test.go
@@ -226,3 +226,32 @@ func TestS3ServerValidatesCopyBody(t *testing.T) {
}
}
}
+
+func TestS3ServerConditionalReplaceUsesReadETag(t *testing.T) {
+ m := NewVersionedBucket()
+ m.AddBucket("data")
+ h, _ := NewS3Server(m)
+ first := do(t, h, http.MethodPut, "/data/lock.json", "expired", map[string]string{"If-None-Match": "*"})
+ read := do(t, h, http.MethodGet, "/data/lock.json", "", nil)
+ etag := read.Header().Get("ETag")
+ if etag == "" || etag != first.Header().Get("ETag") {
+ t.Fatal("GET did not return current ETag", etag)
+ }
+ if got := do(t, h, http.MethodPut, "/data/lock.json", "new-owner", map[string]string{"If-Match": etag}); got.Code != 200 {
+ t.Fatal("takeover", got.Code)
+ }
+ before := m.Snapshot()
+ if got := do(t, h, http.MethodPut, "/data/lock.json", "stale-owner", map[string]string{"If-Match": etag}); got.Code != http.StatusPreconditionFailed {
+ t.Fatal("stale CAS accepted", got.Code)
+ }
+ if m.Snapshot() != before {
+ t.Fatal("stale CAS mutated versions")
+ }
+ if got := do(t, h, http.MethodPut, "/data/missing", "new", map[string]string{"If-Match": etag}); got.Code != http.StatusPreconditionFailed {
+ t.Fatal("CAS on missing accepted", got.Code)
+ }
+ do(t, h, http.MethodDelete, "/data/lock.json", "", nil)
+ if got := do(t, h, http.MethodPut, "/data/lock.json", "new", map[string]string{"If-Match": etag}); got.Code != http.StatusPreconditionFailed {
+ t.Fatal("CAS over delete marker accepted", got.Code)
+ }
+}
diff --git a/internal/provision/testdata/plan14-root-reads-seed.tsv b/internal/provision/testdata/plan14-root-reads-seed.tsv
index 51c29f1e7..5a2a248fe 100644
--- a/internal/provision/testdata/plan14-root-reads-seed.tsv
+++ b/internal/provision/testdata/plan14-root-reads-seed.tsv
@@ -1,4 +1,4 @@
-# Files seed overlay loads LAST. Run 25; FS-R6 legacy dispositions explicitly ruled; other rows remain proposals.
+# Files seed overlay loads LAST. Run 26; FS-R6 legacy dispositions explicitly ruled; other rows remain proposals.
@legacy c4009ab5fb7d91ab65fde5cda910d796b2a98109831c7e90e1bef18c1a77ff48 internal/activator/activator.go Service.sweepOnce legacy accepted-legacy accepted-legacy;rev3=PROPOSED-seed;basis=census-ruling-FS-R6; diagnostic throttling only; registry remains legacy registry:Hibernate@12209,registry:Hibernate@21380,registry:Hibernate@5124,registry:Hibernate@6794,registry:Hibernate@9200,registry:Host@21449,registry:Host@21507,registry:Host@21879,registry:Host@21972,registry:Host@22707,registry:Host@22789,registry:Host@23854,registry:Host@24033,registry:Host@24082,registry:NextDue@22662,registry:Status@21396,whole:Route@12211,whole:Route@14144,whole:Route@15854,whole:Route@16044,whole:Route@16337,whole:Route@16509,whole:Route@16656,whole:Route@20867,whole:Route@22376,whole:Route@22716,whole:Route@22723,whole:Route@23669,whole:Route@24066,whole:Route@5126,whole:Route@6796,whole:Route@9202 74872baf2607ef6e24d8466dca33aa9f449b45c712377d3474f7d5c87368f62f
@legacy fae097bc67df1153c4fe96526cdbbdc0800fe9f989edae9c34e243962fb5c19d internal/activator/servingretry.go ServingRetries.Wrap legacy accepted-legacy accepted-legacy;rev3=PROPOSED-seed;basis=census-ruling-FS-R6; BeforeReserve refuses only; cleanup deadline bounds lifetime; route identity remains legacy file:Prepare@128,file:Prepare@4325,file:Prepare@4455,file:Read@2888,file:Read@3531,file:Read@5054,file:Read@5991,file:Read@6656,whole:Route@1212,whole:Route@1550,whole:Route@1648,whole:Route@1948,whole:Route@2360,whole:Route@4473,whole:Route@5421,whole:Route@6522,whole:Route@876,whole:Route@954 e0fe1c8acbe4b1631ff9c3ce63dceba8297fbeab55fc6e4ace72be1d2d9d5bfa
@legacy e84357c7d4acebbf3a83055c10796752ad67b32f98248f54361a387d2682d7d6 internal/host/relocationflush.go ReleaseFlushFiles.Flush legacy accepted-legacy accepted-legacy;rev3=PROPOSED-seed;basis=census-ruling-FS-R6; tenant files and env are published data, never authority file:Open@772 7cc12267c16d9d1ae1116c9d10528f97d3983c77fb6486e0aac73142d8999c4b
@@ -286,7 +286,7 @@ internal/host/claimwakeassembly.go type ClaimWakeAssembly authority C,J4 rev3=PR
internal/host/claimwakestop.go ClaimAssembly.collectCapturedStop authority C,G,J4 rev3=PROPOSED-seed; fresh claim and verified local stop identity; session-zero legacy holder may name only the local writer box, with no admissions; root independently proves and retains no-process evidence file:Exec@2994,file:Read@605 d4cd80663dfae5f9737a74e319d3423f4ef853e1843666b02b4cd2341e869d72
internal/host/movesfiles.go movesFiles.Establish authority C,H3 rev3=PROPOSED-seed; ordinary origin proof and single C8 commit; private seed checks exact admitted writer and stages shared snapshot; no armed live provider 172fe8888da88c249b7990bd9fb4bd2821bd402fb96ab52af13c58500f31d50f
internal/host/relocationflush.go ReleaseFlushFiles.Flush legacy accepted-legacy accepted-legacy;rev3=PROPOSED-seed;basis=census-ruling-FS-R6; tenant files and env are published data, never authority file:Open@772 7cc12267c16d9d1ae1116c9d10528f97d3983c77fb6486e0aac73142d8999c4b
-internal/host/rootexecution_recovery.go recoverRootExecution authority D rev3=PROPOSED-seed; root lifecycle and retained recovery; independent crashed-controller recovery, per-journal errors retained, no agent evidence elevated 44d102ddc175fb2df17832a64b34063cc17e6abf4fe8779dd3843344336dd5c0
+internal/host/rootexecution_recovery.go recoverRootExecution authority D rev3=PROPOSED-seed; root lifecycle and retained recovery; independent crashed-controller recovery, per-journal errors retained, no agent evidence elevated b9dfb1821d4bd2cfe67db3c2dcae872930c17f4e31d3994ecc6e4b0cd855b518
internal/host/rootexecution_runtime.go claimExecutionRuntime.Close authority C,J4 rev3=PROPOSED-seed; existing execution monitor lifetime plus serving-park lifetime; both share caller deadline before authority stores close 08352d84ef3a8cd55924338abc3d33229f2216a608c0daef707aec8c472e2a8e
internal/hostbox/coldhandover.go ColdHandover.Run authority C,J4 rev3=PROPOSED-seed; production operation-only gate refuses destructive serving handover, including old pending journals; existing move effects unchanged file:Exec@1027,file:Lstat@9838,file:OpenRoot@9760,file:Read@1379,registry:Addr@3654,registry:Addrs@3672,registry:Backing@3691,registry:Canary@3748,registry:ClaimIncarnation@3532,registry:ClaimIncarnation@4034,registry:ClaimOwner@10588,registry:ClaimOwner@4116,registry:ClaimOwner@4136,registry:ClaimOwner@6011,registry:ClaimOwner@7439,registry:ClaimOwnership@10180,registry:ClaimOwnership@3502,registry:ClaimOwnership@4480,registry:ClaimOwnership@4698,registry:ClaimOwnership@4731,registry:ClaimRowID@10478,registry:ClaimRowID@10559,registry:ClaimRowID@4154,registry:ClaimRowID@4762,registry:ClaimRowID@7464,registry:ClaimStamp@3577,registry:ClaimStamp@4081,registry:Host@7401,registry:Kind@3610,registry:Name@4816,registry:Name@7420,registry:Platform@3732,registry:Registration@4180,registry:Registration@7489,registry:Replica@3762,registry:Route@10180,registry:Route@4480,registry:Route@4731,registry:Route@5262,registry:Route@6011,registry:Timers@4679,registry:Timers@4698,registry:Timers@4762,registry:Timers@4816,registry:Timers@7276,registry:Tunnel@3712,registry:Unit@3636,whole:ClaimOwnership@10194,whole:ClaimOwnership@3506,whole:ClaimOwnership@4492,whole:ClaimOwnership@4714,whole:ClaimOwnership@4743,whole:ColdRows@10182,whole:ColdRows@11294,whole:ColdRows@11304,whole:ColdRows@4977,whole:ColdRows@4985,whole:ColdRows@4993,whole:ColdRows@5000,whole:ColdRows@5264,whole:ColdRows@6013,whole:ColdRows@7278,whole:Route@10188,whole:Route@3487,whole:Route@3847,whole:Route@4279,whole:Route@4287,whole:Route@4338,whole:Route@4346,whole:Route@4486,whole:Route@4737,whole:Route@5271,whole:Route@6020,whole:Timer@4018,whole:Timer@4292,whole:Timer@4300,whole:Timer@4351,whole:Timer@4389,whole:Timer@4685,whole:Timer@4704,whole:Timer@4768,whole:Timer@4822,whole:Timer@7285 050ae1e6818687e19e7a0cd443c09e837e54bdc00f4ea1feb6430cfedd2b5343
internal/hostbox/coldhandover.go type ColdHandover authority C,J4 rev3=PROPOSED-seed; operation-only production cold-handover policy; no source-preserving serving authority from operation cleanup 874b2b74ea349dd93655d4e11f4d9e97752c3f216593ebb922d819caba7981c4
@@ -325,7 +325,10 @@ system.go runSystem authority B,C,D,F,J4 rev3=PROPOSED-seed; command arguments,
system.go systemVerb authority B,C,D,F,J4 rev3=PROPOSED-seed; command arguments, claim flags, service identity and root delegation capability before constructing agent-mode resources; one-shot skips Configure, Start, duties and listeners; close resources only after private runtime joins ee25513ed72f9b8338d95682c4c76b4ced26ec1f060528fc5804da9afbfbfd83
@coverage . bf3d70b6a2c5d769a3c580c9d5511231499b788d9c08a542895de8381e5a39c9
@coverage internal/activator 41f0ad01ac54bae63f089ef8ec5bbfa9155bb76720d72b2d88563481de40fe5c
-@coverage internal/host 455146512e142affa2ba23b8cf3c49ad2028e78901c23e5ceaac96a1edddbad4
+@coverage internal/host 276b5d95e109772c8af5bc7408bf4d888656ad032ef5d552e169203f55be023e
@coverage internal/hostbox db890260533bdace6ad9a7c23dc1538cb919962894f005fc7c5a59a18e969429
@coverage internal/hostfiles a38dbcd9789ba4acd237e88c834f2b2bfca9847b0d43c32b0116dc8996d7a142
@coverage internal/hostguard a721142f4f3ac5fa17b6b1e2ccfc66ca809cfdcbf2733fdc00fbd0efaa90aeb6
+internal/host/rootexecution_recovery.go recoverRootExecutionBeforeUpdate authority D rev3=PROPOSED-seed; extracted update recovery ordering; root instance-control identity; broker durable controller state then best-effort local park and retirement journals; retained park failures cannot gate update eb97b162b0d712dd2fe8971984353ebc10efe1142077a3c2531781dd969cbbf5
+internal/lease/leasetest/s3server.go NewS3Server authority C,J4 rev3=PROPOSED-seed; test-process S3 fixture only, including privileged runner; HTTP query selects simulated versions, GET exposes stored ETag and conditional PUT refuses stale owners under the fixture mutex; not called by production root paths file:Query@479 ac2435bd4d5114736ba9b7e964f62d5fac3dd97eee5f11cbadd4facc342a9583
+@coverage internal/lease/leasetest a589d673ded02391c4dfbbe6a9b86890f44522d3985bfa219c19d6ed40a0fd07