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>
2 files changed,
+243
−6
internal/host/plan14_installed_launch_root_linux_test.go+58 −6internal/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) |
| + } |
| + }) |
| + } |
| +} |