carlos / platform Public

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 886a66ba0f9650dc51f6844085e153ddc0d16270 parent e3d87f7
7 files changed, +371 −3
  • internal/host/plan14_generation_payload_files_linux_test.go +109 −0
  • internal/host/plan14_generation_payload_root_linux_test.go +165 −0
  • internal/host/rootgeneration_inventory.go +6 −0
  • internal/host/rootgeneration_payload_linux.go +65 −0
  • internal/host/rootgeneration_payload_other.go +21 −0
  • internal/provision/plan14_e_write_census_test.go +1 −1
  • internal/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