carlos / platform Public

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

R-E50 review fixes (per-unit isolation, startup retirement replay, drop-ins, no legacy systemctl, claimInputPolicy, census), separate always-running template, TCP isolation, reboot identity handling.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 1bc551a62de36ee8ee6b729e1e664af76dea9911 parent e336c73
34 files changed, +1053 −80
  • internal/host/claimunit_binding.go +3 −1
  • internal/host/claimunit_definition.go +15 −5
  • internal/host/claimunit_definition_test.go +33 −2
  • internal/host/claimunit_dispatch.go +1 −1
  • internal/host/claimunit_effective.go +18 −2
  • internal/host/claimunit_effective_test.go +4 −1
  • internal/host/claimunit_files_linux.go +2 −2
  • internal/host/claimunit_handover.go +3 −3
  • internal/host/claimunit_materialize.go +4 −4
  • internal/host/claimunit_migrate.go +2 −1
  • internal/host/claimunit_migrate_kernel.go +21 −1
  • internal/host/claimunit_migrate_kernel_test.go +29 −0
  • internal/host/claimunit_migrate_linux.go +12 −0
  • internal/host/claimunit_migrate_other.go +2 −0
  • internal/host/claimunit_predecessor_reboot_root_linux_test.go +92 −0
  • internal/host/claimunit_production.go +1 −1
  • internal/host/claimunit_reboot_root_linux_test.go +178 −0
  • internal/host/claimunit_template.go +64 −0
  • internal/host/plan14_bound_retirement_root_linux_test.go +59 −1
  • internal/host/plan14_installed_launch_root_linux_test.go +84 −3
  • internal/host/rootgeneration_kernel_linux.go +51 −17
  • internal/host/rootgenerations_launch.go +20 −6
  • internal/instancectl/claimexec.go +8 −0
  • internal/instancectl/claimnetwork_linux.go +114 −0
  • internal/instancectl/claimnetwork_linux_test.go +63 −0
  • internal/instancectl/claimnetwork_other.go +5 −0
  • internal/instancectl/claimunit.go +1 −0
  • internal/provision/plan14_e_write_census_test.go +2 −1
  • internal/provision/testdata/plan14-root-reads-e.tsv +35 −26
  • internal/release/rootgenerations.go +1 −0
  • internal/rootstate/persistent_linux.go +59 −0
  • internal/rootstate/persistent_other.go +7 −0
  • internal/rootstate/persistent_test.go +41 −0
  • internal/rootstate/state.go +19 −2

CI passed — run details

CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok  	amadan.net/carlos/platform.git/launchctx	0.012s
?   	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	306.219s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.984s
ok  	github.com/carlosframework/platform/cmd/moveprobe	94.031s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.777s
ok  	github.com/carlosframework/platform/internal/activator	250.422s
ok  	github.com/carlosframework/platform/internal/agentsock	0.037s
ok  	github.com/carlosframework/platform/internal/alerting	0.010s
ok  	github.com/carlosframework/platform/internal/analytics	0.034s
ok  	github.com/carlosframework/platform/internal/appconfig	3.090s
ok  	github.com/carlosframework/platform/internal/apperr	0.130s
ok  	github.com/carlosframework/platform/internal/applog	0.271s
ok  	github.com/carlosframework/platform/internal/awsinv	0.029s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.013s
ok  	github.com/carlosframework/platform/internal/boxid	1.404s
?   	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.033s
ok  	github.com/carlosframework/platform/internal/certwarm	0.008s
ok  	github.com/carlosframework/platform/internal/cgroup	0.785s
ok  	github.com/carlosframework/platform/internal/changelog	0.521s
ok  	github.com/carlosframework/platform/internal/clamd	0.057s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	395.387s
?   	github.com/carlosframework/platform/internal/controlname	[no test files]
?   	github.com/carlosframework/platform/internal/controlsock	[no test files]
ok  	github.com/carlosframework/platform/internal/dbdoor	5.817s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.016s
ok  	github.com/carlosframework/platform/internal/dbverify	0.008s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.007s
ok  	github.com/carlosframework/platform/internal/docsite	0.029s
ok  	github.com/carlosframework/platform/internal/domains	2.616s
ok  	github.com/carlosframework/platform/internal/doorkey	0.014s
ok  	github.com/carlosframework/platform/internal/economics	0.008s
ok  	github.com/carlosframework/platform/internal/edge	28.940s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.025s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.183s
ok  	github.com/carlosframework/platform/internal/gate	1.673s
ok  	github.com/carlosframework/platform/internal/genselect	0.025s
ok  	github.com/carlosframework/platform/internal/geo	0.052s
ok  	github.com/carlosframework/platform/internal/geoip	0.061s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.220s
ok  	github.com/carlosframework/platform/internal/host	150.654s
ok  	github.com/carlosframework/platform/internal/hostactors	0.813s
ok  	github.com/carlosframework/platform/internal/hostbox	4.085s
ok  	github.com/carlosframework/platform/internal/hostclaim	1.195s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.012s
ok  	github.com/carlosframework/platform/internal/hostexec	8.570s
ok  	github.com/carlosframework/platform/internal/hostfence	0.133s
ok  	github.com/carlosframework/platform/internal/hostfiles	11.926s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.017s
ok  	github.com/carlosframework/platform/internal/hostguard	1.130s
ok  	github.com/carlosframework/platform/internal/hostops	2.763s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.005s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.003s
ok  	github.com/carlosframework/platform/internal/hostrelocate	1.065s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.301s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.008s
ok  	github.com/carlosframework/platform/internal/infraview	0.006s
ok  	github.com/carlosframework/platform/internal/instancectl	5.297s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.946s
ok  	github.com/carlosframework/platform/internal/instancefs	0.009s
ok  	github.com/carlosframework/platform/internal/instancekey	0.005s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.018s
ok  	github.com/carlosframework/platform/internal/layout	2.363s
ok  	github.com/carlosframework/platform/internal/lease	9.942s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.017s
ok  	github.com/carlosframework/platform/internal/ledger	0.018s
ok  	github.com/carlosframework/platform/internal/mail	0.403s
ok  	github.com/carlosframework/platform/internal/maintenance	0.125s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.834s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.167s
ok  	github.com/carlosframework/platform/internal/metrics	0.009s
ok  	github.com/carlosframework/platform/internal/moves	24.101s
ok  	github.com/carlosframework/platform/internal/moves/archive	1.597s
ok  	github.com/carlosframework/platform/internal/objstore	0.357s
ok  	github.com/carlosframework/platform/internal/ops	0.448s
ok  	github.com/carlosframework/platform/internal/park	0.371s
ok  	github.com/carlosframework/platform/internal/privatefile	0.013s
ok  	github.com/carlosframework/platform/internal/provision	24.045s
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	7.672s
ok  	github.com/carlosframework/platform/internal/relay	7.305s
ok  	github.com/carlosframework/platform/internal/release	26.138s
ok  	github.com/carlosframework/platform/internal/rollout	4.929s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.012s
ok  	github.com/carlosframework/platform/internal/rootaws	0.015s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.016s
ok  	github.com/carlosframework/platform/internal/rootstate	0.029s
ok  	github.com/carlosframework/platform/internal/runs	0.559s
ok  	github.com/carlosframework/platform/internal/s3compat	0.006s
ok  	github.com/carlosframework/platform/internal/safefile	0.006s
ok  	github.com/carlosframework/platform/internal/schedule	3.088s
ok  	github.com/carlosframework/platform/internal/seeds	0.017s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	20.154s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.917s
ok  	github.com/carlosframework/platform/internal/status	0.151s
ok  	github.com/carlosframework/platform/internal/steering	0.011s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.252s
ok  	github.com/carlosframework/platform/internal/svcrole	0.014s
ok  	github.com/carlosframework/platform/internal/telemetry	0.014s
ok  	github.com/carlosframework/platform/internal/termfence	0.006s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.012s
ok  	github.com/carlosframework/platform/internal/unixdial	0.003s
ok  	github.com/carlosframework/platform/internal/updatewords	0.009s
ok  	github.com/carlosframework/platform/internal/usage	0.007s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.875s
=== 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 22:13:04 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DED39FBB0ACACB, 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/claimunit_binding.go b/internal/host/claimunit_binding.go
index cef141054..bc4690d6c 100644
--- a/internal/host/claimunit_binding.go
+++ b/internal/host/claimunit_binding.go
@@ -9,6 +9,7 @@ import (
"net"
"os"
"path/filepath"
+ "strconv"
"strings"
"github.com/carlosframework/platform/internal/hostguard"
@@ -59,7 +60,8 @@ func (s claimUnitShape) valid() bool {
}
if s.Addr != "" {
host, port, err := net.SplitHostPort(s.Addr)
- if err != nil || port == "" || net.ParseIP(host) == nil || !net.ParseIP(host).IsLoopback() {
+ n, e := strconv.Atoi(port)
+ if err != nil || e != nil || n < 1 || n > 65535 || net.ParseIP(host) == nil || !net.ParseIP(host).IsLoopback() {
return false
}
}
diff --git a/internal/host/claimunit_definition.go b/internal/host/claimunit_definition.go
index 7d7223ed6..6e105c5f0 100644
--- a/internal/host/claimunit_definition.go
+++ b/internal/host/claimunit_definition.go
@@ -1,8 +1,10 @@
package host
import (
+ "net"
"os"
"path/filepath"
+ "strconv"
"strings"
"github.com/carlosframework/platform/internal/instancectl"
@@ -13,10 +15,7 @@ func claimBoundUnitDefinition(s claimUnitShape) ([]byte, error) {
if !s.valid() || !claimHexDigest(s.Program) {
return nil, os.ErrPermission
}
- template, err := unitsFS.ReadFile("units/carlos-instance@.service")
- if err != nil {
- return nil, err
- }
+ template := claimBoundTemplate
body := strings.ReplaceAll(strings.ReplaceAll(string(template), "%i", instancectl.Escape(provision.InstanceID(s.Host))), "%I", provision.InstanceID(s.Host))
// With no live root gate, even a direct systemctl restart must execute no
// tenant code. The temporary reservation drop-in replaces this command.
@@ -29,7 +28,18 @@ func claimBoundUnitDefinition(s claimUnitShape) ([]byte, error) {
body += "\n[Service]\nBindPaths=" + provision.InstanceRunDir(s.Host) + ":/run/carlos\nReadWritePaths=/run/carlos\n"
}
if s.Addr != "" {
- body += "\n[Service]\nIPAddressAllow=localhost\n"
+ host, port, err := net.SplitHostPort(s.Addr)
+ n, e := strconv.Atoi(port)
+ if err != nil || e != nil || n < 1 || n > 65535 {
+ return nil, os.ErrPermission
+ }
+ family, bits := "ipv4", "/32"
+ if ip4 := net.ParseIP(host).To4(); ip4 != nil {
+ host = ip4.String()
+ } else {
+ family, bits = "ipv6", "/128"
+ }
+ body += "\n[Service]\nIPAddressAllow=" + host + bits + "\nSocketBindDeny=any\nSocketBindAllow=" + family + ":tcp:" + port + "\n"
}
return []byte(body), nil
}
diff --git a/internal/host/claimunit_definition_test.go b/internal/host/claimunit_definition_test.go
index 46570b851..8da9e6981 100644
--- a/internal/host/claimunit_definition_test.go
+++ b/internal/host/claimunit_definition_test.go
@@ -23,8 +23,14 @@ func TestPlan14BoundUnitDefinition(t *testing.T) {
if !strings.Contains(body, "ExecStart=/usr/bin/false\n") || strings.Contains(body, "ExecStart=/opt/carlos/carlos instance-exec") || !strings.Contains(body, "Restart=no\n") || !strings.Contains(body, "Slice="+instancectl.SliceName(shape.Host)+"\n") || strings.Contains(body, "%I") || strings.Contains(body, "%i") {
t.Fatal("definition permits unreserved execution or foreign placement")
}
- if strings.Contains(body, "IPAddressAllow=localhost") != tcp {
- t.Fatal("loopback permission escaped named TCP scope")
+ if strings.Contains(body, "IPAddressAllow=localhost") {
+ t.Fatal("broad loopback allowance")
+ }
+ if tcp && (!strings.Contains(body, "SocketBindDeny=any\n") || !strings.Contains(body, "SocketBindAllow=ipv4:tcp:8090\n")) {
+ t.Fatal("missing exact listener bind restriction")
+ }
+ if strings.Contains(body, "BindsTo=carlos-edge.service") {
+ t.Fatal("edge restart stops bound operator unit")
}
for _, setting := range []string{"ProtectControlGroups=yes", "PrivatePIDs=yes", "NoNewPrivileges=yes", "CapabilityBoundingSet=", "TemporaryFileSystem=/opt/carlos:ro"} {
if !strings.Contains(body, setting+"\n") {
@@ -64,3 +70,28 @@ func TestPlan14FlatBoundSocketDefinition(t *testing.T) {
}
}
}
+
+func TestPlan14BoundTemplateNotBootstrapped(t *testing.T) {
+ names, err := unitNames()
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, name := range names {
+ if name == "carlos-claim-bound.service" {
+ t.Fatal("claim template changes no-census bootstrap")
+ }
+ }
+}
+
+func TestPlan14MappedLoopbackDefinition(t *testing.T) {
+ _, _, s := plan14UnitBindingFixture(t, "console.service", "", "")
+ s.Program = strings.Repeat("a", 64)
+ s.Addr = "[::ffff:127.0.0.1]:8090"
+ raw, err := claimBoundUnitDefinition(s)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !strings.Contains(string(raw), "IPAddressAllow=127.0.0.1/32\n") || strings.Contains(string(raw), "IPAddressAllow=::ffff") {
+ t.Fatal("mapped address broadened IPv6 allowance")
+ }
+}
diff --git a/internal/host/claimunit_dispatch.go b/internal/host/claimunit_dispatch.go
index bd9fa3095..a72e23780 100644
--- a/internal/host/claimunit_dispatch.go
+++ b/internal/host/claimunit_dispatch.go
@@ -25,7 +25,7 @@ func (d claimUnitDispatch) selected(host string) (instancectl.ClaimUnits, error)
}
b, err := d.bindings.read(host)
if err == nil {
- u.BoundHost, u.BoundUnit = host, b.Shape.Unit
+ u.BoundHost, u.BoundUnit, u.BoundAddr = host, b.Shape.Unit, b.Shape.Addr
return u, nil
}
if !errors.Is(err, os.ErrNotExist) {
diff --git a/internal/host/claimunit_effective.go b/internal/host/claimunit_effective.go
index 3163cbd16..727e9df20 100644
--- a/internal/host/claimunit_effective.go
+++ b/internal/host/claimunit_effective.go
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"io"
+ "net"
"net/netip"
"os"
"path/filepath"
@@ -33,7 +34,19 @@ func claimEffectiveUnit(ctx context.Context, s claimUnitShape, ctl func(context.
return claimEffectiveRefusal{"unknown", os.ErrPermission}
}
scalar := map[string]string{"User": "carlos", "Group": "carlos", "NoNewPrivileges": "yes", "ProtectSystem": "strict", "ProtectHome": "yes", "PrivateTmp": "yes", "PrivateDevices": "yes", "PrivateIPC": "yes", "ProtectControlGroups": "yes", "ProtectProc": "invisible", "ProcSubset": "pid", "PrivatePIDs": "yes", "RestrictNamespaces": "yes", "CapabilityBoundingSet": "", "MemoryDenyWriteExecute": "yes", "ProtectKernelTunables": "yes", "ProtectKernelModules": "yes", "ProtectKernelLogs": "yes", "ProtectClock": "yes", "ProtectHostname": "yes", "RemoveIPC": "yes", "LockPersonality": "yes", "RestrictRealtime": "yes", "Restart": "no", "Slice": instancectl.SliceName(s.Host)}
- keys := []string{"FragmentPath", "DropInPaths", "NeedDaemonReload", "ExecStart", "IPAddressAllow", "IPAddressDeny"}
+ if s.Addr != "" {
+ host, port, e := net.SplitHostPort(s.Addr)
+ if e != nil {
+ return e
+ }
+ family := "ipv4"
+ if net.ParseIP(host).To4() == nil {
+ family = "ipv6"
+ }
+ scalar["SocketBindAllow"] = family + ":tcp:" + port
+ scalar["SocketBindDeny"] = "any"
+ }
+ keys := []string{"FragmentPath", "DropInPaths", "NeedDaemonReload", "ExecStart", "IPAddressAllow", "IPAddressDeny", "BindsTo", "PartOf"}
for key := range scalar {
keys = append(keys, key)
}
@@ -60,10 +73,13 @@ func claimEffectiveUnit(ctx context.Context, s claimUnitShape, ctl func(context.
return claimEffectiveRefusal{"unknown", os.ErrPermission}
}
for key, want := range scalar {
- if props[key] != want {
+ if props[key] != want && !(key == "SocketBindAllow" && props[key] == strings.Replace(want, ":tcp:", ":tcp", 1)) {
return claimEffectiveRefusal{"value:" + key + "=" + props[key], os.ErrPermission}
}
}
+ if strings.Contains(props["BindsTo"], "carlos-edge.service") || strings.Contains(props["PartOf"], "carlos-edge.service") {
+ return claimEffectiveRefusal{"edge-coupling", os.ErrPermission}
+ }
executable := "/usr/bin/false"
if props["DropInPaths"] != "" {
path := filepath.Join("/run/systemd/system", s.Unit+".d", "60-claim-launch.conf")
diff --git a/internal/host/claimunit_effective_test.go b/internal/host/claimunit_effective_test.go
index 6b7664119..71b04ee5c 100644
--- a/internal/host/claimunit_effective_test.go
+++ b/internal/host/claimunit_effective_test.go
@@ -27,7 +27,7 @@ func TestPlan14EffectiveBoundUnit(t *testing.T) {
}
values[k] = v
}
- for _, attack := range []string{"none", "DropInPaths", "User", "Group", "NoNewPrivileges", "ProtectSystem", "PrivatePIDs", "IPAddressAllow", "ExecStart", "NeedDaemonReload", "FragmentPath"} {
+ for _, attack := range []string{"none", "DropInPaths", "User", "Group", "NoNewPrivileges", "ProtectSystem", "PrivatePIDs", "IPAddressAllow", "ExecStart", "NeedDaemonReload", "FragmentPath", "SocketBindAllow", "SocketBindDeny", "BindsTo", "PartOf"} {
t.Run(attack, func(t *testing.T) {
ctl := func(_ context.Context, args ...string) (string, error) {
var out strings.Builder
@@ -35,6 +35,9 @@ func TestPlan14EffectiveBoundUnit(t *testing.T) {
value := values[key]
if key == attack {
value = "forged"
+ if key == "BindsTo" || key == "PartOf" {
+ value = "carlos-edge.service"
+ }
}
out.WriteString(key + "=" + value + "\n")
}
diff --git a/internal/host/claimunit_files_linux.go b/internal/host/claimunit_files_linux.go
index 32b1bfd31..e249b5acc 100644
--- a/internal/host/claimunit_files_linux.go
+++ b/internal/host/claimunit_files_linux.go
@@ -31,7 +31,7 @@ func (f claimUnitFiles) read(unit string) ([]byte, rootstate.FileIdentity, error
if len(raw) > rootstate.MaxRecord {
return nil, rootstate.FileIdentity{}, os.ErrPermission
}
- id, err := rootstate.Identity(file)
+ id, err := rootstate.PersistentIdentity(file)
return raw, id, err
}
func (f claimUnitFiles) create(unit string, body []byte) error {
@@ -79,7 +79,7 @@ func (f claimUnitFiles) write(unit string, id rootstate.FileIdentity, body []byt
if err != nil {
return err
}
- if current != id {
+ if !current.Same(id) {
return os.ErrPermission
}
flags = 0
diff --git a/internal/host/claimunit_handover.go b/internal/host/claimunit_handover.go
index 239dee805..208d9bff7 100644
--- a/internal/host/claimunit_handover.go
+++ b/internal/host/claimunit_handover.go
@@ -37,7 +37,7 @@ func (m claimUnitMaterializer) handover(ctx context.Context, old, next claimUnit
if err != nil {
return err
}
- if id != r.Identity || !bytes.Equal(raw, claimRetiredUnitDefinition(old)) {
+ if !id.Same(r.Identity) || !bytes.Equal(raw, claimRetiredUnitDefinition(old)) {
return os.ErrPermission
}
r = claimUnitMaterialization{Binding: next, Digest: claimUnitDigest(want), Prior: old, PriorIdentity: r.Identity}
@@ -52,7 +52,7 @@ func (m claimUnitMaterializer) handover(ctx context.Context, old, next claimUnit
if err != nil {
return err
}
- if r.Identity == (rootstate.FileIdentity{}) && id == r.PriorIdentity && bytes.Equal(raw, claimRetiredUnitDefinition(old)) {
+ if r.Identity == (rootstate.FileIdentity{}) && id.Same(r.PriorIdentity) && bytes.Equal(raw, claimRetiredUnitDefinition(old)) {
if err = m.idle(next.Shape.Host); err != nil {
return err
}
@@ -64,7 +64,7 @@ func (m claimUnitMaterializer) handover(ctx context.Context, old, next claimUnit
return err
}
}
- if !bytes.Equal(raw, want) || id == (rootstate.FileIdentity{}) || (r.Identity != (rootstate.FileIdentity{}) && r.Identity != id) {
+ if !bytes.Equal(raw, want) || id == (rootstate.FileIdentity{}) || (r.Identity != (rootstate.FileIdentity{}) && !r.Identity.Same(id)) {
return os.ErrPermission
}
r.Identity = id
diff --git a/internal/host/claimunit_materialize.go b/internal/host/claimunit_materialize.go
index 84b7568ef..a0aee374f 100644
--- a/internal/host/claimunit_materialize.go
+++ b/internal/host/claimunit_materialize.go
@@ -127,7 +127,7 @@ func (m claimUnitMaterializer) install(ctx context.Context, b claimUnitBinding)
if err != nil {
return err
}
- if !bytes.Equal(raw, want) || id == (rootstate.FileIdentity{}) || (r.Identity != (rootstate.FileIdentity{}) && r.Identity != id) {
+ if !bytes.Equal(raw, want) || id == (rootstate.FileIdentity{}) || (r.Identity != (rootstate.FileIdentity{}) && !r.Identity.Same(id)) {
return os.ErrPermission
}
r.Identity = id
@@ -150,7 +150,7 @@ func (m claimUnitMaterializer) verify(b claimUnitBinding) error {
if err != nil {
return err
}
- if id != r.Identity || !bytes.Equal(raw, want) || claimUnitDigest(raw) != r.Digest {
+ if !id.Same(r.Identity) || !bytes.Equal(raw, want) || claimUnitDigest(raw) != r.Digest {
return os.ErrPermission
}
return nil
@@ -178,14 +178,14 @@ func (m claimUnitMaterializer) retire(ctx context.Context, b claimUnitBinding, g
}
tombstone := claimRetiredUnitDefinition(b)
if r.Retiring && bytes.Equal(raw, tombstone) {
- if r.Retired && r.Identity != id {
+ if r.Retired && !r.Identity.Same(id) {
return os.ErrPermission
}
r.Identity = id
r.Retired = true
return m.save(r)
}
- if r.Retired || id != r.Identity || claimUnitDigest(raw) != r.Digest {
+ if r.Retired || !id.Same(r.Identity) || claimUnitDigest(raw) != r.Digest {
return os.ErrPermission
}
r.Retiring = true
diff --git a/internal/host/claimunit_migrate.go b/internal/host/claimunit_migrate.go
index 08f63e357..fd44f23c9 100644
--- a/internal/host/claimunit_migrate.go
+++ b/internal/host/claimunit_migrate.go
@@ -13,6 +13,7 @@ import (
type claimUnitPredecessor struct {
Unit, Invocation, Group string
+ Boot string `json:",omitempty"`
GroupIdentity rootstate.FileIdentity
}
@@ -55,7 +56,7 @@ func (m claimUnitMaterializer) finishMigration(ctx context.Context, r claimUnitM
if err != nil {
return err
}
- if id == r.Migration.Definition && claimUnitDigest(raw) == r.Migration.Digest {
+ if id.Same(r.Migration.Definition) && claimUnitDigest(raw) == r.Migration.Digest {
if err = m.stopPredecessor(ctx, r.Migration.Predecessor); err != nil {
return err
}
diff --git a/internal/host/claimunit_migrate_kernel.go b/internal/host/claimunit_migrate_kernel.go
index ab50043f9..aed8bf333 100644
--- a/internal/host/claimunit_migrate_kernel.go
+++ b/internal/host/claimunit_migrate_kernel.go
@@ -12,6 +12,7 @@ import (
type claimPredecessorKernel struct {
ctl func(context.Context, ...string) (string, error)
+ boot func() (string, error)
group func(string) (rootstate.FileIdentity, bool, error)
}
@@ -66,7 +67,14 @@ func (k claimPredecessorKernel) capture(ctx context.Context, b claimUnitBinding)
return p, os.ErrPermission
}
}
- return claimUnitPredecessor{Unit: b.Shape.Unit, Invocation: props["InvocationID"], Group: group, GroupIdentity: id}, nil
+ boot := ""
+ if k.boot != nil {
+ boot, err = k.boot()
+ if err != nil || boot == "" {
+ return p, os.ErrPermission
+ }
+ }
+ return claimUnitPredecessor{Unit: b.Shape.Unit, Invocation: props["InvocationID"], Group: group, GroupIdentity: id, Boot: boot}, nil
}
func (k claimPredecessorKernel) stop(ctx context.Context, p claimUnitPredecessor) error {
@@ -88,6 +96,18 @@ func (k claimPredecessorKernel) stop(ctx context.Context, p claimUnitPredecessor
if err != nil {
return err
}
+ if k.boot != nil {
+ boot, e := k.boot()
+ if e != nil || boot == "" || p.Boot == "" {
+ return os.ErrPermission
+ }
+ if boot != p.Boot {
+ if (props["ActiveState"] == "inactive" || props["ActiveState"] == "failed") && !populated {
+ return nil
+ }
+ return os.ErrPermission
+ }
+ }
if id != (rootstate.FileIdentity{}) && id != p.GroupIdentity {
return os.ErrPermission
}
diff --git a/internal/host/claimunit_migrate_kernel_test.go b/internal/host/claimunit_migrate_kernel_test.go
index 6c9cba806..8841e0d96 100644
--- a/internal/host/claimunit_migrate_kernel_test.go
+++ b/internal/host/claimunit_migrate_kernel_test.go
@@ -97,3 +97,32 @@ func TestPlan14OperatorUnitPredecessorStop(t *testing.T) {
})
}
}
+
+func TestPlan14PredecessorDeviceAcrossBoot(t *testing.T) {
+ p := claimUnitPredecessor{Unit: "console.service", Invocation: "before", Group: "system.slice/console.service", GroupIdentity: rootstate.FileIdentity{Device: 1, Inode: 2}, Boot: "boot-before"}
+ state := "inactive"
+ calls := 0
+ k := claimPredecessorKernel{
+ boot: func() (string, error) { return "boot-after", nil },
+ group: func(string) (rootstate.FileIdentity, bool, error) {
+ return rootstate.FileIdentity{Device: 8, Inode: 9}, state == "active", nil
+ },
+ ctl: func(_ context.Context, args ...string) (string, error) {
+ if args[0] != "show" {
+ calls++
+ return "", nil
+ }
+ return "ActiveState=" + state + "\nInvocationID=\nControlGroup=\nFragmentPath=/etc/systemd/system/console.service\nDropInPaths=\n", nil
+ },
+ }
+ if err := k.stop(t.Context(), p); err != nil {
+ t.Fatal("old boot empty group refused", err)
+ }
+ state = "active"
+ if err := k.stop(t.Context(), p); err == nil {
+ t.Fatal("old boot authorized stopping new population")
+ }
+ if calls != 0 {
+ t.Fatal("systemctl stop crossed a boot")
+ }
+}
diff --git a/internal/host/claimunit_migrate_linux.go b/internal/host/claimunit_migrate_linux.go
index 994bac35d..a29090b65 100644
--- a/internal/host/claimunit_migrate_linux.go
+++ b/internal/host/claimunit_migrate_linux.go
@@ -59,3 +59,15 @@ func claimPredecessorGroup(group string) (rootstate.FileIdentity, bool, error) {
}
return zero, false, os.ErrPermission
}
+
+func claimUnitBoot() (string, error) {
+ raw, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
+ if err != nil {
+ return "", err
+ }
+ boot := strings.TrimSpace(string(raw))
+ if boot == "" {
+ return "", os.ErrPermission
+ }
+ return boot, nil
+}
diff --git a/internal/host/claimunit_migrate_other.go b/internal/host/claimunit_migrate_other.go
index 102a3eafa..22772a7b8 100644
--- a/internal/host/claimunit_migrate_other.go
+++ b/internal/host/claimunit_migrate_other.go
@@ -10,3 +10,5 @@ import (
func claimPredecessorGroup(string) (rootstate.FileIdentity, bool, error) {
return rootstate.FileIdentity{}, false, os.ErrPermission
}
+
+func claimUnitBoot() (string, error) { return "", os.ErrPermission }
diff --git a/internal/host/claimunit_predecessor_reboot_root_linux_test.go b/internal/host/claimunit_predecessor_reboot_root_linux_test.go
new file mode 100644
index 000000000..7de2001da
--- /dev/null
+++ b/internal/host/claimunit_predecessor_reboot_root_linux_test.go
@@ -0,0 +1,92 @@
+//go:build linux && rootrunner
+
+package host
+
+import (
+ "context"
+ "encoding/json"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+const rebootPredecessorUnit = "plan14-e51-predecessor.service"
+
+func rebootPredecessorCtl(ctx context.Context, args ...string) (string, error) {
+ raw, err := exec.CommandContext(ctx, "systemctl", args...).CombinedOutput()
+ return strings.TrimSpace(string(raw)), err
+}
+
+func claimRebootPredecessorPhase1(t *testing.T) {
+ path := "/etc/systemd/system/" + rebootPredecessorUnit
+ file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
+ if err != nil {
+ t.Fatal(err)
+ }
+ _, err = file.WriteString("[Service]\nType=simple\nExecStart=/bin/sleep infinity\nKillMode=control-group\n")
+ file.Close()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if out, err := rebootPredecessorCtl(t.Context(), "daemon-reload"); err != nil {
+ t.Fatal(out, err)
+ }
+ if out, err := rebootPredecessorCtl(t.Context(), "start", rebootPredecessorUnit); err != nil {
+ t.Fatal(out, err)
+ }
+ _, in, shape := plan14UnitBindingFixture(t, rebootPredecessorUnit, "", "")
+ binding := claimUnitBinding{Schema: 1, Owner: unitBindingOwner(in), Shape: shape}
+ kernel := claimPredecessorKernel{ctl: rebootPredecessorCtl, group: claimPredecessorGroup, boot: claimUnitBoot}
+ predecessor, err := kernel.capture(t.Context(), binding)
+ if err != nil {
+ t.Fatal(err)
+ }
+ raw, _ := json.Marshal(predecessor)
+ if err := os.WriteFile(filepath.Join(claimUnitRebootDir, "predecessor.json"), raw, 0600); err != nil {
+ t.Fatal(err)
+ }
+ t.Logf("predecessor before boot=%s device=%d inode=%d", predecessor.Boot, predecessor.GroupIdentity.Device, predecessor.GroupIdentity.Inode)
+}
+
+func claimRebootPredecessorPhase2(t *testing.T) {
+ t.Cleanup(func() {
+ rebootPredecessorCtl(context.Background(), "stop", rebootPredecessorUnit)
+ os.Remove("/etc/systemd/system/" + rebootPredecessorUnit)
+ rebootPredecessorCtl(context.Background(), "daemon-reload")
+ })
+ var predecessor claimUnitPredecessor
+ raw, err := os.ReadFile(filepath.Join(claimUnitRebootDir, "predecessor.json"))
+ if err != nil || json.Unmarshal(raw, &predecessor) != nil {
+ t.Fatal(err)
+ }
+ boot, err := claimUnitBoot()
+ if err != nil || boot == predecessor.Boot {
+ t.Fatal("actual reboot required", err)
+ }
+ stops := 0
+ kernel := claimPredecessorKernel{group: claimPredecessorGroup, boot: claimUnitBoot, ctl: func(ctx context.Context, args ...string) (string, error) {
+ if args[0] == "stop" {
+ stops++
+ }
+ return rebootPredecessorCtl(ctx, args...)
+ }}
+ if err = kernel.stop(t.Context(), predecessor); err != nil {
+ t.Fatal("old boot empty predecessor", err)
+ }
+ if out, err := rebootPredecessorCtl(t.Context(), "start", rebootPredecessorUnit); err != nil {
+ t.Fatal(out, err)
+ }
+ invocation, err := rebootPredecessorCtl(t.Context(), "show", "-p", "InvocationID", "--value", rebootPredecessorUnit)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err = kernel.stop(t.Context(), predecessor); err == nil {
+ t.Fatal("old boot stopped replacement")
+ }
+ after, err := rebootPredecessorCtl(t.Context(), "show", "-p", "InvocationID", "--value", rebootPredecessorUnit)
+ if err != nil || invocation == "" || invocation != after || stops != 0 {
+ t.Fatal("replacement touched", invocation, after, stops, err)
+ }
+}
diff --git a/internal/host/claimunit_production.go b/internal/host/claimunit_production.go
index e3ba8c5db..daf1f0597 100644
--- a/internal/host/claimunit_production.go
+++ b/internal/host/claimunit_production.go
@@ -98,7 +98,7 @@ func bindProductionClaimUnits(ctx context.Context, h *ClaimRootWithLaunch, a *Cl
unit.BoundHost, unit.BoundUnit = host, binding.Shape.Unit
return unit.Gone(ctx, host, "")
}, state: materialState, read: files.read, create: files.create, replace: files.replace}
- predecessor := claimPredecessorKernel{ctl: base.Systemctl, group: claimPredecessorGroup}
+ predecessor := claimPredecessorKernel{ctl: base.Systemctl, group: claimPredecessorGroup, boot: claimUnitBoot}
material.predecessor, material.stopPredecessor = predecessor.capture, predecessor.stop
b.handover = material.handover
dispatch := claimUnitDispatch{base: base, bindings: b, shapes: productionClaimUnitShapes, effective: func(ctx context.Context, shape claimUnitShape) error {
diff --git a/internal/host/claimunit_reboot_root_linux_test.go b/internal/host/claimunit_reboot_root_linux_test.go
new file mode 100644
index 000000000..212483f0c
--- /dev/null
+++ b/internal/host/claimunit_reboot_root_linux_test.go
@@ -0,0 +1,178 @@
+//go:build linux && rootrunner
+
+package host
+
+import (
+ "context"
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/rootstate"
+ "golang.org/x/sys/unix"
+)
+
+const claimUnitRebootDir = "/var/lib/carlos-e51-unit-reboot"
+
+func claimUnitRebootFixture(t *testing.T, create bool) (claimUnitMaterializer, claimUnitBinding, claimUnitFiles) {
+ t.Helper()
+ dir := claimUnitRebootDir
+ if create {
+ if err := os.Mkdir(dir, 0755); err != nil {
+ t.Fatal(err)
+ }
+ }
+ state, err := rootstate.Open(filepath.Join(dir, "records"), create, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { state.Close() })
+ if create {
+ if err := os.Mkdir(filepath.Join(dir, "units"), 0755); err != nil {
+ t.Fatal(err)
+ }
+ }
+ files := claimUnitFiles{dir: filepath.Join(dir, "units")}
+ m := claimUnitMaterializer{state: state, read: files.read, create: files.create, replace: files.replace, idle: func(string) error { return nil }}
+ _, in, s := plan14UnitBindingFixture(t, "plan14-reboot.service", "", "")
+ s.Program = strings.Repeat("a", 64)
+ b := claimUnitBinding{Schema: 1, Owner: unitBindingOwner(in), Shape: s}
+ return m, b, files
+}
+
+func TestRootRebootClaimUnitDevicePhase1(t *testing.T) {
+ m, b, _ := claimUnitRebootFixture(t, true)
+ if err := m.install(t.Context(), b); err != nil {
+ t.Fatal(err)
+ }
+ r, err := m.load(b.Owner.Host)
+ if err != nil || r.Identity.Handle == "" {
+ t.Fatal("persistent unit identity", r, err)
+ }
+ raw, _ := json.Marshal(r.Identity)
+ if err := os.WriteFile(filepath.Join(claimUnitRebootDir, "before.json"), raw, 0600); err != nil {
+ t.Fatal(err)
+ }
+ dir, err := os.Open(filepath.Join(claimUnitRebootDir, "units"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ treeProof, err := rootstate.PersistentIdentity(dir)
+ dir.Close()
+ if err != nil {
+ t.Fatal(err)
+ }
+ binding := release.GenerationBinding{Path: filepath.Join(claimUnitRebootDir, "units", b.Shape.Unit), Tree: filepath.Join(claimUnitRebootDir, "units"), Executable: rootstate.FileIdentity{Device: r.Identity.Device, Inode: r.Identity.Inode}, TreeIdentity: rootstate.FileIdentity{Device: treeProof.Device, Inode: treeProof.Inode}, ExecutableProof: r.Identity, TreeProof: treeProof, Target: release.GenerationOwner{BootID: r.Identity.Boot}}
+ raw, _ = json.Marshal(binding)
+ if err := os.WriteFile(filepath.Join(claimUnitRebootDir, "generation.json"), raw, 0600); err != nil {
+ t.Fatal(err)
+ }
+ t.Logf("before boot=%s device=%d mount=%d", r.Identity.Boot, r.Identity.Device, r.Identity.Mount)
+ claimRebootPredecessorPhase1(t)
+}
+
+func TestRootRebootClaimUnitDevicePhase2(t *testing.T) {
+ m, b, files := claimUnitRebootFixture(t, false)
+ t.Cleanup(func() { os.RemoveAll(claimUnitRebootDir) })
+ var before rootstate.FileIdentity
+ raw, err := os.ReadFile(filepath.Join(claimUnitRebootDir, "before.json"))
+ if err != nil || json.Unmarshal(raw, &before) != nil {
+ t.Fatal(err)
+ }
+ _, current, err := files.read(b.Shape.Unit)
+ if err != nil || before.Boot == current.Boot {
+ t.Fatal("actual reboot required", err)
+ }
+ t.Logf("after boot=%s device=%d mount=%d", current.Boot, current.Device, current.Mount)
+ claimRebootPredecessorPhase2(t)
+ if err = m.verify(b); err != nil {
+ t.Fatal("persistent unit rejected after reboot", err)
+ }
+ r, err := m.load(b.Owner.Host)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Force numerical drift too, even if this reboot reused a number.
+ r.Identity.Device = current.Device + 1
+ r.Identity.Mount = current.Mount + 1
+ if err = m.save(r); err != nil {
+ t.Fatal(err)
+ }
+ if err = m.verify(b); err != nil {
+ t.Fatal("boot-local numbers treated as persistent", err)
+ }
+ var binding release.GenerationBinding
+ raw, err = os.ReadFile(filepath.Join(claimUnitRebootDir, "generation.json"))
+ if err != nil || json.Unmarshal(raw, &binding) != nil {
+ t.Fatal(err)
+ }
+ binding.Executable.Device = current.Device + 1
+ binding.TreeIdentity.Device = current.Device + 1
+ binding.ExecutableProof.Device = current.Device + 1
+ binding.TreeProof.Device = current.Device + 1
+ local, err := generationCurrentFiles(binding, rootstate.OpenTrustedFile)
+ if err != nil || local.Executable.Device != current.Device || local.TreeIdentity.Device != current.Device {
+ t.Fatal("generation remapping", local, err)
+ }
+ if _, err = generationTree(t.Context(), local); err != nil {
+ t.Fatal("generation tree census", err)
+ }
+ mounts, err := os.ReadFile("/proc/self/mountinfo")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err = generationMounts(mounts, local); err != nil {
+ t.Fatal("generation mount census", err)
+ }
+ // Same filesystem, same inode through a substituted bind mount still refuses.
+ unitPath := filepath.Join(files.dir, b.Shape.Unit)
+ if err = unix.Mount(unitPath, unitPath, "", unix.MS_BIND, ""); err != nil {
+ t.Fatal(err)
+ }
+ rejected := m.verify(b)
+ _, generationRejected := generationCurrentFiles(binding, rootstate.OpenTrustedFile)
+ if generationRejected == nil {
+ t.Error("generation accepted substituted mount")
+ }
+ unmount := unix.Unmount(unitPath, 0)
+ if rejected == nil || unmount != nil {
+ t.Fatal("substituted mount", rejected, unmount)
+ }
+ // A different filesystem cannot use identical file bytes as identity.
+ original, _, err := files.read(b.Shape.Unit)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err = unix.Mount("tmpfs", files.dir, "tmpfs", 0, "mode=0755"); err != nil {
+ t.Fatal(err)
+ }
+ writeErr := os.WriteFile(unitPath, original, 0644)
+ rejected = m.verify(b)
+ unmount = unix.Unmount(files.dir, 0)
+ if writeErr != nil || rejected == nil || unmount != nil {
+ t.Fatal("substituted filesystem", writeErr, rejected, unmount)
+ }
+ if err = m.verify(b); err != nil {
+ t.Fatal("restored unit", err)
+ }
+ if err = m.retire(t.Context(), b, func() error { return nil }); err != nil {
+ t.Fatal("cross-boot retirement", err)
+ }
+ old := b
+ old.Retiring = true
+ old.Retired = true
+ next := b
+ next.Owner.Stamp++
+ if err = m.handover(t.Context(), old, next); err != nil {
+ t.Fatal("successor handover", err)
+ }
+ if err = m.verify(next); err != nil {
+ t.Fatal(err)
+ }
+ if err = m.retire(context.Background(), b, func() error { return nil }); err == nil {
+ t.Fatal("old owner retired successor")
+ }
+}
diff --git a/internal/host/claimunit_template.go b/internal/host/claimunit_template.go
new file mode 100644
index 000000000..62de03963
--- /dev/null
+++ b/internal/host/claimunit_template.go
@@ -0,0 +1,64 @@
+package host
+
+const claimBoundTemplate = `[Unit]
+Description=carlos exec-backed instance %I
+Requires=data-instances.mount run-carlos-instances.mount run-carlos-logs.mount
+After=data-instances.mount run-carlos-instances.mount run-carlos-logs.mount
+
+[Service]
+Type=simple
+ExecStart=/opt/carlos/carlos instance-exec run %I
+ExecStopPost=/opt/carlos/carlos instance-exit run %I
+User=carlos
+Group=carlos
+Slice=carlos-instances-%i.slice
+Restart=no
+KillMode=mixed
+TimeoutStopSec=20
+SuccessExitStatus=129 130 141 143
+StandardOutput=file:/run/carlos/logs/%I.run.sock
+StandardError=inherit
+BindPaths=/data/instances/%I /run/carlos/instances/%I
+ReadWritePaths=/data/instances/%I /run/carlos/instances/%I
+WorkingDirectory=/data/instances/%I
+BindReadOnlyPaths=/run/carlos-control/exe/%I/app:/opt/carlos/app /run/carlos-control/exe/%I/live:/opt/carlos/live
+
+ProtectSystem=strict
+TemporaryFileSystem=/data:ro
+TemporaryFileSystem=/run/carlos:ro
+TemporaryFileSystem=/var/cache:ro
+TemporaryFileSystem=/var/lib:ro
+TemporaryFileSystem=/opt/carlos:ro
+BindReadOnlyPaths=/opt/carlos/carlos
+InaccessiblePaths=/etc/carlos /run/carlos-control
+ProtectHome=yes
+PrivateTmp=yes
+PrivateDevices=yes
+TemporaryFileSystem=/dev/shm:mode=1777,nosuid,nodev
+PrivateIPC=yes
+TemporaryFileSystem=/dev/mqueue:ro
+ProtectControlGroups=yes
+ProtectProc=invisible
+ProcSubset=pid
+PrivatePIDs=yes
+RestrictNamespaces=yes
+NoNewPrivileges=yes
+CapabilityBoundingSet=
+RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
+IPAddressDeny=localhost multicast 0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 169.254.0.0/16 169.254.169.254/32 172.16.0.0/12 192.0.0.0/24 192.168.0.0/16 198.18.0.0/15 240.0.0.0/4 64:ff9b::/96 fc00::/7 fd00:ec2::254/128 fe80::/10 fec0::/10
+BindReadOnlyPaths=-/run/systemd/resolve/resolv.conf:/etc/resolv.conf
+ProtectKernelTunables=yes
+ProtectKernelModules=yes
+ProtectKernelLogs=yes
+ProtectClock=yes
+ProtectHostname=yes
+RemoveIPC=yes
+LockPersonality=yes
+RestrictRealtime=yes
+MemoryDenyWriteExecute=yes
+SystemCallArchitectures=native
+
+TasksMax=256
+OOMScoreAdjust=500
+OOMPolicy=kill
+`
diff --git a/internal/host/plan14_bound_retirement_root_linux_test.go b/internal/host/plan14_bound_retirement_root_linux_test.go
index e9225df39..31850c698 100644
--- a/internal/host/plan14_bound_retirement_root_linux_test.go
+++ b/internal/host/plan14_bound_retirement_root_linux_test.go
@@ -5,6 +5,9 @@ package host
import (
"context"
"encoding/json"
+ "errors"
+ "github.com/carlosframework/platform/internal/instancectl"
+ "github.com/carlosframework/platform/internal/rootstate"
"os"
"path/filepath"
"testing"
@@ -25,7 +28,7 @@ func TestPlan14CombinedBoundRetirementRootRunner(t *testing.T) {
// Installation, admission and the completed receipt are fixture authority.
// The caller has already joined the real controller and stopped both real unit
// invocations. This adds the production retirement composition and registry.
-func plan14CombinedBoundRetirement(t *testing.T, dir string, f *launchFixture, m *claimUnitMaterializer) (func() error, func()) {
+func plan14CombinedBoundRetirement(t *testing.T, dir string, f *launchFixture, m *claimUnitMaterializer, startup bool) (func() error, func()) {
t.Helper()
t.Setenv("CARLOS_HOST_CLAIM", "true")
target := f.target
@@ -81,8 +84,59 @@ func plan14CombinedBoundRetirement(t *testing.T, dir string, f *launchFixture, m
root := ClaimRootHTTP{Stop: ClaimStopHTTP{Coordinator: ClaimStopCoordinator{Guard: f.root.Guard}}, Roots: map[string]string{"data": "/data"}, Reconcile: hostbox.ReconcileAdapter{Inventory: inventory, Registry: reg, Retirement: hostbox.TypedRetirement{JournalDir: journal}}, retireBoundUnit: func(ctx context.Context, t hostguard.FenceTarget) error {
return retireClaimBoundUnit(ctx, t, f.root.unitBindings, *m, f.root.Units.Gone)
}}
+ crashed := false
+ replace := m.replace
+ if startup {
+ m.replace = func(unit string, id rootstate.FileIdentity, raw []byte) error {
+ if !crashed {
+ crashed = true
+ return os.ErrClosed
+ }
+ return replace(unit, id, raw)
+ }
+ }
+ restarted := false
retire := func() error {
err := root.retire(t.Context(), target)
+ if startup && crashed && !restarted {
+ if !errors.Is(err, os.ErrClosed) {
+ t.Fatal("crash boundary", err)
+ }
+ partial, e := m.load(target.Host)
+ if e != nil || !partial.Retiring || partial.Retired {
+ t.Fatal("missing retirement intention", e)
+ }
+ oldOpen, oldMaterials, oldShapes := openClaimUnitBindings, openClaimUnitMaterializations, productionClaimUnitShapes
+ defer func() {
+ openClaimUnitBindings, openClaimUnitMaterializations, productionClaimUnitShapes = oldOpen, oldMaterials, oldShapes
+ }()
+ openClaimUnitBindings = func() (rootstate.Store, error) {
+ return rootstate.Open(filepath.Join(dir, "unit-bindings"), false, false)
+ }
+ openClaimUnitMaterializations = func() (rootstate.Store, error) {
+ return rootstate.Open(filepath.Join(dir, "unit-materializations"), false, false)
+ }
+ productionClaimUnitShapes = func() (map[string]claimUnitShape, error) { return nil, os.ErrPermission }
+ copyLaunch := *f.root
+ copyLaunch.Units = f.root.Units.(claimUnitDispatch).base
+ copyRoot := root
+ copyRoot.Stop.Coordinator.Kernel = claimUnitStopKernel{Units: copyLaunch.Units.(instancectl.ClaimUnits)}
+ wrapped := ClaimRootWithLaunch{Root: copyRoot, Launch: &copyLaunch}
+ assembly := &ClaimAssembly{}
+ if e = bindProductionClaimUnits(t.Context(), &wrapped, assembly); e != nil {
+ t.Fatal("startup crash replay", e)
+ }
+ defer assembly.Close(t.Context())
+ saved, e := wrapped.Launch.unitBindings.read(target.Host)
+ if e != nil || !saved.Retired {
+ t.Fatal("startup did not finish bound retirement", saved, e)
+ }
+ if _, _, e = claimLaunchPlacement(wrapped.Launch.Units, "ordinary.example"); e != nil {
+ t.Fatal("ordinary host blocked", e)
+ }
+ restarted = true
+ err = nil
+ }
records, e := generationRetirementRecords(t.Context(), &rollbackDisk{dir: journal})
if e == nil && len(records) == 1 && records[0].Complete {
return nil
@@ -137,3 +191,7 @@ func plan14CombinedBoundRetirement(t *testing.T, dir string, f *launchFixture, m
}
return retire, verify
}
+
+func TestPlan14BoundStartupRetirementRootRunner(t *testing.T) {
+ plan14InstalledClaimLaunch(t, "startup:combined:materialized:tcp")
+}
diff --git a/internal/host/plan14_installed_launch_root_linux_test.go b/internal/host/plan14_installed_launch_root_linux_test.go
index d3889b1e4..4e7edc329 100644
--- a/internal/host/plan14_installed_launch_root_linux_test.go
+++ b/internal/host/plan14_installed_launch_root_linux_test.go
@@ -11,6 +11,7 @@ import (
"fmt"
"net"
"net/http"
+ "net/url"
"os"
"os/exec"
"path/filepath"
@@ -53,7 +54,19 @@ func plan14InstalledTenant() {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
- if err := http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("running")) })); err != nil {
+ if err := http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if target := r.URL.Query().Get("dial"); target != "" {
+ conn, err := net.DialTimeout("tcp", target, 200*time.Millisecond)
+ if err != nil {
+ w.WriteHeader(http.StatusForbidden)
+ return
+ }
+ conn.Close()
+ w.WriteHeader(http.StatusNoContent)
+ return
+ }
+ w.Write([]byte("running"))
+ })); err != nil {
os.Exit(1)
}
}
@@ -88,7 +101,17 @@ func TestPlan14MigratedBoundRouteRootRunner(t *testing.T) {
}
}
+func TestPlan14BoundNetworkEdgeRootRunner(t *testing.T) {
+ for _, unit := range []string{"tcp", "socket-program", "backing-program"} {
+ t.Run(unit, func(t *testing.T) { plan14InstalledClaimLaunch(t, "isolation:materialized:"+unit) })
+ }
+}
+
func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) {
+ isolation := strings.HasPrefix(boundUnit, "isolation:")
+ boundUnit = strings.TrimPrefix(boundUnit, "isolation:")
+ startup := strings.HasPrefix(boundUnit, "startup:")
+ boundUnit = strings.TrimPrefix(boundUnit, "startup:")
combined := strings.HasPrefix(boundUnit, "combined:")
boundUnit = strings.TrimPrefix(boundUnit, "combined:")
migrated := strings.HasPrefix(boundUnit, "migrated:")
@@ -484,7 +507,7 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) {
}
f.req.Reservation.Inputs = inputs
f.claims.set(func(c *hostclaim.Claim) { c.Holder.WakeRetry.Attempt.LaunchInputs = inputs })
- f.root.generations = &rootGenerationSource{store: store, optDir: "/opt/carlos", pub: func() (ed25519.PublicKey, error) { return pub, nil }, open: func() (rootstate.Store, error) { return rootstate.Open(filepath.Join(dir, "generations"), true, false) }, openFile: rootstate.OpenTrustedFile}
+ f.root.generations = &rootGenerationSource{store: store, optDir: "/opt/carlos", pub: func() (ed25519.PublicKey, error) { return pub, nil }, open: func() (rootstate.Store, error) { return rootstate.Open(filepath.Join(dir, "generations"), true, false) }, openFile: rootstate.OpenTrustedFile, persistent: rootstate.PersistentIdentity}
lock := &release.ReleaseExclusion{Open: func() (rootstate.Store, error) { return rootstate.Open(filepath.Join(dir, "release"), true, false) }}
checkSibling := func() {}
if programMode {
@@ -528,6 +551,64 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) {
if resp.StatusCode != 200 {
t.Fatal(resp.Status)
}
+ if isolation {
+ if tcpMode {
+ siblingTransport := &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
+ return (&net.Dialer{}).DialContext(ctx, "unix", filepath.Join(provision.InstanceRunDir("sibling.example"), "app.sock"))
+ }}
+ siblingClient := &http.Client{Transport: siblingTransport, Timeout: 3 * time.Second}
+ response, err := siblingClient.Get("http://sibling/?dial=" + url.QueryEscape(endpoint))
+ if err != nil {
+ t.Fatal("sibling isolation probe", err)
+ }
+ response.Body.Close()
+ siblingTransport.CloseIdleConnections()
+ if response.StatusCode != http.StatusForbidden {
+ t.Fatal("sibling reached bound TCP upstream", response.StatusCode)
+ }
+ }
+ for _, name := range []string{"console", "sibling"} {
+ listener, err := net.Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ t.Fatal(err)
+ }
+ conn, err := net.DialTimeout("tcp", listener.Addr().String(), time.Second)
+ if err != nil {
+ t.Fatal("control listener unavailable", err)
+ }
+ conn.Close()
+ for _, address := range []string{listener.Addr().String(), "[::ffff:127.0.0.1]:" + strings.Split(listener.Addr().String(), ":")[1]} {
+ response, err := client.Get("http://tenant/?dial=" + url.QueryEscape(address))
+ if err != nil {
+ t.Fatal(err)
+ }
+ response.Body.Close()
+ if response.StatusCode != http.StatusForbidden {
+ t.Fatal("bound tenant reached", name, address, response.StatusCode)
+ }
+ }
+ listener.Close()
+ }
+ before, err := ctl(t.Context(), "show", "-p", "InvocationID", "--value", unit)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := ctl(t.Context(), "restart", "carlos-edge.service"); err != nil {
+ t.Fatal(err)
+ }
+ after, err := ctl(t.Context(), "show", "-p", "InvocationID", "--value", unit)
+ if err != nil || before == "" || after != before {
+ t.Fatal("edge restart changed bound invocation", before, after, err)
+ }
+ response, err := client.Get("http://tenant/")
+ if err != nil {
+ t.Fatal("edge restart stopped bound service", err)
+ }
+ response.Body.Close()
+ if response.StatusCode != http.StatusOK {
+ t.Fatal(response.Status)
+ }
+ }
checkSibling()
if materialized {
b, err := f.root.unitBindings.read(host)
@@ -665,7 +746,7 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) {
}
verifyCombined := func() {}
if combined {
- retireUnit, verifyCombined = plan14CombinedBoundRetirement(t, dir, f, material)
+ retireUnit, verifyCombined = plan14CombinedBoundRetirement(t, dir, f, material, startup)
}
original, err := os.ReadFile(unitPath)
if err != nil {
diff --git a/internal/host/rootgeneration_kernel_linux.go b/internal/host/rootgeneration_kernel_linux.go
index cfb22640a..53ab37962 100644
--- a/internal/host/rootgeneration_kernel_linux.go
+++ b/internal/host/rootgeneration_kernel_linux.go
@@ -500,9 +500,11 @@ func productionGenerationAbsence(ctx context.Context, r release.GenerationReserv
if r.Binding.Path != release.VersionPath(release.OptDir(), r.Binding.Binding.Account, r.Binding.Version, r.Binding.Binding.App) {
return "", os.ErrPermission
}
- if err := generationRetainedFiles(r.Binding, rootstate.OpenTrustedFile); err != nil {
+ current, err := generationCurrentFiles(r.Binding, rootstate.OpenTrustedFile)
+ if err != nil {
return "", err
}
+ r.Binding = current
if err := generationGroupGone(ctx, "/sys/fs/cgroup", r); err != nil {
return "", err
}
@@ -539,32 +541,64 @@ func generationGroupGone(ctx context.Context, root string, r release.GenerationR
if r.Process == (release.GenerationProcess{}) || r.Process.Group != group {
return os.ErrPermission
}
- _, err := scanStopCgroups(ctx, root, []hostbox.StopCgroupIdentity{{RelativePath: group, Device: r.Process.GroupIdentity.Device, Inode: r.Process.GroupIdentity.Inode}})
+ boot, err := (generationKernel{proc: "/proc"}).boot()
+ if err != nil {
+ return err
+ }
+ if r.Binding.Target.BootID != "" && r.Binding.Target.BootID != boot {
+ _, err := scanRebootCgroups(ctx, root, []hostbox.StopCgroupIdentity{{RelativePath: group, Device: r.Process.GroupIdentity.Device, Inode: r.Process.GroupIdentity.Inode}}, r.Binding.Target.BootID, boot)
+ return err
+ }
+ _, err = scanStopCgroups(ctx, root, []hostbox.StopCgroupIdentity{{RelativePath: group, Device: r.Process.GroupIdentity.Device, Inode: r.Process.GroupIdentity.Inode}})
return err
}
func generationRetainedFiles(b release.GenerationBinding, open func(string) (*os.File, error)) error {
+ _, err := generationCurrentFiles(b, open)
+ return err
+}
+
+func generationCurrentFiles(b release.GenerationBinding, open func(string) (*os.File, error)) (release.GenerationBinding, error) {
f, err := open(b.Path)
if err != nil {
- return err
- }
- executable, err := rootstate.Identity(f)
- f.Close()
- if err != nil || executable != b.Executable {
- return os.ErrPermission
+ return b, err
}
- // The file opener checked every ancestor. The caller holds release
- // exclusion, so another root release writer cannot replace this directory.
+ defer f.Close()
d, err := os.Open(b.Tree)
if err != nil {
- return err
- }
- tree, err := rootstate.Identity(d)
- d.Close()
- if err != nil || tree != b.TreeIdentity {
- return os.ErrPermission
+ return b, err
+ }
+ defer d.Close()
+ for _, item := range []struct {
+ file *os.File
+ identity *rootstate.FileIdentity
+ proof rootstate.FileIdentity
+ }{
+ {f, &b.Executable, b.ExecutableProof}, {d, &b.TreeIdentity, b.TreeProof},
+ } {
+ current, err := rootstate.Identity(item.file)
+ if err != nil {
+ return b, err
+ }
+ if item.proof.Boot == "" {
+ if current != *item.identity {
+ return b, os.ErrPermission
+ }
+ if b.Target.BootID != "" {
+ boot, err := (generationKernel{proc: "/proc"}).boot()
+ if err != nil || boot != b.Target.BootID {
+ return b, os.ErrPermission
+ }
+ }
+ } else {
+ observed, err := rootstate.PersistentIdentity(item.file)
+ if err != nil || !item.proof.Same(observed) {
+ return b, os.ErrPermission
+ }
+ }
+ *item.identity = current
}
- return nil
+ return b, nil
}
func generationTree(ctx context.Context, b release.GenerationBinding) (map[rootstate.FileIdentity]bool, error) {
diff --git a/internal/host/rootgenerations_launch.go b/internal/host/rootgenerations_launch.go
index 62ca32720..33e4fb5d2 100644
--- a/internal/host/rootgenerations_launch.go
+++ b/internal/host/rootgenerations_launch.go
@@ -20,11 +20,12 @@ import (
)
type rootGenerationSource struct {
- store release.Store
- optDir string
- pub func() (ed25519.PublicKey, error)
- open func() (rootstate.Store, error)
- openFile func(string) (*os.File, error)
+ store release.Store
+ optDir string
+ pub func() (ed25519.PublicKey, error)
+ open func() (rootstate.Store, error)
+ openFile func(string) (*os.File, error)
+ persistent func(*os.File) (rootstate.FileIdentity, error)
}
func productionRootGenerations(store release.Store, optDir string) *rootGenerationSource {
@@ -33,7 +34,7 @@ func productionRootGenerations(store release.Store, optDir string) *rootGenerati
return release.ParsePublicKey(os.Getenv("CARLOS_RELEASE_PUBKEY"), "CARLOS_RELEASE_PUBKEY")
},
open: func() (rootstate.Store, error) { return rootstate.Open(release.RootGenerationsDir, true, false) },
- openFile: rootstate.OpenTrustedFile}
+ openFile: rootstate.OpenTrustedFile, persistent: rootstate.PersistentIdentity}
}
// resolve runs under release and host exclusion. The request nominates a build;
@@ -114,6 +115,19 @@ func (s *rootGenerationSource) resolve(ctx context.Context, h *ClaimLaunchRoot,
Installation: rootHistoryInstallation(in), Record: in.Record, RecordETag: etag,
Attempt: q.Reservation.ID, Inputs: q.Reservation.Inputs, Invocation: q.Reservation.InvocationID, ControllerPID: peer.PID, ControllerStart: peer.ProcessStart,
Version: version, Digest: q.Spec.Digest, Path: q.Executable, Tree: tree, Executable: id, TreeIdentity: treeID}
+ if s.persistent != nil {
+ b.ExecutableProof, err = s.persistent(f)
+ if err != nil {
+ return release.GenerationBinding{}, err
+ }
+ b.TreeProof, err = s.persistent(d)
+ if err != nil {
+ return release.GenerationBinding{}, err
+ }
+ if b.ExecutableProof.Boot != peer.BootID || b.TreeProof.Boot != peer.BootID {
+ return release.GenerationBinding{}, os.ErrPermission
+ }
+ }
unit, group, err := claimLaunchPlacement(h.Units, t.Host)
if err != nil || group != q.Reservation.Group.RelativePath {
return release.GenerationBinding{}, os.ErrPermission
diff --git a/internal/instancectl/claimexec.go b/internal/instancectl/claimexec.go
index f7009dcd6..b3163c049 100644
--- a/internal/instancectl/claimexec.go
+++ b/internal/instancectl/claimexec.go
@@ -131,6 +131,14 @@ func (g *claimExecGate) authorize(ctx context.Context, u ClaimUnits, s *ClaimUni
if err = u.Same(ctx, *s); err != nil {
return err
}
+ if u.BoundAddr != "" {
+ if err = u.restrictBoundNetwork(*s); err != nil {
+ return err
+ }
+ if err = u.Same(ctx, *s); err != nil {
+ return err
+ }
+ }
// One connection, one permit. Closing/restarting root cannot replay it.
return json.NewEncoder(conn).Encode(g.permit)
}
diff --git a/internal/instancectl/claimnetwork_linux.go b/internal/instancectl/claimnetwork_linux.go
new file mode 100644
index 000000000..c8eb719cb
--- /dev/null
+++ b/internal/instancectl/claimnetwork_linux.go
@@ -0,0 +1,114 @@
+package instancectl
+
+import (
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "unsafe"
+
+ "golang.org/x/sys/unix"
+)
+
+type claimBPFInsn struct {
+ Code, Registers uint8
+ Offset int16
+ Immediate int32
+}
+
+func claimConnectFilter(ipv6 bool) []claimBPFInsn {
+ // bpf_sock_addr has network-order user_ip4 at 4 and user_ip6 at 8.
+ load := func(offset int16) []claimBPFInsn { return []claimBPFInsn{{0x61, 0x10, offset, 0}, {0xdc, 0, 0, 32}} }
+ var p []claimBPFInsn
+ if !ipv6 {
+ p = append(p, load(4)...)
+ p = append(p, claimBPFInsn{0x54, 0, 0, -16777216}, claimBPFInsn{0x15, 0, 2, 0x7f000000})
+ } else {
+ p = append(p, load(8)...)
+ p = append(p, claimBPFInsn{0x55, 0, 14, 0})
+ p = append(p, load(12)...)
+ p = append(p, claimBPFInsn{0x55, 0, 11, 0})
+ p = append(p, load(16)...)
+ p = append(p, claimBPFInsn{0x15, 0, 4, 0xffff}, claimBPFInsn{0x55, 0, 7, 0})
+ p = append(p, load(20)...)
+ p = append(p, claimBPFInsn{0x15, 0, 6, 1})
+ p = append(p, load(20)...)
+ p = append(p, claimBPFInsn{0x54, 0, 0, -16777216}, claimBPFInsn{0x15, 0, 2, 0x7f000000})
+ }
+ return append(p, claimBPFInsn{0xb7, 0, 0, 1}, claimBPFInsn{0x95, 0, 0, 0}, claimBPFInsn{0xb7, 0, 0, 0}, claimBPFInsn{0x95, 0, 0, 0})
+}
+
+func claimAttachConnectFilter(group int, kind uint32, ipv6 bool) error {
+ p := claimConnectFilter(ipv6)
+ license := []byte("GPL\x00")
+ attr := struct {
+ Type, Count uint32
+ Instructions, License uint64
+ LogLevel, LogSize uint32
+ LogBuffer uint64
+ Version, Flags uint32
+ Name [16]byte
+ IfIndex, Attach uint32
+ }{Type: unix.BPF_PROG_TYPE_CGROUP_SOCK_ADDR, Count: uint32(len(p)), Instructions: uint64(uintptr(unsafe.Pointer(&p[0]))), License: uint64(uintptr(unsafe.Pointer(&license[0]))), Attach: kind}
+ fd, _, errno := unix.Syscall(unix.SYS_BPF, unix.BPF_PROG_LOAD, uintptr(unsafe.Pointer(&attr)), unsafe.Sizeof(attr))
+ runtime.KeepAlive(p)
+ runtime.KeepAlive(license)
+ if errno != 0 {
+ return errno
+ }
+ defer unix.Close(int(fd))
+ attach := struct{ Target, Program, Kind, Flags uint32 }{uint32(group), uint32(fd), kind, unix.BPF_F_ALLOW_MULTI}
+ _, _, errno = unix.Syscall(unix.SYS_BPF, unix.BPF_PROG_ATTACH, uintptr(unsafe.Pointer(&attach)), unsafe.Sizeof(attach))
+ if errno != 0 {
+ return errno
+ }
+ return nil
+}
+
+func (u ClaimUnits) restrictBoundNetwork(s ClaimUnitStarted) error {
+ _, group, err := u.Placement(u.BoundHost)
+ if err != nil || s.ControlGroup != group || s.Unit != u.BoundUnit || u.CgroupRoot != "/sys/fs/cgroup" {
+ return os.ErrPermission
+ }
+ // Pin each directory and check every ancestor before attaching. No program
+ // is pinned on disk; the kernel retains it until this cgroup is destroyed.
+ fd, err := unix.Open("/", unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
+ if err != nil {
+ return err
+ }
+ defer func() { unix.Close(fd) }()
+ for _, part := range strings.Split(strings.TrimPrefix(filepath.Join(u.CgroupRoot, group), "/"), "/") {
+ next, e := unix.Openat(fd, part, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
+ if e != nil {
+ return e
+ }
+ unix.Close(fd)
+ fd = next
+ var st unix.Stat_t
+ if e = unix.Fstat(fd, &st); e != nil {
+ return e
+ }
+ if st.Uid != 0 || st.Mode&0022 != 0 {
+ return os.ErrPermission
+ }
+ }
+ var fs unix.Statfs_t
+ if err = unix.Fstatfs(fd, &fs); err != nil {
+ return err
+ }
+ if fs.Type != unix.CGROUP2_SUPER_MAGIC {
+ return os.ErrPermission
+ }
+ for _, hook := range []struct {
+ kind uint32
+ ipv6 bool
+ }{
+ {unix.BPF_CGROUP_INET4_CONNECT, false}, {unix.BPF_CGROUP_INET6_CONNECT, true},
+ {unix.BPF_CGROUP_UDP4_SENDMSG, false}, {unix.BPF_CGROUP_UDP6_SENDMSG, true},
+ } {
+ if err = claimAttachConnectFilter(fd, hook.kind, hook.ipv6); err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/internal/instancectl/claimnetwork_linux_test.go b/internal/instancectl/claimnetwork_linux_test.go
new file mode 100644
index 000000000..f25281e03
--- /dev/null
+++ b/internal/instancectl/claimnetwork_linux_test.go
@@ -0,0 +1,63 @@
+package instancectl
+
+import (
+ "encoding/binary"
+ "net"
+ "testing"
+)
+
+func TestPlan14BoundConnectFilter(t *testing.T) {
+ for _, test := range []struct {
+ ip string
+ ipv6, allow bool
+ }{
+ {"127.0.0.1", false, false}, {"127.0.0.53", false, false}, {"127.23.4.5", false, false},
+ {"1.1.1.1", false, true}, {"::1", true, false}, {"::ffff:127.0.0.1", true, false},
+ {"2001:4860:4860::8888", true, true}, {"::ffff:1.1.1.1", true, true},
+ } {
+ t.Run(test.ip, func(t *testing.T) {
+ var ctx [24]byte
+ ip := net.ParseIP(test.ip)
+ if test.ipv6 {
+ copy(ctx[8:], ip.To16())
+ } else {
+ copy(ctx[4:], ip.To4())
+ }
+ p := claimConnectFilter(test.ipv6)
+ var reg uint32
+ for pc, steps := 0, 0; ; pc, steps = pc+1, steps+1 {
+ if pc < 0 || pc >= len(p) || steps > 100 {
+ t.Fatal("invalid program control flow", pc)
+ }
+ i := p[pc]
+ switch i.Code {
+ case 0x61:
+ reg = binary.NativeEndian.Uint32(ctx[i.Offset : i.Offset+4])
+ case 0xdc:
+ var b [4]byte
+ binary.NativeEndian.PutUint32(b[:], reg)
+ reg = binary.BigEndian.Uint32(b[:])
+ case 0x54:
+ reg &= uint32(i.Immediate)
+ case 0x15:
+ if reg == uint32(i.Immediate) {
+ pc += int(i.Offset)
+ }
+ case 0x55:
+ if reg != uint32(i.Immediate) {
+ pc += int(i.Offset)
+ }
+ case 0xb7:
+ reg = uint32(i.Immediate)
+ case 0x95:
+ if (reg == 1) != test.allow {
+ t.Fatal("wrong network verdict", reg)
+ }
+ return
+ default:
+ t.Fatal("unknown instruction", i)
+ }
+ }
+ })
+ }
+}
diff --git a/internal/instancectl/claimnetwork_other.go b/internal/instancectl/claimnetwork_other.go
new file mode 100644
index 000000000..4784f40f5
--- /dev/null
+++ b/internal/instancectl/claimnetwork_other.go
@@ -0,0 +1,5 @@
+//go:build !linux
+
+package instancectl
+
+func (u ClaimUnits) restrictBoundNetwork(ClaimUnitStarted) error { return ErrClaimUnit }
diff --git a/internal/instancectl/claimunit.go b/internal/instancectl/claimunit.go
index b4a278e4f..5d099ab3d 100644
--- a/internal/instancectl/claimunit.go
+++ b/internal/instancectl/claimunit.go
@@ -24,6 +24,7 @@ import (
// guard; it adds no request surface of its own.
type ClaimUnits struct {
BoundHost, BoundUnit string
+ BoundAddr string
Systemctl func(context.Context, ...string) (string, error)
CgroupRoot string
ExeRoot string
diff --git a/internal/provision/plan14_e_write_census_test.go b/internal/provision/plan14_e_write_census_test.go
index 36244a77c..b4c2cebe6 100644
--- a/internal/provision/plan14_e_write_census_test.go
+++ b/internal/provision/plan14_e_write_census_test.go
@@ -2,12 +2,13 @@ package provision
// Track E overlays the reviewed Part 1 write census without changing its baseline.
var plan14EWriteCensusOverlay = []censusEntry{
+ {"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"},
{"internal/host/rootgeneration_requirements.go", "generationLocalObligations", "key-site", "key:RecordKey", "7bf08b3afdf12e81188123e0257ef90cea91091a8ab3bfb975e9a0200619dd1b"},
{"internal/host/rootgeneration_requirements.go", "generationRequiredInventory", "key-site", "key:RecordKey", "bbcbc143061ea6990ec8bc4e34f1a52a07e82039ea44340497d5bc6aab067913"},
{"internal/host/rootgeneration_inventory.go", "generationReconciler.reconcile", "key-site", "key:RecordKey", "1e668bbef6945deb4f0f3a988b5e1a1ecf44e9b85d838c2e01f6a5052b75bbed"},
- {"internal/host/rootgenerations_launch.go", "rootGenerationSource.resolve", "key-site", "key:RecordKey", "2c05d8bfe16ac42bdc612c0008e2ebf584d3ab5970547a02eb78a15fa1b5449c"},
+ {"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.recoverRootInventory", "key-site", "key:RecordKey", "79d1472b90bc7290e655bfe5e8857bad9fd0666c72b88364e89e4e198e60f535"},
diff --git a/internal/provision/testdata/plan14-root-reads-e.tsv b/internal/provision/testdata/plan14-root-reads-e.tsv
index 91585a6b7..99e09d4ca 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 329e742eddf1a5da3cd8f4a6b76a2eda2e38e5e6f0132d66e7b3830cfa95e77f
-@coverage internal/rootstate a773d6dbc0e4150e0054f7a94d0e1493c88d0d57f4f4530138b3427e2c217bc5
+@coverage internal/release 0178b0701766abe8e12fdfe43d9353b1efdbc8bba2f6879bcb2188672293223d
+@coverage internal/rootstate 4ed31a2bd2f7eb4e10eeb918e61b349a020993b5def3d9527d9e422c74dd0fe9
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
@@ -188,11 +188,11 @@ internal/host/claimlaunch_root.go ClaimLaunchRoot.start authority E1,E2 E18: cla
internal/host/claimlaunch_root.go ClaimLaunchRoot.startRecorded authority E1,E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. PROPOSED E47: root-owned immutable unit/shape binding, reserved launch placement and exact invocation stop; retained generation binding carries the canonical unit. Default unit paths remain unchanged. Production named-route wiring and unit/env/timer retirement are not complete. PROPOSED E50: Root passes the retained reservation host into readiness after a recovered start; the binding still controls which endpoint can prove readiness. file:Read@2880 f1bc4c4d327750b5779ea4a4f413dd9620438e7d014202d226200fb793b595ae
internal/host/rootgenerations_launch.go generationLaunchPermit.check authority E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. 788e600dd37fec60c5b98dc42b23c182c337df064088a041e2e20b68573942b5
internal/host/rootgenerations_launch.go generationLaunchPermit.started authority E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. PROPOSED E45: root uses retained reservation identities and live kernel containment checks; unrecognized processes still refuse. Current positive root acceptance requires rerun after proc-mount tightening. a7635d02d9cb682be64a0df3f8932336f5e874260898df9013ff7ffd7e3d7462
-internal/host/rootgenerations_launch.go productionRootGenerations authority E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. file:Open@346 a1132403ace7213de9e12720d5a393a3bb85045d1d2c53f0c37ea2ae8e933b04
+internal/host/rootgenerations_launch.go productionRootGenerations authority E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. PROPOSED E51: Census-gated root production assembly installs persistent file-identity capture for generation reservations. file:Open@346 5e8d01a4d6886164b0c6a1cc8f8f970dd73eedc9e8725ef3eabbd17934b57ba8
internal/host/rootgenerations_launch.go rootGenerationSource.reserve authority E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. c4269e65bcac751cd1a51a439e48eda77b055320554b3489d605fea47fa7f3a8
-internal/host/rootgenerations_launch.go rootGenerationSource.resolve authority E2 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. PROPOSED E47: root-owned immutable unit/shape binding, reserved launch placement and exact invocation stop; retained generation binding carries the canonical unit. Default unit paths remain unchanged. Production named-route wiring and unit/env/timer retirement are not complete. file:Open@2501 2c05d8bfe16ac42bdc612c0008e2ebf584d3ab5970547a02eb78a15fa1b5449c
+internal/host/rootgenerations_launch.go rootGenerationSource.resolve authority E2 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. PROPOSED E47: root-owned immutable unit/shape binding, reserved launch placement and exact invocation stop; retained generation binding carries the canonical unit. Default unit paths remain unchanged. Production named-route wiring and unit/env/timer retirement are not complete. PROPOSED E51: Root captures persistent executable/tree proofs under release exclusion after checked root ancestry and signed artifact verification; both proofs must name the authenticated launch boot. file:Open@2501 3cd7c8132e9c451c983392f4d1cc676ddc14b761c90dec3e631d54ae800f9951
internal/host/rootgenerations_launch.go type generationLaunchPermit authority E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. 52b30a79d7ced7875f435eff76d3c924ff4e460d3a2360cda5a9f12c407a2442
-internal/host/rootgenerations_launch.go type rootGenerationSource authority E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. 59a336027ef7750d36f1a36a27cf3d0449395493c05789291c4ed9bc16724e5f
+internal/host/rootgenerations_launch.go type rootGenerationSource authority E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. PROPOSED E51: Root assembly owns the persistent-identity capture capability; fixtures may omit it but production supplies it. 3e57b2a8f97126462564d52d77bad797bba1caef6ca3600c2512b3b283ff67c9
internal/release/rootgenerations.go GenerationBinding.valid authority E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. PROPOSED E47: root-owned immutable unit/shape binding, reserved launch placement and exact invocation stop; retained generation binding carries the canonical unit. Default unit paths remain unchanged. Production named-route wiring and unit/env/timer retirement are not complete. deb92d925b7291f15f42a2ae7856fafb79258ea42ba8faaee4cb3c07d2dabc52
internal/release/rootgenerations.go GenerationProcess.valid authority E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. PROPOSED E45: root uses retained reservation identities and live kernel containment checks; unrecognized processes still refuse. Current positive root acceptance requires rerun after proc-mount tightening. f5790b759daeea8f3dee287a84f2061d72927ebb5a48ea303917392061e180ea
internal/release/rootgenerations.go RootGenerations.Check authority E2 E20: exact reservation closure precedes retirement verification; retained tombstones forbid replay and require sync recommit. Retired records release only their own pins; completeness remains a separate mandatory verifier, still unavailable in production. Fixtures: TestPlan14GenerationRetirementReleasesExactPin, TestPlan14GenerationRetirementRefusals, TestPlan14GenerationClosedReplayRefused, TestPlan14GenerationRetirementProcessCrash, TestPlan14GenerationRetirementKeepsIndependentReference, TestPlan14GenerationClaimsOffRestageMatchesMain. file:Read@78 8ee72f46a5a45879f0625b90e11057d758b73586e5913067d2cd9493705fff83
@@ -202,7 +202,7 @@ internal/release/rootgenerations.go RootGenerations.Started authority E2 E20: ex
internal/release/rootgenerations.go RootGenerations.write 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. 3b7ee54d0f46efb1507f9a5f421b979047d8c0eb654ac5f31b1c3c42f67763c3
internal/release/rootgenerations.go const RootGenerationsDir authority E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. e235f24f29b1f8511349cae97fbbc3f3472b49fdc6781c781d5b6f93ff3d45ff
internal/release/rootgenerations.go generationKey authority E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. cdd7e0d8cdff508772ab54e421a4ab914dbb15ea5d97847f35a93544f0a9259a
-internal/release/rootgenerations.go type GenerationBinding authority E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. PROPOSED E47: root-owned immutable unit/shape binding, reserved launch placement and exact invocation stop; retained generation binding carries the canonical unit. Default unit paths remain unchanged. Production named-route wiring and unit/env/timer retirement are not complete. bae2ebf9e17d26594533f6240b92355abbd74db11d6e4230fafb994912f35ec3
+internal/release/rootgenerations.go type GenerationBinding authority E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. PROPOSED E47: root-owned immutable unit/shape binding, reserved launch placement and exact invocation stop; retained generation binding carries the canonical unit. Default unit paths remain unchanged. Production named-route wiring and unit/env/timer retirement are not complete. PROPOSED E51: Root generation bindings add persistent executable and containing-tree proofs; original device/inode evidence and launch journal relations remain immutable. fffc440df59bd9833eaaaaa609db352eb99f15f2938e36228ef78c78c91b8fe7
internal/release/rootgenerations.go type GenerationOwner authority E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. 6a6c3576622656c2bc4b65075c8f5d70b29968203101a1916926adc48b20bce5
internal/release/rootgenerations.go type GenerationProcess authority E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. PROPOSED E45: root uses retained reservation identities and live kernel containment checks; unrecognized processes still refuse. Current positive root acceptance requires rerun after proc-mount tightening. 81994fee584ed8780e94cf1f8e471316a04154176c5d29cd96acbe0870daadd5
internal/release/rootgenerations.go type GenerationReservation authority E2 E20: exact reservation closure precedes retirement verification; retained tombstones forbid replay and require sync recommit. Retired records release only their own pins; completeness remains a separate mandatory verifier, still unavailable in production. Fixtures: TestPlan14GenerationRetirementReleasesExactPin, TestPlan14GenerationRetirementRefusals, TestPlan14GenerationClosedReplayRefused, TestPlan14GenerationRetirementProcessCrash, TestPlan14GenerationRetirementKeepsIndependentReference, TestPlan14GenerationClaimsOffRestageMatchesMain. 1917db4836b3b18b45ce6bca1a55f6d5a8082e5de26af1710d0da25db0809551
@@ -225,7 +225,7 @@ internal/host/rootinstallation.go rootInstallationHostApp authority E2,J1 E20: r
internal/hostguard/rootview.go RootAuthority.Installation 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. d819256332950ec26c1b541f8be21a6e8cea839708fcbd0f7b8367d118574f92
internal/release/rootgenerations.go RootGenerations.Retire authority E2 E20: exact reservation closure precedes retirement verification; retained tombstones forbid replay and require sync recommit. Retired records release only their own pins; completeness remains a separate mandatory verifier, still unavailable in production. Fixtures: TestPlan14GenerationRetirementReleasesExactPin, TestPlan14GenerationRetirementRefusals, TestPlan14GenerationClosedReplayRefused, TestPlan14GenerationRetirementProcessCrash, TestPlan14GenerationRetirementKeepsIndependentReference, TestPlan14GenerationClaimsOffRestageMatchesMain. file:Read@297 21bb00d11d6fd8474b4d0a556bd1af7e69dbf6bd75755daeb0f14481166f2a90
@coverage internal/hostguard 470bdfe8e4e0b988846c3b2b56353ad48af545ead27b810721e1cf0e3c7c17ac
-internal/host/rootgeneration_kernel_linux.go generationGroupGone authority E2 E21: bounded kernel reader retains executable, sibling, descriptor, mapping and mount references; checks controller boot/start and cgroup emptiness. Unknown nonroot tasks, foreign namespaces, missing kernel state or renamed trees refuse. Fixtures: TestPlan14GenerationKernelReferences, TestPlan14GenerationKernelMappingIdentity, TestPlan14GenerationKernelControllerIdentity, TestPlan14GenerationKernelRunningExecutable, TestPlan14GenerationKernelCgroup, TestPlan14GenerationKernelRetainedFiles, TestPlan14GenerationKernelSiblingAlias. Real proc executable tests are not full privileged namespace acceptance. PROPOSED E47: root-owned immutable unit/shape binding, reserved launch placement and exact invocation stop; retained generation binding carries the canonical unit. Default unit paths remain unchanged. Production named-route wiring and unit/env/timer retirement are not complete. file:Lstat@194 e2832d7383ea707b31e78d9d4487ccc565715c9be64e056ab1fe097d03073601
+internal/host/rootgeneration_kernel_linux.go generationGroupGone authority E2 E21: bounded kernel reader retains executable, sibling, descriptor, mapping and mount references; checks controller boot/start and cgroup emptiness. Unknown nonroot tasks, foreign namespaces, missing kernel state or renamed trees refuse. Fixtures: TestPlan14GenerationKernelReferences, TestPlan14GenerationKernelMappingIdentity, TestPlan14GenerationKernelControllerIdentity, TestPlan14GenerationKernelRunningExecutable, TestPlan14GenerationKernelCgroup, TestPlan14GenerationKernelRetainedFiles, TestPlan14GenerationKernelSiblingAlias. Real proc executable tests are not full privileged namespace acceptance. PROPOSED E47: root-owned immutable unit/shape binding, reserved launch placement and exact invocation stop; retained generation binding carries the canonical unit. Default unit paths remain unchanged. Production named-route wiring and unit/env/timer retirement are not complete. PROPOSED E51: Root uses the existing reboot cgroup collector after a boot change; a reused populated group still refuses. file:Lstat@194 bf2f4dd388b07ea9ce6c7b13f03d70edeecb8c8d9ce26ad8f9b62a83f615ea35
internal/host/rootgeneration_kernel_linux.go generationIDRef authority E2 E21: bounded kernel reader retains executable, sibling, descriptor, mapping and mount references; checks controller boot/start and cgroup emptiness. Unknown nonroot tasks, foreign namespaces, missing kernel state or renamed trees refuse. Fixtures: TestPlan14GenerationKernelReferences, TestPlan14GenerationKernelMappingIdentity, TestPlan14GenerationKernelControllerIdentity, TestPlan14GenerationKernelRunningExecutable, TestPlan14GenerationKernelCgroup, TestPlan14GenerationKernelRetainedFiles, TestPlan14GenerationKernelSiblingAlias. Real proc executable tests are not full privileged namespace acceptance. 949b4dddb141d7eafd4edc70b41f8dd337069f699ab30d86c46bc6fce986d40b
internal/host/rootgeneration_kernel_linux.go generationKernel.absent authority E2 E21: bounded kernel reader retains executable, sibling, descriptor, mapping and mount references; checks controller boot/start and cgroup emptiness. Unknown nonroot tasks, foreign namespaces, missing kernel state or renamed trees refuse. Fixtures: TestPlan14GenerationKernelReferences, TestPlan14GenerationKernelMappingIdentity, TestPlan14GenerationKernelControllerIdentity, TestPlan14GenerationKernelRunningExecutable, TestPlan14GenerationKernelCgroup, TestPlan14GenerationKernelRetainedFiles, TestPlan14GenerationKernelSiblingAlias. Real proc executable tests are not full privileged namespace acceptance. file:Readlink@312 18269bf4b64e05138dff05e7894d4925bc24bcc012fa64a336723d981a9c52dd
internal/host/rootgeneration_kernel_linux.go generationKernel.boot authority E2 E21: bounded kernel reader retains executable, sibling, descriptor, mapping and mount references; checks controller boot/start and cgroup emptiness. Unknown nonroot tasks, foreign namespaces, missing kernel state or renamed trees refuse. Fixtures: TestPlan14GenerationKernelReferences, TestPlan14GenerationKernelMappingIdentity, TestPlan14GenerationKernelControllerIdentity, TestPlan14GenerationKernelRunningExecutable, TestPlan14GenerationKernelCgroup, TestPlan14GenerationKernelRetainedFiles, TestPlan14GenerationKernelSiblingAlias. Real proc executable tests are not full privileged namespace acceptance. c3cc84ed2af21ee0e57e91160561a3e4996e75990df2617a14865b084b176ced
@@ -239,12 +239,12 @@ internal/host/rootgeneration_kernel_linux.go generationMounts authority E2 E27:
internal/host/rootgeneration_kernel_linux.go generationNames authority E2 E21: bounded kernel reader retains executable, sibling, descriptor, mapping and mount references; checks controller boot/start and cgroup emptiness. Unknown nonroot tasks, foreign namespaces, missing kernel state or renamed trees refuse. Fixtures: TestPlan14GenerationKernelReferences, TestPlan14GenerationKernelMappingIdentity, TestPlan14GenerationKernelControllerIdentity, TestPlan14GenerationKernelRunningExecutable, TestPlan14GenerationKernelCgroup, TestPlan14GenerationKernelRetainedFiles, TestPlan14GenerationKernelSiblingAlias. Real proc executable tests are not full privileged namespace acceptance. file:Open@65,file:Readdirnames@149 1b3e2537c6d2f768d91517f2f7087bb2ff4a290f89c849e31b05de8b80ec9126
internal/host/rootgeneration_kernel_linux.go generationPathRef authority E2 E21: bounded kernel reader retains executable, sibling, descriptor, mapping and mount references; checks controller boot/start and cgroup emptiness. Unknown nonroot tasks, foreign namespaces, missing kernel state or renamed trees refuse. Fixtures: TestPlan14GenerationKernelReferences, TestPlan14GenerationKernelMappingIdentity, TestPlan14GenerationKernelControllerIdentity, TestPlan14GenerationKernelRunningExecutable, TestPlan14GenerationKernelCgroup, TestPlan14GenerationKernelRetainedFiles, TestPlan14GenerationKernelSiblingAlias. Real proc executable tests are not full privileged namespace acceptance. 1b980d5b7083aea4353446743ab723f8008963d6504d480c8898b7a0d0d94549
internal/host/rootgeneration_kernel_linux.go generationRead authority E2 E21: bounded kernel reader retains executable, sibling, descriptor, mapping and mount references; checks controller boot/start and cgroup emptiness. Unknown nonroot tasks, foreign namespaces, missing kernel state or renamed trees refuse. Fixtures: TestPlan14GenerationKernelReferences, TestPlan14GenerationKernelMappingIdentity, TestPlan14GenerationKernelControllerIdentity, TestPlan14GenerationKernelRunningExecutable, TestPlan14GenerationKernelCgroup, TestPlan14GenerationKernelRetainedFiles, TestPlan14GenerationKernelSiblingAlias. Real proc executable tests are not full privileged namespace acceptance. file:Open@62,file:ReadAll@144 6ccd7554fc1f5683c585eb4fd3613d8f2e834d5dfb6c77e4732cb003d1e53065
-internal/host/rootgeneration_kernel_linux.go generationRetainedFiles authority E2 E21: bounded kernel reader retains executable, sibling, descriptor, mapping and mount references; checks controller boot/start and cgroup emptiness. Unknown nonroot tasks, foreign namespaces, missing kernel state or renamed trees refuse. Fixtures: TestPlan14GenerationKernelReferences, TestPlan14GenerationKernelMappingIdentity, TestPlan14GenerationKernelControllerIdentity, TestPlan14GenerationKernelRunningExecutable, TestPlan14GenerationKernelCgroup, TestPlan14GenerationKernelRetainedFiles, TestPlan14GenerationKernelSiblingAlias. Real proc executable tests are not full privileged namespace acceptance. file:Open@446 70869a008771c9b336b12c1a07915f196e2b78c4a12a9f61062bde8ce2f6f431
+internal/host/rootgeneration_kernel_linux.go generationRetainedFiles authority E2 E21: bounded kernel reader retains executable, sibling, descriptor, mapping and mount references; checks controller boot/start and cgroup emptiness. Unknown nonroot tasks, foreign namespaces, missing kernel state or renamed trees refuse. Fixtures: TestPlan14GenerationKernelReferences, TestPlan14GenerationKernelMappingIdentity, TestPlan14GenerationKernelControllerIdentity, TestPlan14GenerationKernelRunningExecutable, TestPlan14GenerationKernelCgroup, TestPlan14GenerationKernelRetainedFiles, TestPlan14GenerationKernelSiblingAlias. Real proc executable tests are not full privileged namespace acceptance. PROPOSED E51: Root delegates retained-file verification to generationCurrentFiles without rewriting a reservation. 3847d8d1699585a5d5a1c271daadf7b0f3987b8da571d1c1c0250acff23abcd6
internal/host/rootgeneration_kernel_linux.go generationRootTask authority E2 E21: bounded kernel reader retains executable, sibling, descriptor, mapping and mount references; checks controller boot/start and cgroup emptiness. Unknown nonroot tasks, foreign namespaces, missing kernel state or renamed trees refuse. Fixtures: TestPlan14GenerationKernelReferences, TestPlan14GenerationKernelMappingIdentity, TestPlan14GenerationKernelControllerIdentity, TestPlan14GenerationKernelRunningExecutable, TestPlan14GenerationKernelCgroup, TestPlan14GenerationKernelRetainedFiles, TestPlan14GenerationKernelSiblingAlias. Real proc executable tests are not full privileged namespace acceptance. dc08dbd3f09c33fb0012566ee77d0c8541eb4eee418549120dd40488e793ed3b
internal/host/rootgeneration_kernel_linux.go generationStart authority E2 E21: bounded kernel reader retains executable, sibling, descriptor, mapping and mount references; checks controller boot/start and cgroup emptiness. Unknown nonroot tasks, foreign namespaces, missing kernel state or renamed trees refuse. Fixtures: TestPlan14GenerationKernelReferences, TestPlan14GenerationKernelMappingIdentity, TestPlan14GenerationKernelControllerIdentity, TestPlan14GenerationKernelRunningExecutable, TestPlan14GenerationKernelCgroup, TestPlan14GenerationKernelRetainedFiles, TestPlan14GenerationKernelSiblingAlias. Real proc executable tests are not full privileged namespace acceptance. 89515661c91a80144edf027064edf86878d9e2ec8f4ff313d172db26cffda38a
internal/host/rootgeneration_kernel_linux.go generationTree authority E2 E21: bounded kernel reader retains executable, sibling, descriptor, mapping and mount references; checks controller boot/start and cgroup emptiness. Unknown nonroot tasks, foreign namespaces, missing kernel state or renamed trees refuse. Fixtures: TestPlan14GenerationKernelReferences, TestPlan14GenerationKernelMappingIdentity, TestPlan14GenerationKernelControllerIdentity, TestPlan14GenerationKernelRunningExecutable, TestPlan14GenerationKernelCgroup, TestPlan14GenerationKernelRetainedFiles, TestPlan14GenerationKernelSiblingAlias. Real proc executable tests are not full privileged namespace acceptance. file:Lstat@529,file:WalkDir@216 be154099cae5ba8d363bf4ec43d7debe437867aff98fbbd7c335b8ac11e5e6ac
internal/host/rootgeneration_kernel_linux.go generationUnescape authority E2 E21: bounded kernel reader retains executable, sibling, descriptor, mapping and mount references; checks controller boot/start and cgroup emptiness. Unknown nonroot tasks, foreign namespaces, missing kernel state or renamed trees refuse. Fixtures: TestPlan14GenerationKernelReferences, TestPlan14GenerationKernelMappingIdentity, TestPlan14GenerationKernelControllerIdentity, TestPlan14GenerationKernelRunningExecutable, TestPlan14GenerationKernelCgroup, TestPlan14GenerationKernelRetainedFiles, TestPlan14GenerationKernelSiblingAlias. Real proc executable tests are not full privileged namespace acceptance. 4809282a7fbd2413b10e72886eb22d73abe245a5e267e43605982934e1cb6cc8
-internal/host/rootgeneration_kernel_linux.go productionGenerationAbsence authority E2 E21: bounded kernel reader retains executable, sibling, descriptor, mapping and mount references; checks controller boot/start and cgroup emptiness. Unknown nonroot tasks, foreign namespaces, missing kernel state or renamed trees refuse. Fixtures: TestPlan14GenerationKernelReferences, TestPlan14GenerationKernelMappingIdentity, TestPlan14GenerationKernelControllerIdentity, TestPlan14GenerationKernelRunningExecutable, TestPlan14GenerationKernelCgroup, TestPlan14GenerationKernelRetainedFiles, TestPlan14GenerationKernelSiblingAlias. Real proc executable tests are not full privileged namespace acceptance. bb7efb2afc24229ad55129c4342bb1a6fc98fbadb42267d7039b5f549c4bdbf6
+internal/host/rootgeneration_kernel_linux.go productionGenerationAbsence authority E2 E21: bounded kernel reader retains executable, sibling, descriptor, mapping and mount references; checks controller boot/start and cgroup emptiness. Unknown nonroot tasks, foreign namespaces, missing kernel state or renamed trees refuse. Fixtures: TestPlan14GenerationKernelReferences, TestPlan14GenerationKernelMappingIdentity, TestPlan14GenerationKernelControllerIdentity, TestPlan14GenerationKernelRunningExecutable, TestPlan14GenerationKernelCgroup, TestPlan14GenerationKernelRetainedFiles, TestPlan14GenerationKernelSiblingAlias. Real proc executable tests are not full privileged namespace acceptance. PROPOSED E51: After persistent file verification root uses current-boot device numbers for kernel reference scans; saved generation bindings are never rewritten. 7cc710f4cb441dc1bd0c3133d411616946bcda3e09a6bd90d34b547b40f55be4
internal/host/rootgeneration_kernel_linux.go type generationKernel authority E2 E21: bounded kernel reader retains executable, sibling, descriptor, mapping and mount references; checks controller boot/start and cgroup emptiness. Unknown nonroot tasks, foreign namespaces, missing kernel state or renamed trees refuse. Fixtures: TestPlan14GenerationKernelReferences, TestPlan14GenerationKernelMappingIdentity, TestPlan14GenerationKernelControllerIdentity, TestPlan14GenerationKernelRunningExecutable, TestPlan14GenerationKernelCgroup, TestPlan14GenerationKernelRetainedFiles, TestPlan14GenerationKernelSiblingAlias. Real proc executable tests are not full privileged namespace acceptance. PROPOSED E45: root uses retained reservation identities and live kernel containment checks; unrecognized processes still refuse. Current positive root acceptance requires rerun after proc-mount tightening. 36ff9458289dc2ae5e168017bcd1788470a967e508dad3a26687151bdb36f284
internal/host/rootgeneration_kernel_linux.go type generationMount authority E2 E21: bounded kernel reader retains executable, sibling, descriptor, mapping and mount references; checks controller boot/start and cgroup emptiness. Unknown nonroot tasks, foreign namespaces, missing kernel state or renamed trees refuse. Fixtures: TestPlan14GenerationKernelReferences, TestPlan14GenerationKernelMappingIdentity, TestPlan14GenerationKernelControllerIdentity, TestPlan14GenerationKernelRunningExecutable, TestPlan14GenerationKernelCgroup, TestPlan14GenerationKernelRetainedFiles, TestPlan14GenerationKernelSiblingAlias. Real proc executable tests are not full privileged namespace acceptance. b4cd115a225b26a7906357404f8d04692a8d1b062662f80672280972049a8acb
internal/host/rootgeneration_kernel_other.go productionGenerationAbsence authority E2 E21: non-Linux production generation retirement refuses. Darwin, Windows and Linux ARM64 compile gates; no non-Linux retirement authority. de47d997019075326e1d5513d7576390d085566fd69edc35efd00f067fb1db24
@@ -385,7 +385,7 @@ internal/host/rootgeneration_containment_linux.go generationKernel.containedTask
internal/host/claimstop_run.go claimActivationReady authority E2 PROPOSED E45: root enforces census-gated release ordering, bounded acquisition and scoped recovery; registry timer primitives preserve supplied identity but producer integration remains pending. 82d893dc252c02813cd492f44dab8adc75b0e6fe5eaf4b7314c17759fdd5c83e
internal/host/rootreleaselock.go withClaimReleaseCensus authority E2 PROPOSED E45: root enforces census-gated release ordering, bounded acquisition and scoped recovery; registry timer primitives preserve supplied identity but producer integration remains pending. 35bcc5e18e89f9755cf1f749bbaa4cbf493380b75500183687476abd71c946b6
internal/rootparticipation/ledger.go IsAcquisitionRecovery authority E2 PROPOSED E45: root enforces census-gated release ordering, bounded acquisition and scoped recovery; registry timer primitives preserve supplied identity but producer integration remains pending. d71ec87152a9ba780aaf6e08fd54288bd6da40d61ad96495e7c3b29a0be200cd
-@coverage internal/instancectl 74916842a6089b3a848dbe114a8c56cb5c68d9d49b802c6533698333db76a6ba
+@coverage internal/instancectl 96042e357644a4d4928e7b71d4c942ae07f2866d8ac001f005ac8d3b58eb4f42
@coverage internal/instanceexec 1a4b9c7a5c57c99ecb39b56c724f08f7e0226dc4809f31000e49e4b38cb4d059
@fields A,Account,Accounts,AddedAt,Addr,Addrs,Alias,Aliases,Alive,AllowAbsent,App,Archives,Args,At,Attempts,B,Backing,Bucket,BuildState,Callback,CallbackError,CallbackFailed,Canary,Caps,CertExpiresAt,Changed,Channel,CheckedAt,ClaimIncarnation,ClaimOwner,ClaimOwnership,ClaimRowID,ClaimStamp,Class,Contents,CreatedAt,DBPath,DNSCheckedAt,DNSStatus,Deadline,Demand,Digest,DoneBefore,Due,DueAt,Env,EnvBefore,Environment,Error,ErrorKind,ExcludeHosts,ExpectGood,ExpectOp,Expected,F,Failed,FenceGen,Fleet,FleetBox,FleetBoxes,Fleets,From,FromDB,FromOp,FromSock,Gate,GoodDemand,GoodDigest,GoodVersion,Hash,Health,Held,Hibernate,HoldOp,Host,Hosts,ID,Id,IdValue,Idle,IdleSeconds,ImportGate,Journal,Keep,Kind,Label,LastAt,LastError,LastMs,LastStatus,Limit,MaxRun,MaxRunSeconds,Method,Ms,Name,Next,NextAt,NextDue,NextGood,NextValue,Now,Number,Number64,Observed,ObservedValue,Old,On,Op,Original,Owner,Ownership,P,Path,Paths,Platform,Pool,Prefix,Prev,PrevVersion,Provisioned,RecordKey,Region,Registration,Replica,Replicas,Replication,Retirement,Route,Routes,Row,Rows,Seed,SeedID,SeedSource,ServingVersion,Slug,Socket,Spec,Sqid,State,Status,StatusNamed,Steering,StringMap,T,TValue,Tags,Target,Targets,Text,Timer,Timers,Times,To,ToDB,ToOp,ToSock,TokenHash,Tunnel,Unit,UpdateSettings,Ver,Version,W,WakeIncarnation,Wildcards,closed,conn,connect,ctx,d,dec,done,enc,full,input,mu,output,path,remote,sql,wait
internal/host/agent.go runAgent authority B,C,D,F rev3=census-ruling-seeded-instances;basis=the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos): main's body with wireSeedPlacement called in the activator start's Configure step, before armMovesPass, and the startup sweep's result stored for the import rollback in Recover; read sites unchanged, offsets moved; main's B,C,D,F authority retained 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@13802,file:Open@731,registry:App@10065,whole:Route@10057,whole:Route@23109 098dab554d79d132385030a61a266947c39d91b9c2670a3be1141e7eacfbab72
@@ -434,14 +434,14 @@ internal/instancectl/claimexec.go ClaimUnits.prepareExecGate authority C,G,J2,J4
internal/instancectl/claimunit.go ClaimUnits.cgroupEmpty authority C,G,J2,J4,E2 root-input;kept=C,G;scope=protected-decision-only PROPOSED E47: root-owned immutable unit/shape binding, reserved launch placement and exact invocation stop; retained generation binding carries the canonical unit. Default unit paths remain unchanged. Production named-route wiring and unit/env/timer retirement are not complete. file:ReadFile@196 9e874fe959a3a381442e2900e8184f91ef803dd4f47839de8dbd6d3ab395d03e
internal/host/claimlaunch_authority.go ClaimLaunchRoot.authorize authority D,E2 Both root launch authorizations read root participation and match authenticated peer; TestPlan14ExecutionFinalLaunchUsesRootParticipation. PROPOSED E47: final named-unit launch authorization rechecks the retained root binding and refuses missing, retired, foreign or changed canonical shape. 9e00346c2012343327a230067f2e35f9f960569fbb3b6ee0dc401e7f566cbef6
internal/host/claimlaunch_authority.go ClaimLaunchRoot.launchSpec authority E2 PROPOSED E48: root; retained canonical unit and program ownership, checked root configuration, deny-only candidate matching, reservation-bearing per-host dispatch, and invocation/cgroup-bound executable peer confirmation. No no-census path is added. Full named-route admission, flat sockets and unit/env/timer retirement remain incomplete. 4703055ca1e26021e448839b43b5e281a020e492aac61dadca12f03675a688a4
-internal/host/claimunit_binding.go claimUnitShape.valid authority E2 PROPOSED E48: root; retained canonical unit and program ownership, checked root configuration, deny-only candidate matching, reservation-bearing per-host dispatch, and invocation/cgroup-bound executable peer confirmation. No no-census path is added. Full named-route admission, flat sockets and unit/env/timer retirement remain incomplete. PROPOSED E50: Root validates optional exact predecessor definition digest in canonical configuration; no service route may authorize migration. 539b4d461ed8d25c3c61c40a26eb330dadaf7217092d0b78de56e50c7cf31f26
+internal/host/claimunit_binding.go claimUnitShape.valid authority E2 PROPOSED E48: root; retained canonical unit and program ownership, checked root configuration, deny-only candidate matching, reservation-bearing per-host dispatch, and invocation/cgroup-bound executable peer confirmation. No no-census path is added. Full named-route admission, flat sockets and unit/env/timer retirement remain incomplete. PROPOSED E50: Root validates optional exact predecessor definition digest in canonical configuration; no service route may authorize migration. PROPOSED E51: Root canonical TCP endpoints require a numeric port in 1..65535. b080f7295cacb6db0b4f6cb7873b41c225ae7a83ad5cac0d110f61cf14ceaf0d
internal/host/claimunit_dispatch.go claimUnitDispatch.StartReserved authority E2 PROPOSED E48: root; retained canonical unit and program ownership, checked root configuration, deny-only candidate matching, reservation-bearing per-host dispatch, and invocation/cgroup-bound executable peer confirmation. No no-census path is added. Full named-route admission, flat sockets and unit/env/timer retirement remain incomplete. 02dfbc7e31f9a13ba210d2bed3181da5e1874ae388c2d1060cbdb57c398a04e5
internal/host/claimunit_dispatch.go claimUnitDispatch.forStart authority E2 PROPOSED E48: root; retained canonical unit and program ownership, checked root configuration, deny-only candidate matching, reservation-bearing per-host dispatch, and invocation/cgroup-bound executable peer confirmation. No no-census path is added. Full named-route admission, flat sockets and unit/env/timer retirement remain incomplete. PROPOSED E50: Root start requires canonical shape, retained binding, exact owned fragment and effective systemd properties; missing binding selects ordinary placement only when no configured named host exists. 3e0672074eadb441f2272a1d6f42a9e58759c3146d363939bea9805090d2c32c
-internal/host/claimunit_dispatch.go claimUnitDispatch.selected authority E2 PROPOSED E48: root; retained canonical unit and program ownership, checked root configuration, deny-only candidate matching, reservation-bearing per-host dispatch, and invocation/cgroup-bound executable peer confirmation. No no-census path is added. Full named-route admission, flat sockets and unit/env/timer retirement remain incomplete. PROPOSED E50: Root cleanup placement uses retained binding only; malformed current configuration cannot redirect cleanup or prevent an ordinary host stop. 99727f591118caf27949085ad19efa522f595f085092a7df0f47f434ae441372
+internal/host/claimunit_dispatch.go claimUnitDispatch.selected authority E2 PROPOSED E48: root; retained canonical unit and program ownership, checked root configuration, deny-only candidate matching, reservation-bearing per-host dispatch, and invocation/cgroup-bound executable peer confirmation. No no-census path is added. Full named-route admission, flat sockets and unit/env/timer retirement remain incomplete. PROPOSED E50: Root cleanup placement uses retained binding only; malformed current configuration cannot redirect cleanup or prevent an ordinary host stop. PROPOSED E51: Root retained binding supplies both unit placement and the TCP address requiring the pre-exec kernel filter. 1b4a8e6ddb43675f4a1d492089312f6dd8553f993adeafe601080a8954d67425
internal/host/claimunit_production.go var openClaimUnitBindings authority E2 PROPOSED E48: root; retained canonical unit and program ownership, checked root configuration, deny-only candidate matching, reservation-bearing per-host dispatch, and invocation/cgroup-bound executable peer confirmation. No no-census path is added. Full named-route admission, flat sockets and unit/env/timer retirement remain incomplete. file:Open@70 f332e5eeef4116b6a9242f7f5c6c47bef78639a042ef0db3993a54dfae71963c
internal/host/claimunit_program.go readClaimUnitProgram authority E2 PROPOSED E48: root; retained canonical unit and program ownership, checked root configuration, deny-only candidate matching, reservation-bearing per-host dispatch, and invocation/cgroup-bound executable peer confirmation. No no-census path is added. Full named-route admission, flat sockets and unit/env/timer retirement remain incomplete. file:ReadAll@353 28ee039bab9f2461f10797082d8cfafe751f094271f377d04565e410127808ec
internal/instancectl/claimexec.go ClaimUnits.pinExecPeer authority E2 PROPOSED E48: root; retained canonical unit and program ownership, checked root configuration, deny-only candidate matching, reservation-bearing per-host dispatch, and invocation/cgroup-bound executable peer confirmation. No no-census path is added. Full named-route admission, flat sockets and unit/env/timer retirement remain incomplete. 6966c6e7ca873e312051e94c473f59331b33a1bf6b1017a2a83cd05c66ec01c4
-internal/instancectl/claimexec.go claimExecGate.authorize authority E2 PROPOSED E48: root; retained canonical unit and program ownership, checked root configuration, deny-only candidate matching, reservation-bearing per-host dispatch, and invocation/cgroup-bound executable peer confirmation. No no-census path is added. Full named-route admission, flat sockets and unit/env/timer retirement remain incomplete. 96964cb1eb39b1dd8ecbd47e9e6139e92a61bc72a83ebacfe9cf5886b6d08712
+internal/instancectl/claimexec.go claimExecGate.authorize authority E2 PROPOSED E48: root; retained canonical unit and program ownership, checked root configuration, deny-only candidate matching, reservation-bearing per-host dispatch, and invocation/cgroup-bound executable peer confirmation. No no-census path is added. Full named-route admission, flat sockets and unit/env/timer retirement remain incomplete. PROPOSED E51: Root attaches mandatory bound TCP filters before returning the one-use exec permit and rechecks invocation afterward. e933c359be78b89c1deb36618b9494897943008182dc724aa7ab6767ed67e032
internal/instancectl/claimunit.go ClaimUnits.start authority E2 PROPOSED E48: root; retained canonical unit and program ownership, checked root configuration, deny-only candidate matching, reservation-bearing per-host dispatch, and invocation/cgroup-bound executable peer confirmation. No no-census path is added. Full named-route admission, flat sockets and unit/env/timer retirement remain incomplete. 415efaf2a2a3b304cc9b623c19f0f772e941939322f5752eb8c74ed49e93b1dd
# PROPOSED E49
@@ -457,9 +457,9 @@ internal/host/claimretire_transport.go ClaimRootHTTP.retire authority E2 PROPOSE
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/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. file:ReadFile@165 b71194042fe61687350b9dc960744579a8d357d0f66fedc693d8589710f90837
-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. file:ReadAll@354 cca15dca6e1d79a12975a567a497008abc6a1e57962ea609631dcdd0d2902760
-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. file:CreateTemp@381,file:Open@182 6367edf5ddca8081479ff9fae8b536dca9df86a265ce0e873269752f5297ef48
+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
internal/host/claimunit_files_linux.go projectClaimBoundSocket hint 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. file:Readlinkat@433 4f3c8daab1f68102e7be9511d1ec29d89c584c99aed79641f2b740031e503ab4
internal/host/claimunit_inputs.go ClaimLaunchRoot.boundInputs 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 returns ordinary inputs only without a retained binding or named candidate; malformed named configuration refuses bound input without blocking ordinary hosts. registry:Backing@462,registry:Host@294,registry:Host@385,registry:Host@630,registry:Tunnel@482,registry:Unit@445,whole:Route@640 c68aca34b07fd44193f024d99d7333dda2d0e2501a10c016496c465c017852a8
internal/host/claimunit_inputs.go claimBoundSnapshot hint 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. registry:Host@339,registry:Host@437,registry:Host@728,whole:Route@205 3dca2da123f0cff4415c6ef9ef7102b002452e6dbe3b7f526ded14062c51acca
@@ -472,9 +472,9 @@ internal/hostbox/rolelaunch.go RoleLaunchRequest.Validate hint E2 PROPOSED E49:
@coverage internal/activator 5076bca3ca2fda41c60b04aa927ac99c8756291e6a9b1a1e2417fa35ee81ad24
@coverage internal/hostbox 0b04568917345170ab505138980c1cb4f0484c146837b81daa44fea12429af0b
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. 69be79a22720565df213dfd7dc5291333d4b9fefe10478cbf17862075f0635b1
-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. 87b28d127a9df0ddacce0856ab6eac8d6967b2dc85d9af32b2938dbaa3ec5ab1
-internal/host/claimunit_materialize.go claimUnitMaterializer.retire 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. 8f65239a6f2d0c826258fe6446f746fabcf060d633f29b694ac2c6ef2045b0d0
+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
+internal/host/claimunit_materialize.go claimUnitMaterializer.retire 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 retires only the persistently identified definition or exact retained tombstone; substituted mounts/filesystems refuse. f9a5fdd174918a4823e175c2e10811c9e236f2e1a85552372eee46942d2efd2b
internal/host/claimunit_retire.go retireClaimBoundUnit 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 retirement uses the retained binding even when canonical config is unavailable; absence of a binding leaves ordinary typed retirement unchanged. 43beae021aa4272d21b5beb8ffb59746b1194885576c131934b9f29946029f95
internal/host/claimunit_retire.go claimBoundUnitCleanup.WithCleanup 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. b1bd5b012973d2abdd29ffcbc65ab6980eef0cbb75caefde7ea62e0738c81b7a
internal/host/claimunit_ready_linux.go claimBoundUnitReady 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. 62e76b8a9ab27be350f3da6d75224600c6cb02d3259e2bd9efbd52e1df28eb95
@@ -483,11 +483,11 @@ internal/host/claimunit_ready_linux.go claimBoundUnitReady authority E2 PROPOSED
internal/host/claimunit_binding.go claimUnitBindings.establish 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 startup may supply a pre-resolved claim owner, rechecked against the local installation under host exclusion. A corrupt unrelated record does not block this host; canonical unit uniqueness and owned materialization remain required. f98a38666f269684126ca51e1947e81f5276b7c297a654bbca097958b6f3b5a9
internal/host/claimunit_materialize.go claimUnitMaterializer.load 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 durable materialization decode validates the migration intention, predecessor unit/invocation/cgroup identity and definition digest. file:Read@130 552a22f6d99ad0efd966645e988ad4f839ec2009060cdbd64befe23b7d908b7c
internal/host/claimunit_program.go ClaimLaunchRoot.boundLaunchSpec 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. 7738f116f2fffb3d2ded6c31c2a7680ab509bba27d52fac85e3ea64f416318f4
-internal/host/claimunit_handover.go claimUnitMaterializer.handover authority E2 PROPOSED E49: root; retained retired predecessor binding and exact tombstone inode authorize only same-shape successor installation. The caller independently resolves the current root installation. 06834223f9165c117262813eebef7a1512d23305c937345f30dec1c938a358e4
+internal/host/claimunit_handover.go claimUnitMaterializer.handover authority E2 PROPOSED E49: root; retained retired predecessor binding and exact tombstone inode authorize only same-shape successor installation. The caller independently resolves the current root installation. PROPOSED E51: Root compares persistent predecessor/tombstone identity before successor replacement; owner and shape checks remain exact. dfd500b03a93044c4a8c9f67a8eeee0408e4ff163184197a8eb8fe40ee8ff351
# 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. file:Read@1150 4383753c8c41977ade1e11ab3576b75c002cb25f4b4a29e36184f6f8da722164
-@coverage internal/host f1399857df0854a1c64120273f6e9dea6ba736a3102fd722c2b80c80fe263539
+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. file:Read@1150 428a2b2b685954f9060c750f7408258bf1c0f9768955b4b740129ec9b20408ef
+@coverage internal/host a41f3316fd51ce72142b6a3927415fbcd4c1b7e02976097e97fcbf60d6b3687f
# 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
@@ -495,17 +495,17 @@ internal/caps/converge.go Restart authority E2 PROPOSED E50: Root capability res
internal/caps/converge.go Restore authority E2 PROPOSED E50: Root capability rollback calls CheckHost before restoring a drop-in or restarting. cf8fe4fd012530c45b28cc61653edba94021efe99e1fe49b1d1b91767f08e337
internal/caps/converge.go type Ops authority E2 schema-only PROPOSED E50: Root capability callback includes an independent pre-effect host gate used by census-bound operations; unrelated callers keep nil semantics. file:Query@322 c13796aea72fe9cf11991e2426f22254d488e25f1d3e2b06da362bb06962e3ee
internal/host/caps_pass.go capabilityPass authority E4,J1,J4 root-route PROPOSED E50: Root capability pass consumes its caller-installed CheckHost callback. Production adoption installs the census-bound refusal on both convergence paths; fixture/legacy nil callbacks keep prior semantics. whole:Route@2320,whole:Route@596,whole:Route@657 00565b36d74d8700ca17d9b190e49db161b98117e7d98249012901a6f1aec7de
-internal/host/claimunit_effective.go claimEffectiveUnit authority E2 PROPOSED E50: Root checks loaded fragment, all drop-ins, ExecStart, UID/GID, network and sandbox properties. Only exact root-generated reservation bytes in the private runtime directory are permitted. file:ReadAll@2269 7d6f505c8ea12a2df461f20f2a75301b6933d734f6adb2a47f6af1e966e014c2
+internal/host/claimunit_effective.go claimEffectiveUnit authority E2 PROPOSED E50: Root checks loaded fragment, all drop-ins, ExecStart, UID/GID, network and sandbox properties. Only exact root-generated reservation bytes in the private runtime directory are permitted. PROPOSED E51: Root verifies loaded socket-bind restrictions and absence of edge stop/restart coupling alongside its fragment/drop-in and sandbox checks. file:ReadAll@2841 ea61db9fbf557b92600ad659c07b571c9cd5a21b4d20cd124515fcdd75c08f94
internal/host/claimunit_effective.go claimIPSet authority E2 PROPOSED E50: Root normalizes systemd IP prefixes for exact network-policy comparison; malformed prefixes refuse. afa75b50fae4f7d0a785fc2fcfc03f27b68b4accbc8bcf6b1404984a7df282fc
internal/host/claimunit_effective.go claimReservedDropin authority E2 PROPOSED E50: Root constructs exact expected reservation drop-in bytes from the retained host identity and fixed private paths; a caller cannot choose bind destinations. data-literal@253 759da720bdcb3a0e3f0b6b80325653d6704a56b3187871c5e763b1a2040f670e
internal/host/claimunit_migrate.go claimPredecessorPlacement authority E2 PROPOSED E50: Root accepts only the exact system.slice service path or systemd default instantiated-service slice for the canonical unit. 7c2248d3c2a76d2c46d813ab960dbc80095db5348a21bd1fc709633810a4be4a
-internal/host/claimunit_migrate.go claimUnitMaterializer.finishMigration authority E2 PROPOSED E50: Root resumes durable migration; exact definition identity gates replacement and the complete predecessor cgroup must be empty before materialization completes. 928197b15114af72563e60f1e9661c5f33798b5bcfc31b6f8d5d85860cfe4e6c
+internal/host/claimunit_migrate.go claimUnitMaterializer.finishMigration authority E2 PROPOSED E50: Root resumes durable migration; exact definition identity gates replacement and the complete predecessor cgroup must be empty before materialization completes. PROPOSED E51: Root compares persistent operator-definition identity before stopping the predecessor and replacing its bytes. 3eca31ebde16e7beb00dd66aed088debd6ce0fbc25fada1511c0c4db01efd1c9
internal/host/claimunit_migrate.go claimUnitMaterializer.migration authority E2 PROPOSED E50: Root accepts an existing operator definition only with canonical digest consent and independently captured systemd invocation plus cgroup inode. f40694cd913fb9db331ec82e81209f120c2cd115ecbd4e1c0947f2a6c1973500
internal/host/claimunit_migrate.go claimUnitMigration.valid authority E2 PROPOSED E50: Root decoded migration must match its binding digest, definition identity and independently retained predecessor placement. e94154551a8a463be04b2f0591c01336cf9a2c18c6ac2211d1a38701844029e7
internal/host/claimunit_migrate.go claimUnitPredecessor.valid authority E2 PROPOSED E50: Root validates retained predecessor unit, invocation and cgroup identity before a stop can be attempted. 6216b37e15df78ff7ec405158c9956013cf0db7318440dba500bc5f7f0025094
-internal/host/claimunit_migrate_kernel.go claimPredecessorKernel.capture authority E2 PROPOSED E50: Root brackets the complete predecessor cgroup identity/population read with matching systemd observations; foreign fragment or drop-in refuses capture. 424fb0bf16467b900b87aa708784ce0bca96c06178033d72acaf550176894501
+internal/host/claimunit_migrate_kernel.go claimPredecessorKernel.capture authority E2 PROPOSED E50: Root brackets the complete predecessor cgroup identity/population read with matching systemd observations; foreign fragment or drop-in refuses capture. PROPOSED E51: Root records the kernel boot beside exact systemd invocation and cgroup identity. 5b7b188fa99b61b1034589cfea3871188f6c571cc40f10669a37c291b2763667
internal/host/claimunit_migrate_kernel.go claimPredecessorKernel.show authority E2 PROPOSED E50: Root reads systemd state, invocation, group, fragment and drop-ins; duplicate or missing properties refuse migration. 6732081d7f5dac8d9791850eeda7290ab8c8c7eb9a8ad68cc611d765ee9e8859
-internal/host/claimunit_migrate_kernel.go claimPredecessorKernel.stop authority E2 PROPOSED E50: Root rechecks retained invocation and cgroup inode, stops once, and requires inactive state plus the whole cgroup empty; lost replies retry without accepting a replacement. bf56fc2b01a7fe0bb411e192b85b783c6fcc27856ee47e3c2590ae427100cec6
+internal/host/claimunit_migrate_kernel.go claimPredecessorKernel.stop authority E2 PROPOSED E50: Root rechecks retained invocation and cgroup inode, stops once, and requires inactive state plus the whole cgroup empty; lost replies retry without accepting a replacement. PROPOSED E51: Root treats saved cgroup device/inode as boot-local; on a new boot only independently idle, unpopulated placement succeeds without systemctl stop. 0ddfb4fecf13e086ab599ae3720aec23c5fab657c5fce0e9ca05af9aa9caf7dc
internal/host/claimunit_migrate_linux.go claimPredecessorGroup authority E2 PROPOSED E50: Root opens the exact no-follow cgroup on verified cgroup2 and reads bounded cgroup.events; population and inode are kernel evidence, not registry authority. file:Fstatfs@434,file:Openat@688,file:Read@928 a654799005a2027ecbcc2da5eea5e12f5f651c63b2c2eaf304f88fb9c49dbc97
internal/host/claimunit_migrate_other.go claimPredecessorGroup authority E2 PROPOSED E50: Root non-Linux predecessor probe refuses; it cannot supply kernel stop authority. 242ad27ad3ef9d094e21816686ead9ba2a31e16f68ac611c11c833b7d20d727f
internal/host/claimunit_policy.go checkClaimUnitShape authority E2 PROPOSED E50: Root checks candidate shape against retained canonical binding and exact installation owner; removal or mutation of Unit/Backing cannot switch authority paths. registry:Backing@288,registry:Host@172,registry:Host@247,registry:Host@473,registry:Tunnel@308,registry:Unit@271,whole:Route@506 4eb217cb198109f864078577d9875679fa5f5e1b724eb80453c6098d3c6a99d7
@@ -529,3 +529,12 @@ internal/host/claimunit_effective.go claimEffectiveRefusal.Unwrap diagnostic E2
# PROPOSED E50: root unit migration and R-E50 safety gates
internal/host/claimunit_dropin_linux.go openClaimUnitDropin authority E2 PROPOSED E50: Root opens the validated unit name beneath checked /run/systemd/system using no-follow descriptors; the unit directory and regular single-link file must be root-owned and not group/world-writable. Instantiated @ names are accepted without relaxing trust. file:Fstat@594,file:Fstat@884,file:Open@190,file:Openat@396 bd769502743c400fa31ee7b1cf125d52fc03f7dd6f5bd99e3195c2e60be95cb9
internal/host/claimunit_dropin_other.go openClaimUnitDropin authority E2 PROPOSED E50: Root opens the validated unit name beneath checked /run/systemd/system using no-follow descriptors; the unit directory and regular single-link file must be root-owned and not group/world-writable. Instantiated @ names are accepted without relaxing trust. bb3a29d1ad8840139f7236971ecb2bae93e6323007f2fdd625114bb3ff105397
+internal/host/claimunit_migrate_linux.go claimUnitBoot authority E2 PROPOSED E51: Root reads the kernel boot UUID to scope predecessor cgroup identities. file:ReadFile@52 2b30b38b42041e83aff15674c164ede8702a800110f3c944a761244b3e47ef33
+internal/host/rootgeneration_kernel_linux.go generationCurrentFiles authority E2 PROPOSED E51: Root reopens the canonical executable through trusted ancestry and its containing tree; persistent proof rebinds only the transient scanner copy to current devices. Historical pins without proof refuse across boots. file:Open@219 5d36d1df21114b731a98c294200617a8da2f9f65bbc1a7f0f3e7dda38cffa6c7
+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/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
diff --git a/internal/release/rootgenerations.go b/internal/release/rootgenerations.go
index 1207316c0..0d0538ede 100644
--- a/internal/release/rootgenerations.go
+++ b/internal/release/rootgenerations.go
@@ -31,6 +31,7 @@ type GenerationBinding struct {
ControllerStart string
Version, Digest, Path, Tree string
Executable, TreeIdentity rootstate.FileIdentity
+ ExecutableProof, TreeProof rootstate.FileIdentity `json:",omitzero"`
}
type GenerationProcess struct {
diff --git a/internal/rootstate/persistent_linux.go b/internal/rootstate/persistent_linux.go
new file mode 100644
index 000000000..c3235131b
--- /dev/null
+++ b/internal/rootstate/persistent_linux.go
@@ -0,0 +1,59 @@
+package rootstate
+
+import (
+ "encoding/hex"
+ "fmt"
+ "os"
+ "strconv"
+ "strings"
+
+ "golang.org/x/sys/unix"
+)
+
+func PersistentIdentity(f *os.File) (FileIdentity, error) {
+ var id FileIdentity
+ fd := int(f.Fd())
+ var st unix.Stat_t
+ var fs unix.Statfs_t
+ var sx unix.Statx_t
+ if err := unix.Fstat(fd, &st); err != nil {
+ return id, err
+ }
+ if err := unix.Fstatfs(fd, &fs); err != nil {
+ return id, err
+ }
+ if err := unix.Statx(fd, "", unix.AT_EMPTY_PATH|unix.AT_SYMLINK_NOFOLLOW, unix.STATX_MNT_ID, &sx); err != nil {
+ return id, err
+ }
+ handle, mount, err := unix.NameToHandleAt(fd, "", unix.AT_EMPTY_PATH)
+ if err != nil {
+ 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) {
+ 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")
+ if err != nil {
+ return id, err
+ }
+ mounts, err := os.ReadFile("/proc/self/mountinfo")
+ if err != nil {
+ return id, err
+ }
+ namespace, err := os.Readlink("/proc/self/ns/mnt")
+ if err != nil || namespace == "" {
+ return id, os.ErrPermission
+ }
+ id = FileIdentity{MountNamespace: namespace, Device: uint64(st.Dev), Inode: st.Ino, Boot: strings.TrimSpace(string(boot)), Filesystem: fmt.Sprintf("%x:%x", fs.Type, fs.Fsid.Val), Handle: hex.EncodeToString(handle.Bytes()), HandleType: handle.Type(), Mount: sx.Mnt_id, UID: st.Uid, GID: st.Gid, Mode: st.Mode}
+ for _, line := range strings.Split(string(mounts), "\n") {
+ fields := strings.Fields(line)
+ if len(fields) >= 10 && fields[0] == strconv.FormatUint(id.Mount, 10) {
+ id.MountRoot, id.MountPoint = fields[3], fields[4]
+ break
+ }
+ }
+ if id.Boot == "" || id.MountRoot == "" || id.MountPoint == "" {
+ return FileIdentity{}, os.ErrPermission
+ }
+ return id, nil
+}
diff --git a/internal/rootstate/persistent_other.go b/internal/rootstate/persistent_other.go
new file mode 100644
index 000000000..0a646dcd2
--- /dev/null
+++ b/internal/rootstate/persistent_other.go
@@ -0,0 +1,7 @@
+//go:build !linux
+
+package rootstate
+
+import "os"
+
+func PersistentIdentity(*os.File) (FileIdentity, error) { return FileIdentity{}, os.ErrPermission }
diff --git a/internal/rootstate/persistent_test.go b/internal/rootstate/persistent_test.go
new file mode 100644
index 000000000..dab26da53
--- /dev/null
+++ b/internal/rootstate/persistent_test.go
@@ -0,0 +1,41 @@
+package rootstate
+
+import "testing"
+
+func TestPlan14PersistentDeviceIdentity(t *testing.T) {
+ a := FileIdentity{Device: 259, Inode: 7, Boot: "before", Filesystem: "ext4:fs", Handle: "abcd", HandleType: 1, Mount: 64, MountRoot: "/", MountPoint: "/", MountNamespace: "mnt:[1]", Mode: 0100644}
+ b := a
+ b.Boot, b.Device, b.Mount = "after", 513, 127
+ if !a.Same(b) {
+ t.Fatal("device renumbering rejected persistent identity")
+ }
+ for _, mutate := range []func(*FileIdentity){
+ func(b *FileIdentity) { b.Boot = a.Boot },
+ func(b *FileIdentity) { b.Handle = "reused-inode" },
+ func(b *FileIdentity) { b.Filesystem = "other" },
+ func(b *FileIdentity) { b.MountRoot = "/replacement" },
+ func(b *FileIdentity) { b.MountPoint = "/etc" },
+ func(b *FileIdentity) { b.UID = 1000 },
+ func(b *FileIdentity) { b.Mode = 0100664 },
+ } {
+ bad := b
+ mutate(&bad)
+ if a.Same(bad) {
+ t.Fatal("substitution accepted", bad)
+ }
+ }
+ clone := a
+ clone.MountNamespace = "mnt:[2]"
+ clone.Mount++
+ if !a.Same(clone) {
+ t.Fatal("root namespace clone lost persistent identity")
+ }
+ clone.MountPoint = "/etc"
+ if a.Same(clone) {
+ t.Fatal("namespace clone hid substituted mount")
+ }
+ old := FileIdentity{Device: a.Device, Inode: a.Inode}
+ if old.Same(a) || a.Same(old) {
+ t.Fatal("historical identity acquired inferred proof")
+ }
+}
diff --git a/internal/rootstate/state.go b/internal/rootstate/state.go
index 24ca9aa22..2bee9e4d3 100644
--- a/internal/rootstate/state.go
+++ b/internal/rootstate/state.go
@@ -38,6 +38,23 @@ func fileMode(public bool) os.FileMode {
}
type FileIdentity struct {
- Device uint64
- Inode uint64
+ Device uint64
+ Inode uint64
+ Boot, Filesystem, Handle, MountRoot, MountPoint, MountNamespace string `json:",omitempty"`
+ HandleType int32 `json:",omitempty"`
+ Mount uint64 `json:",omitempty"`
+ UID, GID, Mode uint32 `json:",omitempty"`
+}
+
+func (a FileIdentity) Same(b FileIdentity) bool {
+ if a.Boot == "" || b.Boot == "" {
+ return a == b
+ }
+ if a.Filesystem == "" || a.Handle == "" || a.MountRoot == "" || a.MountPoint == "" || a.MountNamespace == "" || b.MountNamespace == "" {
+ return false
+ }
+ if a.Inode != b.Inode || a.Filesystem != b.Filesystem || a.Handle != b.Handle || a.HandleType != b.HandleType || a.UID != b.UID || a.GID != b.GID || a.Mode != b.Mode || a.MountRoot != b.MountRoot || a.MountPoint != b.MountPoint {
+ return false
+ }
+ return a.Boot != b.Boot || (a.Device == b.Device && (a.MountNamespace != b.MountNamespace || a.Mount == b.Mount))
}