carlos / platform Public

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>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 3dbc8758d6c6ab6387284a21c437cd152af84157 parent 9739b80
39 files changed, +1004 −116
  • internal/host/claimroleassembly_linux.go +8 −4
  • internal/host/claimstop_writers_linux.go +5 −2
  • internal/host/claimstop_writers_linux_test.go +1 −1
  • internal/host/generationpins_reboot_root_linux_test.go +158 −0
  • internal/host/inventory_identity_linux_test.go +65 −0
  • internal/host/inventory_reboot_root_linux_test.go +293 −0
  • internal/host/plan12_restore_linux_test.go +1 −1
  • internal/host/plan14_generation_parked_linux_test.go +3 −3
  • internal/host/plan14_generation_payload_files_linux_test.go +1 −1
  • internal/host/plan14_generation_payload_root_linux_test.go +1 −1
  • internal/host/plan14_generation_requirements_linux_test.go +1 −1
  • internal/host/plan14_generation_requirements_root_linux_test.go +1 −1
  • internal/host/plan9fix2_inventory_linux_test.go +2 −2
  • internal/host/rootgeneration_parked.go +1 −1
  • internal/host/rootgeneration_payload_linux.go +2 −27
  • internal/host/rootinventory.go +31 −26
  • internal/host/rootinventory_state_linux.go +7 −3
  • internal/hostbox/coldhandover.go +1 −1
  • internal/hostbox/coldhandover_test.go +1 −1
  • internal/hostbox/inventory_identity.go +13 −0
  • internal/hostbox/inventory_identity_linux.go +92 −0
  • internal/hostbox/inventory_identity_other.go +10 −0
  • internal/hostbox/inventory_identity_test.go +60 −0
  • internal/hostbox/journalservice.go +17 −0
  • internal/hostbox/plan14_cold_process_test.go +1 −1
  • internal/hostbox/plan9fix4_inventory_linux_test.go +1 −1
  • internal/hostbox/reconcile_adapter.go +1 −1
  • internal/hostbox/stopinventory.go +4 −1
  • internal/hostbox/typedpark_linux.go +25 −7
  • internal/hostbox/typedretire.go +11 −9
  • internal/provision/plan14_e_write_census_test.go +2 −1
  • internal/provision/testdata/plan14-root-reads-e.tsv +33 −13
  • internal/release/plan14_generation_boot_test.go +39 −0
  • internal/release/plan14_generation_pins_test.go +6 −0
  • internal/release/rootgenerationpins.go +37 −4
  • internal/rootstate/authoritydir_linux.go +43 −0
  • internal/rootstate/authoritydir_other.go +7 −0
  • internal/rootstate/persistent_linux.go +17 −2
  • internal/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 }