WIP claim Plan 14 track E checkpoint 53 (not for merge)
Cross-boot identity for inventory, payloads, typed retirement and generation pins.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
39 files changed,
+1004
−116
internal/host/claimroleassembly_linux.go+8 −4internal/host/claimstop_writers_linux.go+5 −2internal/host/claimstop_writers_linux_test.go+1 −1internal/host/generationpins_reboot_root_linux_test.go+158 −0internal/host/inventory_identity_linux_test.go+65 −0internal/host/inventory_reboot_root_linux_test.go+293 −0internal/host/plan12_restore_linux_test.go+1 −1internal/host/plan14_generation_parked_linux_test.go+3 −3internal/host/plan14_generation_payload_files_linux_test.go+1 −1internal/host/plan14_generation_payload_root_linux_test.go+1 −1internal/host/plan14_generation_requirements_linux_test.go+1 −1internal/host/plan14_generation_requirements_root_linux_test.go+1 −1internal/host/plan9fix2_inventory_linux_test.go+2 −2internal/host/rootgeneration_parked.go+1 −1internal/host/rootgeneration_payload_linux.go+2 −27internal/host/rootinventory.go+31 −26internal/host/rootinventory_state_linux.go+7 −3internal/hostbox/coldhandover.go+1 −1internal/hostbox/coldhandover_test.go+1 −1internal/hostbox/inventory_identity.go+13 −0internal/hostbox/inventory_identity_linux.go+92 −0internal/hostbox/inventory_identity_other.go+10 −0internal/hostbox/inventory_identity_test.go+60 −0internal/hostbox/journalservice.go+17 −0internal/hostbox/plan14_cold_process_test.go+1 −1internal/hostbox/plan9fix4_inventory_linux_test.go+1 −1internal/hostbox/reconcile_adapter.go+1 −1internal/hostbox/stopinventory.go+4 −1internal/hostbox/typedpark_linux.go+25 −7internal/hostbox/typedretire.go+11 −9internal/provision/plan14_e_write_census_test.go+2 −1internal/provision/testdata/plan14-root-reads-e.tsv+33 −13internal/release/plan14_generation_boot_test.go+39 −0internal/release/plan14_generation_pins_test.go+6 −0internal/release/rootgenerationpins.go+37 −4internal/rootstate/authoritydir_linux.go+43 −0internal/rootstate/authoritydir_other.go+7 −0internal/rootstate/persistent_linux.go+17 −2internal/rootstate/persistent_other.go+2 −0
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 283.005s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 2.725s
ok github.com/carlosframework/platform/cmd/moveprobe 92.220s
ok github.com/carlosframework/platform/cmd/zddprobe 0.703s
ok github.com/carlosframework/platform/internal/activator 250.650s
ok github.com/carlosframework/platform/internal/agentsock 0.017s
ok github.com/carlosframework/platform/internal/alerting 0.004s
ok github.com/carlosframework/platform/internal/analytics 0.019s
ok github.com/carlosframework/platform/internal/appconfig 2.441s
ok github.com/carlosframework/platform/internal/apperr 0.077s
ok github.com/carlosframework/platform/internal/applog 0.123s
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.734s
? 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.010s
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.693s
ok github.com/carlosframework/platform/internal/changelog 0.313s
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 384.757s
? 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.356s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.008s
ok github.com/carlosframework/platform/internal/dbverify 0.004s
ok github.com/carlosframework/platform/internal/dnsrec 0.003s
ok github.com/carlosframework/platform/internal/docsite 0.005s
ok github.com/carlosframework/platform/internal/domains 2.349s
ok github.com/carlosframework/platform/internal/doorkey 0.006s
ok github.com/carlosframework/platform/internal/economics 0.003s
ok github.com/carlosframework/platform/internal/edge 26.670s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.008s
ok github.com/carlosframework/platform/internal/fleetapi 0.128s
ok github.com/carlosframework/platform/internal/gate 1.654s
ok github.com/carlosframework/platform/internal/genselect 0.015s
ok github.com/carlosframework/platform/internal/geo 0.018s
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 141.787s
ok github.com/carlosframework/platform/internal/hostactors 0.273s
ok github.com/carlosframework/platform/internal/hostbox 2.225s
ok github.com/carlosframework/platform/internal/hostclaim 0.497s
ok github.com/carlosframework/platform/internal/hostevidence 0.013s
ok github.com/carlosframework/platform/internal/hostexec 4.847s
ok github.com/carlosframework/platform/internal/hostfence 0.031s
ok github.com/carlosframework/platform/internal/hostfiles 10.606s
ok github.com/carlosframework/platform/internal/hostfloor 0.013s
ok github.com/carlosframework/platform/internal/hostguard 1.021s
ok github.com/carlosframework/platform/internal/hostops 1.507s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.013s
ok github.com/carlosframework/platform/internal/hostquarantine 0.013s
ok github.com/carlosframework/platform/internal/hostrelocate 0.425s
ok github.com/carlosframework/platform/internal/hostresolve 0.146s
ok github.com/carlosframework/platform/internal/hostwrite 0.013s
ok github.com/carlosframework/platform/internal/infraview 0.013s
ok github.com/carlosframework/platform/internal/instancectl 5.228s
ok github.com/carlosframework/platform/internal/instanceexec 2.731s
ok github.com/carlosframework/platform/internal/instancefs 0.013s
ok github.com/carlosframework/platform/internal/instancekey 0.015s
ok github.com/carlosframework/platform/internal/instanceupdates 0.019s
ok github.com/carlosframework/platform/internal/layout 1.496s
ok github.com/carlosframework/platform/internal/lease 9.797s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.005s
ok github.com/carlosframework/platform/internal/ledger 0.009s
ok github.com/carlosframework/platform/internal/mail 0.154s
ok github.com/carlosframework/platform/internal/maintenance 0.083s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.514s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.069s
ok github.com/carlosframework/platform/internal/metrics 0.005s
ok github.com/carlosframework/platform/internal/moves 20.950s
ok github.com/carlosframework/platform/internal/moves/archive 0.751s
ok github.com/carlosframework/platform/internal/objstore 0.178s
ok github.com/carlosframework/platform/internal/ops 0.171s
ok github.com/carlosframework/platform/internal/park 0.204s
ok github.com/carlosframework/platform/internal/privatefile 0.003s
ok github.com/carlosframework/platform/internal/provision 17.213s
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.456s
ok github.com/carlosframework/platform/internal/relay 7.223s
ok github.com/carlosframework/platform/internal/release 21.667s
ok github.com/carlosframework/platform/internal/rollout 3.736s
ok github.com/carlosframework/platform/internal/rolloutwords 0.003s
ok github.com/carlosframework/platform/internal/rootaws 0.006s
ok github.com/carlosframework/platform/internal/rootparticipation 0.006s
ok github.com/carlosframework/platform/internal/rootstate 0.026s
ok github.com/carlosframework/platform/internal/runs 0.569s
ok github.com/carlosframework/platform/internal/s3compat 0.005s
ok github.com/carlosframework/platform/internal/safefile 0.003s
ok github.com/carlosframework/platform/internal/schedule 2.494s
ok github.com/carlosframework/platform/internal/seeds 0.010s
ok github.com/carlosframework/platform/internal/sim/hostlife 9.450s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 4.036s
ok github.com/carlosframework/platform/internal/status 0.037s
ok github.com/carlosframework/platform/internal/steering 0.007s
ok github.com/carlosframework/platform/internal/stopevidence 0.198s
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.003s
ok github.com/carlosframework/platform/internal/tunnelclient 0.004s
ok github.com/carlosframework/platform/internal/unixdial 0.002s
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.001s
ok github.com/carlosframework/platform/internal/vet 1.545s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
linux/arm64 carlos binary: 47382690 bytes (ceiling 52000000)
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
minio: cached, digest verified
mc: cached, digest verified
minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
buckets and scoped user ready
=== RUN TestDeliveredContractMinio
=== RUN TestDeliveredContractMinio/put
=== RUN TestDeliveredContractMinio/get_round-trips_the_body
=== RUN TestDeliveredContractMinio/list_shows_the_key
=== RUN TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN TestDeliveredContractMinio/delete_removes_it
=== RUN TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
--- PASS: TestDeliveredContractMinio/put (0.00s)
--- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
--- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
--- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
--- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok github.com/carlosframework/platform/internal/objstore 0.022s
=== RUN TestT4StorageProbeMinio
2026/10/15 23:37:39 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DED83D55274F0A, 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.020s
minio contract: PASS
diff --git a/internal/host/claimroleassembly_linux.go b/internal/host/claimroleassembly_linux.go| index fdf59e3e2..92462c5c4 100644 |
| --- a/internal/host/claimroleassembly_linux.go |
| +++ b/internal/host/claimroleassembly_linux.go |
| @@ -148,7 +148,7 @@ func (a *ClaimRoleAssembly) Launch(ctx context.Context, o hostclaim.Observed, wo |
| if err = unix.Lstat(stateDir, &stat); err != nil { |
| return nil, err |
| } |
| - if stat.Mode&unix.S_IFMT != unix.S_IFDIR || uint64(stat.Dev) != entry.Device || stat.Ino != entry.Inode { |
| + if stat.Mode&unix.S_IFMT != unix.S_IFDIR || hostbox.VerifyInventoryIdentity(root, entry) != nil { |
| return nil, hostbox.ErrChanged |
| } |
| owned = true |
| @@ -398,7 +398,7 @@ func (a *ClaimRoleAssembly) PrepareState(ctx context.Context, o hostclaim.Observ |
| return hostbox.ErrTypedInventory |
| } |
| e := inventory.Entries[index] |
| - if e.Materialization != "materialized" || stat.Mode&unix.S_IFMT != unix.S_IFDIR || uint64(stat.Dev) != e.Device || stat.Ino != e.Inode { |
| + if e.Materialization != "materialized" || stat.Mode&unix.S_IFMT != unix.S_IFDIR || hostbox.VerifyInventoryIdentity(root, e) != nil { |
| return hostbox.ErrTypedInventory |
| } |
| return nil |
| @@ -427,8 +427,12 @@ func (a *ClaimRoleAssembly) PrepareState(ctx context.Context, o hostclaim.Observ |
| return err |
| } |
| entry := &inventory.Entries[index] |
| - entry.Device = uint64(stat.Dev) |
| - entry.Inode = stat.Ino |
| + entry.Identity, err = hostbox.CaptureInventoryIdentity(root, relative) |
| + if err != nil { |
| + return err |
| + } |
| + entry.Device = entry.Identity.Device |
| + entry.Inode = entry.Identity.Inode |
| entry.Type = unix.S_IFDIR |
| entry.Materialization = "materialized" |
| return a.Inventory.WriteStopInventory(ctx, inventory) |
diff --git a/internal/host/claimstop_writers_linux.go b/internal/host/claimstop_writers_linux.go| index 3ff206d34..4f0e47a9d 100644 |
| --- a/internal/host/claimstop_writers_linux.go |
| +++ b/internal/host/claimstop_writers_linux.go |
| @@ -210,6 +210,10 @@ func rootInstallationCoverage(managed map[string]string, host string, inv StopIn |
| return nil, nil, ErrClaimStopInventory |
| } |
| entry.Device, entry.Inode, entry.Materialization = uint64(st.Dev), st.Ino, "materialized" |
| + entry.Identity, err = hostbox.CaptureInventoryIdentity(managed["data"], rel) |
| + if err != nil { |
| + return nil, nil, err |
| + } |
| } else if !errors.Is(err, os.ErrNotExist) { |
| return nil, nil, err |
| } |
| @@ -260,8 +264,7 @@ func installationCoverage(managed map[string]string, inv StopInventory, dataOnly |
| if err != nil { |
| return nil, nil, err |
| } |
| - st, ok := info.Sys().(*syscall.Stat_t) |
| - if !ok || !info.IsDir() || uint64(st.Dev) != e.Device || st.Ino != e.Inode { |
| + if !info.IsDir() || hostbox.VerifyInventoryIdentity(root, e) != nil { |
| return nil, nil, fmt.Errorf("%w: %s is not the inventory's directory", ErrClaimStopInventory, path) |
| } |
| entries, err := os.ReadDir(path) |
diff --git a/internal/host/claimstop_writers_linux_test.go b/internal/host/claimstop_writers_linux_test.go| index 3c2059215..2f8d81d62 100644 |
| --- a/internal/host/claimstop_writers_linux_test.go |
| +++ b/internal/host/claimstop_writers_linux_test.go |
| @@ -188,7 +188,7 @@ func newWritersKernelFixture(t *testing.T) *writersKernelFixture { |
| } |
| sdev, sino := fileID(t, state) |
| fx.inv = StopInventory{Complete: true, Host: fx.target.Host, Incarnation: "inc-1", InstallationStamp: 11, Box: "box-1", Enrollment: "enrollment-1", |
| - Entries: []StopInventoryEntry{{Kind: "path", ManagedRoot: "data", RelativePath: "shop.example", Type: syscall.S_IFDIR, Device: sdev, Inode: sino, Materialization: "materialized"}}} |
| + Entries: []StopInventoryEntry{{Kind: "path", ManagedRoot: "data", RelativePath: "shop.example", Identity: inventoryIdentityFixture(t, fx.dataRoot, "shop.example"), Type: syscall.S_IFDIR, Device: sdev, Inode: sino, Materialization: "materialized"}}} |
| fx.holdDB(t, 20, 7) |
| dev, ino := fileID(t, fx.db) |
| fx.hints = []stopevidence.DescriptorHint{{FD: 7, Device: dev, Inode: ino}} |
diff --git a/internal/host/generationpins_reboot_root_linux_test.go b/internal/host/generationpins_reboot_root_linux_test.go| new file mode 100644 |
| index 000000000..b9f244992 |
| --- /dev/null |
| +++ b/internal/host/generationpins_reboot_root_linux_test.go |
| @@ -0,0 +1,158 @@ |
| +//go:build linux && rootrunner |
| + |
| +package host |
| + |
| +import ( |
| + "context" |
| + "crypto/sha256" |
| + "encoding/hex" |
| + "encoding/json" |
| + "os" |
| + "path/filepath" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/release" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| + "golang.org/x/sys/unix" |
| +) |
| + |
| +const generationPinRebootRoot = "/var/lib/carlos-e53-pins" |
| + |
| +func TestRootRebootGenerationPinIdentityPhase1(t *testing.T) { |
| + if os.Geteuid() != 0 { |
| + t.Fatal("requires root") |
| + } |
| + if err := os.Mkdir(generationPinRebootRoot, 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + path := release.VersionPath(filepath.Join(generationPinRebootRoot, "opt"), "bab", "v1", "app") |
| + tree := filepath.Dir(path) |
| + if err := os.MkdirAll(tree, 0755); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(path, []byte("app"), 0755); err != nil { |
| + t.Fatal(err) |
| + } |
| + capture := func(path string, dir bool) rootstate.FileIdentity { |
| + var f *os.File |
| + var err error |
| + if dir { |
| + f, err = rootstate.OpenAuthorityDirectory(path) |
| + } else { |
| + f, err = rootstate.OpenAuthorityFile(path) |
| + } |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer f.Close() |
| + id, err := rootstate.PersistentIdentity(f) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + return id |
| + } |
| + exe, parent := capture(path, false), capture(tree, true) |
| + // Force boot-local renumbering even when this runner reuses device numbers. |
| + exe.Device, parent.Device = exe.Device^0x100000, parent.Device^0x100000 |
| + exe.Mount, parent.Mount = exe.Mount+1000, parent.Mount+1000 |
| + digest := sha256.Sum256([]byte("app")) |
| + b := release.GenerationBinding{Binding: release.HistoryBinding{Host: "inventory.example", Account: "bab", App: "app"}, Installation: "installation", Record: "record", RecordETag: "etag", Attempt: "main", Inputs: "inputs", Invocation: "invocation", ControllerPID: os.Getpid(), ControllerStart: "fixture", Version: "v1", Digest: hex.EncodeToString(digest[:]), Path: path, Tree: tree, Executable: rootstate.FileIdentity{Device: exe.Device, Inode: exe.Inode}, TreeIdentity: rootstate.FileIdentity{Device: parent.Device, Inode: parent.Inode}, ExecutableProof: exe, TreeProof: parent, Target: release.GenerationOwner{Host: "inventory.example", Incarnation: "inc", Stamp: 1, Session: 1, Box: "box", Enrollment: "enrollment", BootID: exe.Boot, ControllerID: "fixture"}} |
| + state, err := rootstate.Open(filepath.Join(generationPinRebootRoot, "generations"), true, false) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer state.Close() |
| + if _, err := (&release.RootGenerations{Store: state}).Reserve(b); err != nil { |
| + t.Fatal(err) |
| + } |
| + raw, err := json.Marshal(b) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := writeFileDurable(filepath.Join(generationPinRebootRoot, "binding.json"), raw, 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + t.Logf("generation saved boot=%s device=%d mount=%d", exe.Boot, exe.Device, exe.Mount) |
| +} |
| + |
| +func TestRootRebootGenerationPinIdentityPhase2(t *testing.T) { |
| + if os.Geteuid() != 0 { |
| + t.Fatal("requires root") |
| + } |
| + defer os.RemoveAll(generationPinRebootRoot) |
| + raw, err := os.ReadFile(filepath.Join(generationPinRebootRoot, "binding.json")) |
| + var b release.GenerationBinding |
| + if err != nil || json.Unmarshal(raw, &b) != nil { |
| + t.Fatal("binding", err) |
| + } |
| + current, err := claimUnitBoot() |
| + if err != nil || current == b.Target.BootID { |
| + t.Fatal("requires actual reboot", err) |
| + } |
| + state, err := rootstate.Open(filepath.Join(generationPinRebootRoot, "generations"), false, false) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer state.Close() |
| + ledger := &release.RootGenerations{Store: state} |
| + // Completeness is supplied; persisted file and directory pin checks are real. |
| + source := release.RootGenerationPins{Ledger: ledger, Verify: func(context.Context, []release.GenerationReservation) error { return nil }} |
| + check := func() error { |
| + pins, err := source.StagedPins(t.Context(), filepath.Join(generationPinRebootRoot, "opt")) |
| + if err != nil { |
| + return err |
| + } |
| + held, err := pins.Refers(b.Path) |
| + if err != nil { |
| + return err |
| + } |
| + if !held { |
| + return os.ErrPermission |
| + } |
| + return nil |
| + } |
| + if err := check(); err != nil { |
| + t.Fatal("cross-boot staged pin", err) |
| + } |
| + if err := unix.Mount(b.Tree, b.Tree, "", unix.MS_BIND, ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := check(); err == nil { |
| + t.Error("staged pin accepted substituted mount") |
| + } |
| + if err := unix.Unmount(b.Tree, 0); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Rename(b.Path, b.Path+".saved"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(b.Path, []byte("replacement"), 0755); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := check(); err == nil { |
| + t.Error("staged pin accepted replacement") |
| + } |
| + if err := os.Remove(b.Path); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Rename(b.Path+".saved", b.Path); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Chown(filepath.Join(generationPinRebootRoot, "opt"), 65534, 65534); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := check(); err == nil { |
| + t.Error("staged pin accepted untrusted ancestry") |
| + } |
| + if err := os.Chown(filepath.Join(generationPinRebootRoot, "opt"), 0, 0); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := check(); err != nil { |
| + t.Fatal("restored staged pin", err) |
| + } |
| + saved, err := ledger.Read(b) |
| + if err != nil || saved.Binding != b { |
| + t.Fatal("rewrote old generation identity", err) |
| + } |
| + t.Logf("staged pin retained across boot %s -> %s", b.Target.BootID, current) |
| +} |
diff --git a/internal/host/inventory_identity_linux_test.go b/internal/host/inventory_identity_linux_test.go| new file mode 100644 |
| index 000000000..4a597e970 |
| --- /dev/null |
| +++ b/internal/host/inventory_identity_linux_test.go |
| @@ -0,0 +1,65 @@ |
| +//go:build linux |
| + |
| +package host |
| + |
| +import ( |
| + "os" |
| + "path/filepath" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| + "golang.org/x/sys/unix" |
| +) |
| + |
| +func TestPlan14InventoryCapturePhysicalTypes(t *testing.T) { |
| + root := t.TempDir() |
| + path := filepath.Join(root, "payload") |
| + if err := unix.Mkfifo(path, 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := hostbox.CaptureInventoryIdentity(root, "payload"); err == nil { |
| + t.Fatal("captured fifo as inventory payload") |
| + } |
| + if err := os.Remove(path); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(path, []byte("payload"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Link(path, filepath.Join(root, "alias")); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := hostbox.CaptureInventoryIdentity(root, "payload"); err == nil { |
| + t.Fatal("captured multiply-linked inventory payload") |
| + } |
| +} |
| + |
| +func TestPlan14InventoryCaptureCannotClaimOldBoot(t *testing.T) { |
| + root := t.TempDir() |
| + if err := os.Mkdir(filepath.Join(root, "state"), 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + id := inventoryIdentityFixture(t, root, "state") |
| + e := hostbox.StopInventoryEntry{ArtifactID: "state", Kind: "path", ManagedRoot: "data", RelativePath: "state", Materialization: "materialized", Identity: id, Device: id.Device, Inode: id.Inode, Type: id.Mode & 0170000} |
| + h := ClaimRootHTTP{Roots: map[string]string{"data": root}} |
| + for _, prior := range []hostbox.StopInventory{{}, {Entries: []hostbox.StopInventoryEntry{{ArtifactID: "state", Materialization: "intended"}}}} { |
| + if err := h.verifyNewInventoryIdentities(prior, hostbox.StopInventory{Entries: []hostbox.StopInventoryEntry{e}}); err != nil { |
| + t.Fatal(err) |
| + } |
| + forged := e |
| + forged.Identity.Boot = "earlier-boot" |
| + if err := h.verifyNewInventoryIdentities(prior, hostbox.StopInventory{Entries: []hostbox.StopInventoryEntry{forged}}); err == nil { |
| + t.Fatal("accepted caller's cross-boot capture") |
| + } |
| + } |
| +} |
| + |
| +func inventoryIdentityFixture(t *testing.T, root, relative string) rootstate.FileIdentity { |
| + t.Helper() |
| + id, err := hostbox.CaptureInventoryIdentity(root, relative) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + return id |
| +} |
diff --git a/internal/host/inventory_reboot_root_linux_test.go b/internal/host/inventory_reboot_root_linux_test.go| new file mode 100644 |
| index 000000000..74f99c4bd |
| --- /dev/null |
| +++ b/internal/host/inventory_reboot_root_linux_test.go |
| @@ -0,0 +1,293 @@ |
| +//go:build linux && rootrunner |
| + |
| +package host |
| + |
| +import ( |
| + "context" |
| + "crypto/sha256" |
| + "encoding/hex" |
| + "encoding/json" |
| + "errors" |
| + "fmt" |
| + "os" |
| + "path/filepath" |
| + "reflect" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostguard" |
| + "golang.org/x/sys/unix" |
| +) |
| + |
| +const inventoryRebootRoot = "/var/lib/carlos-e53-inventory" |
| + |
| +type inventoryRebootFixture struct { |
| + Inventory hostbox.StopInventory |
| + File, Link, Retry hostbox.Artifact |
| + Journal hostbox.RetirementJournal |
| +} |
| + |
| +func inventoryRebootParker() hostbox.TypedParker { |
| + return hostbox.TypedParker{Roots: map[string]string{"data": inventoryRebootRoot}, ParkedRoot: inventoryRebootRoot, Incarnation: "inc", Host: "inventory.example", Retirement: "retire"} |
| +} |
| + |
| +func TestRootRebootInventoryIdentityPhase1(t *testing.T) { |
| + if os.Geteuid() != 0 { |
| + t.Fatal("requires root") |
| + } |
| + if err := os.Mkdir(inventoryRebootRoot, 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, dir := range []string{"instances", "instances/inventory.example"} { |
| + if err := os.Mkdir(filepath.Join(inventoryRebootRoot, dir), 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + for _, name := range []string{"db", "retry"} { |
| + if err := os.WriteFile(filepath.Join(inventoryRebootRoot, name), []byte(name), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + if err := os.Symlink("db", filepath.Join(inventoryRebootRoot, "live")); err != nil { |
| + t.Fatal(err) |
| + } |
| + p := inventoryRebootParker() |
| + f := inventoryRebootFixture{Inventory: hostbox.StopInventory{Complete: true, Host: "inventory.example", Incarnation: "inc", InstallationStamp: 1, Box: "box", Enrollment: "enrollment", DirectoryRevision: "1"}} |
| + for _, v := range []struct { |
| + name string |
| + kind hostbox.ArtifactKind |
| + out *hostbox.Artifact |
| + }{{"db", hostbox.ArtifactPath, &f.File}, {"live", hostbox.ArtifactLiveLink, &f.Link}, {"retry", hostbox.ArtifactPath, &f.Retry}} { |
| + var err error |
| + *v.out, err = p.CaptureArtifact("data", v.name, "owner", v.kind) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if v.out.Identity.Handle == "" { |
| + t.Fatal("persistent handle required on runner") |
| + } |
| + } |
| + // A lost reply after rename must verify the destination after reboot. |
| + if err := p.Park(t.Context(), "retire", f.Retry); err != nil { |
| + t.Fatal(err) |
| + } |
| + id, err := hostbox.CaptureInventoryIdentity(inventoryRebootRoot, "instances/inventory.example") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + owner := sha256.Sum256([]byte(fmt.Sprintf("%s\x00%s\x00%d\x00%s", f.Inventory.Host, f.Inventory.Incarnation, f.Inventory.InstallationStamp, "box"))) |
| + f.Inventory.Entries = []hostbox.StopInventoryEntry{{Kind: "path", ManagedRoot: "data", RelativePath: "instances/inventory.example", ArtifactID: "state", Ownership: hex.EncodeToString(owner[:]), Materialization: "materialized", Device: id.Device, Inode: id.Inode, Type: unix.S_IFDIR, Identity: id}} |
| + db, err := hostbox.OpenStopInventoryDB(filepath.Join(inventoryRebootRoot, "inventory.db")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := db.WriteStopInventory(t.Context(), f.Inventory); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := db.Close(); err != nil { |
| + t.Fatal(err) |
| + } |
| + f.Journal, err = hostbox.NewRetirementJournal("retire", hostbox.RetirementInventory{Host: "inventory.example", Captured: hostguard.Captured{Incarnation: "inc", Stamp: 1}, Ownership: "owner", Artifacts: []hostbox.Artifact{f.File, f.Link, f.Retry}}) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + target := hostguard.FenceTarget{Host: "inventory.example", Incarnation: "inc", Stamp: 1} |
| + retirement := inventoryRebootRetirement(p) |
| + retirement.Filesystem = inventoryRebootInterrupt{p} |
| + if err := retirement.Run(t.Context(), target, f.Journal); !errors.Is(err, os.ErrClosed) { |
| + t.Fatal("retirement interruption", err) |
| + } |
| + raw, err := json.Marshal(f) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := writeFileDurable(filepath.Join(inventoryRebootRoot, "fixture.json"), raw, 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + t.Logf("saved boot=%s device=%d mount=%d fs=%s", id.Boot, id.Device, id.Mount, id.Filesystem) |
| +} |
| + |
| +func TestRootRebootInventoryIdentityPhase2(t *testing.T) { |
| + if os.Geteuid() != 0 { |
| + t.Fatal("requires root") |
| + } |
| + defer os.RemoveAll(inventoryRebootRoot) |
| + raw, err := os.ReadFile(filepath.Join(inventoryRebootRoot, "fixture.json")) |
| + var f inventoryRebootFixture |
| + if err != nil || json.Unmarshal(raw, &f) != nil { |
| + t.Fatal("load", err) |
| + } |
| + db, err := hostbox.OpenStopInventoryDB(filepath.Join(inventoryRebootRoot, "inventory.db")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer db.Close() |
| + snapshot, revision, err := db.Snapshot(t.Context(), f.Inventory.Host, hostguard.Captured{Incarnation: "inc", Stamp: 1}) |
| + if err != nil || revision != 1 || !reflect.DeepEqual(snapshot, f.Inventory) { |
| + t.Fatal("persistent inventory snapshot", err) |
| + } |
| + reader, err := hostbox.OpenGenerationLaunchReader(filepath.Join(inventoryRebootRoot, "inventory.db")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer reader.Close() |
| + payloads, err := reader.GenerationPayloads(t.Context()) |
| + if err != nil || len(payloads) != 1 || !reflect.DeepEqual(payloads[0].Inventory, f.Inventory) { |
| + t.Fatal("persistent payload", err) |
| + } |
| + e := f.Inventory.Entries[0] |
| + now, err := hostbox.CaptureInventoryIdentity(inventoryRebootRoot, e.RelativePath) |
| + if err != nil || now.Boot == e.Identity.Boot { |
| + t.Fatal("requires actual reboot", err) |
| + } |
| + t.Logf("new boot=%s device=%d mount=%d fs=%s", now.Boot, now.Device, now.Mount, now.Filesystem) |
| + // Force renumbered values too, so a runner retaining its numbers cannot |
| + // accidentally make a raw-device comparison pass this acceptance. |
| + e.Device += 1000 |
| + e.Identity.Device = e.Device |
| + e.Identity.Mount += 1000 |
| + f.Inventory.Entries[0] = e |
| + h := ClaimRootHTTP{Roots: map[string]string{"data": inventoryRebootRoot}} |
| + a := ClaimRoleAssembly{ManagedRoots: h.Roots} |
| + check := func() error { |
| + if err := h.verifyInventoryPath(e); err != nil { |
| + return fmt.Errorf("root inventory: %w", err) |
| + } |
| + if _, _, err := installationCoverage(h.Roots, f.Inventory, false); err != nil { |
| + return fmt.Errorf("stop coverage: %w", err) |
| + } |
| + if err := generationPayloadFiles(t.Context(), inventoryRebootRoot, []hostbox.GenerationPayload{{Inventory: f.Inventory, Revision: 1}}); err != nil { |
| + return fmt.Errorf("payload: %w", err) |
| + } |
| + changed, err := a.prepareStateInventory(ClaimLaunchInputs{DBPath: filepath.Join(inventoryRebootRoot, e.RelativePath, "app.db")}, &f.Inventory, hostbox.Actor{Box: "box"}) |
| + if changed { |
| + return fmt.Errorf("rewrote retained inventory") |
| + } |
| + return err |
| + } |
| + if err := check(); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := h.verifyNewInventoryIdentities(hostbox.StopInventory{}, f.Inventory); err == nil { |
| + t.Error("old-boot evidence accepted as new inventory") |
| + } |
| + path := filepath.Join(inventoryRebootRoot, e.RelativePath) |
| + if err := unix.Mount(path, path, "", unix.MS_BIND, ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := check(); err == nil { |
| + t.Error("accepted substituted mount") |
| + } |
| + if err := unix.Unmount(path, 0); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Chown(path, 65534, 65534); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := check(); err == nil { |
| + t.Error("accepted changed ownership") |
| + } |
| + if err := os.Chown(path, int(e.Identity.UID), int(e.Identity.GID)); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := unix.Mount("tmpfs", path, "tmpfs", 0, "mode=0700"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := check(); err == nil { |
| + t.Error("accepted different filesystem") |
| + } |
| + if err := unix.Unmount(path, 0); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := check(); err != nil { |
| + t.Fatal("restored original", err) |
| + } |
| + if err := os.Chown(inventoryRebootRoot, 65534, 65534); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := check(); err == nil { |
| + t.Error("accepted untrusted root ancestry") |
| + } |
| + if err := os.Chown(inventoryRebootRoot, 0, 0); err != nil { |
| + t.Fatal(err) |
| + } |
| + |
| + p := inventoryRebootParker() |
| + for _, v := range []*hostbox.Artifact{&f.File, &f.Link, &f.Retry} { |
| + v.Dev += 1000 |
| + v.Identity.Device = v.Dev |
| + v.Identity.Mount += 1000 |
| + } |
| + // Existing-file replacement must survive refused retirement. |
| + if err := os.Rename(filepath.Join(inventoryRebootRoot, "db"), filepath.Join(inventoryRebootRoot, "saved")); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(filepath.Join(inventoryRebootRoot, "db"), []byte("replacement"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := p.Park(t.Context(), "retire", f.File); err == nil { |
| + t.Fatal("parked replacement") |
| + } |
| + if raw, err := os.ReadFile(filepath.Join(inventoryRebootRoot, "db")); err != nil || string(raw) != "replacement" { |
| + t.Fatal("replacement changed", err) |
| + } |
| + if err := os.Remove(filepath.Join(inventoryRebootRoot, "db")); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Rename(filepath.Join(inventoryRebootRoot, "saved"), filepath.Join(inventoryRebootRoot, "db")); err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, v := range []hostbox.Artifact{f.File, f.Link, f.Retry} { |
| + if err := p.Park(context.Background(), "retire", v); err != nil { |
| + t.Fatalf("park %s: %v", v.Name, err) |
| + } |
| + if err := p.Park(context.Background(), "retire", v); err != nil { |
| + t.Fatalf("replay %s: %v", v.Name, err) |
| + } |
| + } |
| + retirement := inventoryRebootRetirement(p) |
| + target := hostguard.FenceTarget{Host: "inventory.example", Incarnation: "inc", Stamp: 1} |
| + if err := retirement.Run(t.Context(), target, f.Journal); err != nil { |
| + t.Fatal("durable retirement recovery", err) |
| + } |
| + if err := retirement.Run(t.Context(), target, f.Journal); err != nil { |
| + t.Fatal("durable retirement replay", err) |
| + } |
| + if target, err := os.Readlink(filepath.Join(inventoryRebootRoot, ".parked/inc/inventory.example/retire/data/live")); err != nil || target != "db" { |
| + t.Fatal("link changed", err) |
| + } |
| + if retained, err := os.ReadFile(filepath.Join(inventoryRebootRoot, "fixture.json")); err != nil || string(retained) != string(raw) { |
| + t.Fatal("rewrote durable evidence", err) |
| + } |
| +} |
| + |
| +// Cleanup authority is a fixture; persistence, inode checks and journal replay |
| +// use the production implementations across the real reboot. |
| +type inventoryRebootAuthority struct{} |
| + |
| +func (inventoryRebootAuthority) WithCleanup(_ context.Context, _ hostguard.FenceTarget, fn func() error) error { |
| + return fn() |
| +} |
| + |
| +type inventoryRebootRows struct{} |
| + |
| +func (inventoryRebootRows) RetireRows(context.Context, string, []hostbox.Artifact) error { |
| + return os.ErrPermission |
| +} |
| + |
| +type inventoryRebootInterrupt struct{ parker hostbox.TypedParker } |
| + |
| +func (p inventoryRebootInterrupt) Park(ctx context.Context, id string, a hostbox.Artifact) error { |
| + if a.Name != "retry" { |
| + return os.ErrClosed |
| + } |
| + return p.parker.Park(ctx, id, a) |
| +} |
| +func inventoryRebootRetirement(p hostbox.TypedParker) hostbox.TypedRetirement { |
| + return hostbox.TypedRetirement{JournalDir: filepath.Join(inventoryRebootRoot, "retirements"), Authority: inventoryRebootAuthority{}, Filesystem: p, Rows: inventoryRebootRows{}, Ack: func(_ context.Context, j hostbox.RetirementJournal) error { |
| + if !j.Complete { |
| + return os.ErrPermission |
| + } |
| + return nil |
| + }} |
| +} |
diff --git a/internal/host/plan12_restore_linux_test.go b/internal/host/plan12_restore_linux_test.go| index ef479bea6..600cadb06 100644 |
| --- a/internal/host/plan12_restore_linux_test.go |
| +++ b/internal/host/plan12_restore_linux_test.go |
| @@ -147,7 +147,7 @@ func TestPlan12RecoveryRestoreSourceDatabase(t *testing.T) { |
| if e = unix.Lstat(filepath.Dir(db), &stat); e != nil { |
| t.Fatal(e) |
| } |
| - inv.Entries = append(inv.Entries, hostbox.StopInventoryEntry{Kind: "path", ArtifactID: "state", ManagedRoot: "data", RelativePath: dirRel, Ownership: "old-owner", Device: uint64(stat.Dev), Inode: stat.Ino, Type: unix.S_IFDIR, Materialization: "materialized"}) |
| + inv.Entries = append(inv.Entries, hostbox.StopInventoryEntry{Identity: inventoryIdentityFixture(t, f.guard.DataDir, dirRel), Kind: "path", ArtifactID: "state", ManagedRoot: "data", RelativePath: dirRel, Ownership: "old-owner", Device: uint64(stat.Dev), Inode: stat.Ino, Type: unix.S_IFDIR, Materialization: "materialized"}) |
| if e = h.Inventory.WriteStopInventory(t.Context(), inv); e != nil { |
| t.Fatal(e) |
| } |
diff --git a/internal/host/plan14_generation_parked_linux_test.go b/internal/host/plan14_generation_parked_linux_test.go| index eec9a6032..248581263 100644 |
| --- a/internal/host/plan14_generation_parked_linux_test.go |
| +++ b/internal/host/plan14_generation_parked_linux_test.go |
| @@ -42,8 +42,8 @@ func generationParkedFixture(t *testing.T) (generationInventory, hostbox.Retirem |
| if err := unix.Stat(path, &st); err != nil { |
| t.Fatal(err) |
| } |
| - in.Entries = append(in.Entries, hostbox.StopInventoryEntry{Kind: "path", ArtifactID: "state", ManagedRoot: "data", RelativePath: "instances/" + host, Ownership: "owner", Materialization: "materialized", Device: uint64(st.Dev), Inode: st.Ino, Type: unix.S_IFDIR}) |
| - target := hostguard.FenceTarget{Host: host, Incarnation: in.Incarnation, Stamp: 1, Session: 1, Box: in.Box, Enrollment: in.Enrollment, DirectoryRevision: in.DirectoryRevision, FenceID: "retire", BootID: "boot", ControllerID: "controller", Op: "op", Phase: "retiring", ObligationID: "park"} |
| + in.Entries = append(in.Entries, hostbox.StopInventoryEntry{Identity: inventoryIdentityFixture(t, root, "instances/"+host), Kind: "path", ArtifactID: "state", ManagedRoot: "data", RelativePath: "instances/" + host, Ownership: "owner", Materialization: "materialized", Device: uint64(st.Dev), Inode: st.Ino, Type: unix.S_IFDIR}) |
| + target := hostguard.FenceTarget{Host: host, Incarnation: in.Incarnation, Stamp: 1, Session: 1, Box: in.Box, Enrollment: in.Enrollment, DirectoryRevision: in.DirectoryRevision, FenceID: "retire", BootID: in.Entries[len(in.Entries)-1].Identity.Boot, ControllerID: "controller", Op: "op", Phase: "retiring", ObligationID: "park"} |
| receipt := StopReceipt{V: 1, ID: "receipt", Kind: "stopped", Host: host, Incarnation: target.Incarnation, InstallationStamp: target.Stamp, WriterSession: target.Session, Box: target.Box, Enrollment: target.Enrollment, DirectoryRevision: target.DirectoryRevision, FenceID: target.FenceID, BootID: target.BootID, ControllerID: target.ControllerID, OperationID: target.Op, Phase: target.Phase, ObligationID: target.ObligationID, EvidenceAt: time.Unix(100, 0).UTC()} |
| raw, err := json.Marshal(receipt) |
| if err != nil { |
| @@ -51,7 +51,7 @@ func generationParkedFixture(t *testing.T) (generationInventory, hostbox.Retirem |
| } |
| inventory := hostbox.RetirementInventory{Host: host, Captured: hostguard.Captured{Incarnation: in.Incarnation, Stamp: 1}, Ownership: "owner"} |
| for _, e := range in.Entries { |
| - inventory.Artifacts = append(inventory.Artifacts, hostbox.Artifact{Kind: hostbox.ArtifactKind(e.Kind), Root: e.ManagedRoot, Name: e.RelativePath, Owner: e.Ownership, RowID: e.RowIdentity, Dev: e.Device, Ino: e.Inode, Type: e.Type}) |
| + inventory.Artifacts = append(inventory.Artifacts, hostbox.Artifact{Kind: hostbox.ArtifactKind(e.Kind), Root: e.ManagedRoot, Name: e.RelativePath, Owner: e.Ownership, RowID: e.RowIdentity, Identity: e.Identity, Dev: e.Device, Ino: e.Inode, Type: e.Type}) |
| } |
| j, err := hostbox.NewRetirementJournal(target.FenceID, inventory) |
| if err != nil { |
diff --git a/internal/host/plan14_generation_payload_files_linux_test.go b/internal/host/plan14_generation_payload_files_linux_test.go| index ce445c3eb..f65c822ad 100644 |
| --- a/internal/host/plan14_generation_payload_files_linux_test.go |
| +++ b/internal/host/plan14_generation_payload_files_linux_test.go |
| @@ -49,7 +49,7 @@ func TestPlan14GenerationPayloadPhysicalIdentity(t *testing.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} |
| + entry := hostbox.StopInventoryEntry{Identity: inventoryIdentityFixture(t, dir, "state/retained"), 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 { |
diff --git a/internal/host/plan14_generation_payload_root_linux_test.go b/internal/host/plan14_generation_payload_root_linux_test.go| index 612eef835..3d3209d09 100644 |
| --- a/internal/host/plan14_generation_payload_root_linux_test.go |
| +++ b/internal/host/plan14_generation_payload_root_linux_test.go |
| @@ -83,7 +83,7 @@ func TestPlan14GenerationPayloadPhysicalRootRunner(t *testing.T) { |
| 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}} |
| + payload.Entries = []hostbox.StopInventoryEntry{{Identity: inventoryIdentityFixture(t, dataDir(), "retained"), 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() |
diff --git a/internal/host/plan14_generation_requirements_linux_test.go b/internal/host/plan14_generation_requirements_linux_test.go| index 43dbde977..cd88581a9 100644 |
| --- a/internal/host/plan14_generation_requirements_linux_test.go |
| +++ b/internal/host/plan14_generation_requirements_linux_test.go |
| @@ -62,7 +62,7 @@ func plan14GenerationRequiredArtifacts(t *testing.T, local bool) { |
| 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)} |
| - entry := hostbox.StopInventoryEntry{Kind: "path", ArtifactID: "state", ManagedRoot: "data", RelativePath: rel, Ownership: "owner", Materialization: "materialized", Device: uint64(st.Dev), Inode: st.Ino, Type: unix.S_IFDIR} |
| + entry := hostbox.StopInventoryEntry{Identity: inventoryIdentityFixture(t, dataDir(), rel), Kind: "path", ArtifactID: "state", ManagedRoot: "data", RelativePath: rel, Ownership: "owner", Materialization: "materialized", Device: uint64(st.Dev), Inode: st.Ino, Type: unix.S_IFDIR} |
| snapshot.Payloads[0].Inventory.Entries = append(snapshot.Payloads[0].Inventory.Entries, entry) |
| store := f.root.generations.store |
| rec.Schema = 1 |
diff --git a/internal/host/plan14_generation_requirements_root_linux_test.go b/internal/host/plan14_generation_requirements_root_linux_test.go| index 12ad48166..910c47b65 100644 |
| --- a/internal/host/plan14_generation_requirements_root_linux_test.go |
| +++ b/internal/host/plan14_generation_requirements_root_linux_test.go |
| @@ -159,7 +159,7 @@ func plan14RequiredArtifactsRootRunner(t *testing.T, local bool) { |
| } |
| payload := hostbox.StopInventory{Complete: true, Host: in.Host, Incarnation: inc, InstallationStamp: 2, Box: in.Box, Enrollment: in.Enrollment, DirectoryRevision: "1", Entries: []hostbox.StopInventoryEntry{ |
| {Kind: "route", ArtifactID: "route", ManagedRoot: "registry", RelativePath: in.Host, RowIdentity: "row", Ownership: "owner", Materialization: "materialized"}, |
| - {Kind: "path", ArtifactID: "state", ManagedRoot: "data", RelativePath: rel, Ownership: "owner", Materialization: "materialized", Device: uint64(st.Dev), Inode: st.Ino, Type: unix.S_IFDIR}, |
| + {Identity: inventoryIdentityFixture(t, dataDir(), rel), Kind: "path", ArtifactID: "state", ManagedRoot: "data", RelativePath: rel, Ownership: "owner", Materialization: "materialized", Device: uint64(st.Dev), Inode: st.Ino, Type: unix.S_IFDIR}, |
| }} |
| journal, err := hostbox.OpenLaunchJournal("/var/lib/carlos-claims/journals.db") |
| if err != nil { |
diff --git a/internal/host/plan9fix2_inventory_linux_test.go b/internal/host/plan9fix2_inventory_linux_test.go| index aab0fe7ce..de66f0a90 100644 |
| --- a/internal/host/plan9fix2_inventory_linux_test.go |
| +++ b/internal/host/plan9fix2_inventory_linux_test.go |
| @@ -52,7 +52,7 @@ func TestR2InventoryNamesForeignArtifact(t *testing.T) { |
| if err != nil { |
| t.Fatal(err) |
| } |
| - in.Entries = append(in.Entries, hostbox.StopInventoryEntry{Kind: "path", ArtifactID: "foreign", ManagedRoot: rootName, RelativePath: name, Ownership: row.ClaimOwner, Materialization: "materialized", Device: uint64(st.Dev), Inode: st.Ino, Type: st.Mode & syscall.S_IFMT}) |
| + in.Entries = append(in.Entries, hostbox.StopInventoryEntry{Identity: inventoryIdentityFixture(t, base, name), Kind: "path", ArtifactID: "foreign", ManagedRoot: rootName, RelativePath: name, Ownership: row.ClaimOwner, Materialization: "materialized", Device: uint64(st.Dev), Inode: st.Ino, Type: st.Mode & syscall.S_IFMT}) |
| // Simulate a durable pre-fix or migrated agent inventory. |
| if err = root.Reconcile.Inventory.WriteStopInventory(t.Context(), in); err != nil { |
| t.Fatal(err) |
| @@ -149,7 +149,7 @@ func TestPlan9Fix2RetireOwnStateAndRecoverArchivedRoute(t *testing.T) { |
| if err != nil { |
| t.Fatal(err) |
| } |
| - in.Entries = append(in.Entries, hostbox.StopInventoryEntry{Kind: "path", ArtifactID: "state", ManagedRoot: "data", RelativePath: filepath.Join("instances", target.Host, target.Host+".db"), Ownership: row.ClaimOwner, Materialization: "materialized", Device: uint64(st.Dev), Inode: st.Ino, Type: st.Mode & syscall.S_IFMT}) |
| + in.Entries = append(in.Entries, hostbox.StopInventoryEntry{Identity: inventoryIdentityFixture(t, base, filepath.Join("instances", target.Host, target.Host+".db")), Kind: "path", ArtifactID: "state", ManagedRoot: "data", RelativePath: filepath.Join("instances", target.Host, target.Host+".db"), Ownership: row.ClaimOwner, Materialization: "materialized", Device: uint64(st.Dev), Inode: st.Ino, Type: st.Mode & syscall.S_IFMT}) |
| if err = root.Reconcile.Inventory.WriteStopInventory(t.Context(), in); err != nil { |
| t.Fatal(err) |
| } |
diff --git a/internal/host/rootgeneration_parked.go b/internal/host/rootgeneration_parked.go| index 99f2f159a..23b9b9c8e 100644 |
| --- a/internal/host/rootgeneration_parked.go |
| +++ b/internal/host/rootgeneration_parked.go |
| @@ -69,7 +69,7 @@ func generationRetirements(ctx context.Context, snapshot generationInventory) (m |
| if e.Ownership != j.Inventory.Ownership || (e.Materialization != "materialized" && !(e.Kind == "timer" && e.Materialization == "intended")) { |
| return nil, os.ErrPermission |
| } |
| - a := hostbox.Artifact{AllowAbsent: e.Kind == "timer" && (e.TimerRetired || e.Materialization == "intended"), Kind: hostbox.ArtifactKind(e.Kind), Root: e.ManagedRoot, Name: e.RelativePath, Owner: e.Ownership, RowID: e.RowIdentity, Dev: e.Device, Ino: e.Inode, Type: e.Type} |
| + a := hostbox.Artifact{AllowAbsent: e.Kind == "timer" && (e.TimerRetired || e.Materialization == "intended"), Kind: hostbox.ArtifactKind(e.Kind), Root: e.ManagedRoot, Name: e.RelativePath, Owner: e.Ownership, RowID: e.RowIdentity, Identity: e.Identity, Dev: e.Device, Ino: e.Inode, Type: e.Type} |
| if expected[a] { |
| return nil, os.ErrPermission |
| } |
diff --git a/internal/host/rootgeneration_payload_linux.go b/internal/host/rootgeneration_payload_linux.go| index 659c81f97..1f03dfe26 100644 |
| --- a/internal/host/rootgeneration_payload_linux.go |
| +++ b/internal/host/rootgeneration_payload_linux.go |
| @@ -14,12 +14,6 @@ import ( |
| // 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 { |
| @@ -36,29 +30,10 @@ func generationPayloadFiles(ctx context.Context, root string, payloads []hostbox |
| 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 { |
| + if err := hostbox.VerifyInventoryIdentity(root, e); 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/rootinventory.go b/internal/host/rootinventory.go| index 5d938ff0c..4ff049acf 100644 |
| --- a/internal/host/rootinventory.go |
| +++ b/internal/host/rootinventory.go |
| @@ -19,8 +19,6 @@ import ( |
| "github.com/carlosframework/platform/internal/hostguard" |
| "github.com/carlosframework/platform/internal/registry" |
| "github.com/carlosframework/platform/internal/release" |
| - "github.com/carlosframework/platform/internal/rootstate" |
| - "github.com/carlosframework/platform/internal/safefile" |
| ) |
| func (h ClaimRootHTTP) commitRootInventory(ctx context.Context, q hostbox.JournalRequest) (hostbox.JournalResponse, error) { |
| @@ -231,6 +229,9 @@ func (h ClaimRootHTTP) commitRootInventoryPending(ctx context.Context, q hostbox |
| if revision != q.Revision { |
| return hostbox.ErrChanged |
| } |
| + if err := h.verifyNewInventoryIdentities(prior, in); err != nil { |
| + return err |
| + } |
| if err := h.verifyTimerMutation(prior, in, q.Timer); err != nil { |
| return err |
| } |
| @@ -259,6 +260,7 @@ func (h ClaimRootHTTP) commitRootInventoryPending(ctx context.Context, q hostbox |
| } |
| if old.Materialization == "intended" && next.Materialization == "materialized" { |
| old.Materialization = next.Materialization |
| + old.Identity = next.Identity |
| old.Device = next.Device |
| old.Inode = next.Inode |
| old.Type = next.Type |
| @@ -322,30 +324,33 @@ func (h ClaimRootHTTP) verifyInventoryPath(e hostbox.StopInventoryEntry) error { |
| if e.Materialization != "materialized" { |
| return hostbox.ErrTypedInventory |
| } |
| - f, err := safefile.OpenDir(path) |
| - if err != nil { |
| - f, err = safefile.OpenFile(path, os.O_RDONLY, 0) |
| - } |
| - if err != nil { |
| - return err |
| - } |
| - defer f.Close() |
| - id, err := rootstate.Identity(f) |
| - if err != nil { |
| - return err |
| - } |
| - st, err := f.Stat() |
| - if err != nil { |
| - return err |
| - } |
| - kind := uint32(0100000) |
| - if st.IsDir() { |
| - kind = 0040000 |
| - } else if !st.Mode().IsRegular() { |
| - return hostbox.ErrTypedInventory |
| - } |
| - if id.Device != e.Device || id.Inode != e.Inode || kind != e.Type { |
| - return hostbox.ErrChanged |
| + return hostbox.VerifyInventoryIdentity(h.Roots["data"], e) |
| +} |
| + |
| +func (h ClaimRootHTTP) verifyNewInventoryIdentities(prior, next hostbox.StopInventory) error { |
| + for _, e := range next.Entries { |
| + if e.Kind != "path" || e.Materialization != "materialized" { |
| + continue |
| + } |
| + retained := false |
| + for _, old := range prior.Entries { |
| + if old.ArtifactID == e.ArtifactID && old.Materialization == "materialized" { |
| + retained = true |
| + } |
| + } |
| + if retained { |
| + continue |
| + } |
| + current, err := hostbox.CaptureInventoryIdentity(h.Roots[e.ManagedRoot], e.RelativePath) |
| + if err != nil { |
| + return err |
| + } |
| + if e.Identity.Boot != current.Boot { |
| + return hostbox.ErrChanged |
| + } |
| + if err := h.verifyInventoryPath(e); err != nil { |
| + return err |
| + } |
| } |
| return nil |
| } |
diff --git a/internal/host/rootinventory_state_linux.go b/internal/host/rootinventory_state_linux.go| index 9eaa954bb..e2be59b59 100644 |
| --- a/internal/host/rootinventory_state_linux.go |
| +++ b/internal/host/rootinventory_state_linux.go |
| @@ -106,7 +106,7 @@ func (a *ClaimRoleAssembly) prepareStateInventory(in ClaimLaunchInputs, inventor |
| return false, hostbox.ErrTypedInventory |
| } |
| e := inventory.Entries[index] |
| - if e.Materialization != "materialized" || stat.Mode&unix.S_IFMT != unix.S_IFDIR || uint64(stat.Dev) != e.Device || stat.Ino != e.Inode { |
| + if e.Materialization != "materialized" || stat.Mode&unix.S_IFMT != unix.S_IFDIR || hostbox.VerifyInventoryIdentity(root, e) != nil { |
| return false, hostbox.ErrTypedInventory |
| } |
| return false, nil |
| @@ -132,8 +132,12 @@ func (a *ClaimRoleAssembly) prepareStateInventory(in ClaimLaunchInputs, inventor |
| return false, err |
| } |
| entry := &inventory.Entries[index] |
| - entry.Device = uint64(stat.Dev) |
| - entry.Inode = stat.Ino |
| + entry.Identity, err = hostbox.CaptureInventoryIdentity(root, relative) |
| + if err != nil { |
| + return false, err |
| + } |
| + entry.Device = entry.Identity.Device |
| + entry.Inode = entry.Identity.Inode |
| entry.Type = unix.S_IFDIR |
| entry.Materialization = "materialized" |
| return true, nil |
diff --git a/internal/hostbox/coldhandover.go b/internal/hostbox/coldhandover.go| index 9c023604e..0c8f42acd 100644 |
| --- a/internal/hostbox/coldhandover.go |
| +++ b/internal/hostbox/coldhandover.go |
| @@ -241,7 +241,7 @@ func (h ColdHandover) Run(ctx context.Context, prior hostguard.FenceTarget, next |
| if entry.Kind == "timer" { |
| continue |
| } |
| - inventory.Artifacts = append(inventory.Artifacts, Artifact{Kind: ArtifactKind(entry.Kind), Root: entry.ManagedRoot, Name: entry.RelativePath, Owner: entry.Ownership, RowID: entry.RowIdentity, Dev: entry.Device, Ino: entry.Inode, Type: entry.Type}) |
| + inventory.Artifacts = append(inventory.Artifacts, Artifact{Kind: ArtifactKind(entry.Kind), Root: entry.ManagedRoot, Name: entry.RelativePath, Owner: entry.Ownership, RowID: entry.RowIdentity, Identity: entry.Identity, Dev: entry.Device, Ino: entry.Inode, Type: entry.Type}) |
| } |
| for _, timer := range j.Before.Timers { |
| inventory.Artifacts = append(inventory.Artifacts, Artifact{Kind: ArtifactTimer, Root: "registry", Name: timer.Host + "/" + timer.Name, Owner: timer.ClaimOwner, RowID: timer.ClaimRowID + "~" + timer.Registration}) |
diff --git a/internal/hostbox/coldhandover_test.go b/internal/hostbox/coldhandover_test.go| index 945a2ce93..633258815 100644 |
| --- a/internal/hostbox/coldhandover_test.go |
| +++ b/internal/hostbox/coldhandover_test.go |
| @@ -81,7 +81,7 @@ func coldHandoverTimers(t *testing.T, abandoned bool) { |
| if err != nil { |
| t.Fatal(err) |
| } |
| - in := StopInventory{Complete: true, Host: old.Host, Incarnation: prior.Incarnation, InstallationStamp: prior.Stamp, Box: "box", Enrollment: "enrollment", DirectoryRevision: "1", Entries: []StopInventoryEntry{{Kind: "route", ArtifactID: owner.ClaimRowID, ManagedRoot: "registry", RelativePath: old.Host, RowIdentity: owner.ClaimRowID, Ownership: owner.ClaimOwner, Materialization: "materialized"}, {Kind: "path", ArtifactID: "database", ManagedRoot: "data", RelativePath: artifact.Name, Ownership: artifact.Owner, Materialization: "materialized", Device: artifact.Dev, Inode: artifact.Ino, Type: artifact.Type}}} |
| + in := StopInventory{Complete: true, Host: old.Host, Incarnation: prior.Incarnation, InstallationStamp: prior.Stamp, Box: "box", Enrollment: "enrollment", DirectoryRevision: "1", Entries: []StopInventoryEntry{{Kind: "route", ArtifactID: owner.ClaimRowID, ManagedRoot: "registry", RelativePath: old.Host, RowIdentity: owner.ClaimRowID, Ownership: owner.ClaimOwner, Materialization: "materialized"}, {Kind: "path", ArtifactID: "database", ManagedRoot: "data", RelativePath: artifact.Name, Ownership: artifact.Owner, Materialization: "materialized", Identity: artifact.Identity, Device: artifact.Dev, Inode: artifact.Ino, Type: artifact.Type}}} |
| if abandoned { |
| for _, name := range []string{"abandoned", "withdrawn"} { |
| in.Entries = append(in.Entries, StopInventoryEntry{Kind: "timer", ArtifactID: name, ManagedRoot: "registry", RelativePath: old.Host + "/" + name, Ownership: owner.ClaimOwner, RowIdentity: name, Materialization: "intended", TimerRetired: name == "withdrawn"}) |
diff --git a/internal/hostbox/inventory_identity.go b/internal/hostbox/inventory_identity.go| new file mode 100644 |
| index 000000000..8e96b1669 |
| --- /dev/null |
| +++ b/internal/hostbox/inventory_identity.go |
| @@ -0,0 +1,13 @@ |
| +package hostbox |
| + |
| +import "github.com/carlosframework/platform/internal/rootstate" |
| + |
| +func sameInventoryIdentity(a, b rootstate.FileIdentity) bool { |
| + if a.Boot == "" || b.Boot == "" { |
| + return false |
| + } |
| + if a.Handle == "" || b.Handle == "" { |
| + return a.Boot == b.Boot && a == b |
| + } |
| + return a.Same(b) |
| +} |
diff --git a/internal/hostbox/inventory_identity_linux.go b/internal/hostbox/inventory_identity_linux.go| new file mode 100644 |
| index 000000000..789846801 |
| --- /dev/null |
| +++ b/internal/hostbox/inventory_identity_linux.go |
| @@ -0,0 +1,92 @@ |
| +//go:build linux |
| + |
| +package hostbox |
| + |
| +import ( |
| + "os" |
| + "path/filepath" |
| + "strings" |
| + |
| + "github.com/carlosframework/platform/internal/rootstate" |
| + "golang.org/x/sys/unix" |
| +) |
| + |
| +func CaptureInventoryIdentity(root, name string) (rootstate.FileIdentity, error) { |
| + if !filepath.IsAbs(root) || filepath.Clean(root) != root || !managedName(name) { |
| + return rootstate.FileIdentity{}, ErrTypedInventory |
| + } |
| + 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 rootstate.FileIdentity{}, err |
| + } |
| + defer unix.Close(fd) |
| + return inventoryIdentityAt(fd, name, false) |
| +} |
| + |
| +func inventoryIdentityAt(parent int, name string, symlink bool) (rootstate.FileIdentity, error) { |
| + flags := uint64(unix.O_PATH | unix.O_CLOEXEC) |
| + if symlink { |
| + flags |= unix.O_NOFOLLOW |
| + } |
| + fd, err := unix.Openat2(parent, name, &unix.OpenHow{Flags: flags, Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_SYMLINKS}) |
| + if err != nil { |
| + return rootstate.FileIdentity{}, err |
| + } |
| + f := os.NewFile(uintptr(fd), name) |
| + defer f.Close() |
| + var st unix.Stat_t |
| + if err := unix.Fstat(fd, &st); err != nil { |
| + return rootstate.FileIdentity{}, err |
| + } |
| + kind := st.Mode & unix.S_IFMT |
| + if kind != unix.S_IFDIR && kind != unix.S_IFREG && !(symlink && kind == unix.S_IFLNK) { |
| + return rootstate.FileIdentity{}, ErrTypedInventory |
| + } |
| + if kind == unix.S_IFREG && st.Nlink != 1 { |
| + return rootstate.FileIdentity{}, ErrTypedInventory |
| + } |
| + return rootstate.ObjectIdentity(f) |
| +} |
| + |
| +func VerifyInventoryIdentity(root string, e StopInventoryEntry) error { |
| + id, err := CaptureInventoryIdentity(root, e.RelativePath) |
| + if err != nil { |
| + return err |
| + } |
| + if e.Identity.Device != e.Device || e.Identity.Inode != e.Inode || e.Identity.Mode&unix.S_IFMT != e.Type || !sameInventoryIdentity(e.Identity, id) { |
| + return ErrChanged |
| + } |
| + if e.Identity.Boot != id.Boot { |
| + return checkedInventoryAncestry(root) |
| + } |
| + return nil |
| +} |
| + |
| +func checkedInventoryAncestry(root string) error { |
| + if !filepath.IsAbs(root) || filepath.Clean(root) != root { |
| + return ErrTypedInventory |
| + } |
| + fd, err := unix.Open("/", unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0) |
| + if err != nil { |
| + return err |
| + } |
| + defer func() { unix.Close(fd) }() |
| + for _, part := range append([]string{""}, strings.Split(strings.TrimPrefix(root, "/"), "/")...) { |
| + if part != "" { |
| + next, err := unix.Openat(fd, part, unix.O_PATH|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) |
| + if err != nil { |
| + return err |
| + } |
| + unix.Close(fd) |
| + fd = next |
| + } |
| + var st unix.Stat_t |
| + if err := unix.Fstat(fd, &st); err != nil { |
| + return err |
| + } |
| + if st.Uid != 0 || st.Mode&0022 != 0 { |
| + return os.ErrPermission |
| + } |
| + } |
| + return nil |
| +} |
diff --git a/internal/hostbox/inventory_identity_other.go b/internal/hostbox/inventory_identity_other.go| new file mode 100644 |
| index 000000000..3cd703bd2 |
| --- /dev/null |
| +++ b/internal/hostbox/inventory_identity_other.go |
| @@ -0,0 +1,10 @@ |
| +//go:build !linux |
| + |
| +package hostbox |
| + |
| +import "github.com/carlosframework/platform/internal/rootstate" |
| + |
| +func CaptureInventoryIdentity(string, string) (rootstate.FileIdentity, error) { |
| + return rootstate.FileIdentity{}, ErrUnsupported |
| +} |
| +func VerifyInventoryIdentity(string, StopInventoryEntry) error { return ErrUnsupported } |
diff --git a/internal/hostbox/inventory_identity_test.go b/internal/hostbox/inventory_identity_test.go| new file mode 100644 |
| index 000000000..2f16ade40 |
| --- /dev/null |
| +++ b/internal/hostbox/inventory_identity_test.go |
| @@ -0,0 +1,60 @@ |
| +package hostbox |
| + |
| +import ( |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/rootstate" |
| +) |
| + |
| +func TestPlan14InventoryIdentityBootBoundary(t *testing.T) { |
| + a := rootstate.FileIdentity{Device: 1, Inode: 2, Boot: "old", Filesystem: "fs", Handle: "handle", HandleType: 1, MountRoot: "/", MountPoint: "/data", MountNamespace: "ns", Mount: 3, UID: 1001, GID: 1001, Mode: 0040700} |
| + b := a |
| + b.Boot, b.Device, b.Mount, b.MountNamespace = "new", 4, 5, "new-ns" |
| + if !sameInventoryIdentity(a, b) { |
| + t.Fatal("persistent object refused after renumbering") |
| + } |
| + for _, field := range []string{"boot", "filesystem", "handle", "inode", "owner", "mode", "mount-root", "mount-point"} { |
| + c := b |
| + switch field { |
| + case "boot": |
| + c.Boot = a.Boot |
| + c.MountNamespace = a.MountNamespace |
| + case "filesystem": |
| + c.Filesystem = "other" |
| + case "handle": |
| + c.Handle = "other" |
| + case "inode": |
| + c.Inode++ |
| + case "owner": |
| + c.UID++ |
| + case "mode": |
| + c.Mode++ |
| + case "mount-root": |
| + c.MountRoot = "/other" |
| + case "mount-point": |
| + c.MountPoint = "/other" |
| + } |
| + if sameInventoryIdentity(a, c) { |
| + t.Fatalf("accepted changed %s", field) |
| + } |
| + } |
| + a.Handle, b.Handle = "", "" |
| + if sameInventoryIdentity(a, b) { |
| + t.Fatal("cross-boot match without handle") |
| + } |
| + if !sameInventoryIdentity(a, a) { |
| + t.Fatal("same-boot unsupported filesystem refused") |
| + } |
| + if sameInventoryIdentity(rootstate.FileIdentity{}, rootstate.FileIdentity{}) { |
| + t.Fatal("missing identity accepted") |
| + } |
| +} |
| + |
| +func TestPlan14InventoryRequestBindsPersistentIdentity(t *testing.T) { |
| + q := JournalRequest{V: 3, Op: "inventory-write", RecordKey: "record", RecordETag: "etag", Inventory: StopInventory{Entries: []StopInventoryEntry{{Device: 1, Inode: 2, Identity: rootstate.FileIdentity{Boot: "old", Device: 1, Inode: 2}}}}} |
| + before := journalRequestHash(q) |
| + q.Inventory.Entries[0].Identity.Handle = "substituted" |
| + if before == journalRequestHash(q) { |
| + t.Fatal("inventory identity not covered by request digest") |
| + } |
| +} |
diff --git a/internal/hostbox/journalservice.go b/internal/hostbox/journalservice.go| index bade850b0..d894b9a99 100644 |
| --- a/internal/hostbox/journalservice.go |
| +++ b/internal/hostbox/journalservice.go |
| @@ -14,6 +14,7 @@ import ( |
| "time" |
| "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| ) |
| var ErrLegacyJournalProof = errors.New("legacy journal requires root-signed stop evidence") |
| @@ -210,6 +211,22 @@ func journalRequestV2(q JournalRequest) []byte { |
| return raw |
| } |
| func journalRequestHash(req JournalRequest) [sha256.Size]byte { |
| + identities := make([]rootstate.FileIdentity, len(req.Inventory.Entries)) |
| + present := false |
| + for i, e := range req.Inventory.Entries { |
| + identities[i] = e.Identity |
| + present = present || e.Identity != (rootstate.FileIdentity{}) |
| + } |
| + if present { |
| + base := req |
| + base.Inventory.Entries = append([]StopInventoryEntry(nil), req.Inventory.Entries...) |
| + for i := range base.Inventory.Entries { |
| + base.Inventory.Entries[i].Identity = rootstate.FileIdentity{} |
| + } |
| + raw, _ := json.Marshal([]any{"carlos-inventory-identity-v1", journalRequestHash(base), identities}) |
| + return sha256.Sum256(raw) |
| + } |
| + |
| if req.V == 3 { |
| raw, _ := json.Marshal([]any{"carlos-journal-request-v3", string(journalRequestV2(req)), req.RecordKey, req.RecordETag}) |
| return sha256.Sum256(raw) |
diff --git a/internal/hostbox/plan14_cold_process_test.go b/internal/hostbox/plan14_cold_process_test.go| index 97449ef05..93b684d68 100644 |
| --- a/internal/hostbox/plan14_cold_process_test.go |
| +++ b/internal/hostbox/plan14_cold_process_test.go |
| @@ -142,7 +142,7 @@ func plan14ColdSetup(t *testing.T) plan14ColdManifest { |
| if err != nil { |
| t.Fatal(err) |
| } |
| - in := StopInventory{Complete: true, Host: old.Host, Incarnation: prior.Incarnation, InstallationStamp: prior.Stamp, Box: "box", Enrollment: "enrollment", DirectoryRevision: "1", Entries: []StopInventoryEntry{{Kind: "route", ArtifactID: owner.ClaimRowID, ManagedRoot: "registry", RelativePath: old.Host, RowIdentity: owner.ClaimRowID, Ownership: owner.ClaimOwner, Materialization: "materialized"}, {Kind: "path", ArtifactID: "database", ManagedRoot: "data", RelativePath: artifact.Name, Ownership: artifact.Owner, Materialization: "materialized", Device: artifact.Dev, Inode: artifact.Ino, Type: artifact.Type}}} |
| + in := StopInventory{Complete: true, Host: old.Host, Incarnation: prior.Incarnation, InstallationStamp: prior.Stamp, Box: "box", Enrollment: "enrollment", DirectoryRevision: "1", Entries: []StopInventoryEntry{{Kind: "route", ArtifactID: owner.ClaimRowID, ManagedRoot: "registry", RelativePath: old.Host, RowIdentity: owner.ClaimRowID, Ownership: owner.ClaimOwner, Materialization: "materialized"}, {Kind: "path", ArtifactID: "database", ManagedRoot: "data", RelativePath: artifact.Name, Ownership: artifact.Owner, Materialization: "materialized", Identity: artifact.Identity, Device: artifact.Dev, Inode: artifact.Ino, Type: artifact.Type}}} |
| if err = inventory.WriteStopInventory(ctx, in); err != nil { |
| t.Fatal(err) |
| } |
diff --git a/internal/hostbox/plan9fix4_inventory_linux_test.go b/internal/hostbox/plan9fix4_inventory_linux_test.go| index 67967e803..fbdd50e9f 100644 |
| --- a/internal/hostbox/plan9fix4_inventory_linux_test.go |
| +++ b/internal/hostbox/plan9fix4_inventory_linux_test.go |
| @@ -31,7 +31,7 @@ func TestPlan9Fix4InventoryRefusesFlatRestartMarksArtifact(t *testing.T) { |
| t.Cleanup(func() { inventory.Close() }) |
| in := StopInventory{Complete: true, Host: host, Incarnation: "inc", InstallationStamp: 1, Box: "box", Enrollment: "enrollment", DirectoryRevision: "1", Entries: []StopInventoryEntry{{ |
| Kind: "path", ArtifactID: "restart-marks", ManagedRoot: "data", RelativePath: artifact.Name, Ownership: artifact.Owner, |
| - Materialization: "materialized", Device: artifact.Dev, Inode: artifact.Ino, Type: artifact.Type, |
| + Materialization: "materialized", Identity: artifact.Identity, Device: artifact.Dev, Inode: artifact.Ino, Type: artifact.Type, |
| }}} |
| if err = inventory.WriteStopInventory(t.Context(), in); err != nil { |
| t.Fatal(err) |
diff --git a/internal/hostbox/reconcile_adapter.go b/internal/hostbox/reconcile_adapter.go| index 550cc9356..e6513e4d1 100644 |
| --- a/internal/hostbox/reconcile_adapter.go |
| +++ b/internal/hostbox/reconcile_adapter.go |
| @@ -313,7 +313,7 @@ func (a ReconcileAdapter) Retire(ctx context.Context, row registry.Route) error |
| if e.Ownership != row.ClaimOwner || (e.Materialization != "materialized" && !(a.RequireTimerInventory && e.Kind == "timer" && e.Materialization == "intended")) { |
| return ErrTypedInventory |
| } |
| - inventory.Artifacts = append(inventory.Artifacts, Artifact{AllowAbsent: e.Kind == "timer" && (e.TimerRetired || e.Materialization == "intended"), Kind: ArtifactKind(e.Kind), Root: e.ManagedRoot, Name: e.RelativePath, Owner: e.Ownership, RowID: e.RowIdentity, Dev: e.Device, Ino: e.Inode, Type: e.Type}) |
| + inventory.Artifacts = append(inventory.Artifacts, Artifact{AllowAbsent: e.Kind == "timer" && (e.TimerRetired || e.Materialization == "intended"), Kind: ArtifactKind(e.Kind), Root: e.ManagedRoot, Name: e.RelativePath, Owner: e.Ownership, RowID: e.RowIdentity, Identity: e.Identity, Dev: e.Device, Ino: e.Inode, Type: e.Type}) |
| } |
| if err = checkTrustedJournalDir(a.Retirement.JournalDir, true); err != nil { |
| return err |
diff --git a/internal/hostbox/stopinventory.go b/internal/hostbox/stopinventory.go| index 0053eb898..58a110656 100644 |
| --- a/internal/hostbox/stopinventory.go |
| +++ b/internal/hostbox/stopinventory.go |
| @@ -1,7 +1,10 @@ |
| package hostbox |
| +import "github.com/carlosframework/platform/internal/rootstate" |
| + |
| type StopInventoryEntry struct { |
| - TimerRetired bool `json:",omitempty"` |
| + Identity rootstate.FileIdentity `json:",omitempty"` |
| + TimerRetired bool `json:",omitempty"` |
| Device, Inode uint64 |
| Type uint32 |
| Kind string |
diff --git a/internal/hostbox/typedpark_linux.go b/internal/hostbox/typedpark_linux.go| index 9878227f3..32c17a01f 100644 |
| --- a/internal/hostbox/typedpark_linux.go |
| +++ b/internal/hostbox/typedpark_linux.go |
| @@ -11,6 +11,7 @@ import ( |
| "path/filepath" |
| "strings" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| "golang.org/x/sys/unix" |
| ) |
| @@ -50,7 +51,14 @@ func (p TypedParker) CaptureArtifact(rootID, name, owner string, kind ArtifactKi |
| if err := unix.Fstatat(fd.dir, fd.base, &st, unix.AT_SYMLINK_NOFOLLOW); err != nil { |
| return Artifact{}, err |
| } |
| - return Artifact{Kind: kind, Root: rootID, Name: name, Owner: owner, Dev: uint64(st.Dev), Ino: st.Ino, Type: st.Mode & unix.S_IFMT}, nil |
| + identity, err := inventoryIdentityAt(fd.dir, fd.base, true) |
| + if err != nil { |
| + return Artifact{}, err |
| + } |
| + if identity.Device != uint64(st.Dev) || identity.Inode != st.Ino { |
| + return Artifact{}, ErrChanged |
| + } |
| + return Artifact{Identity: identity, Kind: kind, Root: rootID, Name: name, Owner: owner, Dev: uint64(st.Dev), Ino: st.Ino, Type: st.Mode & unix.S_IFMT}, nil |
| } |
| func (p TypedParker) Park(ctx context.Context, id string, a Artifact) error { |
| @@ -79,7 +87,7 @@ func (p TypedParker) Park(ctx context.Context, id string, a Artifact) error { |
| } |
| return err |
| } |
| - if !sameArtifact(a, &st) { |
| + if !sameArtifactAt(a, src.dir, src.base, &st) || !artifactRootChecked(a, srcRoot) { |
| return ErrChanged |
| } |
| dstRel := filepath.Join(".parked", p.Incarnation, p.Host, p.Retirement, a.Root, a.Name) |
| @@ -132,7 +140,7 @@ func (p TypedParker) verifyDestination(a Artifact, srcParent int) error { |
| if err := unix.Fstatat(fd.dir, fd.base, &st, unix.AT_SYMLINK_NOFOLLOW); err != nil { |
| return err |
| } |
| - if !sameArtifact(a, &st) { |
| + if !sameArtifactAt(a, fd.dir, fd.base, &st) || !artifactRootChecked(a, parkedRoot) { |
| return ErrChanged |
| } |
| return p.sync(srcParent, fd.dir) |
| @@ -151,7 +159,7 @@ type parentFD struct { |
| } |
| func openParent(rootPath, name string, create bool) (parentFD, error) { |
| - root, err := unix.Open(rootPath, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0) |
| + root, err := unix.Openat2(unix.AT_FDCWD, rootPath, &unix.OpenHow{Flags: unix.O_RDONLY | unix.O_DIRECTORY | unix.O_CLOEXEC, Resolve: unix.RESOLVE_NO_SYMLINKS}) |
| if err != nil { |
| return parentFD{}, err |
| } |
| @@ -188,9 +196,19 @@ func openParent(rootPath, name string, create bool) (parentFD, error) { |
| } |
| return parentFD{cur, parts[len(parts)-1]}, nil |
| } |
| -func sameArtifact(a Artifact, st *unix.Stat_t) bool { |
| - return a.Dev == uint64(st.Dev) && a.Ino == st.Ino && a.Type == st.Mode&unix.S_IFMT |
| +func artifactRootChecked(a Artifact, root string) bool { |
| + boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id") |
| + return err == nil && (a.Identity.Boot == strings.TrimSpace(string(boot)) || checkedInventoryAncestry(root) == nil) |
| } |
| + |
| +func sameArtifactAt(a Artifact, parent int, name string, st *unix.Stat_t) bool { |
| + if a.Identity == (rootstate.FileIdentity{}) { |
| + return false |
| + } |
| + current, err := inventoryIdentityAt(parent, name, true) |
| + return err == nil && current.Device == uint64(st.Dev) && current.Inode == st.Ino && a.Identity.Device == a.Dev && a.Identity.Inode == a.Ino && a.Type == current.Mode&unix.S_IFMT && sameInventoryIdentity(a.Identity, current) |
| +} |
| + |
| func syncFDs(a, b int) error { |
| if err := unix.Fsync(a); err != nil { |
| return fmt.Errorf("sync source: %w", err) |
| @@ -226,7 +244,7 @@ func (p TypedParker) VerifyEmptyDirectory(a Artifact) error { |
| if err := unix.Fstat(fd, &st); err != nil { |
| return err |
| } |
| - if !sameArtifact(a, &st) { |
| + if !sameArtifactAt(a, parent.dir, parent.base, &st) || !artifactRootChecked(a, root) { |
| return ErrChanged |
| } |
| if names, err := f.Readdirnames(1); len(names) != 0 || !errors.Is(err, io.EOF) { |
diff --git a/internal/hostbox/typedretire.go b/internal/hostbox/typedretire.go| index 3397d8ba6..ace9f0b87 100644 |
| --- a/internal/hostbox/typedretire.go |
| +++ b/internal/hostbox/typedretire.go |
| @@ -13,6 +13,7 @@ import ( |
| "strings" |
| "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| ) |
| var ( |
| @@ -33,15 +34,16 @@ const ( |
| // Artifact binds a managed name to the installation identity which created it. |
| // Root is a configured root identifier, never an arbitrary absolute pathname. |
| type Artifact struct { |
| - AllowAbsent bool `json:"allow_absent,omitempty"` |
| - Kind ArtifactKind `json:"kind"` |
| - Root string `json:"root"` |
| - Name string `json:"name"` |
| - Owner string `json:"owner"` |
| - RowID string `json:"row_id,omitempty"` |
| - Dev uint64 `json:"dev,omitempty"` |
| - Ino uint64 `json:"ino,omitempty"` |
| - Type uint32 `json:"type,omitempty"` |
| + Identity rootstate.FileIdentity `json:",omitempty"` |
| + AllowAbsent bool `json:"allow_absent,omitempty"` |
| + Kind ArtifactKind `json:"kind"` |
| + Root string `json:"root"` |
| + Name string `json:"name"` |
| + Owner string `json:"owner"` |
| + RowID string `json:"row_id,omitempty"` |
| + Dev uint64 `json:"dev,omitempty"` |
| + Ino uint64 `json:"ino,omitempty"` |
| + Type uint32 `json:"type,omitempty"` |
| } |
| type RetirementInventory struct { |
diff --git a/internal/provision/plan14_e_write_census_test.go b/internal/provision/plan14_e_write_census_test.go| index b4c2cebe6..01a38d6e1 100644 |
| --- a/internal/provision/plan14_e_write_census_test.go |
| +++ b/internal/provision/plan14_e_write_census_test.go |
| @@ -2,6 +2,7 @@ package provision |
| // Track E overlays the reviewed Part 1 write census without changing its baseline. |
| var plan14EWriteCensusOverlay = []censusEntry{ |
| + {"internal/hostbox/journalservice.go", "journalRequestHash", "key-site", "key:RecordKey", "99703d244e91b55863a034eb7b8b7ecae50c1ca092ef9dba9594fdaff088b6e3"}, |
| {"internal/host/claimunit_template.go", "const claimBoundTemplate", "key-site", "lit:instances", "9ff0f84071165eb2d642596f1c4b28241f872fea99f7bf8e3d77f51785ad270a"}, |
| {"internal/hostbox/inventory_expected.go", "StopInventoryDB.writeExpected", "key-site", "key:RecordKey", "6bd77e75ff73ac8e461bb0687b183e031e2864d8455f58513b71aade995cb2b1"}, |
| {"internal/hostbox/journalservice.go", "type JournalRequest", "key-site", "key:RecordKey", "0da545d1fabfb3f49b57c6d997c90e59b9e1103ee2a0c6b0148f476b2620b856"}, |
| @@ -10,7 +11,7 @@ var plan14EWriteCensusOverlay = []censusEntry{ |
| {"internal/host/rootgeneration_inventory.go", "generationReconciler.reconcile", "key-site", "key:RecordKey", "1e668bbef6945deb4f0f3a988b5e1a1ecf44e9b85d838c2e01f6a5052b75bbed"}, |
| {"internal/host/rootgenerations_launch.go", "rootGenerationSource.resolve", "key-site", "key:RecordKey", "3cd7c8132e9c451c983392f4d1cc676ddc14b761c90dec3e631d54ae800f9951"}, |
| {"internal/historyreadback/workflow.go", "Operation.deliverGrant", "nonrecord", "sel:Put", "fb410ef6274ef7de0997a804c837ad1ecf29d8826ba1c4dcda00c2ae986fc434"}, |
| - {"internal/host/rootinventory.go", "ClaimRootHTTP.commitRootInventoryPending", "key-site", "key:RecordKey", "4f083cb29dd1bd8f7d2eeed20f30c9b51387082ac8c8ab1745efbb8a0270ac43"}, |
| + {"internal/host/rootinventory.go", "ClaimRootHTTP.commitRootInventoryPending", "key-site", "key:RecordKey", "ee45634958578f2386798a024348e11e88ca37c88b076305b3d48c40d7d74c37"}, |
| {"internal/host/rootinventory.go", "ClaimRootHTTP.recoverRootInventory", "key-site", "key:RecordKey", "79d1472b90bc7290e655bfe5e8857bad9fd0666c72b88364e89e4e198e60f535"}, |
| {"internal/host/roothistory_creation.go", "rootRestartRequestSource.creation", "key-site", "key:RecordKey", "af79a66b746b0f42388010b38ea8130dca6a8d29c4ba5623bfc9c3cc40dd9601"}, |
| {"internal/host/rootrestart_requests.go", "rootRestartRequests.consume", "key-site", "key:RecordKey", "0a9fbbe6f201077e276298b7e666e1ee2a345f1c420335290fec38a676b1ad84"}, |
diff --git a/internal/provision/testdata/plan14-root-reads-e.tsv b/internal/provision/testdata/plan14-root-reads-e.tsv| index cba8e575e..573333c68 100644 |
| --- a/internal/provision/testdata/plan14-root-reads-e.tsv |
| +++ b/internal/provision/testdata/plan14-root-reads-e.tsv |
| @@ -54,8 +54,8 @@ 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/release 0178b0701766abe8e12fdfe43d9353b1efdbc8bba2f6879bcb2188672293223d |
| -@coverage internal/rootstate 4ed31a2bd2f7eb4e10eeb918e61b349a020993b5def3d9527d9e422c74dd0fe9 |
| +@coverage internal/release ab22d309202356d0499b298fdc424bd53e7202fcfbd9586c27a520ecb2ab3c12 |
| +@coverage internal/rootstate 3e238e993e14b1ae85221845422055b42efa91144485c33db3a4453a0dcf2829 |
| 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 |
| internal/host/legacyrestart.go restartEffectFailed authority B3 E8 merge ruling 5: optional identity failures preserve the same restart outcome; no second restart; real not-up takes main rollback. Fixtures: TestPlan14ERecoveryRestartOutcome, TestPlan14ERecoveryBindingRefusalKeepsPromotedFix, TestPlan14ERecoveryNotUpPrecedesBinding. b68bb65884a14fe0cf78a65f56ded4ea41584bba99af9195827d1ea191b340a9 |
| internal/host/legacyrestart.go legacyRootRestart authority B3,G E9 merge ruling 6: E effects require a census; no-census recovery delegates to main or records evidence only. Fixtures: TestPlan14ERecoveryNoCensus, TestPlan14ERecoveryNoCensusResume, TestR2ERecoveryFlagWithoutCensus, TestPlan14ERecoveryRestartOutcome. Census-backed recovery retains captured authority and outcome precedence. 5fd48683abd82664baa77c2ddc7528f92f557b04586225d64b4a6e77147df931 |
| @@ -165,7 +165,7 @@ internal/host/roothistory_creation.go rootHistoryInstallation authority E2 E37: |
| internal/host/roothistory_creation.go rootRestartRequestSource.creation authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. registry:Account@219,registry:App@243,registry:Host@133,registry:Host@194 af79a66b746b0f42388010b38ea8130dca6a8d29c4ba5623bfc9c3cc40dd9601 |
| internal/host/roothistory_creation.go type rootHistoryAccess authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. 9c97bcd7878d79022ed942d8e6c00e944a00e0fa27a1bc5c732b4f4c10f4e9dd |
| internal/host/roothistory_launch.go ClaimLaunchRoot.rootInstalledProgram authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. file:OpenFile@541,file:Read@1043 e8d530dd1b8f613dfbd41d34a1b08b19aa3f74e0522c8f41db768ccd4e9e80d2 |
| -internal/host/rootinventory.go ClaimRootHTTP.commitRootInventoryPending authority E2 E37: retain independently initialized state-directory and timer requirements in the root installation transaction; preserve them through crash/reopen and rebind. Unknown history cannot certify completeness. Recommit under root-host exclusion before generation use. Canonical record and root retained obligations constrain agent candidate lists; timer enumeration is deny-only. Pre-flight 33, 80, 83. Fixtures: TestPlan14GenerationRequiredArtifacts, TestPlan14GenerationRegistryRows, TestPlan14RootTimerInventory, TestPlan14InventoryRequirementsRegistration, TestPlan14InventoryRequirementsUnknownAndRatchet, TestPlan14InventoryArtifactsCrash, TestPlan14InventoryRequirementsHistoryIdentity. Changed RequiredArtifacts and ExactRetirement root-runner witnesses await acceptance; remaining E2 launch/parked/containment integration is incomplete. PROPOSED E44: root timer intention must precede materialization; exact registration and row identity required; root retirement cannot infer ownership from registry enumeration. Tests: TestPlan14TimerIntentionBeforeMaterialization, TestPlan14GenerationTimerRetirement; root timer intention, sync-failure retry and retirement acceptance. Scheduler producer wiring remains incomplete. PROPOSED E46: local root installation obligations, exact root intention and inventory revision; service SQL is a deny-only consistency check. Executing identity: root instance-control or root participation/update. file:Read@1383,file:Read@3423,registry:Account@4787,registry:Account@4802,registry:App@4823,registry:App@4834,registry:ClaimIncarnation@4293,registry:ClaimOwner@4343,registry:ClaimOwner@5398,registry:ClaimOwnership@4885,registry:ClaimOwnership@4907,registry:ClaimOwnership@5714,registry:ClaimOwnership@7810,registry:ClaimRowID@4507,registry:ClaimRowID@4584,registry:ClaimStamp@4321,registry:DBPath@4851,registry:DBPath@4865,registry:Host@4757,registry:Host@4769,registry:Host@8286,registry:Name@8309,whole:ClaimOwnership@4889,whole:ClaimOwnership@4917,whole:ClaimOwnership@5724,whole:ClaimOwnership@7821,whole:Route@10010,whole:Route@3615,whole:Route@3998,whole:Route@4014,whole:Route@4069,whole:Route@4720,whole:Route@4733,whole:Route@4742,whole:Route@4980,whole:Route@5212,whole:Route@5971,whole:Route@6107,whole:Route@6331,whole:Route@7654,whole:Route@9305,whole:Route@9366,whole:Timer@8294,whole:Timer@8317 4f083cb29dd1bd8f7d2eeed20f30c9b51387082ac8c8ab1745efbb8a0270ac43 |
| +internal/host/rootinventory.go ClaimRootHTTP.commitRootInventoryPending authority E2 E37: retain independently initialized state-directory and timer requirements in the root installation transaction; preserve them through crash/reopen and rebind. Unknown history cannot certify completeness. Recommit under root-host exclusion before generation use. Canonical record and root retained obligations constrain agent candidate lists; timer enumeration is deny-only. Pre-flight 33, 80, 83. Fixtures: TestPlan14GenerationRequiredArtifacts, TestPlan14GenerationRegistryRows, TestPlan14RootTimerInventory, TestPlan14InventoryRequirementsRegistration, TestPlan14InventoryRequirementsUnknownAndRatchet, TestPlan14InventoryArtifactsCrash, TestPlan14InventoryRequirementsHistoryIdentity. Changed RequiredArtifacts and ExactRetirement root-runner witnesses await acceptance; remaining E2 launch/parked/containment integration is incomplete. PROPOSED E44: root timer intention must precede materialization; exact registration and row identity required; root retirement cannot infer ownership from registry enumeration. Tests: TestPlan14TimerIntentionBeforeMaterialization, TestPlan14GenerationTimerRetirement; root timer intention, sync-failure retry and retirement acceptance. Scheduler producer wiring remains incomplete. PROPOSED E46: local root installation obligations, exact root intention and inventory revision; service SQL is a deny-only consistency check. Executing identity: root instance-control or root participation/update. PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. file:Read@1383,file:Read@3423,registry:Account@4787,registry:Account@4802,registry:App@4823,registry:App@4834,registry:ClaimIncarnation@4293,registry:ClaimOwner@4343,registry:ClaimOwner@5398,registry:ClaimOwnership@4885,registry:ClaimOwnership@4907,registry:ClaimOwnership@5714,registry:ClaimOwnership@7896,registry:ClaimRowID@4507,registry:ClaimRowID@4584,registry:ClaimStamp@4321,registry:DBPath@4851,registry:DBPath@4865,registry:Host@4757,registry:Host@4769,registry:Host@8372,registry:Name@8395,whole:ClaimOwnership@4889,whole:ClaimOwnership@4917,whole:ClaimOwnership@5724,whole:ClaimOwnership@7907,whole:Route@10132,whole:Route@3615,whole:Route@3998,whole:Route@4014,whole:Route@4069,whole:Route@4720,whole:Route@4733,whole:Route@4742,whole:Route@4980,whole:Route@5212,whole:Route@5971,whole:Route@6107,whole:Route@6331,whole:Route@7740,whole:Route@9427,whole:Route@9488,whole:Timer@8380,whole:Timer@8403 ee45634958578f2386798a024348e11e88ca37c88b076305b3d48c40d7d74c37 |
| internal/release/roothistory_creation.go RootHistory.ConsumeCreation authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. 7ec65199b5afc894ae4aefded6a40f485086d0f8a4c86391eca193f473dca2a1 |
| internal/release/roothistory_creation.go RootHistory.FreshInstallation authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. 71b326a36350139c9c592b0b5b4b91409831fcffdd0c61d68cf5ec2bf54bbd5b |
| internal/release/roothistory_creation.go RootHistory.RecordCreation authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. 0b545ab63902eb8f2c8988991786c964a2af53d65eff46d8e53a0747da492b6d |
| @@ -216,9 +216,9 @@ internal/host/versionsweep.go sweepStagedVersionsPass authority E2 E19: both des |
| internal/release/adopt.go stageFrom authority E2 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree. E22: root generation pins guard existing-tree artifact and manifest writes in adoption and moves-held staging/publication. Held snapshot is read under release exclusion before the leaf held-stage lock; failures remain restrictive for writes. Claims-off source returns no root pins. Fresh wholly absent version trees retain signed staging. Fixtures: TestPlan14GenerationSiblingWrites, TestPlan14GenerationVerifiedSiblingWrites, TestPlan14GenerationHeldStage (production source assembly, both flag values without census, lock order, pin/error/fresh cases). Production completeness remains unavailable; no new completeness grant. 30efb69f9a7ee5aca5a1909d94d3c6669065c7880207430e7e0d2a23ae90a8b6 |
| internal/release/adopt.go type passConfig authority E2 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree. 1726cb5cee66d8aa0b04c44ad758b05a6aec239bb6a963ec2cfec13431ab765c |
| internal/release/adopt.go type stageCheck authority E2 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree. b031290fdcac7eb495da71ffbe395b151504078d237ff279fe5fc2143e6b0f34 |
| -internal/release/rootgenerationpins.go RootGenerationPins.StagedPins authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore passed on the E25 root runner. dff534cec0d4bea04750c4ea0774e71c0fbd3bf2d10dda7806df5bf797a68b36 |
| +internal/release/rootgenerationpins.go RootGenerationPins.StagedPins authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore passed on the E25 root runner. PROPOSED E53: Executing identity root; generation pins retain checked root ancestry and persistent filesystem/handle/inode/owner/topology evidence across boot; legacy device-only pins refuse a different boot. Tests: TestPlan14GenerationLegacyPinBootBoundary, TestRootRebootGenerationPinIdentityPhase1/2. 0bcf3e831097fd3d1e2f3a58fd1e0707f98e04dc058fbea1f923b073c3691d50 |
| internal/release/rootgenerationpins.go WithRootGenerationPins authority E2 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree. befc123585ff625901fd8b0516b2fa73b51968db21a28fa0d64c417382d54f79 |
| -internal/release/rootgenerationpins.go generationFileIdentity authority E2 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree. file:Lstat@96,file:Open@222 20a8f6c7c1c20fc2007dd89f2244b7797faede8d43f2206cd534a8033183b6ff |
| +internal/release/rootgenerationpins.go generationFileIdentity authority E2 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree. PROPOSED E53: Executing identity root; generation pins retain checked root ancestry and persistent filesystem/handle/inode/owner/topology evidence across boot; legacy device-only pins refuse a different boot. Tests: TestPlan14GenerationLegacyPinBootBoundary, TestRootRebootGenerationPinIdentityPhase1/2. file:Lstat@659,file:Open@785 38404f3b434b829ad551344259dcf9e7cadac906dcda262ca1eb4bd9d95ff824 |
| internal/release/rootgenerationpins.go passConfig.generationWriteCheck authority E2 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree. file:Lstat@537 f32259e8d00c8a196c9326fdaec2de5727211bd61117a2e1620339f68791e6ee |
| internal/release/rootgenerationpins.go type RootGenerationPins authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore passed on the E25 root runner. b10f7631296398380745d742ab966bbb1e39fe0768a054589fee7c1ac8c999e3 |
| internal/host/rootinstallation.go rootInstallationHostApp authority E2,J1 E20: read the atomic root installation under the existing host exclusion without recursive lock acquisition; retain fresh claim, canonical record, signed artifact and final reservation checks. Fixtures: TestPlan14GenerationLaunchDoesNotReenterRootLock, TestPlan14GenerationLaunchRefusals, TestPlan14RootDispatchRejectsRegistryMutation. file:Read@488,registry:Account@1062,registry:App@1091 0bbe17aff8c82841ae47fb01e060e1e1a80865e635d4a5c95dc5a638d2754d1c |
| @@ -309,7 +309,7 @@ internal/host/rootgeneration_kernel_linux.go generationTaskFDs authority E2 E25: |
| internal/host/rootgeneration_inventory.go generationPayloadBindings authority E2 E29: current and retained predecessor payloads must have completed materialization for every artifact kind, including route and timer. Tests: TestPlan14GenerationRegistryPayloadUnfinished, TestPlan14GenerationPayloadCompleteness, TestPlan14GenerationPayloadRetainedPredecessor. E27 checked root reader passed; extended physical/root-unit witnesses passed at E29. Required-artifact derivation and running-tenant containment remain incomplete. PROPOSED E46: local root installation obligations, exact root intention and inventory revision; service SQL is a deny-only consistency check. Executing identity: root instance-control or root participation/update. 58aaf33ef5fd544aeedb9d152058c86f319f594a79e607267f952dde3d1812e5 |
| 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 passed at E27; 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 passed at E27; 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_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. PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. 2c899f743f9953ce2c8d9223d15f00914e9a09c7675226260d1c4d4c1fe1fd5d |
| 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 |
| internal/host/rootgeneration_requirements.go generationRequiredInventory authority E2 E37: retain independently initialized state-directory and timer requirements in the root installation transaction; preserve them through crash/reopen and rebind. Unknown history cannot certify completeness. Recommit under root-host exclusion before generation use. Canonical record and root retained obligations constrain agent candidate lists; timer enumeration is deny-only. Pre-flight 33, 80, 83. Fixtures: TestPlan14GenerationRequiredArtifacts, TestPlan14GenerationRegistryRows, TestPlan14RootTimerInventory, TestPlan14InventoryRequirementsRegistration, TestPlan14InventoryRequirementsUnknownAndRatchet, TestPlan14InventoryArtifactsCrash, TestPlan14InventoryRequirementsHistoryIdentity. Changed RequiredArtifacts and ExactRetirement root-runner witnesses await acceptance; remaining E2 launch/parked/containment integration is incomplete. PROPOSED E46: local root installation obligations, exact root intention and inventory revision; service SQL is a deny-only consistency check. Executing identity: root instance-control or root participation/update. bbcbc143061ea6990ec8bc4e34f1a52a07e82039ea44340497d5bc6aab067913 |
| internal/host/rootgeneration_requirements.go type generationRecord authority E2 E30: derive route and ordinary-instance state-container requirements from schema- and shape-validated canonical deployment records bound to retained installation incarnation; include record contents and ETag in both source reads and the certificate digest. Missing, mismatched, omitted or changed evidence refuses without retiring pins. Fixtures: TestPlan14GenerationRequiredArtifacts, TestPlan14GenerationProductionModeGate. Production fixed-path witness TestPlan14GenerationRequiredArtifactsRootRunner is runner-pending. Timer classes, parked artifacts, positive empty-installation evidence and running-tenant containment remain incomplete. ff50bc959af62afbd73b01e9640346f9c79d6b6578e03cb3a7d6bdff4d90fc28 |
| @@ -335,9 +335,9 @@ internal/hostguard/rootrebind.go Guard.RebindRootInventory authority E2 E37: ret |
| internal/host/rootgeneration_parked.go generationPhysicalPayloads authority E2 E38: reconcile completed root retirement against the retained stop receipt and exact inventory; inspect parked identities, require retired rows absent or an independently inventoried successor, and retain generation pins. Fixtures: TestPlan14GenerationParkedPayload, TestPlan14GenerationParkedCrash, TestPlan14GenerationParkedKeepsReservation, TestPlan14GenerationParkedReadback; TestPlan14GenerationParkedRootRunner is runner-pending. Timer producer matches retained entries once and preserves successors on replay: TestPlan14GenerationTimerRetirement. No claims-off path changes. PROPOSED E46: Root rejects unfinished live inventory but accepts intended timer entries only behind exact completed retirement. 29a39e3ac81a6f7762155600de35faeaf1d7355437983e88ce9078cdb60207f7 |
| internal/host/rootgeneration_parked.go generationReadRetirements authority E2 E38: reconcile completed root retirement against the retained stop receipt and exact inventory; inspect parked identities, require retired rows absent or an independently inventoried successor, and retain generation pins. Fixtures: TestPlan14GenerationParkedPayload, TestPlan14GenerationParkedCrash, TestPlan14GenerationParkedKeepsReservation, TestPlan14GenerationParkedReadback; TestPlan14GenerationParkedRootRunner is runner-pending. Timer producer matches retained entries once and preserves successors on replay: TestPlan14GenerationTimerRetirement. No claims-off path changes. ba341f9665adbb204cf8ecf87a417b65022ce947291a4b99b1e8fa1bd16f3dec |
| internal/host/rootgeneration_parked.go generationRetirementRecords authority E2 E38: reconcile completed root retirement against the retained stop receipt and exact inventory; inspect parked identities, require retired rows absent or an independently inventoried successor, and retain generation pins. Fixtures: TestPlan14GenerationParkedPayload, TestPlan14GenerationParkedCrash, TestPlan14GenerationParkedKeepsReservation, TestPlan14GenerationParkedReadback; TestPlan14GenerationParkedRootRunner is runner-pending. Timer producer matches retained entries once and preserves successors on replay: TestPlan14GenerationTimerRetirement. No claims-off path changes. file:Read@707 02eca3d34294974c09e93199efb37a223e7615eaa32faffdafb982b5d4ba53e7 |
| -internal/host/rootgeneration_parked.go generationRetirements authority E2 E38: reconcile completed root retirement against the retained stop receipt and exact inventory; inspect parked identities, require retired rows absent or an independently inventoried successor, and retain generation pins. Fixtures: TestPlan14GenerationParkedPayload, TestPlan14GenerationParkedCrash, TestPlan14GenerationParkedKeepsReservation, TestPlan14GenerationParkedReadback; TestPlan14GenerationParkedRootRunner is runner-pending. Timer producer matches retained entries once and preserves successors on replay: TestPlan14GenerationTimerRetirement. No claims-off path changes. PROPOSED E46: local root installation obligations, exact root intention and inventory revision; service SQL is a deny-only consistency check. Executing identity: root instance-control or root participation/update. 48eef6024e9437337e59956c4cf6393a5ca401196195f1e0c4a69674d70aed82 |
| +internal/host/rootgeneration_parked.go generationRetirements authority E2 E38: reconcile completed root retirement against the retained stop receipt and exact inventory; inspect parked identities, require retired rows absent or an independently inventoried successor, and retain generation pins. Fixtures: TestPlan14GenerationParkedPayload, TestPlan14GenerationParkedCrash, TestPlan14GenerationParkedKeepsReservation, TestPlan14GenerationParkedReadback; TestPlan14GenerationParkedRootRunner is runner-pending. Timer producer matches retained entries once and preserves successors on replay: TestPlan14GenerationTimerRetirement. No claims-off path changes. PROPOSED E46: local root installation obligations, exact root intention and inventory revision; service SQL is a deny-only consistency check. Executing identity: root instance-control or root participation/update. PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. bae5991fb380bc83c9cdf49e1dd2e388a1c07aa66a989c59019221f4b6cc3bbf |
| internal/host/rootgeneration_parked.go productionGenerationRetirements authority E2 E38: reconcile completed root retirement against the retained stop receipt and exact inventory; inspect parked identities, require retired rows absent or an independently inventoried successor, and retain generation pins. Fixtures: TestPlan14GenerationParkedPayload, TestPlan14GenerationParkedCrash, TestPlan14GenerationParkedKeepsReservation, TestPlan14GenerationParkedReadback; TestPlan14GenerationParkedRootRunner is runner-pending. Timer producer matches retained entries once and preserves successors on replay: TestPlan14GenerationTimerRetirement. No claims-off path changes. file:Open@221,file:Open@306 fe83ff81bb9966f50de7fb3a049b8acd8e87f5d86346809090fc46b5c1e65189 |
| -internal/hostbox/reconcile_adapter.go ReconcileAdapter.Retire authority C,E2 E38: reconcile completed root retirement against the retained stop receipt and exact inventory; inspect parked identities, require retired rows absent or an independently inventoried successor, and retain generation pins. Fixtures: TestPlan14GenerationParkedPayload, TestPlan14GenerationParkedCrash, TestPlan14GenerationParkedKeepsReservation, TestPlan14GenerationParkedReadback; TestPlan14GenerationParkedRootRunner is runner-pending. Timer producer matches retained entries once and preserves successors on replay: TestPlan14GenerationTimerRetirement. No claims-off path changes. PROPOSED E44: root timer intention must precede materialization; exact registration and row identity required; root retirement cannot infer ownership from registry enumeration. Tests: TestPlan14TimerIntentionBeforeMaterialization, TestPlan14GenerationTimerRetirement; root timer intention, sync-failure retry and retirement acceptance. Scheduler producer wiring remains incomplete. PROPOSED E46: captured claim selection and root journal mutation; root RPC outside service host exclusion; SQL/route data never grants root authority. Executing identity: service controller; root for inventory/retirement callbacks. registry:ClaimIncarnation@1309,registry:ClaimIncarnation@322,registry:ClaimIncarnation@3592,registry:ClaimIncarnation@3614,registry:ClaimOwner@1366,registry:ClaimOwner@1434,registry:ClaimOwner@2365,registry:ClaimOwner@2771,registry:ClaimOwner@3672,registry:ClaimOwner@3688,registry:ClaimOwner@3926,registry:ClaimOwnership@147,registry:ClaimRowID@3706,registry:ClaimRowID@3949,registry:ClaimStamp@1338,registry:ClaimStamp@362,registry:ClaimStamp@3638,registry:ClaimStamp@3654,registry:Host@1257,registry:Host@2270,registry:Host@2816,registry:Host@2873,registry:Host@288,registry:Host@3511,registry:Host@3894,registry:Name@3911,registry:Registration@3728,registry:Registration@3970,whole:ClaimOwnership@151,whole:Route@1069,whole:Route@1152,whole:Route@231,whole:Timer@3577 3559413c25c77eed78b72cb8614e01bfa7443db51993479dc02febb4e1be1e34 |
| +internal/hostbox/reconcile_adapter.go ReconcileAdapter.Retire authority C,E2 E38: reconcile completed root retirement against the retained stop receipt and exact inventory; inspect parked identities, require retired rows absent or an independently inventoried successor, and retain generation pins. Fixtures: TestPlan14GenerationParkedPayload, TestPlan14GenerationParkedCrash, TestPlan14GenerationParkedKeepsReservation, TestPlan14GenerationParkedReadback; TestPlan14GenerationParkedRootRunner is runner-pending. Timer producer matches retained entries once and preserves successors on replay: TestPlan14GenerationTimerRetirement. No claims-off path changes. PROPOSED E44: root timer intention must precede materialization; exact registration and row identity required; root retirement cannot infer ownership from registry enumeration. Tests: TestPlan14TimerIntentionBeforeMaterialization, TestPlan14GenerationTimerRetirement; root timer intention, sync-failure retry and retirement acceptance. Scheduler producer wiring remains incomplete. PROPOSED E46: captured claim selection and root journal mutation; root RPC outside service host exclusion; SQL/route data never grants root authority. Executing identity: service controller; root for inventory/retirement callbacks. PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. registry:ClaimIncarnation@1309,registry:ClaimIncarnation@322,registry:ClaimIncarnation@3614,registry:ClaimIncarnation@3636,registry:ClaimOwner@1366,registry:ClaimOwner@1434,registry:ClaimOwner@2387,registry:ClaimOwner@2793,registry:ClaimOwner@3694,registry:ClaimOwner@3710,registry:ClaimOwner@3948,registry:ClaimOwnership@147,registry:ClaimRowID@3728,registry:ClaimRowID@3971,registry:ClaimStamp@1338,registry:ClaimStamp@362,registry:ClaimStamp@3660,registry:ClaimStamp@3676,registry:Host@1257,registry:Host@2292,registry:Host@2838,registry:Host@288,registry:Host@2895,registry:Host@3533,registry:Host@3916,registry:Name@3933,registry:Registration@3750,registry:Registration@3992,whole:ClaimOwnership@151,whole:Route@1069,whole:Route@1152,whole:Route@231,whole:Timer@3599 161f3654abb12c42f016704228b550d132c4e9da3d29906db4f908dc40994e4d |
| internal/hostbox/retirement_timers.go retirementWithTimers authority C,E2 E38: reconcile completed root retirement against the retained stop receipt and exact inventory; inspect parked identities, require retired rows absent or an independently inventoried successor, and retain generation pins. Fixtures: TestPlan14GenerationParkedPayload, TestPlan14GenerationParkedCrash, TestPlan14GenerationParkedKeepsReservation, TestPlan14GenerationParkedReadback; TestPlan14GenerationParkedRootRunner is runner-pending. Timer producer matches retained entries once and preserves successors on replay: TestPlan14GenerationTimerRetirement. No claims-off path changes. PROPOSED E44: root timer intention must precede materialization; exact registration and row identity required; root retirement cannot infer ownership from registry enumeration. Tests: TestPlan14TimerIntentionBeforeMaterialization, TestPlan14GenerationTimerRetirement; root timer intention, sync-failure retry and retirement acceptance. Scheduler producer wiring remains incomplete. 67060d1e6077a5cb6a15ea2749e070e97f830177987570037564916f5ed9bda0 |
| internal/activator/activator.go Service.respawnWorkerLocked authority E2 E39: close the unsupported claimed worker path before backend entry, replacement or supervised stop. Claims-off paths remain unchanged. This is refusal coverage, not positive root-reserved worker launch support. Fixtures: TestPlan14GenerationWorkerStartBoundary, TestPlan14GenerationWorkerRealBackendRefusal, TestPlan14GenerationWorkerReplacementBoundary, TestPlan14GenerationWorkerSupervisionBoundary. Pre-flight 29 and E39 worker launch audit. whole:Route@11828,whole:Route@13334,whole:Route@360,whole:Route@4164,whole:Route@6157,whole:Route@6338 706f2a4268a06dae2eca22093f841750139c5bd9dcaa0e91657621c5eea1bbe9 |
| internal/activator/workerbuild.go Service.startWorker authority E2 E39: close the unsupported claimed worker path before backend entry, replacement or supervised stop. Claims-off paths remain unchanged. This is refusal coverage, not positive root-reserved worker launch support. Fixtures: TestPlan14GenerationWorkerStartBoundary, TestPlan14GenerationWorkerRealBackendRefusal, TestPlan14GenerationWorkerReplacementBoundary, TestPlan14GenerationWorkerSupervisionBoundary. Pre-flight 29 and E39 worker launch audit. registry:Host@1201,registry:Host@1379,registry:Host@2314,whole:Route@2420,whole:Route@931 be27c7d049a6009a8a22798569e457f5126f14d5e7c56aafb5b1bd260717f6f5 |
| @@ -394,7 +394,7 @@ internal/host/rootinventory_timer_mutation.go ClaimRootHTTP.applyTimerMutation a |
| internal/host/rootinventory_timer_mutation.go ClaimRootHTTP.verifyTimerMutation authority E2 PROPOSED E46: local root installation obligations, exact root intention and inventory revision; service SQL is a deny-only consistency check. Executing identity: root instance-control or root participation/update. registry:Host@232,registry:Name@253,whole:Timer@234,whole:Timer@255 581329ce2bbc4ebc643553ec65f1ed922999629bcd29bc7ddf30f6d1cd3dbdd6 |
| internal/host/rootinventory_timer_mutation.go ClaimRootHTTP.verifyTimerMutationRow hint E2 PROPOSED E46: R-E45-5: service-writable timer SQL is deny-only; root pending/inventory supplies ownership. Executing identity: root instance-control or root participation/update. registry:ClaimIncarnation@1236,registry:ClaimIncarnation@1260,registry:ClaimIncarnation@367,registry:ClaimIncarnation@389,registry:ClaimOwner@1316,registry:ClaimOwner@1334,registry:ClaimOwner@451,registry:ClaimOwner@467,registry:ClaimOwner@612,registry:ClaimOwnership@339,registry:ClaimRowID@1350,registry:ClaimRowID@487,registry:ClaimStamp@1282,registry:ClaimStamp@1300,registry:ClaimStamp@415,registry:ClaimStamp@431,registry:Host@188,registry:Host@289,registry:Kind@1388,registry:Kind@1400,registry:Kind@825,registry:Kind@856,registry:Name@1055,registry:Name@205,registry:Name@327,registry:Path@951,registry:Registration@1368,registry:Registration@1457,registry:Registration@503,registry:Registration@521,registry:Registration@649,whole:ClaimOwnership@258,whole:ClaimOwnership@341,whole:Timer@1222,whole:Timer@1445,whole:Timer@1452,whole:Timer@190,whole:Timer@207,whole:Timer@279 a815c5cb716cb5dad1bddaa84600abcaf2fbef2dd8e8df3b24c313a90cdebf82 |
| internal/host/rootinventory_timer_recovery.go ClaimRootHTTP.settleTimerInventory authority E2 PROPOSED E46: exact persisted root pending intent only; finish journal after effect matches, preserve denial, no new SQL effect or network claim authority. Executing identity: root instance-control or root participation/update. registry:Host@353,registry:Name@367,whole:Timer@355,whole:Timer@369,whole:Timer@512,whole:Timer@527,whole:Timer@536 88fa248f2431066f1b9ccca604351d3078a42ad8f244dcc38ea55cf405a3d3cd |
| -internal/hostbox/coldhandover.go ColdHandover.Run authority C,J2,J4 route-and-input PROPOSED E46: captured claim selection and root journal mutation; root RPC outside service host exclusion; SQL/route data never grants root authority. Executing identity: service controller; root for inventory/retirement callbacks. file:Exec@938,file:Lstat@10641,file:OpenRoot@10563,file:Read@1290,registry:Addr@3565,registry:Addrs@3583,registry:Backing@3602,registry:Canary@3659,registry:ClaimIncarnation@3443,registry:ClaimIncarnation@3945,registry:ClaimOwner@11391,registry:ClaimOwner@4027,registry:ClaimOwner@4047,registry:ClaimOwner@5922,registry:ClaimOwner@7014,registry:ClaimOwner@8242,registry:ClaimOwnership@10983,registry:ClaimOwnership@3413,registry:ClaimOwnership@4391,registry:ClaimOwnership@4609,registry:ClaimOwnership@4642,registry:ClaimRowID@11281,registry:ClaimRowID@11362,registry:ClaimRowID@4065,registry:ClaimRowID@4673,registry:ClaimRowID@6973,registry:ClaimRowID@8267,registry:ClaimStamp@3488,registry:ClaimStamp@3992,registry:Host@6850,registry:Host@8204,registry:Kind@3521,registry:Name@4727,registry:Name@6865,registry:Name@8223,registry:Platform@3643,registry:Registration@4091,registry:Registration@6930,registry:Registration@8292,registry:Replica@3673,registry:Route@10983,registry:Route@4391,registry:Route@4642,registry:Route@5173,registry:Route@5922,registry:Timers@4590,registry:Timers@4609,registry:Timers@4673,registry:Timers@4727,registry:Timers@6802,registry:Timers@8079,registry:Tunnel@3623,registry:Unit@3547,whole:ClaimOwnership@10997,whole:ClaimOwnership@3417,whole:ClaimOwnership@4403,whole:ClaimOwnership@4625,whole:ClaimOwnership@4654,whole:ColdRows@10985,whole:ColdRows@12097,whole:ColdRows@12107,whole:ColdRows@4888,whole:ColdRows@4896,whole:ColdRows@4904,whole:ColdRows@4911,whole:ColdRows@5175,whole:ColdRows@5924,whole:ColdRows@6804,whole:ColdRows@8081,whole:Route@10991,whole:Route@3398,whole:Route@3758,whole:Route@4190,whole:Route@4198,whole:Route@4249,whole:Route@4257,whole:Route@4397,whole:Route@4648,whole:Route@5182,whole:Route@5931,whole:Timer@3929,whole:Timer@4203,whole:Timer@4211,whole:Timer@4262,whole:Timer@4300,whole:Timer@4596,whole:Timer@4615,whole:Timer@4679,whole:Timer@4733,whole:Timer@6811,whole:Timer@8088 f93ed708bfa43eb371e597f32ca26f5e204bba0057663d38b64d65e2302a6bc1 |
| +internal/hostbox/coldhandover.go ColdHandover.Run authority C,J2,J4 route-and-input PROPOSED E46: captured claim selection and root journal mutation; root RPC outside service host exclusion; SQL/route data never grants root authority. Executing identity: service controller; root for inventory/retirement callbacks. PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. file:Exec@938,file:Lstat@10667,file:OpenRoot@10589,file:Read@1290,registry:Addr@3565,registry:Addrs@3583,registry:Backing@3602,registry:Canary@3659,registry:ClaimIncarnation@3443,registry:ClaimIncarnation@3945,registry:ClaimOwner@11417,registry:ClaimOwner@4027,registry:ClaimOwner@4047,registry:ClaimOwner@5922,registry:ClaimOwner@7014,registry:ClaimOwner@8268,registry:ClaimOwnership@11009,registry:ClaimOwnership@3413,registry:ClaimOwnership@4391,registry:ClaimOwnership@4609,registry:ClaimOwnership@4642,registry:ClaimRowID@11307,registry:ClaimRowID@11388,registry:ClaimRowID@4065,registry:ClaimRowID@4673,registry:ClaimRowID@6973,registry:ClaimRowID@8293,registry:ClaimStamp@3488,registry:ClaimStamp@3992,registry:Host@6850,registry:Host@8230,registry:Kind@3521,registry:Name@4727,registry:Name@6865,registry:Name@8249,registry:Platform@3643,registry:Registration@4091,registry:Registration@6930,registry:Registration@8318,registry:Replica@3673,registry:Route@11009,registry:Route@4391,registry:Route@4642,registry:Route@5173,registry:Route@5922,registry:Timers@4590,registry:Timers@4609,registry:Timers@4673,registry:Timers@4727,registry:Timers@6802,registry:Timers@8105,registry:Tunnel@3623,registry:Unit@3547,whole:ClaimOwnership@11023,whole:ClaimOwnership@3417,whole:ClaimOwnership@4403,whole:ClaimOwnership@4625,whole:ClaimOwnership@4654,whole:ColdRows@11011,whole:ColdRows@12123,whole:ColdRows@12133,whole:ColdRows@4888,whole:ColdRows@4896,whole:ColdRows@4904,whole:ColdRows@4911,whole:ColdRows@5175,whole:ColdRows@5924,whole:ColdRows@6804,whole:ColdRows@8107,whole:Route@11017,whole:Route@3398,whole:Route@3758,whole:Route@4190,whole:Route@4198,whole:Route@4249,whole:Route@4257,whole:Route@4397,whole:Route@4648,whole:Route@5182,whole:Route@5931,whole:Timer@3929,whole:Timer@4203,whole:Timer@4211,whole:Timer@4262,whole:Timer@4300,whole:Timer@4596,whole:Timer@4615,whole:Timer@4679,whole:Timer@4733,whole:Timer@6811,whole:Timer@8114 c181cbba4969a1d2e7fc2c0e25ff1b9240282f1dcbbfa3fc7ee021f332b2cffd |
| internal/hostbox/inventory_expected.go TimerMutation.MarshalJSON hint E2 PROPOSED E46: captured claim selection and root journal mutation; root RPC outside service host exclusion; SQL/route data never grants root authority. Executing identity: service controller; root for inventory/retirement callbacks. registry:Registration@151,whole:Timer@153 c7f03dad631ed3926e08bbdf5f733a544f631245b15f63ada0deed292abfa733 |
| internal/hostbox/inventory_expected.go TimerMutation.UnmarshalJSON hint E2 PROPOSED E46: captured claim selection and root journal mutation; root RPC outside service host exclusion; SQL/route data never grants root authority. Executing identity: service controller; root for inventory/retirement callbacks. registry:Registration@225,whole:Timer@227 a8e97d7bb426306519f06e0d4148af082d3d9ac907ce52779f5558bb5955eeca |
| internal/hostbox/inventoryscope.go ValidateAgentInventory authority C,J2,J4 root-route PROPOSED E46: captured claim selection and root journal mutation; root RPC outside service host exclusion; SQL/route data never grants root authority. Executing identity: service controller; root for inventory/retirement callbacks. registry:ClaimIncarnation@189,registry:ClaimOwner@601,registry:ClaimOwnership@274,registry:ClaimRowID@825,registry:ClaimStamp@231,registry:DBPath@1271,registry:DBPath@1325,registry:DBPath@1383,registry:DBPath@1656,registry:DBPath@1794,registry:DBPath@2102,registry:Host@166,whole:ClaimOwnership@278,whole:Route@1186,whole:Route@151,whole:Route@572,whole:Route@794,whole:Route@845,whole:Route@950 ce8c02515e68984b648cb76e9a6c3be9737cbc29588ba08c438ceaed5ddcba91 |
| @@ -456,7 +456,7 @@ internal/host/claimproduction.go claimServingLaunchSource authority E2 PROPOSED |
| internal/host/claimretire_transport.go ClaimRootHTTP.retire authority E2 PROPOSED E44: root timer intention must precede materialization; exact registration and row identity required; root retirement cannot infer ownership from registry enumeration. Tests: TestPlan14TimerIntentionBeforeMaterialization, TestPlan14GenerationTimerRetirement; root timer intention, sync-failure retry and retirement acceptance. Scheduler producer wiring remains incomplete. PROPOSED E49: root; named placement/program readback from retained root binding, durable unit intent and inode-conditional retirement, and readiness socket ownership. Service route fields are candidates only; final root launch rechecks independently. No no-census path added. Full always-running adoption/handover remains pending. PROPOSED E50: Root: R-E50-7 restores the pre-E49 basis verbatim; current declaration behavior is retained. registry:ClaimOwnership@1081,registry:Host@1062,registry:Host@792,whole:ClaimOwnership@1085,whole:ClaimOwnership@894,whole:Route@1971,whole:Route@854 38ea70052f78cc0d9fdb95cf1bcbb30cae504c7a833ab45769fedf26978c50c4 |
| internal/host/claimretire_transport.go type ClaimRootHTTP authority E2 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. PROPOSED E49: root; named placement/program readback from retained root binding, durable unit intent and inode-conditional retirement, and readiness socket ownership. Service route fields are candidates only; final root launch rechecks independently. No no-census path added. Full always-running adoption/handover remains pending. PROPOSED E50: Root: R-E50-7 restores the pre-E49 basis verbatim; current declaration behavior is retained. 674c85dcb180b4818e5c6117cbed7ef7301c8094a9806d854762521d8d41d315 |
| internal/host/claimroleassembly.go ClaimRoleAssembly.PreflightLaunch authority E2 PROPOSED E49: agent (service UID); named placement/program readback from retained root binding, durable unit intent and inode-conditional retirement, and readiness socket ownership. Service route fields are candidates only; final root launch rechecks independently. No no-census path added. Full always-running adoption/handover remains pending. PROPOSED E50: Agent (service UID): retain authority classification under R-E50-7 because this declaration gates work; independent final root validation does not make the agent gate a passive hint. data-literal@200 5bf02fb5b6911924dbd18f063fec53552a994bcc9ae790a67db5786fa877de1e |
| -internal/host/claimroleassembly_linux.go ClaimRoleAssembly.Launch authority E2 PROPOSED E49: agent (service UID); named placement/program readback from retained root binding, durable unit intent and inode-conditional retirement, and readiness socket ownership. Service route fields are candidates only; final root launch rechecks independently. No no-census path added. Full always-running adoption/handover remains pending. PROPOSED E50: Agent (service UID): retain authority classification under R-E50-7 because this declaration gates work; independent final root validation does not make the agent gate a passive hint. file:Command@5897,file:EvalSymlinks@5446,file:Fstat@3529,file:Lstat@2269,file:OpenFile@3119,file:OpenFile@3306,file:Read@711 6f5cf726038aad19bf8247979e4073d57c804ced0c655cd6e0787b7749ac0d40 |
| +internal/host/claimroleassembly_linux.go ClaimRoleAssembly.Launch authority E2 PROPOSED E49: agent (service UID); named placement/program readback from retained root binding, durable unit intent and inode-conditional retirement, and readiness socket ownership. Service route fields are candidates only; final root launch rechecks independently. No no-census path added. Full always-running adoption/handover remains pending. PROPOSED E50: Agent (service UID): retain authority classification under R-E50-7 because this declaration gates work; independent final root validation does not make the agent gate a passive hint. PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. file:Command@5889,file:EvalSymlinks@5438,file:Fstat@3521,file:Lstat@2269,file:OpenFile@3111,file:OpenFile@3298,file:Read@711 ff3c65c83908a6803f53b0f61eedf9840eba50c109429bbb2c3620e9154bf689 |
| internal/host/claimunit_definition.go claimBoundUnitDefinition authority E2 PROPOSED E49: root; named placement/program readback from retained root binding, durable unit intent and inode-conditional retirement, and readiness socket ownership. Service route fields are candidates only; final root launch rechecks independently. No no-census path added. Full always-running adoption/handover remains pending. PROPOSED E51: Root renders the independent bound-unit profile; TCP permits only the exact configured address and bind port, with reservation-time connect/sendmsg filtering. PROPOSED E51: Root renders the independent bound-unit profile; TCP permits only the exact configured address and bind port, with reservation-time connect/sendmsg filtering. PROPOSED E51: Root renders the independent bound-unit profile; TCP permits only the exact configured address and bind port, with reservation-time connect/sendmsg filtering. 81006df22045ec86dc586e9abf7986b3c2b5eb1634911fcc6a849c6472e74263 |
| internal/host/claimunit_files_linux.go claimUnitFiles.read authority E2 PROPOSED E49: root; named placement/program readback from retained root binding, durable unit intent and inode-conditional retirement, and readiness socket ownership. Service route fields are candidates only; final root launch rechecks independently. No no-census path added. Full always-running adoption/handover remains pending. PROPOSED E51: Root reads the owned unit through checked public ancestry and captures a filesystem handle, filesystem identity, boot, mount topology and ownership. file:ReadAll@354 b7f65702d1502250c459874857f9e64ba144b851c0e5a0eb923c91eb11ffab2f |
| internal/host/claimunit_files_linux.go claimUnitFiles.write authority E2 PROPOSED E49: root; named placement/program readback from retained root binding, durable unit intent and inode-conditional retirement, and readiness socket ownership. Service route fields are candidates only; final root launch rechecks independently. No no-census path added. Full always-running adoption/handover remains pending. PROPOSED E51: Root conditionally replaces only a persistently matching definition; current-boot device numbers remain exact within a boot. file:CreateTemp@381,file:Open@182 e36085aeb00a20ae11f056111dcef5272621cc88d90e9e75a927660a0834e9b1 |
| @@ -470,7 +470,7 @@ internal/host/claimwakeassembly.go ClaimWakeAssembly.Build authority E2 PROPOSED |
| internal/host/claimwakeassembly.go ClaimWakeAssembly.readiness authority E2 PROPOSED E49: agent (service UID); named placement/program readback from retained root binding, durable unit intent and inode-conditional retirement, and readiness socket ownership. Service route fields are candidates only; final root launch rechecks independently. No no-census path added. Full always-running adoption/handover remains pending. PROPOSED E50: Agent (service UID): retain authority classification under R-E50-7 because this declaration gates work; independent final root validation does not make the agent gate a passive hint. registry:Socket@472,whole:Route@1132,whole:Route@653 923fed5500b805bcf31f5d53a5a5fce2859e7bedd68bd22c06b1087ec0a4e635 |
| internal/hostbox/rolelaunch.go RoleLaunchRequest.Validate hint E2 PROPOSED E49: root/server and agent/client; named placement/program readback from retained root binding, durable unit intent and inode-conditional retirement, and readiness socket ownership. Service route fields are candidates only; final root launch rechecks independently. No no-census path added. Full always-running adoption/handover remains pending. registry:Host@255,whole:Route@136,whole:Route@228,whole:Route@257 ee67fcc3a1d69ee0acb05a32446cc6cd80e97964fe095fc5c83ab4f85935b26c |
| @coverage internal/activator 5076bca3ca2fda41c60b04aa927ac99c8756291e6a9b1a1e2417fa35ee81ad24 |
| -@coverage internal/hostbox 0b04568917345170ab505138980c1cb4f0484c146837b81daa44fea12429af0b |
| +@coverage internal/hostbox af4783a0f42985bb5c3e218e8f29299a78c4b033f64bf7feb55b084306911c52 |
| internal/host/claimlaunch_authority.go ClaimLaunchRoot.waitReady authority E2 PROPOSED E49: root; delegated retained binding, exact unit identity and receipt-authorized retirement or per-connection kernel readiness. No service candidate grants authority. PROPOSED E50: Root checks the exact reserved host binding and endpoint before accepting the already verified invocation; no stale startup shape list decides whether readiness applies. 1f4fec01731cdaed47c201f92164d9e0843392959ffae870f45b1434d1a982ea |
| internal/host/claimunit_materialize.go claimUnitMaterializer.install authority E2 PROPOSED E49: root; delegated retained binding, exact unit identity and receipt-authorized retirement or per-connection kernel readiness. No service candidate grants authority. PROPOSED E50: Root journals explicit digest-authorized predecessor capture before stopping it and replacing only the recorded definition inode; replay never infers consent from route fields. PROPOSED E51: Root accepts an existing definition only when its persistent identity and exact bytes match the root materialization record. 2ec82ffc3e4f78bfa4fd5ca0111d402fa8b58a6d1ad89fcc088f8c7d12bfc146 |
| internal/host/claimunit_materialize.go claimUnitMaterializer.verify authority E2 PROPOSED E49: root; delegated retained binding, exact unit identity and receipt-authorized retirement or per-connection kernel readiness. No service candidate grants authority. PROPOSED E51: Root verifies persistent identity and bytes of a bound definition, allowing device renumbering only across different boots. 1e0b517da0c037f2a56611b80422ecaa05bb9c026c19b1093b3b1f4b2ec82246 |
| @@ -487,7 +487,7 @@ internal/host/claimunit_handover.go claimUnitMaterializer.handover authority E2 |
| # PROPOSED E49 |
| internal/host/claimunit_production.go bindProductionClaimUnits authority E2 PROPOSED E49: root; named placement/program readback from retained root binding, durable unit intent and inode-conditional retirement, and readiness socket ownership. Service route fields are candidates only; final root launch rechecks independently. No no-census path added. Full always-running adoption/handover remains pending. PROPOSED E50: Root installs retained-placement stop/retirement callbacks, resumes saved retirement before binding, and isolates each configuration/program/materialization failure. Initial binding reads occur outside host exclusion then local owner is checked inside. Readiness looks up the reserved host, not a startup list. PROPOSED E51: Root supplies the kernel boot reader to predecessor capture and stop; startup retirement replay remains ahead of per-unit binding. PROPOSED E52: Root wires peer-verified flat-socket capture and exact retained socket parking into canonical unit migration; no-census path unchanged. file:Read@1150 ffaf6904dece68748905b0ddba9ee94a12f50050402f6bef48f882a88c7ad934 |
| -@coverage internal/host bc0c5ed13d503fc6167b6821eae3aafe9dc965c59960031fdf5a01fe080879a1 |
| +@coverage internal/host 5036bd72c90bf1e8416ef729277fee1ac422f205d95c6e6e965ab9508a67bddc |
| # PROPOSED E50: root unit migration and R-E50 safety gates |
| internal/caps/converge.go Converge authority E2 PROPOSED E50: Root capability producer calls CheckHost before any filesystem or systemd effect. 7231c4e38b0093565412265a540af518b60fa8b0f0263502ce8845b2d7c2967d |
| @@ -534,7 +534,7 @@ internal/host/rootgeneration_kernel_linux.go generationCurrentFiles authority E2 |
| internal/instancectl/claimnetwork_linux.go ClaimUnits.restrictBoundNetwork authority E2 PROPOSED E51: Root pins/checks every cgroup ancestor and cgroup2 filesystem before attaching connect4/connect6/sendmsg4/sendmsg6 programs to the reserved unit. file:Fstat@841,file:Fstatfs@995,file:Open@412,file:Openat@662 fb674e9318edc3b6507e6894acb4ca13f750995a8711b67b9603b8a2b8568793 |
| internal/instancectl/claimnetwork_linux.go claimAttachConnectFilter authority E2 PROPOSED E51: Root loads and attaches its fixed socket-address program through BPF syscalls before app execution. Kernel retains programs until cgroup destruction. file:Syscall@580,file:Syscall@921 2bef9f8029b2bde1306b23954f5dd8333c2dfbd1d6511c9033d32cb8d020bccb |
| internal/instancectl/claimnetwork_linux.go claimConnectFilter authority E2 PROPOSED E51: Root-owned bytecode denies loopback including IPv4-mapped IPv6 connections and UDP destinations; replies to accepted connections do not take these hooks. 2be397090ac118cb12c48133b7cb19139fb558097f0a2d618adeb7388ef0ca70 |
| -internal/rootstate/persistent_linux.go PersistentIdentity authority E2 PROPOSED E51: Root reads descriptor stat, filesystem ID, export handle, boot ID and mount topology; unsupported proof capture refuses. Callers establish root-owned ancestry. PROPOSED E51: Root reads descriptor stat, filesystem ID, export handle, boot ID and mount topology; unsupported proof capture refuses. Callers establish root-owned ancestry. file:Fstat@174,file:Fstatfs@239,file:ReadFile@864,file:ReadFile@964,file:Readlink@1056,file:Statx@306 286c5bd9d7166a443102d1f5f2ef1c8bafeeb04ee82cdc7e99b0b85b019e7f9a |
| +internal/rootstate/persistent_linux.go PersistentIdentity authority E2 PROPOSED E51: Root reads descriptor stat, filesystem ID, export handle, boot ID and mount topology; unsupported proof capture refuses. Callers establish root-owned ancestry. PROPOSED E51: Root reads descriptor stat, filesystem ID, export handle, boot ID and mount topology; unsupported proof capture refuses. Callers establish root-owned ancestry. PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. b92208227b1f26d645547dee32d6e44743ae044d36ce8259c66e9bf1806fe0e5 |
| internal/rootstate/state.go FileIdentity.Same authority E2 PROPOSED E51: Persistent equality requires filesystem, handle, inode, ownership, mode and mount topology; device/mount numbers compare only on the same boot. PROPOSED E51: Persistent equality requires filesystem, handle, inode, ownership, mode and mount topology; device/mount numbers compare only on the same boot. PROPOSED E51 clarification: within one boot devices remain exact; mount IDs compare within one mount namespace. A root namespace clone still requires identical filesystem, handle, inode, ownership and mount topology. d32f66f3ca64f96d9ad85c2d4b67ee6b69fd113100552f83df75c4ece8d8881e |
| @coverage internal/host/units d5cb4f1dbfb14212e7d032e47b2c078f2e34cb5edfad2d41afa5e00618a4e8e8 |
| internal/host/claimunit_template.go const claimBoundTemplate authority E2 PROPOSED E51: Root-only bound-unit template is held inside the claim renderer; ordinary bootstrap never enumerates or installs it. data-literal@27 9ff0f84071165eb2d642596f1c4b28241f872fea99f7bf8e3d77f51785ad270a |
| @@ -545,3 +545,23 @@ internal/host/claimunit_socket_migration_linux.go parkProductionClaimUnitSocket |
| internal/host/claimunit_socket_migration_linux.go captureClaimUnitSocket authority E2 PROPOSED E52: Root opens the socket parent without following symlinks, then captures its nofollow identity. 01d268a02f01e3553f82e66be078a8c6f9dffa56272f2649766002aab69aaa2b |
| internal/host/claimunit_socket_migration_linux.go parkClaimUnitSocket authority E2 PROPOSED E52: Root delegates socket parking to no-replace rename with identity verification. 3bc751b4b76d46fff1e02e07540688c4c1bc018213b0ab284564fbcdc6b07729 |
| internal/host/claimunit_socket_migration_linux.go parkClaimUnitSocketWith authority E2 PROPOSED E52: Root requires exact same-boot socket identity before and after no-replace rename; on races preserves the replacement without overwriting a new source. Absence after independently verified predecessor stop permits replay; a replacement socket or archive refuses. c024aca802ed8a0009c68d992838cb636a03660c86cd58dab23e3f7b1e1b4ffa |
| +internal/host/claimroleassembly_linux.go ClaimRoleAssembly.PrepareState authority C,J1 root installation and fence authority; registration and recovery; agent RPC lock ordering PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. file:Fstatat@2113,file:Fstatat@3196,file:Open@1597,file:Openat@1884 c610385f0932f8aa5e1b79ad34ad7b10f654e03016d8cae453ebe0aa25769f65 |
| +internal/host/claimstop_writers_linux.go installationCoverage authority C,G,J4 root-input;kept=C,G;scope=protected-decision-only PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. file:Lstat@820,file:ReadDir@1171 5b17c0581b7010785b8ee5f1b591c41863eebf19defde576ea7b8dc11ab429c6 |
| +internal/host/claimstop_writers_linux.go rootInstallationCoverage authority C,G,J4 root-input;kept=C,G;scope=protected-decision-only PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. file:Lstat@545 73c8f9b564a42be1fe3b98ec4a2efcfdaac38b5aa7c86d7c5cc7f61e8b6a0fa3 |
| +internal/host/rootinventory.go ClaimRootHTTP.verifyInventoryPath authority C,J1 root installation and fence authority; registration and recovery; agent RPC lock ordering PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. file:Lstat@315 2ee5519ed6e8ff27bcfdb5db7748a71f4f331a271d84d7688835798792824a78 |
| +internal/host/rootinventory_state_linux.go ClaimRoleAssembly.prepareStateInventory authority C,J1 root installation and fence authority; registration and recovery; agent RPC lock ordering PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. file:Fstatat@1313,file:Fstatat@2304,file:Open@799,file:Openat@1086 c5355a26fa146eb0ddb4cc7c1e536de5781e171d52363a0be38ffb2074651a53 |
| +internal/hostbox/inventory_identity_linux.go CaptureInventoryIdentity authority E2,J2,J4 PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. file:Openat2@233 8e911a8f63b004b02b1951a8d3fb8bd1b3bcc38488c6ee49f5540a576718407a |
| +internal/hostbox/inventory_identity_linux.go checkedInventoryAncestry authority E2,J2,J4 PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. file:Fstat@610,file:Open@153,file:Openat@417 da0526c8520bb230c7a6b442dc7e2c3a8ed1913f943803a9ce95d6e7394e4579 |
| +internal/hostbox/inventory_identity_linux.go inventoryIdentityAt authority E2,J2,J4 PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. file:Fstat@458,file:Openat2@201 94eee53c3769eb92e3f0a80554c5125e2ab1e7b2cb7b11a4c67f0f7b1d9db3cb |
| +internal/hostbox/typedpark_linux.go TypedParker.CaptureArtifact authority C,J2,J4 root-input PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. file:Fstatat@440 8388e25cc65bd51896aef372e76dd9dd66947b3e67bbca0b76461b10742e17a2 |
| +internal/hostbox/typedpark_linux.go TypedParker.Park authority C,J2,J4 root-input PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. file:Fstat@1172,file:Fstat@1241,file:Fstatat@506 47bbbc88d213fceeaabdba59486390571e3ef414372faf11c55040c6f6cd38dc |
| +internal/hostbox/typedpark_linux.go TypedParker.VerifyEmptyDirectory authority C,J2,J4 root-input PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. file:Fstat@580,file:Openat@358,file:Readdirnames@759 d4fdca8354d98482e830657a175b638c36a373827c35f703968d0f248224fb30 |
| +internal/hostbox/typedpark_linux.go TypedParker.verifyDestination authority C,J2,J4 root-input PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. file:Fstatat@441 03ee911c7ad44442df0189fef46af32746dbb2ee816bac2601056ae6074e410c |
| +internal/hostbox/typedpark_linux.go artifactRootChecked authority E2,J2,J4 PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. file:ReadFile@71 0715c3aaced7fb63c0cf143da204358e3f93571b498ed5503d2766558f4a05be |
| +internal/hostbox/typedpark_linux.go openParent authority C,J2,J4 root-input PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. file:Openat2@86,file:Openat@423,file:Openat@845 179858428fa609635d0e11c8a80d8b6b120b53059ce6d416e9eee1663b0a401f |
| +internal/rootstate/persistent_linux.go ObjectIdentity authority E2,J2,J4 PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. file:Fstat@170,file:Fstatfs@235,file:ReadFile@1036,file:ReadFile@936,file:Readlink@1128,file:Statx@302 ea7cb48ea5452d613f57f1244d70c8570b9b8ac75e1667f070ebf0fc3f75c292 |
| +internal/host/rootinventory.go ClaimRootHTTP.verifyNewInventoryIdentities authority E2,J2,J4 PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. 32a7f0fc50c9d75194c4b613290da73b3919340c9905e18203ff7496dafe2da3 |
| +internal/hostbox/inventory_identity_linux.go VerifyInventoryIdentity authority E2,J2,J4 PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. f6ce5b5945f4e413078731bcd3ebb430a6fe9d522096bdeb87eef2b288ff504b |
| +internal/hostbox/journalservice.go journalRequestHash authority E2,J2,J4 PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. 99703d244e91b55863a034eb7b8b7ecae50c1ca092ef9dba9594fdaff088b6e3 |
| +internal/hostbox/typedpark_linux.go sameArtifactAt authority E2,J2,J4 PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. a0e3b899df85823cc4549d49f6577a7eff1549b7a0e4f1b53f4a23573ee4b948 |
| +internal/rootstate/authoritydir_linux.go OpenAuthorityDirectory authority E2,J2,J4 PROPOSED E53: Executing identity root; generation pins retain checked root ancestry and persistent filesystem/handle/inode/owner/topology evidence across boot; legacy device-only pins refuse a different boot. Tests: TestPlan14GenerationLegacyPinBootBoundary, TestRootRebootGenerationPinIdentityPhase1/2. file:Open@167,file:Openat@524 52a957fff0ec22967acc9627456cf2f5289a0563ffa94bf92f4ad59961073d7a |
diff --git a/internal/release/plan14_generation_boot_test.go b/internal/release/plan14_generation_boot_test.go| new file mode 100644 |
| index 000000000..03d81b032 |
| --- /dev/null |
| +++ b/internal/release/plan14_generation_boot_test.go |
| @@ -0,0 +1,39 @@ |
| +//go:build linux |
| + |
| +package release |
| + |
| +import ( |
| + "os" |
| + "path/filepath" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/rootstate" |
| +) |
| + |
| +func TestPlan14GenerationLegacyPinBootBoundary(t *testing.T) { |
| + path := filepath.Join(t.TempDir(), "app") |
| + if err := os.WriteFile(path, []byte("app"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + f, err := os.Open(path) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer f.Close() |
| + id, err := rootstate.Identity(f) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + current, err := rootstate.ObjectIdentity(f) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := generationFileIdentity(path, id, rootstate.FileIdentity{}, current.Boot); err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, boot := range []string{"", "old-boot"} { |
| + if err := generationFileIdentity(path, id, rootstate.FileIdentity{}, boot); err == nil { |
| + t.Fatal("legacy identity accepted across boot") |
| + } |
| + } |
| +} |
diff --git a/internal/release/plan14_generation_pins_test.go b/internal/release/plan14_generation_pins_test.go| index 85ee9c0e6..c7e56c079 100644 |
| --- a/internal/release/plan14_generation_pins_test.go |
| +++ b/internal/release/plan14_generation_pins_test.go |
| @@ -32,6 +32,12 @@ func generationPinFixture(t *testing.T, opt, account, app, version string) (*Roo |
| if err != nil { |
| t.Fatal(err) |
| } |
| + current, e := rootstate.ObjectIdentity(f) |
| + if e != nil { |
| + f.Close() |
| + t.Fatal(e) |
| + } |
| + b.Target.BootID = current.Boot |
| *dst, err = rootstate.Identity(f) |
| f.Close() |
| if err != nil { |
diff --git a/internal/release/rootgenerationpins.go b/internal/release/rootgenerationpins.go| index 6c2617d5d..a9bb46a42 100644 |
| --- a/internal/release/rootgenerationpins.go |
| +++ b/internal/release/rootgenerationpins.go |
| @@ -37,8 +37,13 @@ func (s RootGenerationPins) StagedPins(ctx context.Context, optDir string) (Stag |
| if r.Retirement != "" { |
| continue |
| } |
| - for path, expected := range map[string]rootstate.FileIdentity{b.Path: b.Executable, b.Tree: b.TreeIdentity} { |
| - if err := generationFileIdentity(path, expected); err != nil { |
| + for _, item := range []struct { |
| + path string |
| + expected, proof rootstate.FileIdentity |
| + }{ |
| + {b.Path, b.Executable, b.ExecutableProof}, {b.Tree, b.TreeIdentity, b.TreeProof}, |
| + } { |
| + if err := generationFileIdentity(item.path, item.expected, item.proof, b.Target.BootID); err != nil { |
| return nil, err |
| } |
| } |
| @@ -60,7 +65,31 @@ func (s RootGenerationPins) StagedPins(ctx context.Context, optDir string) (Stag |
| return pins, nil |
| } |
| -func generationFileIdentity(path string, expected rootstate.FileIdentity) error { |
| +func generationFileIdentity(path string, expected, proof rootstate.FileIdentity, boot string) error { |
| + if proof.Boot != "" { |
| + if proof.Boot != boot || proof.Device != expected.Device || proof.Inode != expected.Inode { |
| + return os.ErrPermission |
| + } |
| + var f *os.File |
| + var err error |
| + if proof.Mode&0170000 == 0040000 { |
| + f, err = rootstate.OpenAuthorityDirectory(path) |
| + } else { |
| + f, err = rootstate.OpenAuthorityFile(path) |
| + } |
| + if err != nil { |
| + return err |
| + } |
| + defer f.Close() |
| + actual, err := rootstate.PersistentIdentity(f) |
| + if err != nil { |
| + return err |
| + } |
| + if !proof.Same(actual) { |
| + return os.ErrPermission |
| + } |
| + return nil |
| + } |
| info, err := os.Lstat(path) |
| if err != nil { |
| return err |
| @@ -77,7 +106,11 @@ func generationFileIdentity(path string, expected rootstate.FileIdentity) error |
| if err != nil { |
| return err |
| } |
| - if actual != expected { |
| + observed, err := rootstate.ObjectIdentity(f) |
| + if err != nil { |
| + return err |
| + } |
| + if boot == "" || observed.Boot != boot || actual != expected { |
| return os.ErrPermission |
| } |
| return nil |
diff --git a/internal/rootstate/authoritydir_linux.go b/internal/rootstate/authoritydir_linux.go| new file mode 100644 |
| index 000000000..a52ff1831 |
| --- /dev/null |
| +++ b/internal/rootstate/authoritydir_linux.go |
| @@ -0,0 +1,43 @@ |
| +package rootstate |
| + |
| +import ( |
| + "os" |
| + "path/filepath" |
| + "strings" |
| + |
| + "golang.org/x/sys/unix" |
| +) |
| + |
| +func OpenAuthorityDirectory(path string) (*os.File, error) { |
| + if !filepath.IsAbs(path) || filepath.Clean(path) != path { |
| + return nil, os.ErrPermission |
| + } |
| + fd, err := unix.Open("/", unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC, 0) |
| + if err != nil { |
| + return nil, err |
| + } |
| + defer func() { |
| + if fd >= 0 { |
| + unix.Close(fd) |
| + } |
| + }() |
| + for _, part := range append([]string{""}, strings.Split(strings.TrimPrefix(path, "/"), "/")...) { |
| + if part != "" { |
| + if !component(part) { |
| + return nil, os.ErrPermission |
| + } |
| + next, err := unix.Openat(fd, part, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) |
| + if err != nil { |
| + return nil, err |
| + } |
| + unix.Close(fd) |
| + fd = next |
| + } |
| + if err := trusted(fd, 0, unix.S_IFDIR); err != nil { |
| + return nil, err |
| + } |
| + } |
| + f := os.NewFile(uintptr(fd), path) |
| + fd = -1 |
| + return f, nil |
| +} |
diff --git a/internal/rootstate/authoritydir_other.go b/internal/rootstate/authoritydir_other.go| new file mode 100644 |
| index 000000000..49a23da75 |
| --- /dev/null |
| +++ b/internal/rootstate/authoritydir_other.go |
| @@ -0,0 +1,7 @@ |
| +//go:build !linux |
| + |
| +package rootstate |
| + |
| +import "os" |
| + |
| +func OpenAuthorityDirectory(string) (*os.File, error) { return nil, os.ErrPermission } |
diff --git a/internal/rootstate/persistent_linux.go b/internal/rootstate/persistent_linux.go| index c3235131b..4f63d5820 100644 |
| --- a/internal/rootstate/persistent_linux.go |
| +++ b/internal/rootstate/persistent_linux.go |
| @@ -2,6 +2,7 @@ package rootstate |
| import ( |
| "encoding/hex" |
| + "errors" |
| "fmt" |
| "os" |
| "strconv" |
| @@ -11,6 +12,17 @@ import ( |
| ) |
| func PersistentIdentity(f *os.File) (FileIdentity, error) { |
| + id, err := ObjectIdentity(f) |
| + if err != nil { |
| + return FileIdentity{}, err |
| + } |
| + if id.UID != 0 || id.Mode&0022 != 0 || id.Handle == "" { |
| + return FileIdentity{}, os.ErrPermission |
| + } |
| + return id, nil |
| +} |
| + |
| +func ObjectIdentity(f *os.File) (FileIdentity, error) { |
| var id FileIdentity |
| fd := int(f.Fd()) |
| var st unix.Stat_t |
| @@ -26,10 +38,13 @@ func PersistentIdentity(f *os.File) (FileIdentity, error) { |
| return id, err |
| } |
| handle, mount, err := unix.NameToHandleAt(fd, "", unix.AT_EMPTY_PATH) |
| - if err != nil { |
| + if err != nil && !errors.Is(err, unix.EOPNOTSUPP) { |
| return id, err |
| } |
| - if st.Uid != 0 || st.Mode&0022 != 0 || len(handle.Bytes()) == 0 || sx.Mask&unix.STATX_MNT_ID == 0 || sx.Mnt_id != uint64(mount) { |
| + if err != nil { |
| + handle = unix.NewFileHandle(0, nil) |
| + } |
| + if sx.Mask&unix.STATX_MNT_ID == 0 || (err == nil && (len(handle.Bytes()) == 0 || sx.Mnt_id != uint64(mount))) { |
| return id, fmt.Errorf("persistent identity: uid=%d mode=%o handle=%d mount=%d/%d mask=%x: %w", st.Uid, st.Mode, len(handle.Bytes()), sx.Mnt_id, mount, sx.Mask, os.ErrPermission) |
| } |
| boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id") |
diff --git a/internal/rootstate/persistent_other.go b/internal/rootstate/persistent_other.go| index 0a646dcd2..de007ee65 100644 |
| --- a/internal/rootstate/persistent_other.go |
| +++ b/internal/rootstate/persistent_other.go |
| @@ -5,3 +5,5 @@ package rootstate |
| import "os" |
| func PersistentIdentity(*os.File) (FileIdentity, error) { return FileIdentity{}, os.ErrPermission } |
| + |
| +func ObjectIdentity(*os.File) (FileIdentity, error) { return FileIdentity{}, os.ErrPermission } |