carlos / platform Public

WIP claim Plan 14 track E checkpoint 55 (not for merge)

Always-running units: gated independent controller lifetime; root acceptance and mutations pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev d1d5ff09ad69c44a46725e26692f819dbc0f41b3 parent 7523012
2 files changed, +243 −6
  • internal/host/plan14_installed_launch_root_linux_test.go +58 −6
  • internal/host/plan14_installed_program_root_linux_test.go +185 −0

CI passed — run details

CI log
=== gate ===
go: downloading github.com/aws/aws-sdk-go-v2/service/ec2 v1.324.0
go: downloading github.com/aws/aws-sdk-go-v2 v1.44.0
go: downloading github.com/aws/aws-sdk-go-v2/config v1.32.6
go: downloading github.com/aws/aws-sdk-go-v2/service/sns v1.43.0
go: downloading github.com/benbjohnson/litestream v0.5.14
go: downloading github.com/superfly/ltx v0.5.1
go: downloading golang.org/x/crypto v0.52.0
go: downloading modernc.org/sqlite v1.55.0
go: downloading github.com/aws/aws-sdk-go-v2/service/acm v1.45.0
go: downloading github.com/aws/aws-sdk-go-v2/service/route53 v1.64.1
go: downloading github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.68.0
go: downloading github.com/aws/aws-sdk-go-v2/service/iam v1.58.2
go: downloading github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.37.0
go: downloading github.com/aws/aws-sdk-go-v2/service/s3 v1.105.0
go: downloading github.com/aws/aws-sdk-go-v2/service/sts v1.45.6
go: downloading github.com/aws/smithy-go v1.28.1
go: downloading github.com/oschwald/maxminddb-golang v1.13.1
go: downloading golang.org/x/sys v0.46.0
go: downloading github.com/aws/aws-sdk-go-v2/credentials v1.19.6
go: downloading golang.org/x/net v0.55.0
go: downloading github.com/klauspost/compress v1.18.0
go: downloading github.com/pion/logging v0.2.4
go: downloading github.com/pion/stun/v3 v3.0.1
go: downloading github.com/pion/turn/v4 v4.1.4
go: downloading github.com/aws/aws-sdk-go-v2/service/sesv2 v1.66.6
go: downloading github.com/aws/aws-sdk-go-v2/service/costexplorer v1.67.5
go: downloading github.com/pierrec/lz4/v4 v4.1.22
go: downloading github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.40
go: downloading github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.38
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.40
go: downloading github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16
go: downloading github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4
go: downloading github.com/aws/aws-sdk-go-v2/service/sso v1.30.8
go: downloading github.com/aws/aws-sdk-go-v2/service/signin v1.0.4
go: downloading github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12
go: downloading github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31
go: downloading github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.40
go: downloading github.com/pion/randutil v0.1.0
go: downloading github.com/pion/transport/v4 v4.0.1
go: downloading github.com/prometheus/client_golang v1.17.0
go: downloading golang.org/x/sync v0.21.0
go: downloading github.com/mattn/go-isatty v0.0.20
go: downloading github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.18
go: downloading github.com/lmittmann/tint v1.1.3
go: downloading github.com/pion/transport/v3 v3.0.8
go: downloading github.com/pion/dtls/v3 v3.0.7
go: downloading github.com/wlynxg/anet v0.0.5
go: downloading golang.org/x/text v0.37.0
go: downloading github.com/prometheus/client_model v0.5.0
go: downloading google.golang.org/protobuf v1.36.10
go: downloading github.com/beorn7/perks v1.0.1
go: downloading github.com/prometheus/common v0.45.0
go: downloading github.com/cespare/xxhash/v2 v2.3.0
go: downloading github.com/prometheus/procfs v0.12.0
go: downloading github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0
go: downloading modernc.org/libc v1.74.1
go: downloading modernc.org/memory v1.11.0
go: downloading modernc.org/mathutil v1.7.1
go: downloading github.com/google/uuid v1.6.0
go: downloading github.com/dustin/go-humanize v1.0.1
go: downloading github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec
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	283.690s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	2.617s
ok  	github.com/carlosframework/platform/cmd/moveprobe	93.279s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.710s
ok  	github.com/carlosframework/platform/internal/activator	250.336s
ok  	github.com/carlosframework/platform/internal/agentsock	0.017s
ok  	github.com/carlosframework/platform/internal/alerting	0.003s
ok  	github.com/carlosframework/platform/internal/analytics	0.022s
ok  	github.com/carlosframework/platform/internal/appconfig	2.442s
ok  	github.com/carlosframework/platform/internal/apperr	0.070s
ok  	github.com/carlosframework/platform/internal/applog	0.124s
ok  	github.com/carlosframework/platform/internal/awsinv	0.004s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.004s
ok  	github.com/carlosframework/platform/internal/boxid	0.847s
?   	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.697s
ok  	github.com/carlosframework/platform/internal/changelog	0.318s
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	388.950s
?   	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.295s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.009s
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.005s
ok  	github.com/carlosframework/platform/internal/domains	2.354s
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.984s
?   	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.130s
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.016s
ok  	github.com/carlosframework/platform/internal/geoip	0.030s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.149s
ok  	github.com/carlosframework/platform/internal/host	145.573s
ok  	github.com/carlosframework/platform/internal/hostactors	0.506s
ok  	github.com/carlosframework/platform/internal/hostbox	2.307s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.483s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.005s
ok  	github.com/carlosframework/platform/internal/hostexec	5.330s
ok  	github.com/carlosframework/platform/internal/hostfence	0.038s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.980s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.012s
ok  	github.com/carlosframework/platform/internal/hostguard	0.986s
ok  	github.com/carlosframework/platform/internal/hostops	1.531s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.002s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.002s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.427s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.126s
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	5.238s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.721s
ok  	github.com/carlosframework/platform/internal/instancefs	0.005s
ok  	github.com/carlosframework/platform/internal/instancekey	0.002s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.006s
ok  	github.com/carlosframework/platform/internal/layout	1.489s
ok  	github.com/carlosframework/platform/internal/lease	9.830s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.006s
ok  	github.com/carlosframework/platform/internal/ledger	0.015s
ok  	github.com/carlosframework/platform/internal/mail	0.158s
ok  	github.com/carlosframework/platform/internal/maintenance	0.080s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.516s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.064s
ok  	github.com/carlosframework/platform/internal/metrics	0.006s
ok  	github.com/carlosframework/platform/internal/moves	21.526s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.774s
ok  	github.com/carlosframework/platform/internal/objstore	0.168s
ok  	github.com/carlosframework/platform/internal/ops	0.167s
ok  	github.com/carlosframework/platform/internal/park	0.202s
ok  	github.com/carlosframework/platform/internal/privatefile	0.003s
ok  	github.com/carlosframework/platform/internal/provision	34.887s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.006s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	5.903s
ok  	github.com/carlosframework/platform/internal/relay	7.288s
ok  	github.com/carlosframework/platform/internal/release	22.206s
ok  	github.com/carlosframework/platform/internal/rollout	3.808s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.003s
ok  	github.com/carlosframework/platform/internal/rootaws	0.007s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.007s
ok  	github.com/carlosframework/platform/internal/rootstate	0.026s
ok  	github.com/carlosframework/platform/internal/runs	0.567s
ok  	github.com/carlosframework/platform/internal/s3compat	0.005s
ok  	github.com/carlosframework/platform/internal/safefile	0.004s
ok  	github.com/carlosframework/platform/internal/schedule	2.469s
ok  	github.com/carlosframework/platform/internal/seeds	0.012s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	11.339s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.598s
ok  	github.com/carlosframework/platform/internal/status	0.040s
ok  	github.com/carlosframework/platform/internal/steering	0.005s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.199s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.004s
ok  	github.com/carlosframework/platform/internal/termfence	0.004s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.005s
ok  	github.com/carlosframework/platform/internal/updatewords	0.004s
ok  	github.com/carlosframework/platform/internal/usage	0.006s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.550s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
go: downloading github.com/ncruces/go-strftime v1.0.0
--- shard 1 of 5 ---
cross-compiling darwin/amd64
go: downloading github.com/ncruces/go-strftime v1.0.0
--- shard 2 of 5 ---
cross-compiling linux/arm64
linux/arm64 carlos binary: 47382690 bytes (ceiling 52000000)
--- shard 3 of 5 ---
cross-compiling windows/amd64
go: downloading github.com/ncruces/go-strftime v1.0.0
--- shard 4 of 5 ---
cross-compiling windows/arm64
go: downloading github.com/ncruces/go-strftime v1.0.0
=== 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/16 02:21:33 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DEE12F05F15C64, 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/plan14_installed_launch_root_linux_test.go b/internal/host/plan14_installed_launch_root_linux_test.go
index 69ecf2df3..af2a83380 100644
--- a/internal/host/plan14_installed_launch_root_linux_test.go
+++ b/internal/host/plan14_installed_launch_root_linux_test.go
@@ -95,6 +95,18 @@ func TestPlan14MaterializedBoundRouteRootRunner(t *testing.T) {
}
}
+func TestPlan14InstalledProgramAuthorityRootRunner(t *testing.T) {
+ for _, unit := range []string{"tcp", "socket-program", "backing-program"} {
+ t.Run(unit, func(t *testing.T) { plan14InstalledClaimLaunch(t, "authority:materialized:"+unit) })
+ }
+}
+
+func TestPlan14InstalledProgramLostReplyRootRunner(t *testing.T) {
+ for _, unit := range []string{"tcp", "socket-program", "backing-program"} {
+ t.Run(unit, func(t *testing.T) { plan14InstalledClaimLaunch(t, "lost-installed:authority:materialized:"+unit) })
+ }
+}
+
func TestPlan14MigratedBoundRouteRootRunner(t *testing.T) {
for _, unit := range []string{"tcp", "socket-program", "backing-program"} {
t.Run(unit, func(t *testing.T) { plan14InstalledClaimLaunch(t, "migrated:materialized:"+unit) })
@@ -117,14 +129,14 @@ func TestPlan14RealConsoleRootRunner(t *testing.T) {
if len(plan14ConsoleExecutable) == 0 {
t.Fatal("build the real executable with scripts/plan14-console-root-runner.sh")
}
- plan14InstalledClaimLaunch(t, "binary:console:materialized:tcp")
+ plan14InstalledClaimLaunch(t, "authority:binary:console:materialized:tcp")
}
func TestPlan14RealWoodstarRootRunner(t *testing.T) {
if len(plan14WoodstarExecutable) == 0 {
t.Fatal("set PLAN14_WOODSTAR_SOURCE when running scripts/plan14-console-root-runner.sh")
}
- plan14InstalledClaimLaunch(t, "woodstar:materialized:tcp")
+ plan14InstalledClaimLaunch(t, "authority:woodstar:materialized:tcp")
}
func TestPlan14NamedStartupDefersReplacementRootRunner(t *testing.T) {
@@ -134,6 +146,10 @@ func TestPlan14NamedStartupDefersReplacementRootRunner(t *testing.T) {
}
func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) {
+ lostInstalledReply := strings.HasPrefix(boundUnit, "lost-installed:")
+ boundUnit = strings.TrimPrefix(boundUnit, "lost-installed:")
+ installedAuthority := strings.HasPrefix(boundUnit, "authority:")
+ boundUnit = strings.TrimPrefix(boundUnit, "authority:")
woodstar := strings.HasPrefix(boundUnit, "woodstar:")
boundUnit = strings.TrimPrefix(boundUnit, "woodstar:")
deferReplacement := strings.HasPrefix(boundUnit, "defer:")
@@ -604,6 +620,15 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) {
f.req.Reservation.Inputs = inputs
f.claims.set(func(c *hostclaim.Claim) { c.Holder.WakeRetry.Attempt.LaunchInputs = inputs })
f.root.generations = &rootGenerationSource{store: store, optDir: "/opt/carlos", pub: func() (ed25519.PublicKey, error) { return pub, nil }, open: func() (rootstate.Store, error) { return rootstate.Open(filepath.Join(dir, "generations"), true, false) }, openFile: rootstate.OpenTrustedFile, persistent: rootstate.PersistentIdentity}
+ if installedAuthority {
+ plan14ProductionInstalledAuthority(t, f.root, store, pub)
+ f.root.ValidateExecutable = claimStagedExecutable(&instancectl.Server{OptDir: "/opt/carlos", HostApp: rootInstallationHostApp(f.root.Guard, store)})
+ f.root.installedProgram = f.root.rootInstalledProgram
+ }
+ var lostReply *bool
+ if lostInstalledReply {
+ lostReply = plan14LoseInstalledReply(f.root)
+ }
lock := &release.ReleaseExclusion{Open: func() (rootstate.Store, error) { return rootstate.Open(filepath.Join(dir, "release"), true, false) }}
checkSibling := func() {}
if programMode {
@@ -625,11 +650,22 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) {
response, err = f.root.Serve(ctx, f.peer, f.req)
return err
})
+ if lostInstalledReply {
+ plan14RecoverInstalledReply(t, f, lostReply, err)
+ err = lock.With(t.Context(), func(ctx context.Context) error {
+ var err error
+ response, err = f.root.Serve(ctx, f.peer, f.req)
+ return err
+ })
+ }
if err != nil {
logs, _ := exec.Command("journalctl", "-u", unit, "--no-pager", "-n", "80").CombinedOutput()
output, _ := os.ReadFile(provision.LogSocket(host, "run"))
t.Fatalf("production launch: %v\n%s\noutput: %s", err, logs, output)
}
+ if installedAuthority {
+ plan14InstalledProgramAuthority(t, f.root, f.req, *response.Grant, rec)
+ }
transport := &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
if tcpMode {
return (&net.Dialer{}).DialContext(ctx, "tcp", endpoint)
@@ -727,17 +763,26 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) {
t.Fatal("readiness accepted unrelated process or group")
}
}
- generations, err := rootstate.Open(filepath.Join(dir, "generations"), false, false)
+ generations, err := f.root.generations.open()
if err != nil {
t.Fatal(err)
}
defer generations.Close()
ledger := &release.RootGenerations{Store: generations}
records, err := ledger.Reservations(t.Context())
- if err != nil || len(records) != 1 {
+ wantRecords := 1
+ if lostInstalledReply {
+ wantRecords++
+ }
+ if err != nil || len(records) != wantRecords {
t.Fatal(records, err)
}
- v1 := records[0]
+ var v1 release.GenerationReservation
+ for _, r := range records {
+ if r.Process.PID == response.Grant.PID {
+ v1 = r
+ }
+ }
if consoleBinary || woodstar {
running, err := os.ReadFile(fmt.Sprintf("/proc/%d/exe", v1.Process.TenantPID))
if err != nil || sha256.Sum256(running) != sha256.Sum256(body) {
@@ -847,6 +892,9 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) {
// the outer namespace first; the child uses the retained root records.
cmd := exec.CommandContext(t.Context(), "unshare", "--mount", "--pid", "--fork", "--mount-proc", binary, "-test.run=^TestPlan14InstalledClaimLaunchRootRunner$", "-test.v", "-test.timeout=2m")
cmd.Env = append(os.Environ(), "PLAN14_INSTALLED_RETIRE_CHILD="+dir)
+ if installedAuthority {
+ cmd.Env = append(cmd.Env, "PLAN14_INSTALLED_GENERATIONS="+release.RootGenerationsDir)
+ }
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("exact pin release: %v\n%s", err, output)
} else {
@@ -969,7 +1017,11 @@ func plan14InstalledRetire(t *testing.T, dir string) {
t.Fatal(err)
}
defer journal.Close()
- generations, err := rootstate.Open(filepath.Join(dir, "generations"), false, false)
+ generationDir := filepath.Join(dir, "generations")
+ if os.Getenv("PLAN14_INSTALLED_GENERATIONS") == release.RootGenerationsDir {
+ generationDir = release.RootGenerationsDir
+ }
+ generations, err := rootstate.Open(generationDir, false, false)
if err != nil {
t.Fatal(err)
}
diff --git a/internal/host/plan14_installed_program_root_linux_test.go b/internal/host/plan14_installed_program_root_linux_test.go
new file mode 100644
index 000000000..4b9a4840d
--- /dev/null
+++ b/internal/host/plan14_installed_program_root_linux_test.go
@@ -0,0 +1,185 @@
+//go:build linux && rootrunner
+
+package host
+
+import (
+ "context"
+ "crypto/ed25519"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "os"
+ "reflect"
+ "strings"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/instancectl"
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/rootparticipation"
+ "github.com/carlosframework/platform/internal/rootstate"
+)
+
+type plan14InstalledWriteReply struct {
+ rootstate.Store
+ fired *bool
+}
+
+func (s plan14InstalledWriteReply) Write(name string, raw []byte) error {
+ if err := s.Store.Write(name, raw); err != nil {
+ return err
+ }
+ var record struct{ Programs struct{ Values []string } }
+ if err := json.Unmarshal(raw, &record); err != nil {
+ return err
+ }
+ if !*s.fired && len(record.Programs.Values) != 0 {
+ *s.fired = true
+ return os.ErrPermission
+ }
+ return nil
+}
+
+func plan14LoseInstalledReply(root *ClaimLaunchRoot) *bool {
+ fired := new(bool)
+ root.history = func(ctx context.Context, effect func(*release.RootHistory) error) error {
+ return productionCreationHistory(ctx, func(h *release.RootHistory) error {
+ h.Store = plan14InstalledWriteReply{Store: h.Store, fired: fired}
+ return effect(h)
+ })
+ }
+ return fired
+}
+
+func plan14RecoverInstalledReply(t *testing.T, f *launchFixture, fired *bool, launchErr error) {
+ t.Helper()
+ if fired == nil || !*fired || !errors.Is(launchErr, os.ErrPermission) {
+ t.Fatal("installed-program write did not lose its reply", launchErr)
+ }
+ s, err := f.root.generations.open()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer s.Close()
+ ledger := &release.RootGenerations{Store: s}
+ before, err := ledger.Reservations(t.Context())
+ if err != nil || len(before) != 1 || before[0].Process.PID == 0 || before[0].Retirement != "" {
+ t.Fatal("failed reply lost the running generation pin", before, err)
+ }
+ r := before[0]
+ started := instancectl.ClaimUnitStarted{Unit: r.Binding.Unit, InvocationID: r.Process.Invocation, MainPID: r.Process.PID, ControlGroup: r.Process.Group}
+ if err := f.root.Units.Same(t.Context(), started); err != nil {
+ t.Fatal("lost-reply witness has no running invocation", err)
+ }
+ stop := f.req
+ stop.Op, stop.Spec, stop.Executable, stop.MemoryMax = hostbox.RoleLaunchStop, nil, "", 0
+ if _, err := f.root.Serve(t.Context(), f.peer, stop); err != nil {
+ t.Fatal("lost reply prevented exact stop", err)
+ }
+ if err := f.root.Units.Gone(t.Context(), f.target.Host, r.Process.Invocation); err != nil {
+ t.Fatal("lost-reply invocation remains", err)
+ }
+ after, err := ledger.Reservations(t.Context())
+ if err != nil || !reflect.DeepEqual(before, after) {
+ t.Fatal("stop removed the pending retirement pin", err)
+ }
+ next := *f.req.Reservation
+ next.ID, next.InvocationID = "attempt-recovered:main", "inv-recovered"
+ f.req.Reservation = &next
+ f.claims.set(func(c *hostclaim.Claim) {
+ c.Holder.WakeRetry.AttemptsReserved++
+ c.Holder.WakeRetry.Attempt.ID = "attempt-recovered"
+ c.Holder.WakeRetry.Attempt.InvocationID = "inv-recovered"
+ })
+}
+
+func plan14ProductionInstalledAuthority(t *testing.T, root *ClaimLaunchRoot, store release.Store, pub ed25519.PublicKey) {
+ t.Helper()
+ for _, path := range []string{"/var/lib/carlos-claims", "/var/lib/carlos-release"} {
+ if err := os.Mkdir(path, 0700); err != nil {
+ t.Fatal("production authority fixture root already exists", path, err)
+ }
+ t.Cleanup(func() {
+ if err := os.RemoveAll(path); err != nil {
+ t.Error(err)
+ }
+ })
+ }
+ state, err := rootstate.Open(rootparticipation.Namespace, true, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer state.Close()
+ actor, err := root.Actor(t.Context())
+ if err != nil {
+ t.Fatal(err)
+ }
+ raw, err := json.Marshal(rootparticipation.Census{Schema: 1, Box: actor.Box, Enrollment: actor.Enrollment, Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := state.Write("census.json", raw); err != nil {
+ t.Fatal(err)
+ }
+ t.Setenv("CARLOS_RELEASE_PUBKEY", base64.StdEncoding.EncodeToString(pub))
+ root.generations = productionRootGenerations(store, "/opt/carlos")
+ root.history = productionCreationHistory
+}
+
+func plan14InstalledProgramAuthority(t *testing.T, root *ClaimLaunchRoot, q hostbox.RoleLaunchRequest, grant hostbox.RoleLaunchGrant, rec provision.Record) {
+ t.Helper()
+ started := instancectl.ClaimUnitStarted{Unit: grant.Unit, InvocationID: grant.UnitInvocationID, MainPID: grant.PID, ControlGroup: grant.Group.RelativePath}
+ seen, err := root.Kernel.Observe(started)
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := release.FileProgram(q.Executable)
+ if want == "" {
+ t.Fatal("built executable has no program identity")
+ }
+ program, err := root.rootInstalledProgram(t.Context(), q, started, seen)
+ if err != nil || program != want {
+ t.Fatal("production installed program", program, want, err)
+ }
+ binding := release.HistoryBinding{Host: rec.Host, Account: rec.Account, App: rec.App}
+ if err := root.history(t.Context(), func(h *release.RootHistory) error {
+ programs, err := h.Programs(binding)
+ if err != nil || !reflect.DeepEqual(programs, []string{want}) {
+ t.Fatal("launch did not retain the observed program", programs, want, err)
+ }
+ return nil
+ }); err != nil {
+ t.Fatal(err)
+ }
+ for _, attack := range []string{"digest", "host", "staged-only", "running-file", "tenant-start", "invocation", "account-path"} {
+ t.Run("installed-program-"+attack, func(t *testing.T) {
+ request, observed, unit := q, seen, started
+ reservation, spec := *q.Reservation, *q.Spec
+ request.Reservation, request.Spec = &reservation, &spec
+ switch attack {
+ case "digest":
+ spec.Digest = strings.Repeat("0", 64)
+ case "host":
+ reservation.Target.Host = "sibling.example"
+ case "staged-only":
+ observed.TenantPID = 0
+ case "running-file":
+ observed.Running.Inode++
+ case "tenant-start":
+ observed.TenantStart += "0"
+ case "invocation":
+ unit.InvocationID = strings.Repeat("0", 32)
+ case "account-path":
+ request.Executable = release.VersionPath("/opt/carlos", "bap", "v1", rec.App)
+ }
+ if _, err := root.rootInstalledProgram(t.Context(), request, unit, observed); err == nil {
+ t.Fatal("accepted substituted installed authority", attack)
+ }
+ if err := root.Units.Same(t.Context(), started); err != nil {
+ t.Fatal("read-only refusal changed the running invocation", err)
+ }
+ })
+ }
+}