WIP claim Plan 14 track E checkpoint 28 (not for merge)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 files changed,
+371
−3
internal/host/plan14_generation_payload_files_linux_test.go+109 −0internal/host/plan14_generation_payload_root_linux_test.go+165 −0internal/host/rootgeneration_inventory.go+6 −0internal/host/rootgeneration_payload_linux.go+65 −0internal/host/rootgeneration_payload_other.go+21 −0internal/provision/plan14_e_write_census_test.go+1 −1internal/provision/testdata/plan14-root-reads-e.tsv+4 −2
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 352.044s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 3.273s
ok github.com/carlosframework/platform/cmd/moveprobe 93.291s
ok github.com/carlosframework/platform/cmd/zddprobe 0.708s
ok github.com/carlosframework/platform/internal/activator 233.277s
ok github.com/carlosframework/platform/internal/agentsock 0.018s
ok github.com/carlosframework/platform/internal/alerting 0.005s
ok github.com/carlosframework/platform/internal/analytics 0.036s
ok github.com/carlosframework/platform/internal/appconfig 2.345s
ok github.com/carlosframework/platform/internal/apperr 0.080s
ok github.com/carlosframework/platform/internal/applog 0.155s
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.594s
? 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.009s
ok github.com/carlosframework/platform/internal/caps 0.012s
ok github.com/carlosframework/platform/internal/certwarm 0.002s
ok github.com/carlosframework/platform/internal/cgroup 0.702s
ok github.com/carlosframework/platform/internal/changelog 0.309s
ok github.com/carlosframework/platform/internal/clamd 0.055s
? github.com/carlosframework/platform/internal/cliflag [no test files]
ok github.com/carlosframework/platform/internal/console 366.433s
? 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 5.601s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.011s
ok github.com/carlosframework/platform/internal/dbverify 0.005s
ok github.com/carlosframework/platform/internal/dnsrec 0.004s
ok github.com/carlosframework/platform/internal/docsite 0.006s
ok github.com/carlosframework/platform/internal/domains 2.134s
ok github.com/carlosframework/platform/internal/doorkey 0.006s
ok github.com/carlosframework/platform/internal/economics 0.006s
ok github.com/carlosframework/platform/internal/edge 26.117s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.009s
ok github.com/carlosframework/platform/internal/fleetapi 0.134s
ok github.com/carlosframework/platform/internal/gate 1.664s
ok github.com/carlosframework/platform/internal/genselect 0.015s
ok github.com/carlosframework/platform/internal/geo 0.013s
ok github.com/carlosframework/platform/internal/geoip 0.036s
ok github.com/carlosframework/platform/internal/historyreadback 0.171s
ok github.com/carlosframework/platform/internal/host 133.092s
ok github.com/carlosframework/platform/internal/hostactors 0.353s
ok github.com/carlosframework/platform/internal/hostbox 2.286s
ok github.com/carlosframework/platform/internal/hostclaim 0.540s
ok github.com/carlosframework/platform/internal/hostevidence 0.007s
ok github.com/carlosframework/platform/internal/hostexec 4.860s
ok github.com/carlosframework/platform/internal/hostfence 0.027s
ok github.com/carlosframework/platform/internal/hostfiles 10.504s
ok github.com/carlosframework/platform/internal/hostfloor 0.013s
ok github.com/carlosframework/platform/internal/hostguard 1.010s
ok github.com/carlosframework/platform/internal/hostops 1.461s
? 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.004s
ok github.com/carlosframework/platform/internal/hostrelocate 0.467s
ok github.com/carlosframework/platform/internal/hostresolve 0.152s
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 1.819s
ok github.com/carlosframework/platform/internal/instanceexec 2.716s
ok github.com/carlosframework/platform/internal/instancefs 0.004s
ok github.com/carlosframework/platform/internal/instancekey 0.015s
ok github.com/carlosframework/platform/internal/instanceupdates 0.027s
ok github.com/carlosframework/platform/internal/layout 1.472s
ok github.com/carlosframework/platform/internal/lease 9.480s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.018s
ok github.com/carlosframework/platform/internal/ledger 0.014s
ok github.com/carlosframework/platform/internal/mail 0.173s
ok github.com/carlosframework/platform/internal/maintenance 0.079s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.519s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.078s
ok github.com/carlosframework/platform/internal/metrics 0.005s
ok github.com/carlosframework/platform/internal/moves 21.106s
ok github.com/carlosframework/platform/internal/moves/archive 0.777s
ok github.com/carlosframework/platform/internal/objstore 0.179s
ok github.com/carlosframework/platform/internal/ops 0.175s
ok github.com/carlosframework/platform/internal/park 0.211s
ok github.com/carlosframework/platform/internal/privatefile 0.006s
ok github.com/carlosframework/platform/internal/provision 11.334s
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.053s
ok github.com/carlosframework/platform/internal/relay 7.232s
ok github.com/carlosframework/platform/internal/release 21.796s
ok github.com/carlosframework/platform/internal/rollout 3.641s
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/s3compat 0.005s
ok github.com/carlosframework/platform/internal/safefile 0.004s
ok github.com/carlosframework/platform/internal/schedule 2.282s
ok github.com/carlosframework/platform/internal/sim/hostlife 9.111s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 3.622s
ok github.com/carlosframework/platform/internal/status 0.041s
ok github.com/carlosframework/platform/internal/steering 0.006s
ok github.com/carlosframework/platform/internal/stopevidence 0.307s
ok github.com/carlosframework/platform/internal/svcrole 0.002s
ok github.com/carlosframework/platform/internal/telemetry 0.004s
ok github.com/carlosframework/platform/internal/termfence 0.005s
ok github.com/carlosframework/platform/internal/tunnelclient 0.004s
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.004s
ok github.com/carlosframework/platform/internal/version 0.002s
ok github.com/carlosframework/platform/internal/vet 1.529s
=== 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
--- 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.025s
=== RUN TestT4StorageProbeMinio
2026/10/14 17:43:41 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE7657D58F6514, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok github.com/carlosframework/platform/internal/host 0.022s
minio contract: PASS
diff --git a/internal/host/plan14_generation_payload_files_linux_test.go b/internal/host/plan14_generation_payload_files_linux_test.go| new file mode 100644 |
| index 000000000..ce445c3eb |
| --- /dev/null |
| +++ b/internal/host/plan14_generation_payload_files_linux_test.go |
| @@ -0,0 +1,109 @@ |
| +//go:build linux |
| + |
| +package host |
| + |
| +import ( |
| + "context" |
| + "os" |
| + "path/filepath" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/release" |
| + "github.com/carlosframework/platform/internal/rootparticipation" |
| + "golang.org/x/sys/unix" |
| +) |
| + |
| +func TestPlan14GenerationPayloadPhysicalIdentity(t *testing.T) { |
| + for _, attack := range []string{"none", "directory", "missing", "replaced", "symlink", "parent-symlink", "root-symlink", "wrong-device", "wrong-inode", "wrong-type", "intended", "intended-missing", "escape", "unknown-root", "changed-during-kernel"} { |
| + t.Run(attack, func(t *testing.T) { |
| + f, disk, _ := generationLaunchFixture(t) |
| + if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil { |
| + t.Fatal(err) |
| + } |
| + r := readGenerationLaunch(t, disk) |
| + views, err := f.root.Guard.Root.GenerationInstallations(t.Context()) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + launch, err := f.journal.Read(t.Context(), f.target) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + dir := t.TempDir() |
| + t.Setenv("CARLOS_DATA_DIR", dir) |
| + parent := filepath.Join(dir, "state") |
| + if err := os.Mkdir(parent, 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + path := filepath.Join(parent, "retained") |
| + if attack == "directory" { |
| + err = os.Mkdir(path, 0700) |
| + } else { |
| + err = os.WriteFile(path, []byte("retained payload"), 0600) |
| + } |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + var st unix.Stat_t |
| + if err := unix.Lstat(path, &st); err != nil { |
| + t.Fatal(err) |
| + } |
| + entry := hostbox.StopInventoryEntry{Kind: "path", ArtifactID: "retained", ManagedRoot: "data", RelativePath: "state/retained", Ownership: "owner", Materialization: "materialized", Device: uint64(st.Dev), Inode: st.Ino, Type: st.Mode & unix.S_IFMT} |
| + switch attack { |
| + case "missing", "replaced", "symlink": |
| + if err := os.Rename(path, path+".old"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if attack == "replaced" { |
| + err = os.WriteFile(path, []byte("replacement"), 0600) |
| + } else if attack == "symlink" { |
| + err = os.Symlink("retained.old", path) |
| + } |
| + case "parent-symlink": |
| + if err := os.Rename(parent, parent+".old"); err != nil { |
| + t.Fatal(err) |
| + } |
| + err = os.Symlink("state.old", parent) |
| + case "root-symlink": |
| + link := dir + ".link" |
| + err = os.Symlink(dir, link) |
| + t.Cleanup(func() { os.Remove(link) }) |
| + t.Setenv("CARLOS_DATA_DIR", link) |
| + case "wrong-device": |
| + entry.Device++ |
| + case "wrong-inode": |
| + entry.Inode++ |
| + case "wrong-type": |
| + entry.Type = unix.S_IFDIR |
| + case "intended", "intended-missing": |
| + entry.Materialization = "intended" |
| + if attack == "intended-missing" { |
| + entry.Device, entry.Inode, entry.Type = 0, 0, 0 |
| + err = os.Remove(path) |
| + } |
| + case "escape": |
| + entry.RelativePath = "../" + filepath.Base(dir) + "/state/retained" |
| + case "unknown-root": |
| + entry.ManagedRoot = "other" |
| + } |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + snapshot := generationInventory{Census: rootparticipation.Census{Schema: 1, Box: f.target.Box, Enrollment: f.target.Enrollment, Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true}, Installations: views, Launches: []hostbox.LaunchInventory{launch}, Payloads: generationPayloadFixture(views)} |
| + snapshot.Payloads[0].Inventory.Entries = append(snapshot.Payloads[0].Inventory.Entries, entry) |
| + v := generationReconciler{read: func(context.Context) (generationInventory, error) { return snapshot, nil }, kernel: func(context.Context) (string, string, error) { |
| + if attack == "changed-during-kernel" { |
| + if err := os.Rename(path, path+".old"); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + return "boot", r.Binding.Digest, nil |
| + }} |
| + _, err = v.reconcile(t.Context(), []release.GenerationReservation{r}) |
| + if (err == nil) != (attack == "none" || attack == "directory") { |
| + t.Fatalf("physical payload %s: %v", attack, err) |
| + } |
| + }) |
| + } |
| +} |
diff --git a/internal/host/plan14_generation_payload_root_linux_test.go b/internal/host/plan14_generation_payload_root_linux_test.go| new file mode 100644 |
| index 000000000..472d9a792 |
| --- /dev/null |
| +++ b/internal/host/plan14_generation_payload_root_linux_test.go |
| @@ -0,0 +1,165 @@ |
| +//go:build linux && rootrunner |
| + |
| +package host |
| + |
| +import ( |
| + "encoding/json" |
| + "os" |
| + "os/exec" |
| + "path/filepath" |
| + "reflect" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/provision" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| + "golang.org/x/sys/unix" |
| +) |
| + |
| +func TestPlan14GenerationPayloadPhysicalRootRunner(t *testing.T) { |
| + const childKey = "PLAN14_E28_PAYLOAD_CHILD" |
| + dir := os.Getenv(childKey) |
| + if dir == "" { |
| + dir = generationRootRunnerDir(t) |
| + cmd := exec.CommandContext(t.Context(), "unshare", "--mount", "--pid", "--fork", "--mount-proc", os.Args[0], "-test.run=^TestPlan14GenerationPayloadPhysicalRootRunner$", "-test.v", "-test.timeout=2m") |
| + cmd.Dir = dir |
| + cmd.Env = append(os.Environ(), childKey+"="+dir) |
| + if out, err := cmd.CombinedOutput(); err != nil { |
| + t.Fatalf("isolated payload census: %v\n%s", err, out) |
| + } else { |
| + t.Logf("%s", out) |
| + } |
| + return |
| + } |
| + if os.Geteuid() != 0 { |
| + t.Fatal("designated runner must execute as root") |
| + } |
| + t.Setenv("CARLOS_OPT_DIR", dir) |
| + t.Setenv("CARLOS_DATA_DIR", filepath.Join(dir, "data")) |
| + for _, path := range []string{filepath.Join(dir, "versions"), dataDir()} { |
| + if err := os.Mkdir(path, 0755); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + pins, certificate := generationNamespacePinFixture(t, dir) |
| + path := filepath.Join(dataDir(), "retained") |
| + if err := os.WriteFile(path, []byte("predecessor payload"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + var st unix.Stat_t |
| + if err := unix.Stat(path, &st); err != nil { |
| + t.Fatal(err) |
| + } |
| + // An empty current installation still carries its predecessor's payload. |
| + // These are root-written fixtures, not migration or rebind authorization. |
| + in := hostguard.RootInstallation{Host: "retained.example", Incarnation: "inc", Stamp: 5, Account: "bab", App: "shop", Box: "runner", Enrollment: "en", Migration: 1, InventoryRevision: 1, Empty: true} |
| + in.Record = provision.RecordKey(in.Account, in.App, in.Host) |
| + fences, err := rootstate.Open(filepath.Join(dir, "fences"), false, false) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + raw, err := json.Marshal(map[string]any{ |
| + "Schema": 1, "Installation": in, "Retired": []hostguard.Captured{{Incarnation: in.Incarnation, Stamp: 2}}, |
| + "State": map[string]any{"v": 1, "host": in.Host, "incarnation": in.Incarnation, "active_stamp": 5, "retired_stamps": []uint64{2}}, |
| + }) |
| + if err == nil { |
| + err = fences.Write(in.Host+".json", raw) |
| + } |
| + fences.Close() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + journalPath := filepath.Join(dir, "journals.db") |
| + journal, err := hostbox.OpenLaunchJournal(journalPath) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + service, err := hostbox.NewJournalService(journal) |
| + if err != nil { |
| + journal.Close() |
| + t.Fatal(err) |
| + } |
| + for _, stamp := range []uint64{2, 5} { |
| + payload := hostbox.StopInventory{Complete: true, Host: in.Host, Incarnation: in.Incarnation, InstallationStamp: stamp, Box: in.Box, Enrollment: in.Enrollment, DirectoryRevision: "1"} |
| + if stamp == 2 { |
| + payload.Entries = []hostbox.StopInventoryEntry{{Kind: "path", ArtifactID: "retained", ManagedRoot: "data", RelativePath: "retained", Ownership: "owner", Materialization: "materialized", Device: uint64(st.Dev), Inode: st.Ino, Type: unix.S_IFREG}} |
| + } |
| + if err := service.Inventory.WriteStopInventory(t.Context(), payload); err != nil { |
| + journal.Close() |
| + t.Fatal(err) |
| + } |
| + } |
| + journal.Close() |
| + read := func() generationInventory { |
| + t.Helper() |
| + v, err := generationInventoryAt(t.Context(), filepath.Join(dir, "census"), filepath.Join(dir, "fences"), journalPath) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + return v |
| + } |
| + before := read() |
| + accept := func() { |
| + t.Helper() |
| + if got, err := pins.StagedPins(t.Context(), dir); err != nil || len(got) != 0 || !certificate().Complete { |
| + t.Fatalf("exact retained payload: %v %v", got, err) |
| + } |
| + } |
| + accept() |
| + for _, attack := range []string{"missing", "replacement", "symlink", "bind"} { |
| + t.Run(attack, func(t *testing.T) { |
| + initial := certificate() |
| + if attack == "bind" { |
| + other := filepath.Join(dataDir(), "other") |
| + if err := os.WriteFile(other, []byte("different inode"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := unix.Mount(other, path, "", unix.MS_BIND, ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + t.Cleanup(func() { |
| + if err := unix.Unmount(path, 0); err != nil { |
| + t.Error(err) |
| + } |
| + }) |
| + } else { |
| + if err := os.Rename(path, path+".old"); err != nil { |
| + t.Fatal(err) |
| + } |
| + t.Cleanup(func() { |
| + if attack != "missing" { |
| + if err := os.Remove(path); err != nil { |
| + t.Error(err) |
| + } |
| + } |
| + if err := os.Rename(path+".old", path); err != nil { |
| + t.Error(err) |
| + } |
| + }) |
| + if attack == "replacement" { |
| + err = os.WriteFile(path, []byte("replacement"), 0600) |
| + } else if attack == "symlink" { |
| + err = os.Symlink(path+".old", path) |
| + } |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + if got, err := pins.StagedPins(t.Context(), dir); err == nil || got != nil { |
| + t.Fatalf("changed predecessor payload certified: %v %v", got, err) |
| + } |
| + if c := certificate(); c.Complete || c.Revision <= initial.Revision { |
| + t.Fatal("stale completeness survived physical refusal") |
| + } |
| + if !reflect.DeepEqual(before, read()) { |
| + t.Fatal("physical refusal changed retained authority") |
| + } |
| + }) |
| + denied := certificate() |
| + accept() |
| + if certificate().Revision <= denied.Revision { |
| + t.Fatal("restored payload reused an old certificate") |
| + } |
| + } |
| +} |
diff --git a/internal/host/rootgeneration_inventory.go b/internal/host/rootgeneration_inventory.go| index 13bf24679..5f31fe116 100644 |
| --- a/internal/host/rootgeneration_inventory.go |
| +++ b/internal/host/rootgeneration_inventory.go |
| @@ -77,6 +77,9 @@ func (v generationReconciler) reconcile(ctx context.Context, records []release.G |
| if err := generationPayloadBindings(before.Payloads, installs); err != nil { |
| return zero, err |
| } |
| + if err := generationPayloadFiles(ctx, dataDir(), before.Payloads); err != nil { |
| + return zero, err |
| + } |
| matched := make([]bool, len(records)) |
| hosts := map[string]bool{} |
| for _, launch := range before.Launches { |
| @@ -174,6 +177,9 @@ func (v generationReconciler) reconcile(ctx context.Context, records []release.G |
| if snapshotDigest != generationInventoryDigest(after) { |
| return zero, os.ErrPermission |
| } |
| + if err := generationPayloadFiles(ctx, dataDir(), after.Payloads); err != nil { |
| + return zero, err |
| + } |
| return release.GenerationEvidence{Box: c.Box, Enrollment: c.Enrollment, Migration: c.Migration, Boot: boot, Installations: generationInventoryDigest([]any{before.Installations, before.Payloads}), Launches: generationInventoryDigest([]any{c.LaunchRevision, before.Launches}), Kernel: kernel}, nil |
| } |
diff --git a/internal/host/rootgeneration_payload_linux.go b/internal/host/rootgeneration_payload_linux.go| new file mode 100644 |
| index 000000000..659c81f97 |
| --- /dev/null |
| +++ b/internal/host/rootgeneration_payload_linux.go |
| @@ -0,0 +1,65 @@ |
| +//go:build linux |
| + |
| +package host |
| + |
| +import ( |
| + "context" |
| + "os" |
| + "path/filepath" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "golang.org/x/sys/unix" |
| +) |
| + |
| +// Physical identities only restrict the root inventory. Missing or intended |
| +// payloads remain unresolved; this reader never settles or rewrites them. |
| +func generationPayloadFiles(ctx context.Context, root string, payloads []hostbox.GenerationPayload) error { |
| + fd := -1 |
| + defer func() { |
| + if fd >= 0 { |
| + unix.Close(fd) |
| + } |
| + }() |
| + count := 0 |
| + for _, p := range payloads { |
| + for _, e := range p.Inventory.Entries { |
| + if err := ctx.Err(); err != nil { |
| + return err |
| + } |
| + count++ |
| + if count > 8192 { |
| + return os.ErrPermission |
| + } |
| + if e.Kind == "route" || e.Kind == "timer" { |
| + continue |
| + } |
| + if e.Kind != "path" || e.ManagedRoot != "data" || e.Materialization != "materialized" || !filepath.IsLocal(e.RelativePath) || filepath.Clean(e.RelativePath) != e.RelativePath || e.RelativePath == "." || e.Device == 0 || e.Inode == 0 || (e.Type != unix.S_IFDIR && e.Type != unix.S_IFREG) { |
| + return os.ErrPermission |
| + } |
| + if fd < 0 { |
| + if !filepath.IsAbs(root) || filepath.Clean(root) != root { |
| + return os.ErrPermission |
| + } |
| + var err error |
| + fd, err = unix.Openat2(unix.AT_FDCWD, root, &unix.OpenHow{Flags: unix.O_PATH | unix.O_DIRECTORY | unix.O_CLOEXEC, Resolve: unix.RESOLVE_NO_SYMLINKS}) |
| + if err != nil { |
| + return err |
| + } |
| + } |
| + file, err := unix.Openat2(fd, e.RelativePath, &unix.OpenHow{Flags: unix.O_PATH | unix.O_CLOEXEC, Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_SYMLINKS}) |
| + if err != nil { |
| + return err |
| + } |
| + var st unix.Stat_t |
| + err = unix.Fstat(file, &st) |
| + unix.Close(file) |
| + if err != nil { |
| + return err |
| + } |
| + if uint64(st.Dev) != e.Device || st.Ino != e.Inode || st.Mode&unix.S_IFMT != e.Type { |
| + return os.ErrPermission |
| + } |
| + } |
| + } |
| + return ctx.Err() |
| +} |
diff --git a/internal/host/rootgeneration_payload_other.go b/internal/host/rootgeneration_payload_other.go| new file mode 100644 |
| index 000000000..6b99784ac |
| --- /dev/null |
| +++ b/internal/host/rootgeneration_payload_other.go |
| @@ -0,0 +1,21 @@ |
| +//go:build !linux |
| + |
| +package host |
| + |
| +import ( |
| + "context" |
| + "os" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| +) |
| + |
| +func generationPayloadFiles(ctx context.Context, _ string, payloads []hostbox.GenerationPayload) error { |
| + for _, p := range payloads { |
| + for _, e := range p.Inventory.Entries { |
| + if e.Kind != "route" && e.Kind != "timer" { |
| + return os.ErrPermission |
| + } |
| + } |
| + } |
| + return ctx.Err() |
| +} |
diff --git a/internal/provision/plan14_e_write_census_test.go b/internal/provision/plan14_e_write_census_test.go| index ff7f42b5f..bfe363d28 100644 |
| --- a/internal/provision/plan14_e_write_census_test.go |
| +++ b/internal/provision/plan14_e_write_census_test.go |
| @@ -2,7 +2,7 @@ package provision |
| // Track E overlays the reviewed Part 1 write census without changing its baseline. |
| var plan14EWriteCensusOverlay = []censusEntry{ |
| - {"internal/host/rootgeneration_inventory.go", "generationReconciler.reconcile", "key-site", "key:RecordKey", "f5966ea6b565fa1f881537874a884d9fe7781e44460fde961613b38baaf54ee3"}, |
| + {"internal/host/rootgeneration_inventory.go", "generationReconciler.reconcile", "key-site", "key:RecordKey", "7c47e77af1cd3a71718006376a6ec130b2983dbcb94cebfda813883faac5b9dd"}, |
| {"internal/host/rootgenerations_launch.go", "rootGenerationSource.resolve", "key-site", "key:RecordKey", "d69a3c0c86b03d5469708ac8bf084f4f516c0b61a18519d46cc3c0a0616f5381"}, |
| {"internal/historyreadback/workflow.go", "Operation.deliverGrant", "nonrecord", "sel:Put", "fb410ef6274ef7de0997a804c837ad1ecf29d8826ba1c4dcda00c2ae986fc434"}, |
| {"internal/host/rootinventory.go", "ClaimRootHTTP.commitRootInventoryPending", "key-site", "key:RecordKey", "cbc0c1fa8be968c6dd18a42ff200fe5765f25d35c07ed3ece3b18ad4d619d64c"}, |
diff --git a/internal/provision/testdata/plan14-root-reads-e.tsv b/internal/provision/testdata/plan14-root-reads-e.tsv| index a7f4b3c4a..eaa7eef73 100644 |
| --- a/internal/provision/testdata/plan14-root-reads-e.tsv |
| +++ b/internal/provision/testdata/plan14-root-reads-e.tsv |
| @@ -54,7 +54,7 @@ internal/release/roothistory_replace.go replacementReconstruction authority E1 S |
| internal/release/signedhead.go StageSignedHead authority E1 Canonical route is supplied by the root unit verifier. Verifies signed channel, digest ratchet and manifest/artifact before staging; unavailable pin census never authorizes restaging differing bytes. Fixtures: TestPlan14Incident20261006WithLedger and TestPlan14SignedNamedRecoveryAuthority. registry:Account@202,registry:Account@302,registry:Account@403,registry:App@214,registry:App@415,registry:Channel@222,registry:Host@434 ca02f04e55556ba0233dfa0848b0c533a9d9aa06cad6abd33cfd2968b3bd838a |
| internal/rootstate/link_linux.go ReadTrustedLink authority A,E1 Checks the full root-owned parent chain before descriptor-based live-link reading. Fixture: TestPlan14TrustedLiveLink; privileged acceptance remains required. file:Open@156 90f8e8de4e2c71f07b5d749cbe4bb1c1dfa426576a82138823449f56fc9ec5ab |
| internal/rootstate/link_linux.go directory.readLink authority A,E1 Pins the symlink inode with O_PATH and checks owner/type/link count before readlinkat. Fixture: TestPlan14TrustedLiveLink. file:Fstat@356,file:Openat@183,file:Readlinkat@554 a8559fe63dce48830916004ecfe7fbbe5d419568c7daf7a3073cf1b5bb12c3d7 |
| -@coverage internal/host 5d14baaddbf17444552af2e28a1c98fa761ee81b2617e8a7db076fe961a18643 |
| +@coverage internal/host af1917704cc637141c53fe034777309b7af4b22f21b88677688b25ec67d7303a |
| @coverage internal/release 2bb546a86c1ab49b02e424eb0b366b46776857a1249baa2d6fb61ead4209d6bd |
| @coverage internal/rootstate 6338f74a6b2059cf77cf478bd6111b37b441eb9d65a309464c378359d4bdac05 |
| internal/release/roothistory_status.go RootHistory.MigrationStatus authority E1 Exact completion binds the accepted authorization digest from durable private history to enrolled-root signed readback. Pending originals, changed evidence and canonical sources cannot supply retirement completion. Fixtures: TestPlan14HistoryReconstructionCompletion, TestPlan14HistoryRetirementCompletion, TestPlan14ReadReconstructionCompletion, TestPlan14ReadRetirementCompletion, TestPlan14RecoveryRetirementProductionCallback, TestPlan14RootHistoryReconstructionCrash, TestPlan14HistoryRetirementProcessCrash. file:Read@1062,file:Read@3891 f0ab7ad2d2d70f8c22ebfd575f5a1c6931f7eba30ccdba80ad43d0efffc88154 |
| @@ -301,7 +301,7 @@ internal/host/rootgeneration_census_other.go productionGenerationKernelCensus au |
| internal/host/rootgeneration_inventory.go generationInstallation authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel tests are runner-pending. 3e5e15d50752510acb44b10eb29621757a87bff54ceae015f704dd7f3a848ecb |
| internal/host/rootgeneration_inventory.go generationInventoryDigest authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel tests are runner-pending. 36ff5a31c6a013ac907d85e59b754cf4088df4600646ba7df177cf14e990177a |
| internal/host/rootgeneration_inventory.go generationOwner authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel tests are runner-pending. 69f89153837761f13116937b330ffbf406891299d5580296ea4aade905aa02ce |
| -internal/host/rootgeneration_inventory.go generationReconciler.reconcile authority E2 E26: checked read-only payload enumeration includes inventory and revision orphans; completeness binds current and retained predecessor inventories, exact current revision and both source reads. Fixtures: TestPlan14GenerationPayloadCompleteness, TestPlan14GenerationPayloadRetainedPredecessor, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationPayloadInventory, TestPlan14GenerationPayloadReaderDoesNotInitialize. Checked root reader acceptance remains runner-pending; running-tenant containment remains unfinished. f5966ea6b565fa1f881537874a884d9fe7781e44460fde961613b38baaf54ee3 |
| +internal/host/rootgeneration_inventory.go generationReconciler.reconcile authority E2 E28: retained data payloads require exact device/inode/type through no-symlink descriptor opens before and after kernel inspection. Missing, replaced and intended payloads remain unresolved. Tests: TestPlan14GenerationPayloadPhysicalIdentity; privileged TestPlan14GenerationPayloadPhysicalRootRunner remains runner-pending. Route/timer and running-tenant containment are not accepted by this check. 7c47e77af1cd3a71718006376a6ec130b2983dbcb94cebfda813883faac5b9dd |
| internal/host/rootgeneration_inventory.go openGenerationLaunchInventory authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel tests are runner-pending. 522445e6d8acbe083d9d8f001719c4dbd3af57c56429e6b73b05c55a3826cb8b |
| internal/host/rootgeneration_inventory.go productionGenerationInventory authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel tests are runner-pending. 8c888114e1d40fa8d5496775a7dd136dc1c7a732c997369af61449b2a0f55a41 |
| internal/host/rootgeneration_inventory.go type generationInventory authority E2 E26: checked read-only payload enumeration includes inventory and revision orphans; completeness binds current and retained predecessor inventories, exact current revision and both source reads. Fixtures: TestPlan14GenerationPayloadCompleteness, TestPlan14GenerationPayloadRetainedPredecessor, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationPayloadInventory, TestPlan14GenerationPayloadReaderDoesNotInitialize. Checked root reader acceptance remains runner-pending; running-tenant containment remains unfinished. f8951e4663d33e2f28a6df8071ea87f571acadf9188978faf5b713855f98a94c |
| @@ -313,3 +313,5 @@ internal/host/rootgeneration_kernel_linux.go generationTaskFDs authority E2 E25: |
| internal/host/rootgeneration_inventory.go generationPayloadBindings authority E2 E26: checked read-only payload enumeration includes inventory and revision orphans; completeness binds current and retained predecessor inventories, exact current revision and both source reads. Fixtures: TestPlan14GenerationPayloadCompleteness, TestPlan14GenerationPayloadRetainedPredecessor, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationPayloadInventory, TestPlan14GenerationPayloadReaderDoesNotInitialize. Checked root reader acceptance remains runner-pending; running-tenant containment remains unfinished. 7f2f97393112ab29c308438a294deb306bdd6a4cd6df2c11cdce386ea3696095 |
| internal/hostbox/generationpayload.go LaunchJournal.GenerationPayloads authority E2 E26: checked read-only payload enumeration includes inventory and revision orphans; completeness binds current and retained predecessor inventories, exact current revision and both source reads. Fixtures: TestPlan14GenerationPayloadCompleteness, TestPlan14GenerationPayloadRetainedPredecessor, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationPayloadInventory, TestPlan14GenerationPayloadReaderDoesNotInitialize. Checked root reader acceptance remains runner-pending; running-tenant containment remains unfinished. file:QueryContext@272 c4aa5c5794f91bda7416b569e02bcd6003640b1256e5f8201aafc987fa681626 |
| internal/hostbox/generationpayload.go type GenerationPayload authority E2 E26: checked read-only payload enumeration includes inventory and revision orphans; completeness binds current and retained predecessor inventories, exact current revision and both source reads. Fixtures: TestPlan14GenerationPayloadCompleteness, TestPlan14GenerationPayloadRetainedPredecessor, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationPayloadInventory, TestPlan14GenerationPayloadReaderDoesNotInitialize. Checked root reader acceptance remains runner-pending; running-tenant containment remains unfinished. 992921ba14660af87a8099649ac589a7c2587467c05df052710fa45a9a5aa8bb |
| +internal/host/rootgeneration_payload_linux.go generationPayloadFiles authority E2 E28: retained data payloads require exact device/inode/type through no-symlink descriptor opens before and after kernel inspection. Missing, replaced and intended payloads remain unresolved. Tests: TestPlan14GenerationPayloadPhysicalIdentity; privileged TestPlan14GenerationPayloadPhysicalRootRunner remains runner-pending. Route/timer and running-tenant containment are not accepted by this check. file:Fstat@1333,file:Openat2@1120,file:Openat2@917 7344256ef22fb8dea05e47f70c05094008739c14ae0f95336573c4f23d3e1837 |
| +internal/host/rootgeneration_payload_other.go generationPayloadFiles authority E2 E28: retained data payloads require exact device/inode/type through no-symlink descriptor opens before and after kernel inspection. Missing, replaced and intended payloads remain unresolved. Tests: TestPlan14GenerationPayloadPhysicalIdentity; privileged TestPlan14GenerationPayloadPhysicalRootRunner remains runner-pending. Route/timer and running-tenant containment are not accepted by this check. candidate:Kind@183,candidate:Kind@204 182189d8409113d671cdc76fc5d2d73e7056057bca230d546e65300c428b2c18 |