carlos / platform Public

WIP files seed checkpoint 21 (not for merge)

Persistent artifact evidence and reboot inspection checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev c0b9368c7aa85e31acaef73a427ddaa4ce3e96df parent 6a55739
12 files changed, +931 −61
  • internal/host/claimservingboot_linux.go +0 −11
  • internal/host/claimservingidentity.go +97 −0
  • internal/host/claimservingidentity_linux.go +53 −0
  • internal/host/claimservinglegacy.go +1 −1
  • internal/host/claimservinglegacyroot_linux.go +2 −2
  • internal/host/claimservingpark_linux.go +42 −2
  • internal/host/claimservingpark_records.go +24 −10
  • internal/host/claimservingpark_rootrunner_linux_test.go +55 −3
  • internal/host/claimservingreboot_linux.go +198 −0
  • internal/host/claimservingreboot_linux_test.go +429 −0
  • internal/host/claimservingtopology_linux.go +0 −16
  • internal/provision/testdata/plan14-root-reads-seed.tsv +30 −16

CI passed — run details

CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok  	amadan.net/carlos/platform.git/launchctx	0.011s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok  	github.com/carlosframework/platform	356.191s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.169s
ok  	github.com/carlosframework/platform/cmd/moveprobe	92.197s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.706s
ok  	github.com/carlosframework/platform/internal/activator	234.118s
ok  	github.com/carlosframework/platform/internal/agentsock	0.026s
ok  	github.com/carlosframework/platform/internal/alerting	0.004s
ok  	github.com/carlosframework/platform/internal/analytics	0.021s
ok  	github.com/carlosframework/platform/internal/appconfig	2.279s
ok  	github.com/carlosframework/platform/internal/apperr	0.074s
ok  	github.com/carlosframework/platform/internal/applog	0.141s
ok  	github.com/carlosframework/platform/internal/awsinv	0.005s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.005s
ok  	github.com/carlosframework/platform/internal/boxid	0.689s
?   	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.013s
ok  	github.com/carlosframework/platform/internal/caps	0.017s
ok  	github.com/carlosframework/platform/internal/certwarm	0.002s
ok  	github.com/carlosframework/platform/internal/cgroup	0.696s
ok  	github.com/carlosframework/platform/internal/changelog	0.291s
ok  	github.com/carlosframework/platform/internal/clamd	0.054s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	368.883s
?   	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.896s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.009s
ok  	github.com/carlosframework/platform/internal/dbverify	0.005s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.003s
ok  	github.com/carlosframework/platform/internal/docsite	0.005s
ok  	github.com/carlosframework/platform/internal/domains	2.117s
ok  	github.com/carlosframework/platform/internal/doorkey	0.012s
ok  	github.com/carlosframework/platform/internal/economics	0.005s
ok  	github.com/carlosframework/platform/internal/edge	26.076s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.009s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.128s
ok  	github.com/carlosframework/platform/internal/gate	1.655s
ok  	github.com/carlosframework/platform/internal/genselect	0.017s
ok  	github.com/carlosframework/platform/internal/geo	0.017s
ok  	github.com/carlosframework/platform/internal/geoip	0.032s
ok  	github.com/carlosframework/platform/internal/host	149.080s
ok  	github.com/carlosframework/platform/internal/hostactors	0.331s
ok  	github.com/carlosframework/platform/internal/hostbox	2.297s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.515s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.014s
ok  	github.com/carlosframework/platform/internal/hostexec	4.776s
ok  	github.com/carlosframework/platform/internal/hostfence	0.035s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.461s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.014s
ok  	github.com/carlosframework/platform/internal/hostguard	1.010s
ok  	github.com/carlosframework/platform/internal/hostops	1.436s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.013s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.014s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.443s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.132s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.014s
ok  	github.com/carlosframework/platform/internal/infraview	0.014s
ok  	github.com/carlosframework/platform/internal/instancectl	1.811s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.707s
ok  	github.com/carlosframework/platform/internal/instancefs	0.018s
ok  	github.com/carlosframework/platform/internal/instancekey	0.015s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.020s
ok  	github.com/carlosframework/platform/internal/layout	1.425s
ok  	github.com/carlosframework/platform/internal/lease	9.454s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.019s
ok  	github.com/carlosframework/platform/internal/ledger	0.030s
ok  	github.com/carlosframework/platform/internal/mail	0.163s
ok  	github.com/carlosframework/platform/internal/maintenance	0.076s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.494s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.073s
ok  	github.com/carlosframework/platform/internal/metrics	0.004s
ok  	github.com/carlosframework/platform/internal/moves	21.027s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.771s
ok  	github.com/carlosframework/platform/internal/objstore	0.187s
ok  	github.com/carlosframework/platform/internal/ops	0.171s
ok  	github.com/carlosframework/platform/internal/park	0.205s
ok  	github.com/carlosframework/platform/internal/privatefile	0.003s
ok  	github.com/carlosframework/platform/internal/provision	10.950s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.010s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	4.039s
ok  	github.com/carlosframework/platform/internal/relay	7.231s
ok  	github.com/carlosframework/platform/internal/release	19.140s
ok  	github.com/carlosframework/platform/internal/rollout	3.634s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.004s
ok  	github.com/carlosframework/platform/internal/rootaws	0.008s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.006s
ok  	github.com/carlosframework/platform/internal/rootstate	0.026s
ok  	github.com/carlosframework/platform/internal/s3compat	0.007s
ok  	github.com/carlosframework/platform/internal/safefile	0.004s
ok  	github.com/carlosframework/platform/internal/schedule	2.282s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	8.849s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.820s
ok  	github.com/carlosframework/platform/internal/status	0.037s
ok  	github.com/carlosframework/platform/internal/steering	0.005s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.341s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.004s
ok  	github.com/carlosframework/platform/internal/termfence	0.004s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.002s
ok  	github.com/carlosframework/platform/internal/updatewords	0.007s
ok  	github.com/carlosframework/platform/internal/usage	0.005s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.557s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
  minio: cached, digest verified
  mc: cached, digest verified
  minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
  buckets and scoped user ready
=== RUN   TestDeliveredContractMinio
=== RUN   TestDeliveredContractMinio/put
=== RUN   TestDeliveredContractMinio/get_round-trips_the_body
=== RUN   TestDeliveredContractMinio/list_shows_the_key
=== RUN   TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN   TestDeliveredContractMinio/delete_removes_it
=== RUN   TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
    --- PASS: TestDeliveredContractMinio/put (0.00s)
    --- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
    --- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
    --- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
    --- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
    --- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok  	github.com/carlosframework/platform/internal/objstore	0.030s
=== RUN   TestT4StorageProbeMinio
2026/10/14 21:35:07 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE82F8F0AD28CA, 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.028s
minio contract: PASS
diff --git a/internal/host/claimservingboot_linux.go b/internal/host/claimservingboot_linux.go
index 342c8154b..158d47629 100644
--- a/internal/host/claimservingboot_linux.go
+++ b/internal/host/claimservingboot_linux.go
@@ -3,10 +3,8 @@
package host
import (
- "encoding/hex"
"io"
"os"
- "strings"
"github.com/carlosframework/platform/internal/hostbox"
)
@@ -38,15 +36,6 @@ func (f *servingParkFS) currentBoot() (string, error) {
return id, nil
}
-func validServingParkBootID(id string) bool {
- if len(id) != 36 || id[8] != '-' || id[13] != '-' || id[18] != '-' || id[23] != '-' {
- return false
- }
- raw := strings.ReplaceAll(id, "-", "")
- b, err := hex.DecodeString(raw)
- return err == nil && len(b) == 16 && hex.EncodeToString(b) == raw && raw != strings.Repeat("0", 32)
-}
-
// A mount number is meaningful only in the boot that supplied it. Old records
// without a boot remain readable, but cannot authorize namespace mutations.
func (f *servingParkFS) checkBoot(expected string) error {
diff --git a/internal/host/claimservingidentity.go b/internal/host/claimservingidentity.go
new file mode 100644
index 000000000..02a05e675
--- /dev/null
+++ b/internal/host/claimservingidentity.go
@@ -0,0 +1,97 @@
+package host
+
+import (
+ "encoding/hex"
+ "strings"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+)
+
+// Handles identify an inode within its filesystem across boots. Device and
+// inode numbers remain required too; these handles never authorize an open.
+type servingParkPersistentDirectory struct {
+ Filesystem [2]int32
+ Type int32
+ Handle string
+ Owner uint32
+ Mode uint32
+}
+
+type servingParkPersistentFile servingParkPersistentDirectory
+
+func validServingParkHandle(p servingParkPersistentDirectory) bool {
+ b, err := hex.DecodeString(p.Handle)
+ return err == nil && len(b) > 0 && len(b) <= 128 && hex.EncodeToString(b) == p.Handle && p.Type > 0 && p.Filesystem != [2]int32{}
+}
+
+func (p servingParkPersistentDirectory) valid() bool {
+ return validServingParkHandle(p) && p.Mode&0170000 == 0040000 && p.Mode&0022 == 0
+}
+
+func (p servingParkPersistentFile) valid() bool {
+ return validServingParkHandle(servingParkPersistentDirectory(p)) && p.Mode&0170000 == 0100000
+}
+
+// Historical records without handles remain readable. Partial new evidence
+// refuses rather than being treated as an old record eligible for recapture.
+func validateServingParkPersistentRecord(r servingParkRecord) error {
+ p := r.Source.Persistent
+ if p == (servingParkPersistentDirectory{}) {
+ if r.Source.BootID != "" || r.EventPersistent != (servingParkPersistentDirectory{}) {
+ return hostbox.ErrChanged
+ }
+ for _, f := range r.Source.Files {
+ if f.Persistent != (servingParkPersistentFile{}) {
+ return hostbox.ErrChanged
+ }
+ }
+ return nil
+ }
+ if !p.valid() || !validServingParkBootID(r.Source.BootID) {
+ return hostbox.ErrChanged
+ }
+ for _, f := range r.Source.Files {
+ if f.Present {
+ if !f.Persistent.valid() || f.Persistent.Filesystem != p.Filesystem {
+ return hostbox.ErrChanged
+ }
+ } else if f.Persistent != (servingParkPersistentFile{}) {
+ return hostbox.ErrChanged
+ }
+ }
+ if r.Prepared {
+ if !r.EventPersistent.valid() || r.EventPersistent.Filesystem != p.Filesystem || r.EventPersistent.Mode&0077 != 0 {
+ return hostbox.ErrChanged
+ }
+ } else if r.EventPersistent != (servingParkPersistentDirectory{}) {
+ return hostbox.ErrChanged
+ }
+ return nil
+}
+
+func validServingParkBootID(id string) bool {
+ if len(id) != 36 || id[8] != '-' || id[13] != '-' || id[18] != '-' || id[23] != '-' {
+ return false
+ }
+ raw := strings.ReplaceAll(id, "-", "")
+ b, err := hex.DecodeString(raw)
+ return err == nil && len(b) == 16 && hex.EncodeToString(b) == raw && raw != strings.Repeat("0", 32)
+}
+
+// Only the saved source decides whether persistent evidence was captured. New
+// observations cannot upgrade an old intent or permanent legacy consumption.
+func sameServingParkSource(saved, fresh servingParkSource) bool {
+ if saved.Persistent == (servingParkPersistentDirectory{}) && saved.BootID == "" {
+ for _, item := range saved.Files {
+ if item.Persistent != (servingParkPersistentFile{}) {
+ return false
+ }
+ }
+ fresh.Persistent = servingParkPersistentDirectory{}
+ fresh.BootID = ""
+ for i := range fresh.Files {
+ fresh.Files[i].Persistent = servingParkPersistentFile{}
+ }
+ }
+ return saved == fresh
+}
diff --git a/internal/host/claimservingidentity_linux.go b/internal/host/claimservingidentity_linux.go
new file mode 100644
index 000000000..2ca4dbca8
--- /dev/null
+++ b/internal/host/claimservingidentity_linux.go
@@ -0,0 +1,53 @@
+//go:build linux
+
+package host
+
+import (
+ "encoding/hex"
+ "os"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "golang.org/x/sys/unix"
+)
+
+// O_PATH pins the no-follow inode without opening a device or reading tenant
+// bytes. Compare the descriptor and name around the handle lookup.
+func servingParkPersistentArtifact(parent *os.File, expected servingParkFile) (servingParkPersistentFile, error) {
+ var zero servingParkPersistentFile
+ fd, err := unix.Openat(int(parent.Fd()), expected.Name, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
+ if err != nil {
+ return zero, err
+ }
+ defer unix.Close(fd)
+ var st unix.Statx_t
+ if err = unix.Statx(fd, "", unix.AT_EMPTY_PATH|unix.AT_SYMLINK_NOFOLLOW, unix.STATX_BASIC_STATS|unix.STATX_MNT_ID, &st); err != nil {
+ return zero, err
+ }
+ if !servingParkStatxMatches(expected, st) {
+ return zero, hostbox.ErrChanged
+ }
+ var fs unix.Statfs_t
+ if err = unix.Fstatfs(fd, &fs); err != nil {
+ return zero, err
+ }
+ h, mount, err := unix.NameToHandleAt(fd, "", unix.AT_EMPTY_PATH)
+ if err != nil {
+ return zero, err
+ }
+ out := servingParkPersistentFile{Filesystem: fs.Fsid.Val, Type: h.Type(), Handle: hex.EncodeToString(h.Bytes()), Owner: st.Uid, Mode: uint32(st.Mode)}
+ if !out.valid() || mount <= 0 || uint64(mount) != expected.Mount {
+ return zero, hostbox.ErrChanged
+ }
+ var again unix.Statx_t
+ if err = unix.Statx(int(parent.Fd()), expected.Name, unix.AT_SYMLINK_NOFOLLOW, unix.STATX_BASIC_STATS|unix.STATX_MNT_ID, &again); err != nil {
+ return zero, err
+ }
+ if !servingParkStatxMatches(expected, again) || again.Uid != st.Uid || again.Mode != st.Mode {
+ return zero, hostbox.ErrChanged
+ }
+ return out, nil
+}
+
+func servingParkStatxMatches(expected servingParkFile, st unix.Statx_t) bool {
+ return expected.Present && st.Mask&unix.STATX_MNT_ID != 0 && st.Mode&unix.S_IFMT == unix.S_IFREG && st.Nlink == 1 && unix.Mkdev(st.Dev_major, st.Dev_minor) == expected.Device && st.Ino == expected.Inode && st.Mnt_id == expected.Mount && st.Blocks <= ^uint64(0)/512 && st.Blocks*512 == expected.Bytes
+}
diff --git a/internal/host/claimservinglegacy.go b/internal/host/claimservinglegacy.go
index be162307f..af42f56c0 100644
--- a/internal/host/claimservinglegacy.go
+++ b/internal/host/claimservinglegacy.go
@@ -129,7 +129,7 @@ func (l *servingParkLedger) legacyUnlaunched(ctx context.Context, a servingArtif
}
func sameServingLegacyConsumption(c servingLegacyConsumption, r servingParkRecord) bool {
- if c.Authorization != r.Authorization || !sameServingRootStops(c.Receipts, r.Receipts) || !sameServingLegacyEvidence(c.LegacyEvidence, r.LegacyEvidence) || c.Source.Directory != r.Source.Directory || c.Source.Parent != r.Source.Parent {
+ if c.Authorization != r.Authorization || !sameServingRootStops(c.Receipts, r.Receipts) || !sameServingLegacyEvidence(c.LegacyEvidence, r.LegacyEvidence) || c.Source.Directory != r.Source.Directory || c.Source.Parent != r.Source.Parent || c.Source.Persistent != r.Source.Persistent || c.Source.BootID != r.Source.BootID {
return false
}
for i, item := range c.Source.Files {
diff --git a/internal/host/claimservinglegacyroot_linux.go b/internal/host/claimservinglegacyroot_linux.go
index 5f743ab77..13ffc6246 100644
--- a/internal/host/claimservinglegacyroot_linux.go
+++ b/internal/host/claimservinglegacyroot_linux.go
@@ -258,11 +258,11 @@ func (p *servingParkRoot) parkLegacyServingDatabase(ctx context.Context, conn ne
}
func sameServingLegacySource(a, b servingParkSource) bool {
- if a.Directory != b.Directory || a.Parent != b.Parent {
+ if a.Directory != b.Directory || a.Parent != b.Parent || a.Persistent != b.Persistent || a.BootID != b.BootID {
return false
}
for i := range a.Files {
- if !sameServingParkFile(a.Files[i], b.Files[i]) {
+ if a.Files[i].Persistent != b.Files[i].Persistent || !sameServingParkFile(a.Files[i], b.Files[i]) {
return false
}
}
diff --git a/internal/host/claimservingpark_linux.go b/internal/host/claimservingpark_linux.go
index 6b8cbd9ff..bc4d23e87 100644
--- a/internal/host/claimservingpark_linux.go
+++ b/internal/host/claimservingpark_linux.go
@@ -233,11 +233,12 @@ func servingParkStat(parent *os.File, name string) (servingParkFile, error) {
return out, hostbox.ErrChanged
}
out.Present, out.Device, out.Inode, out.Mount, out.Bytes, out.Mode, out.Links = true, unix.Mkdev(st.Dev_major, st.Dev_minor), st.Ino, st.Mnt_id, st.Blocks*512, uint32(st.Mode)&unix.S_IFMT, st.Nlink
- return out, nil
+ out.Persistent, err = servingParkPersistentArtifact(parent, out)
+ return out, err
}
func sameServingParkFile(a, b servingParkFile) bool {
- return a.Name == b.Name && a.Present == b.Present && a.Device == b.Device && a.Inode == b.Inode && a.Mount == b.Mount && a.Mode == b.Mode && a.Links == b.Links && a.Bytes == b.Bytes
+ return a.Name == b.Name && a.Present == b.Present && a.Device == b.Device && a.Inode == b.Inode && a.Mount == b.Mount && a.Mode == b.Mode && a.Links == b.Links && a.Bytes == b.Bytes && (a.Persistent == (servingParkPersistentFile{}) || a.Persistent == b.Persistent)
}
func (f *servingParkFS) Capture(a servingParkAuthorization) (servingParkSource, error) {
@@ -248,6 +249,20 @@ func (f *servingParkFS) Capture(a servingParkAuthorization) (servingParkSource,
}
defer source.Close()
out.Directory, out.Parent = a.Directory, parent
+ out.BootID, err = f.currentBoot()
+ if err != nil {
+ return out, err
+ }
+ if f.handle == nil {
+ return out, hostbox.ErrUnsupported
+ }
+ out.Persistent, err = f.handle(source)
+ if err != nil {
+ return out, err
+ }
+ if !out.Persistent.valid() {
+ return out, hostbox.ErrChanged
+ }
for i, suffix := range []string{"", ".gen", "-wal", "-shm"} {
file, err := servingParkStat(source, a.Successor.Host+".db"+suffix)
if err != nil {
@@ -283,6 +298,18 @@ func (f *servingParkFS) Capture(a servingParkAuthorization) (servingParkSource,
return out, hostbox.ErrChanged
}
}
+ if f.checkBoot(out.BootID) != nil {
+ return out, hostbox.ErrChanged
+ }
+ again, pid, err := f.source(a)
+ if err != nil {
+ return out, err
+ }
+ defer again.Close()
+ persistent, err := f.handle(again)
+ if err != nil || pid != out.Parent || persistent != out.Persistent {
+ return out, hostbox.ErrChanged
+ }
return out, nil
}
@@ -365,6 +392,19 @@ func (f *servingParkFS) Prepare(r *servingParkRecord, save func() error) error {
if err = f.sync(int(root.Fd())); err != nil {
return err
}
+ if r.Source.Persistent != (servingParkPersistentDirectory{}) {
+ if f.checkBoot(r.Source.BootID) != nil || f.handle == nil {
+ return hostbox.ErrChanged
+ }
+ persistent, e := f.handle(event)
+ if e != nil {
+ return e
+ }
+ if !persistent.valid() || persistent.Owner != f.owner || persistent.Mode&0077 != 0 || persistent.Filesystem != r.Source.Persistent.Filesystem {
+ return hostbox.ErrChanged
+ }
+ r.EventPersistent = persistent
+ }
r.Event, r.Prepared, r.Namespace = id, true, n.Root
return save()
}
diff --git a/internal/host/claimservingpark_records.go b/internal/host/claimservingpark_records.go
index e716abb29..73357dd02 100644
--- a/internal/host/claimservingpark_records.go
+++ b/internal/host/claimservingpark_records.go
@@ -28,6 +28,7 @@ type servingParkLedger struct {
}
type servingParkFile struct {
+ Persistent servingParkPersistentFile `json:",omitzero"`
Name string
Present bool
Device, Inode, Mount, Bytes uint64
@@ -36,17 +37,20 @@ type servingParkFile struct {
}
type servingParkSource struct {
- Directory servingArtifactDirectory
- Parent servingArtifactDirectory
- Files [4]servingParkFile
+ Persistent servingParkPersistentDirectory `json:",omitzero"`
+ BootID string `json:",omitempty"`
+ Directory servingArtifactDirectory
+ Parent servingArtifactDirectory
+ Files [4]servingParkFile
}
type servingParkRecord struct {
- CollectAfter time.Time `json:",omitempty"`
- Namespace servingArtifactDirectory `json:",omitempty"`
- LegacyEvidence *servingLegacyEvidence `json:",omitempty"`
- Continuations []servingParkContinuation `json:",omitempty"`
- EventRemoveIntent, EventRemoved bool `json:",omitempty"`
+ EventPersistent servingParkPersistentDirectory `json:",omitzero"`
+ CollectAfter time.Time `json:",omitempty"`
+ Namespace servingArtifactDirectory `json:",omitempty"`
+ LegacyEvidence *servingLegacyEvidence `json:",omitempty"`
+ Continuations []servingParkContinuation `json:",omitempty"`
+ EventRemoveIntent, EventRemoved bool `json:",omitempty"`
Version int
Authorization servingParkAuthorization
Source servingParkSource
@@ -136,6 +140,9 @@ func (l *servingParkLedger) read(id string) (servingParkRecord, error) {
}
func validateServingParkRecord(r servingParkRecord) error {
+ if err := validateServingParkPersistentRecord(r); err != nil {
+ return err
+ }
a := r.Authorization
s := a.OriginalSuccessor
if r.Version != 1 || a.ID != servingRetirementID(a.Predecessor, s, a.Legacy) || s.Host == "" || s.Box == "" || s.Incarnation == "" || s.Generation == "" || s.Region == "" || s.Location == "" || s.Stamp == 0 || s.Stamp != s.Session || a.Successor != s || a.Directory != r.Source.Directory || r.Source.Directory.Inode == 0 || r.Source.Directory.Device == 0 || r.Source.Directory.Mount == 0 || r.Source.Parent.Inode == 0 || r.Source.Parent.Device != r.Source.Directory.Device || r.Source.Parent.Mount != r.Source.Directory.Mount {
@@ -421,9 +428,16 @@ func (l *servingParkLedger) parkWithEvidenceLocked(ctx context.Context, a servin
want := servingLegacyConsumption{Authorization: a, Source: source, Receipts: receipts, LegacyEvidence: legacy}
body, e := l.state.Read(key)
if e == nil {
- if servingParkDecode(body, &consumed) != nil || !reflect.DeepEqual(consumed, want) {
+ if servingParkDecode(body, &consumed) != nil || !sameServingParkSource(consumed.Source, want.Source) {
+ return zero, hostbox.ErrChanged
+ }
+ // A lost old-format intent keeps its original consumption
+ // evidence. Never infer historical handles from current bytes.
+ want.Source = consumed.Source
+ if !reflect.DeepEqual(consumed, want) {
return zero, hostbox.ErrChanged
}
+ r.Source = consumed.Source
} else if !errors.Is(e, os.ErrNotExist) {
return zero, e
}
@@ -465,7 +479,7 @@ func (l *servingParkLedger) parkWithEvidenceLocked(ctx context.Context, a servin
return zero, hostbox.ErrChanged
}
fresh, e := l.fs.Capture(a)
- if e != nil || fresh != r.Source {
+ if e != nil || !sameServingParkSource(r.Source, fresh) {
return zero, hostbox.ErrChanged
}
if slot.ID != "" {
diff --git a/internal/host/claimservingpark_rootrunner_linux_test.go b/internal/host/claimservingpark_rootrunner_linux_test.go
index d5df3d135..60396684b 100644
--- a/internal/host/claimservingpark_rootrunner_linux_test.go
+++ b/internal/host/claimservingpark_rootrunner_linux_test.go
@@ -17,6 +17,7 @@ import (
"time"
"github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostguard"
"github.com/carlosframework/platform/internal/provision"
"github.com/carlosframework/platform/internal/rootstate"
"golang.org/x/sys/unix"
@@ -125,8 +126,19 @@ func servingRunnerPark(t *testing.T, data string, state rootstate.Store) serving
if err != nil {
t.Fatal(err)
}
- r := servingParkRecord{Version: 1, Authorization: a, Source: source, Disposition: servingParkRetain}
- save := func() error { return (&servingParkLedger{state: state}).write("fixture.json", r) }
+ // Stop authorization remains explicitly fixture-backed. It is encoded in
+ // the production record format so the reboot inspector reads the ledger.
+ at := time.Now().UTC()
+ target := hostguard.FenceTarget{Host: current.Host, Box: current.Box, Region: current.Region, Incarnation: current.Incarnation, Stamp: 1, Session: 1, FenceID: "runner-stop", Enrollment: "runner-enrollment", BootID: source.BootID, ControllerID: "runner-controller", DirectoryRevision: "1", ClaimETag: "runner-claim", ClaimGen: 1}
+ receipt := StopReceipt{V: 1, ID: target.FenceID, Kind: StopReceiptStopped, Region: target.Region, Host: target.Host, Box: target.Box, Incarnation: target.Incarnation, InstallationStamp: target.Stamp, WriterSession: target.Session, FenceID: target.FenceID, Enrollment: target.Enrollment, BootID: target.BootID, ControllerID: target.ControllerID, DirectoryRevision: target.DirectoryRevision, ClaimETag: target.ClaimETag, ClaimGen: target.ClaimGen, EvidenceAt: at}
+ r := servingParkRecord{Version: 1, Authorization: a, Source: source, Disposition: servingParkRetain, Receipts: []hostguard.FenceReceipt{{Target: target, Bytes: mustMarshalStopReceipt(receipt), EvidenceAt: at}}}
+ save := func() error {
+ l := servingParkLedger{state: state}
+ if err := l.save(r); err != nil {
+ return err
+ }
+ return l.write("fixture.json", r)
+ }
if err = save(); err != nil {
t.Fatal(err)
}
@@ -178,6 +190,17 @@ func TestRootServingParkUnitPayload(t *testing.T) {
if _, err := fs.inspectRebootTopology(t.Context()); err != nil {
t.Fatal("installed-unit topology inspection", err)
}
+ beforeRecord, err := state.Read("retirement-" + r.Authorization.ID + ".json")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := fs.inspectRebootRecord(t.Context(), r.Authorization.ID); err != nil {
+ t.Fatal("installed-unit record inspection", err)
+ }
+ afterRecord, err := state.Read("retirement-" + r.Authorization.ID + ".json")
+ if err != nil || !bytes.Equal(beforeRecord, afterRecord) {
+ t.Fatal("record inspection changed history", err)
+ }
fs.boot = servingParkBootID
r.Deleting, r.Cause = true, "expiry"
save := func() error { return (&servingParkLedger{state: state}).write("fixture.json", r) }
@@ -417,7 +440,36 @@ func TestRootRebootServingParkChangedMountPhase2(t *testing.T) {
if err != nil || !bytes.Equal(parentBefore, parentAfter) {
t.Fatal("inspection rewrote parent authority", err)
}
- t.Log("persistent directory handles match across actual reboot; retirement authority remains unchanged")
+ beforeRecord, err := state.Read("retirement-" + saved.Record.Authorization.ID + ".json")
+ if err != nil {
+ t.Fatal(err)
+ }
+ recordObservation, err := fs.inspectRebootRecord(t.Context(), saved.Record.Authorization.ID)
+ if err != nil || recordObservation.Source.Mount != observation.CurrentParent.Mount || recordObservation.Event.Mount != observation.CurrentRoot.Mount {
+ t.Fatal("persistent source/event/artifacts after reboot", recordObservation, err)
+ }
+ // Model a reused file inode by changing only the saved persistent handle.
+ // The same device/inode/size must not suffice to identify an old artifact.
+ corrupted := saved.Record
+ corrupted.Source.Files[0].Persistent.Handle = "aa"
+ ledger := servingParkLedger{state: state}
+ if err := ledger.save(corrupted); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := fs.inspectRebootRecord(t.Context(), corrupted.Authorization.ID); err == nil {
+ t.Fatal("changed persistent artifact handle accepted")
+ }
+ if err := state.Write("retirement-"+saved.Record.Authorization.ID+".json", beforeRecord); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := fs.inspectRebootRecord(t.Context(), saved.Record.Authorization.ID); err != nil {
+ t.Fatal("restored read-only control", err)
+ }
+ afterRecord, err := state.Read("retirement-" + saved.Record.Authorization.ID + ".json")
+ if err != nil || !bytes.Equal(beforeRecord, afterRecord) {
+ t.Fatal("record inspection changed history", err)
+ }
+ t.Log("persistent parent/root/source/event/file handles match across actual reboot; retirement authority remains unchanged")
if err := fs.CheckParked(&saved.Record); !errors.Is(err, hostbox.ErrChanged) {
t.Fatalf("old mount authority reused: %v", err)
}
diff --git a/internal/host/claimservingreboot_linux.go b/internal/host/claimservingreboot_linux.go
new file mode 100644
index 000000000..a1b05a5f7
--- /dev/null
+++ b/internal/host/claimservingreboot_linux.go
@@ -0,0 +1,198 @@
+//go:build linux
+
+package host
+
+import (
+ "context"
+ "errors"
+ "io"
+ "os"
+ "reflect"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/provision"
+ "golang.org/x/sys/unix"
+)
+
+// This observation is not filesystem or transport authority. The eventual
+// transaction must retain Record and commit separate current-boot identities.
+type servingParkRebootRecord struct {
+ Topology servingParkRebootObservation
+ Record servingParkRecord
+ Source, Event servingArtifactDirectory
+}
+
+// The caller holds namespace.lock. Repeating the whole inspection rejects a
+// source/name change during handle reads and a concurrent private-record change.
+func (f *servingParkFS) inspectRebootRecord(ctx context.Context, id string) (servingParkRebootRecord, error) {
+ var zero servingParkRebootRecord
+ first, err := f.observeRebootRecord(ctx, id)
+ if err != nil {
+ return zero, err
+ }
+ again, err := f.observeRebootRecord(ctx, id)
+ if err != nil {
+ return zero, err
+ }
+ if !reflect.DeepEqual(first, again) {
+ return zero, hostbox.ErrChanged
+ }
+ if err := ctx.Err(); err != nil {
+ return zero, err
+ }
+ return again, nil
+}
+
+func (f *servingParkFS) observeRebootRecord(ctx context.Context, id string) (servingParkRebootRecord, error) {
+ var out servingParkRebootRecord
+ topology, err := f.inspectRebootTopology(ctx)
+ if err != nil {
+ return out, err
+ }
+ r, err := (&servingParkLedger{state: f.state}).read(id)
+ if err != nil {
+ return out, err
+ }
+ n := topology.Previous
+ if r.Source.BootID != n.BootID || !r.Source.Persistent.valid() || r.Source.Parent != n.Parent || r.Source.Persistent.Filesystem != n.Topology.Parent.Filesystem || r.Prepared && r.Namespace != n.Root {
+ return out, hostbox.ErrChanged
+ }
+ parent, pid, err := f.parent()
+ if err != nil {
+ return out, err
+ }
+ defer parent.Close()
+ root, rid, err := f.privateDir(parent, ".serving-parked")
+ if err != nil {
+ return out, err
+ }
+ defer root.Close()
+ if pid != topology.CurrentParent || rid != topology.CurrentRoot {
+ return out, hostbox.ErrChanged
+ }
+ current := servingParkNamespace{Version: 1, Parent: pid, Root: rid, BootID: topology.BootID}
+ handles, err := f.captureTopology(parent, root, current)
+ if err != nil {
+ return out, err
+ }
+ if *handles != *n.Topology {
+ return out, hostbox.ErrChanged
+ }
+ var source *os.File
+ // Completed parks are independent of the live installation, which may have
+ // restored a successor or been removed. Incomplete parks still require it.
+ if !r.Complete {
+ source, err = servingParkOpenDir(parent, provision.InstanceID(r.Authorization.Successor.Host))
+ if err != nil {
+ return out, err
+ }
+ defer source.Close()
+ out.Source, err = f.rebootDirectory(source, r.Source.Directory, r.Source.Persistent, pid.Mount)
+ if err != nil {
+ return out, err
+ }
+ }
+ event, eid, err := f.privateDir(root, r.Authorization.ID)
+ if errors.Is(err, unix.ENOENT) {
+ if r.Prepared && !r.EventRemoveIntent {
+ return out, hostbox.ErrChanged
+ }
+ } else if err != nil {
+ return out, err
+ } else {
+ defer event.Close()
+ if !r.Prepared || r.EventRemoved {
+ return out, hostbox.ErrChanged
+ }
+ out.Event, err = f.rebootDirectory(event, r.Event, r.EventPersistent, pid.Mount)
+ if err != nil || out.Event != eid {
+ return out, hostbox.ErrChanged
+ }
+ // Only four fixed artifact names can be evidence. Unknown children remain
+ // untouched and block this event, including after an interrupted unlink.
+ names, e := event.Readdirnames(5)
+ if len(names) > 4 || e != nil && !errors.Is(e, io.EOF) {
+ return out, hostbox.ErrChanged
+ }
+ for _, name := range names {
+ known := false
+ for _, item := range r.Source.Files {
+ if item.Present && !item.Deleted && item.Name == name {
+ known = true
+ }
+ }
+ if !known {
+ return out, hostbox.ErrChanged
+ }
+ }
+ }
+ for _, item := range r.Source.Files {
+ src := servingParkFile{Name: item.Name}
+ dst := servingParkFile{Name: item.Name}
+ if source != nil {
+ src, err = servingParkStat(source, item.Name)
+ if err != nil {
+ return out, err
+ }
+ }
+ if event != nil {
+ dst, err = servingParkStat(event, item.Name)
+ if err != nil {
+ return out, err
+ }
+ }
+ if !servingParkRebootFileMatches(item, src, dst, pid.Mount, r) {
+ return out, hostbox.ErrChanged
+ }
+ }
+ if err := f.checkBoot(topology.BootID); err != nil {
+ return out, err
+ }
+ if err := ctx.Err(); err != nil {
+ return out, err
+ }
+ out.Topology, out.Record = topology, r
+ return out, nil
+}
+
+func (f *servingParkFS) rebootDirectory(d *os.File, old servingArtifactDirectory, proof servingParkPersistentDirectory, mount uint64) (servingArtifactDirectory, error) {
+ var zero servingArtifactDirectory
+ id, err := servingParkDirID(d)
+ if err != nil {
+ return zero, err
+ }
+ if !proof.valid() || id.Device != old.Device || id.Inode != old.Inode || id.Mount != mount {
+ return zero, hostbox.ErrChanged
+ }
+ actual, err := f.handle(d)
+ if err != nil {
+ return zero, err
+ }
+ if actual != proof {
+ return zero, hostbox.ErrChanged
+ }
+ return id, nil
+}
+
+func servingParkRebootFileMatches(old, src, dst servingParkFile, mount uint64, r servingParkRecord) bool {
+ if !old.Present {
+ return !src.Present && !dst.Present
+ }
+ if !old.Persistent.valid() {
+ return false
+ }
+ expected := old
+ expected.Mount = mount
+ if old.Deleted || r.EventRemoved {
+ return !src.Present && !dst.Present
+ }
+ if r.Deleting {
+ return !src.Present && (sameServingParkFile(expected, dst) || !dst.Present && old.DeleteIntent)
+ }
+ if r.Parked || old.Moved {
+ return !src.Present && sameServingParkFile(expected, dst)
+ }
+ // A rename can be durable before its progress write. Accept exactly one
+ // recorded inode, with its handle; a second link or a replacement refuses.
+ return sameServingParkFile(expected, src) && !dst.Present || !src.Present && sameServingParkFile(expected, dst)
+}
diff --git a/internal/host/claimservingreboot_linux_test.go b/internal/host/claimservingreboot_linux_test.go
new file mode 100644
index 000000000..d60a4ef73
--- /dev/null
+++ b/internal/host/claimservingreboot_linux_test.go
@@ -0,0 +1,429 @@
+//go:build linux
+
+package host
+
+import (
+ "context"
+ "errors"
+ "os"
+ "path/filepath"
+ "reflect"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/provision"
+ "golang.org/x/sys/unix"
+)
+
+func servingRebootRecordFixture(t *testing.T, phase string) (*servingParkDiskFixture, servingParkRecord) {
+ t.Helper()
+ f := newServingParkDiskFixture(t)
+ a, receipts := f.evidence(1, true, "missing")
+ f.put("", "last database")
+ f.put("-wal", "last WAL")
+ f.put("-shm", "last SHM")
+ if err := f.fs.recordNamespaceParent(t.Context(), a.Successor, a.Directory); err != nil {
+ t.Fatal(err)
+ }
+ root, _, err := f.fs.namespace()
+ if err != nil {
+ t.Fatal(err)
+ }
+ root.Close()
+ var r servingParkRecord
+ if phase == "captured" || phase == "prepared" || phase == "rename-reply" {
+ r, err = f.ledger.capture(t.Context(), a, receipts, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err = f.ledger.save(r); err != nil {
+ t.Fatal(err)
+ }
+ save := func() error { return f.ledger.save(r) }
+ if phase != "captured" {
+ if err = f.fs.Prepare(&r, save); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if phase == "rename-reply" {
+ f.fs.rename = func(from int, old string, to int, name string, flags uint) error {
+ if err := unix.Renameat2(from, old, to, name, flags); err != nil {
+ return err
+ }
+ return unix.EIO
+ }
+ if err = f.fs.Move(&r, save); !errors.Is(err, unix.EIO) {
+ t.Fatal("missing lost rename reply", err)
+ }
+ f.fs.rename = unix.Renameat2
+ }
+ } else {
+ r, err = f.park(a, receipts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if phase != "parked" {
+ f.complete(a)
+ }
+ if phase == "complete" {
+ f.put("", "successor database")
+ }
+ if phase == "unlink-reply" || phase == "partial-unlink" || phase == "rmdir-reply" || phase == "closed" {
+ f.now = f.now.Add(8 * 24 * time.Hour)
+ calls := 0
+ f.fs.unlink = func(fd int, name string, flags int) error {
+ calls++
+ if phase == "partial-unlink" && calls == 2 {
+ return unix.EIO
+ }
+ if err := unix.Unlinkat(fd, name, flags); err != nil {
+ return err
+ }
+ if phase == "unlink-reply" || phase == "rmdir-reply" && flags == unix.AT_REMOVEDIR {
+ return unix.EIO
+ }
+ return nil
+ }
+ err = f.ledger.withNamespace(t.Context(), func() error { return f.ledger.reapLocked(t.Context(), a.ID) })
+ if phase == "closed" && err != nil || phase != "closed" && !errors.Is(err, unix.EIO) {
+ t.Fatal(phase, err)
+ }
+ f.fs.unlink = unix.Unlinkat
+ }
+ }
+ r, err = f.ledger.read(a.ID)
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.fs.boot = func() (string, error) { return servingOtherBoot, nil }
+ return f, r
+}
+
+func TestServingParkRebootRecordReadOnly(t *testing.T) {
+ for _, phase := range []string{"captured", "prepared", "rename-reply", "parked", "complete", "unlink-reply", "partial-unlink", "rmdir-reply", "closed"} {
+ t.Run(phase, func(t *testing.T) {
+ f, r := servingRebootRecordFixture(t, phase)
+ before := servingBootSnapshot(t, f.disk.dir)
+ var got servingParkRebootRecord
+ err := f.ledger.withNamespace(t.Context(), func() error {
+ var err error
+ got, err = f.fs.inspectRebootRecord(t.Context(), r.Authorization.ID)
+ return err
+ })
+ if err != nil || !reflect.DeepEqual(got.Record, r) || got.Topology.BootID != servingOtherBoot {
+ t.Fatal("record inspection", got, err)
+ }
+ if r.Complete && got.Source != (servingArtifactDirectory{}) {
+ t.Fatal("completed park depended on live source")
+ }
+ if after := servingBootSnapshot(t, f.disk.dir); !reflect.DeepEqual(before, after) {
+ t.Fatal("inspection wrote authority")
+ }
+ if err := f.fs.CheckParked(&r); err == nil {
+ t.Fatal("observation granted old-boot effects")
+ }
+ })
+ }
+}
+
+func TestServingParkRebootRecordRefusesChangedEvidence(t *testing.T) {
+ for _, fault := range []string{"no-source-proof", "no-file-proof", "no-event-proof", "source-boot", "source-handle", "event-handle", "file-handle", "file-filesystem", "file-mode", "event-mode", "namespace", "source-replaced", "event-replaced", "file-replaced", "file-hardlink", "file-symlink", "unexpected-child", "missing-file", "duplicate-source", "old-format", "source-race", "event-race", "record-race", "cancel-race"} {
+ t.Run(fault, func(t *testing.T) {
+ f, r := servingRebootRecordFixture(t, "parked")
+ event := filepath.Dir(f.path(r.Authorization, ""))
+ source := filepath.Dir(provisionDBPath(f))
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+ switch fault {
+ case "no-source-proof":
+ r.Source.Persistent = servingParkPersistentDirectory{}
+ case "no-file-proof":
+ r.Source.Files[0].Persistent = servingParkPersistentFile{}
+ case "no-event-proof":
+ r.EventPersistent = servingParkPersistentDirectory{}
+ case "source-boot":
+ r.Source.BootID = servingOtherBoot
+ case "source-handle":
+ r.Source.Persistent.Handle = "aa"
+ case "event-handle":
+ r.EventPersistent.Handle = "aa"
+ case "file-handle":
+ r.Source.Files[0].Persistent.Handle = "aa"
+ case "file-filesystem":
+ r.Source.Files[0].Persistent.Filesystem[0]++
+ case "file-mode":
+ r.Source.Files[0].Persistent.Mode ^= 0100
+ case "event-mode":
+ r.EventPersistent.Mode ^= 0100
+ case "namespace":
+ r.Namespace.Inode++
+ case "source-replaced", "event-replaced":
+ path := source
+ if fault == "event-replaced" {
+ path = event
+ }
+ if err := os.Rename(path, path+"-old"); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Mkdir(path, 0700); err != nil {
+ t.Fatal(err)
+ }
+ case "file-replaced", "file-hardlink", "file-symlink", "missing-file":
+ path := f.path(r.Authorization, "")
+ if fault == "file-hardlink" {
+ if err := os.Link(path, path+"-link"); err != nil {
+ t.Fatal(err)
+ }
+ break
+ }
+ if err := os.Rename(path, path+"-old"); err != nil {
+ t.Fatal(err)
+ }
+ if fault == "file-symlink" {
+ if err := os.Symlink(path+"-old", path); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if fault == "file-replaced" {
+ if err := os.WriteFile(path, []byte("replacement"), 0600); err != nil {
+ t.Fatal(err)
+ }
+ }
+ // Put the retained original outside the event so unknown-child checks
+ // cannot hide a missing/replaced-file validation bug.
+ if err := os.Rename(path+"-old", source+"/saved-original"); err != nil {
+ t.Fatal(err)
+ }
+ case "unexpected-child":
+ if err := os.WriteFile(filepath.Join(event, "unknown"), []byte("retain"), 0600); err != nil {
+ t.Fatal(err)
+ }
+ case "duplicate-source":
+ f.put("", "replacement")
+ case "old-format":
+ r.Source.Persistent = servingParkPersistentDirectory{}
+ r.Source.BootID = ""
+ r.EventPersistent = servingParkPersistentDirectory{}
+ for i := range r.Source.Files {
+ r.Source.Files[i].Persistent = servingParkPersistentFile{}
+ }
+ case "source-race", "event-race", "record-race", "cancel-race":
+ calls := 0
+ f.fs.handle = func(d *os.File) (servingParkPersistentDirectory, error) {
+ p, err := servingParkPersistentDir(d)
+ calls++
+ // The first topology inspection takes four handles; the pinned
+ // topology takes two, followed by source and event.
+ if calls == 8 {
+ switch fault {
+ case "cancel-race":
+ cancel()
+ case "record-race":
+ changed := r
+ changed.Receipts = nil
+ if err := f.ledger.write("retirement-"+r.Authorization.ID+".json", changed); err != nil {
+ t.Fatal(err)
+ }
+ default:
+ path := source
+ if fault == "event-race" {
+ path = event
+ }
+ if err := os.Rename(path, path+"-old"); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Mkdir(path, 0700); err != nil {
+ t.Fatal(err)
+ }
+ }
+ }
+ return p, err
+ }
+ }
+ if err := f.ledger.write("retirement-"+r.Authorization.ID+".json", r); err != nil {
+ t.Fatal(err)
+ }
+ before := servingBootSnapshot(t, f.disk.dir)
+ got, err := f.fs.inspectRebootRecord(ctx, r.Authorization.ID)
+ if err == nil || !reflect.DeepEqual(got, servingParkRebootRecord{}) {
+ t.Fatal("accepted changed record", got, err)
+ }
+ if fault != "record-race" {
+ if after := servingBootSnapshot(t, f.disk.dir); !reflect.DeepEqual(before, after) {
+ t.Fatal("refusal wrote authority")
+ }
+ }
+ })
+ }
+}
+
+func provisionDBPath(f *servingParkDiskFixture) string {
+ return filepath.Join(f.data, "instances", provision.InstanceID(f.base.Host), f.base.Host+".db")
+}
+
+func TestServingParkPersistentEvidenceNeverUpgradesOldIntent(t *testing.T) {
+ for _, phase := range []string{"intent", "consumption"} {
+ t.Run(phase, func(t *testing.T) {
+ f := newServingParkDiskFixture(t)
+ a, receipts := f.evidence(1, true, "missing")
+ f.put("", "legacy")
+ r, err := f.ledger.capture(t.Context(), a, receipts, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ r.Source.Persistent = servingParkPersistentDirectory{}
+ r.Source.BootID = ""
+ for i := range r.Source.Files {
+ r.Source.Files[i].Persistent = servingParkPersistentFile{}
+ }
+ if phase == "intent" {
+ err = f.ledger.save(r)
+ } else {
+ err = f.ledger.write(servingLegacyKey("legacy", a.Successor), servingLegacyConsumption{Authorization: a, Source: r.Source, Receipts: receipts})
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+ parked, err := f.park(a, receipts)
+ if err != nil {
+ t.Fatal("old intent replay", err)
+ }
+ if parked.Source.Persistent != (servingParkPersistentDirectory{}) || parked.Source.BootID != "" || parked.EventPersistent != (servingParkPersistentDirectory{}) {
+ t.Fatal("upgraded old evidence")
+ }
+ f.complete(a)
+ })
+ }
+}
+
+func TestServingParkRebootFileRequiresDeleteIntent(t *testing.T) {
+ f, r := servingRebootRecordFixture(t, "unlink-reply")
+ r.Source.Files[0].DeleteIntent = false
+ if err := f.ledger.write("retirement-"+r.Authorization.ID+".json", r); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := f.fs.inspectRebootRecord(t.Context(), r.Authorization.ID); !errors.Is(err, hostbox.ErrChanged) {
+ t.Fatal("absent artifact without delete intent accepted", err)
+ }
+}
+
+func TestServingParkRebootRecordChangedMount(t *testing.T) {
+ f, r := servingRebootRecordFixture(t, "rename-reply")
+ oldMount := r.Source.Parent.Mount + 123
+ r.Source.Parent.Mount = oldMount
+ r.Source.Directory.Mount = oldMount
+ r.Authorization.Directory.Mount = oldMount
+ r.Event.Mount = oldMount
+ r.Namespace.Mount = oldMount
+ for i := range r.Source.Files {
+ if r.Source.Files[i].Present {
+ r.Source.Files[i].Mount = oldMount
+ }
+ }
+ if err := f.ledger.save(r); err != nil {
+ t.Fatal(err)
+ }
+ parent, err := f.fs.namespaceParent()
+ if err != nil {
+ t.Fatal(err)
+ }
+ parent.Parent.Mount = oldMount
+ if err := f.ledger.write("namespace-parent.json", parent); err != nil {
+ t.Fatal(err)
+ }
+ var n servingParkNamespace
+ b, err := f.disk.Read("namespace.json")
+ if err != nil || servingParkDecode(b, &n) != nil {
+ t.Fatal(err)
+ }
+ n.Parent.Mount = oldMount
+ n.Root.Mount = oldMount
+ if err := f.ledger.write("namespace.json", n); err != nil {
+ t.Fatal(err)
+ }
+ got, err := f.fs.inspectRebootRecord(t.Context(), r.Authorization.ID)
+ if err != nil || got.Source.Mount == oldMount || got.Event.Mount == oldMount || got.Source.Mount != got.Event.Mount || !reflect.DeepEqual(got.Record, r) {
+ t.Fatal("changed-mount inspection", got, err)
+ }
+}
+
+func TestServingParkRebootCompletedDoesNotReadSource(t *testing.T) {
+ f, r := servingRebootRecordFixture(t, "complete")
+ path := filepath.Dir(provisionDBPath(f))
+ if err := os.Rename(path, path+"-removed"); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Symlink(path+"-removed", path); err != nil {
+ t.Fatal(err)
+ }
+ got, err := f.fs.inspectRebootRecord(t.Context(), r.Authorization.ID)
+ if err != nil || got.Source != (servingArtifactDirectory{}) {
+ t.Fatal("completed park consulted source", got, err)
+ }
+}
+
+func TestServingParkPersistentLegacyConsumption(t *testing.T) {
+ f, r := servingRebootRecordFixture(t, "parked")
+ c, err := f.ledger.legacyConsumption(r.Authorization.Successor)
+ if err != nil || c == nil {
+ t.Fatal(err)
+ }
+ if !sameServingLegacyConsumption(*c, r) || !sameServingLegacySource(r.Source, c.Source) {
+ t.Fatal("fixture consumption mismatch")
+ }
+ old := r.Source
+ old.Persistent = servingParkPersistentDirectory{}
+ old.BootID = ""
+ for i := range old.Files {
+ old.Files[i].Persistent = servingParkPersistentFile{}
+ }
+ partial := old
+ partial.Files[0].Persistent = r.Source.Files[0].Persistent
+ if sameServingLegacySource(old, partial) {
+ t.Fatal("partial consumed proof accepted as old evidence")
+ }
+ for _, field := range []string{"boot", "directory", "file"} {
+ t.Run(field, func(t *testing.T) {
+ changed := *c
+ switch field {
+ case "boot":
+ changed.Source.BootID = servingOtherBoot
+ case "directory":
+ changed.Source.Persistent.Handle = "aa"
+ case "file":
+ changed.Source.Files[0].Persistent.Handle = "aa"
+ }
+ if sameServingLegacyConsumption(changed, r) || sameServingLegacySource(r.Source, changed.Source) {
+ t.Fatal("changed immutable consumption accepted")
+ }
+ })
+ }
+}
+
+func TestServingParkPersistentEventCaptureFailure(t *testing.T) {
+ f := newServingParkDiskFixture(t)
+ a, receipts := f.evidence(1, true, "missing")
+ f.put("", "preserve source")
+ f.fs.handle = func(d *os.File) (servingParkPersistentDirectory, error) {
+ if d.Name() == a.ID {
+ return servingParkPersistentDirectory{}, unix.EIO
+ }
+ return servingParkPersistentDir(d)
+ }
+ if _, err := f.park(a, receipts); !errors.Is(err, unix.EIO) {
+ t.Fatal("event capture failure", err)
+ }
+ r, err := f.ledger.read(a.ID)
+ if err != nil || r.Prepared || r.EventPersistent != (servingParkPersistentDirectory{}) {
+ t.Fatal("failed capture gained event authority", err)
+ }
+ body, err := os.ReadFile(provisionDBPath(f))
+ if err != nil || string(body) != "preserve source" {
+ t.Fatal("capture failure changed source", err)
+ }
+ f.fs.handle = servingParkPersistentDir
+ if _, err := f.park(a, receipts); err != nil {
+ t.Fatal("same-boot event capture recovery", err)
+ }
+}
diff --git a/internal/host/claimservingtopology_linux.go b/internal/host/claimservingtopology_linux.go
index c4de00cb0..ee7d649b5 100644
--- a/internal/host/claimservingtopology_linux.go
+++ b/internal/host/claimservingtopology_linux.go
@@ -12,26 +12,10 @@ import (
"golang.org/x/sys/unix"
)
-// File handles distinguish a retained inode from a later reuse of its number.
-// They are meaningful only within the filesystem that issued them. Neither a
-// mount number nor a device number alone identifies that filesystem after boot.
-type servingParkPersistentDirectory struct {
- Filesystem [2]int32
- Type int32
- Handle string
- Owner uint32
- Mode uint32
-}
-
type servingParkTopology struct {
Parent, Root servingParkPersistentDirectory
}
-func (p servingParkPersistentDirectory) valid() bool {
- b, err := hex.DecodeString(p.Handle)
- return err == nil && len(b) > 0 && len(b) <= 128 && hex.EncodeToString(b) == p.Handle && p.Type > 0 && p.Filesystem != [2]int32{} && p.Mode&unix.S_IFMT == unix.S_IFDIR && p.Mode&0022 == 0
-}
-
// This only obtains a handle for an already-open directory. It never opens a
// supplied handle, follows a request path or changes mount permissions.
func servingParkPersistentDir(d *os.File) (servingParkPersistentDirectory, error) {
diff --git a/internal/provision/testdata/plan14-root-reads-seed.tsv b/internal/provision/testdata/plan14-root-reads-seed.tsv
index 3fe6cc933..404ba08ec 100644
--- a/internal/provision/testdata/plan14-root-reads-seed.tsv
+++ b/internal/provision/testdata/plan14-root-reads-seed.tsv
@@ -1,4 +1,4 @@
-# Files seed checkpoint 20 overlay; pending Paul’s census ruling.
+# Files seed checkpoint 21 overlay; pending Paul’s census ruling.
# Database retirement activation remains refused; serving installation transfer uses the existing root transport.
# Load after zddr4 and its explicit seal check. No legacy fingerprints are added or removed.
# Every classification below is rev3=PROPOSED-seed, not an approved disposition.
@@ -32,26 +32,26 @@ internal/host/claimservingretire.go servingRetirementID authority C,J4 rev3=PROP
internal/host/claimservingretire.go servingWriterIdentity authority C,J4 rev3=PROPOSED-seed; authoritative serving holder and unique cold admission; exact local executor, stamp, session, generation, incarnation, location and region file:Exec@403 f7d8c868ce9856eab448aa72bdf20ee02cb70179ce0f435d45888f72e0c3b7bf
internal/hostbox/servingpark.go LaunchJournal.RootSessionLaunches authority C,J4 rev3=PROPOSED-seed; root-local persisted launch records for exact host/incarnation/stamp/session/box/enrollment; refuses agent journal transport cbbb9378bc53ac499972bed5fb654e05856a9afd7256d55323a717140ffcba18
internal/host/claimservingpark_linux.go newServingParkFS authority C,J4 rev3=PROPOSED-seed; root-only filesystem dependencies, fixed nosymfollow validation and private state store; bound through private root service construction; production boot identity reader is fixed at construction; persistent file-handle reader fixed at production construction 30278f0fb52daaea0247eecaeeeb23aef50051f752817aca3681333c4e538293
-internal/host/claimservingpark_linux.go sameServingParkFile authority C,J4 rev3=PROPOSED-seed; recorded basename, presence, mount, device, inode, regular type, single link and allocated-byte identity d5d684356e793dcfd55fe1ec9c609f2320e0155e052caca42475348075e8fd68
+internal/host/claimservingpark_linux.go sameServingParkFile authority C,J4 rev3=PROPOSED-seed; recorded basename, presence, mount, device, inode, regular type, single link and allocated-byte identity; new recorded files also require the original filesystem handle, owner and mode; old saved identities retain their existing checks 58b2d78cd6a6d35e1da3e43d9f9761a6986da63ff6fb171e73c3874746674dae
internal/host/claimservingpark_linux.go servingParkDirID authority C,J4 rev3=PROPOSED-seed; descriptor statx directory type, device, inode and mount identity file:Statx@102 3480dad6d65fedcc17e678df38cf13352ad3c4a8a9d0a09a395973d92b98094e
-internal/host/claimservingpark_linux.go servingParkFS.Capture authority C,J4 rev3=PROPOSED-seed; canonical nested source directory, four fixed basenames, source mount and bounded generation-marker bytes through the captured descriptor file:Fstat@920,file:Openat@694,file:ReadAll@1138 c3a9348ddd9548f3fcfdcd45abc42b6a758a6399ed34e92481785e26c110763d
+internal/host/claimservingpark_linux.go servingParkFS.Capture authority C,J4 rev3=PROPOSED-seed; canonical nested source directory, four fixed basenames, source mount and bounded generation-marker bytes through the captured descriptor; source boot and persistent directory handle captured before mutation and rechecked after all artifact reads file:Fstat@1199,file:Openat@973,file:ReadAll@1417 1a0278ee90f2d1b7f3a604ba2b6f1e17a8c4e0b2b0a35c6b4f3f0667b78a8c91
internal/host/claimservingpark_linux.go servingParkFS.CheckParked authority C,J4 rev3=PROPOSED-seed; all recorded event identities, unexpected children and source absence before root completion file:Readdirnames@1266 ac675e110e1493ab1ec69359696d462228f1e1265923c2c0d471111851a2c645
internal/host/claimservingpark_linux.go servingParkFS.Delete authority C,J4 rev3=PROPOSED-seed; recorded event and complete artifact set before journaled unlink; persist proven namespace identity before old-record deletion; remove only the recorded empty event file:Readdirnames@402 7fa2b1a5f32a7c67eceb48e6eee5adaef44fbb0c065e05a21fcd60391f40ca34
internal/host/claimservingpark_linux.go servingParkFS.Move authority C,J4 rev3=PROPOSED-seed; recorded source/destination inodes and explicit absences; no-replace same-mount rename, post-rename identity and both parent fsyncs before progress 6e43ae58b6a02d167135fabdde7ade06502511c5237b313e6ca6c2a8e9a2c8d0
-internal/host/claimservingpark_linux.go servingParkFS.Prepare authority C,J4 rev3=PROPOSED-seed; recorded private namespace and empty event; persist namespace and event identities together; old prepared events gain a namespace only after exact identity proof file:Prepare@24,file:Readdirnames@967 1fddbfb85e6381113db4807f41890dac36a104997094a139408eca75b4572eab
+internal/host/claimservingpark_linux.go servingParkFS.Prepare authority C,J4 rev3=PROPOSED-seed; recorded private namespace and empty event; persist namespace and event identities together; old prepared events gain a namespace only after exact identity proof; new intents record the private event filesystem handle before the first rename; old records gain no persistent evidence file:Prepare@24,file:Readdirnames@967 653a70287033f46e8b898d5b3ef7fdec2af31f399e8bebc248e2d3b11e23517c
internal/host/claimservingpark_linux.go servingParkFS.event authority C,J4 rev3=PROPOSED-seed; current or proven displaced namespace and exact private event mount/device/inode; old records may fall back only to the previous root recorded before quarantine 625ed442442a9b07c57392f72dea64e9e91eba65d2e2ef0e9899275756336788
internal/host/claimservingpark_linux.go servingParkFS.namespace authority C,J4 rev3=PROPOSED-seed; private rootstate namespace and quarantine completion records, effective parent mount and root-owned park directory; pending quarantine blocks namespace creation and use; unknown roots refused without adoption; the saved namespace boot must equal the current kernel boot before opening or changing park contents; new namespace publication includes persistent parent/root handles before any event creation file:Read@282 768954e03ac18ebb0e28b04ebb0ad0e1e09a546fee93476367e4be1679b08f1a
internal/host/claimservingpark_linux.go servingParkFS.parent authority C,J4 rev3=PROPOSED-seed; deployment-controlled instances path, descriptor mount identity and effective nosymfollow topology; path replacement refuses 1d241a7d446bbbade6e460efd911863f8a5246b8ac1d47b2548034e3feb2f216
internal/host/claimservingpark_linux.go servingParkFS.privateDir authority C,J4 rev3=PROPOSED-seed; no-follow directory descriptor, exact private owner and mode plus device/inode/mount identity file:Fstat@261 8a4a33c6871fa88cd53f9922f99e173102966827bfea3998b2b3bd749a2ad11b
internal/host/claimservingpark_linux.go servingParkFS.source authority C,J4 rev3=PROPOSED-seed; canonical host-derived nested state directory checked against independently supplied root directory identity; no registry path b7a30539c735e811c366ebb7c853a962500e82305ba1caa64f73402ae6edbce5
internal/host/claimservingpark_linux.go servingParkOpenDir authority C,J4 rev3=PROPOSED-seed; single basename opened relative to a pinned parent with no-follow directory semantics file:Openat@210 665ff3adc3e1cd1c6c4b92439160a0ef8013ea8e35375bcdfa90747888751ff9
-internal/host/claimservingpark_linux.go servingParkStat authority C,J4 rev3=PROPOSED-seed; no-follow artifact statx identity, single links, explicit absence and checked allocated-byte count file:Statx@143 2c5d25be8581568148d13e3cad6f4b5f861fec8bff2545502fbdea0d5d949bb6
+internal/host/claimservingpark_linux.go servingParkStat authority C,J4 rev3=PROPOSED-seed; no-follow artifact statx identity, single links, explicit absence and checked allocated-byte count; regular files additionally obtain a descriptor-pinned persistent filesystem handle file:Statx@143 52bf86dad4d27ade2cb85ddff8faa7bf5a12bf34706641855dc4b43c06bfa9d0
internal/host/claimservingpark_other.go newServingParkFS authority C,J4 rev3=PROPOSED-seed; non-Linux refusal or construction of refusal-only filesystem; no filesystem authority; production boot identity reader is fixed at construction; persistent file-handle reader fixed at production construction 69baa4c747e687578e73b8369f2df9987fe6fb5836b78796cc41c776ba12c802
-internal/host/claimservingpark_other.go servingParkFS.Capture authority C,J4 rev3=PROPOSED-seed; non-Linux refusal or construction of refusal-only filesystem; no filesystem authority a5560b338356792386a5a1b2e27cb08ca0d5aa024cd03b567a78dfd986ce7871
+internal/host/claimservingpark_other.go servingParkFS.Capture authority C,J4 rev3=PROPOSED-seed; non-Linux refusal or construction of refusal-only filesystem; no filesystem authority; source boot and persistent directory handle captured before mutation and rechecked after all artifact reads a5560b338356792386a5a1b2e27cb08ca0d5aa024cd03b567a78dfd986ce7871
internal/host/claimservingpark_other.go servingParkFS.CheckParked authority C,J4 rev3=PROPOSED-seed; non-Linux refusal or construction of refusal-only filesystem; no filesystem authority 3a8cb4d8052bbdea8cf7a300944de72c68012e6efa252c293fc1dc06a3a2150e
internal/host/claimservingpark_other.go servingParkFS.Delete authority C,J4 rev3=PROPOSED-seed; recorded event and artifact identities; validates complete child set before journaled unlink, then removes only the recorded empty event with durable intent and progress 8c169e8428b913d2f05b502ce971be49d5febaad95d8ae811aae21c81f3434ce
internal/host/claimservingpark_other.go servingParkFS.Move authority C,J4 rev3=PROPOSED-seed; non-Linux refusal or construction of refusal-only filesystem; no filesystem authority 9d215b168da646166104db5624727038f793d2ae0c40eb71f23883f12b6b4b13
-internal/host/claimservingpark_other.go servingParkFS.Prepare authority C,J4 rev3=PROPOSED-seed; non-Linux refusal or construction of refusal-only filesystem; no filesystem authority file:Prepare@22 645770372357707f635351a377e943e78f1c160222385e41f42b80d57adedc29
+internal/host/claimservingpark_other.go servingParkFS.Prepare authority C,J4 rev3=PROPOSED-seed; non-Linux refusal or construction of refusal-only filesystem; no filesystem authority; new intents record the private event filesystem handle before the first rename; old records gain no persistent evidence file:Prepare@22 645770372357707f635351a377e943e78f1c160222385e41f42b80d57adedc29
internal/host/claimservingpark_records.go servingParkDecode authority C,J4 rev3=PROPOSED-seed; strict private-record JSON with unknown-field and trailing-value refusal 189c7944ea9f5d269d39ed6a869b6bd739e3033195f06cc1f2c9f23a23180184
internal/host/claimservingpark_records.go servingParkKey authority C,J4 rev3=PROPOSED-seed; box/host identity used for stable per-host namespace and slot keys 67518302550380dbf9b741a73758e5c94307eff70ff8a09cc56763a8aa794d86
internal/host/claimservingpark_records.go servingParkLedger.completeLocked authority C,J4 rev3=PROPOSED-seed; root retirement and pending-slot records plus fresh filesystem absence/identity checks; completion requires outer root claim-tag authority through typed journal completion 00f9c10b8130c134a41dea74848aa1651ae3331c8440a81e1b3f59f54f6a7218
@@ -64,7 +64,7 @@ internal/host/claimservingpark_records.go servingParkLedger.slot authority C,J4
internal/host/claimservingpark_records.go servingParkLedger.withNamespace authority C,J4 rev3=PROPOSED-seed; private rootstate namespace lock and cancellation before invoking root authority; composed with root host authority through the typed journal service b7cfae44fdb7de9ef8130f5581710ae3a1a526bb059b4f616650172490f5c8ac
internal/host/claimservingpark_records.go servingParkLedger.write authority C,J4 rev3=PROPOSED-seed; serialized private record sent only to the supplied rootstate store 0635d2c27871e67079119a0f2e3034e817b765d0fe3a47e7d9bdc66fb2779571
internal/host/claimservingpark_records.go type servingParkFilesystem authority C,J4 rev3=PROPOSED-seed; private filesystem effect interface over root-recorded retirement identities; no wire or public request fields file:Prepare@180 b93f0e2af16b66aee1ff71294e215eeba9e661fe696c917c982d9ebc2dc682c7
-internal/host/claimservingpark_records.go validateServingParkRecord authority C,J4 rev3=PROPOSED-seed; private immutable authorization, source/event identities and durable progress; collection deadline requires complete closed removal and known namespace, no earlier than parked time plus transport receipt retention a199f98a4716aa6315e08b6a71f97b90ea21214b197ed7b219b7515a77ba1235
+internal/host/claimservingpark_records.go validateServingParkRecord authority C,J4 rev3=PROPOSED-seed; private immutable authorization, source/event identities and durable progress; collection deadline requires complete closed removal and known namespace, no earlier than parked time plus transport receipt retention; validates all-or-none persistent source and event proof before accepting progress 23633a2a4949c1de529f192bf19aab927b0218bc0ed8cdbc67a55463c22e15bd
internal/host/claimservingparksource_linux.go servingParkStateDirectory authority C,J4 rev3=PROPOSED-seed; root installation/inventory snapshots and authoritative serving observation; exact account/app/record, revision/enrollment, canonical nested path, ownership and descriptor identity; root acquires these inputs under the host lock through the typed journal service ec4a121330c5c8f27343f21af0bbe458cced885d4754e96c0d87b12d9b4a8f35
internal/hostguard/servingretirement.go Guard.WithServingRetirement authority C,J4 rev3=PROPOSED-seed; fresh serving claim and root installation/fence snapshot under root and shared host locks; refuses pending, quarantined, foreign, retired or already stopped successors; permits an older completed predecessor 3d105c50be7e613777055f561321efadf77fe823d8dc3263a60f2c1211b71108
internal/host/claimservingparkroot_linux.go servingParkRoot.withAuthority authority C,J4 rev3=PROPOSED-seed; live edge peer reauthentication under namespace and host locks before census, inventory and final claim checks; root request entry refuses nil connections d3582f0760b4459721b3e6040cda738b105c1da19cc5d0746d04fea26e313b5c
@@ -75,7 +75,7 @@ internal/host/claimservingparkroot_linux.go validateServingRootHistory authority
internal/host/claimservingpark_records.go servingParkRecord.activeAuthorization authority C,J4 rev3=PROPOSED-seed; last persisted continuation authorization or immutable original authorization; no source or deadline changes fe168fe52317e45ce70c3e97a9bc2c52d2409328ab4844a8d2a34fc4babba36d
internal/host/claimservingpark_records.go servingParkLedger.continueLocked authority C,J4 rev3=PROPOSED-seed; namespace/host-protected slot and record locks; pending original retirement, absent source and retained parked identities before durable bounded continuation 87f297a924f7f153624304233218d8b5548a016fc7dbf0f7cb8dedb789324009
internal/host/claimservingpark_records.go servingParkLedger.replaceLocked authority C,J4 rev3=PROPOSED-seed; completed incumbent record under its record lock; resumes its recorded deletion cause before slot replacement 3e67316e959c9a901c32a2a61fda4e6ea5e92fe51bdb7fdb5f5c53d9d5960aad
-@coverage internal/host 7ab31702b7dc1f909f0c3feb31042727737b908618992b5f3a51facc00480285
+@coverage internal/host 8e632e791c936517445aa9719444af1e2f41dd698f82299b0b989c11a4879e43
@coverage internal/hostbox eb1f4980ba609cc886ae4bd73aedbb6be3fb2eb104165bb1d8404da0814ed190
@coverage internal/hostguard a721142f4f3ac5fa17b6b1e2ccfc66ca809cfdcbf2733fdc00fbd0efaa90aeb6
internal/host/claimservinglegacy.go servingLegacyKey authority C,J4 rev3=PROPOSED-seed; box, host and incarnation derive private stop and consumption keys; never a caller path 93c10e40202ab23986b54a366edd7abfab9f8d942736305b5732e8741e96f3ab
@@ -88,8 +88,8 @@ internal/host/claimservinglegacy.go servingParkLedger.legacyUnlaunched authority
internal/host/claimservinglegacyroot_linux.go servingParkRoot.retainLegacyNoProcess authority C,J4 rev3=PROPOSED-seed; root coordinator result before publication; namespace and host guards, census, roster, fresh original claim and immutable retained stop; repeated preadmission witnesses tolerate only Gen, ETag and retry metadata changes file:Read@1210 5460530ae4367720515089e9fda3105aa1032b8282f4923bbf7f8af9600f4f63
internal/host/claimservinglegacyroot_linux.go servingParkRoot.validateLegacyRootEvidence authority C,J4 rev3=PROPOSED-seed; retained no-process or stopped-admission evidence; exact predecessor fields with canonical recorded directory revision bounded by the current actor aa0dfe36c042ab65fd826e4b83eb81bfb9d03e6a54daafa828d5164492329d0d
internal/host/claimservinglegacyroot_linux.go servingParkRoot.parkLegacyServingDatabase authority C,J4 rev3=PROPOSED-seed; fresh serving claim, root installation, consumed legacy identity and exact stop/empty evidence; collected consumption refuses recapture, while consumption saved before the first intent still resumes file:Read@3525 c192eda0fc53bc5211b02123ed7745e8e2b1b2fc53ad5fb3b0d11fe95fd9672c
-internal/host/claimservinglegacyroot_linux.go sameServingLegacySource authority C,J4 rev3=PROPOSED-seed; immutable directory and artifact identities from consumed source and retirement; mutable rename/delete progress is excluded 925e4f20aee042895a83ea3787f4482ae732b6c6bcd45d9d10226ff87bd37091
-internal/host/claimservingpark_records.go servingParkLedger.parkWithEvidenceLocked authority C,J4 rev3=PROPOSED-seed; root-authenticated source, prior slot and immutable intent; a missing same-incarnation retirement must be newer than the last successor; preserve park ownership and replay refusal after collection file:Prepare@3449,file:Read@1774 099cf4772da0b33d2685e3d2b4de826923a256ba8e8dd576887df1d270c78abd
+internal/host/claimservinglegacyroot_linux.go sameServingLegacySource authority C,J4 rev3=PROPOSED-seed; immutable directory and artifact identities from consumed source and retirement; mutable rename/delete progress is excluded; source boot and persistent source-directory identity also constrain the immutable legacy source comparison af04f97f71570ea92099c78c613d252d1174864d54a5161318fcd9d5ecd958fa
+internal/host/claimservingpark_records.go servingParkLedger.parkWithEvidenceLocked authority C,J4 rev3=PROPOSED-seed; root-authenticated source, prior slot and immutable intent; a missing same-incarnation retirement must be newer than the last successor; preserve park ownership and replay refusal after collection; old-format replay preserves original consumption and source evidence without inferred handles file:Prepare@3772,file:Read@1774 c0cdd64d93a729abd8cf2a48606579516824c35181f977631de4a1a395a0b8c7
internal/host/claimservingpark_records.go sameServingRootStops authority C,J4 rev3=PROPOSED-seed; byte-identical root stop sets including targets, evidence timestamps and original receipt bytes c9e0803635546eecc3a7d07760cb69fc915182f96b0d1581ed655de3382edd88
internal/host/claimstop.go ClaimStopCoordinator.witnessNoProcess authority C,G,J4 rev3=PROPOSED-seed; existing peer, claim, root inventory and positive kernel no-process checks; local adopted holder requires new retained-evidence binding; original observation is retained before receipt publication file:Exec@1845,file:Read@2541,file:Read@668 4a0df51297d2f6ed2e9c284ee11c26b73747bf84e1da5214d00f190eece5eb38
internal/host/claimstop.go type ClaimStopCoordinator authority C,G,J4 rev3=PROPOSED-seed; root-only coordinator dependency for retaining its own session-zero observation; nil preserves existing refusal for adopted legacy holders b7114aa7340cdba82fef42d62b784eae16da7acc48407ae6edba6453a4889f9a
@@ -126,7 +126,7 @@ internal/host/claimretire_transport.go ClaimRootHTTP.RecoverPending authority C,
internal/host/claimservingparkroot_linux.go servingParkRoot.withRootInstallation authority C,J4 rev3=PROPOSED-seed; fresh claim, root installation and directory inventory, eligible enrollment, paired tags, census gate and post-input claim reread under namespace then host guard; startup grants no new identity file:Read@2113 8060d3a2628f86257c3600c986242e0d4957ee918214a89655af828dd3ddf93b
internal/host/claimservingpark_linux.go servingParkFS.removeEvent authority C,J4 rev3=PROPOSED-seed; recorded current or displaced namespace and empty event; durable removal intent, descriptor-relative rmdir and fsync; ambiguous old-root absence refuses file:Readdirnames@790 66aa36321bc2bf3245daec35032783bb164cb21fe9b47620fbec71fd4b56cb89
internal/host/claimservingpark_linux.go servingParkFS.removedEvent authority C,J4 rev3=PROPOSED-seed; absence within the exact recorded current or displaced root; root replacement and ambiguous pre-upgrade absence refuse d4100944fa7e41b83578839a91df3e546fa069f2c2558b9e8216b53fd329306d
-internal/host/claimservingpark_records.go type servingParkRecord authority C,J4 rev3=PROPOSED-seed; private versioned retirement shape; additive collection deadline requires full completion and event removal; original authorization and retention deadline remain immutable ea0f918608527d73d259ca2c0947bea6a24485e1ac0777715282ae43b1fbb53d
+internal/host/claimservingpark_records.go type servingParkRecord authority C,J4 rev3=PROPOSED-seed; private versioned retirement shape; additive collection deadline requires full completion and event removal; original authorization and retention deadline remain immutable; optional persistent event-directory handle recorded before rename 2bfdfe6cffe89e4a070c274463b65496eb2e17457f062fe6bbf6157ec1d14a62
internal/host/claimservingpark_recovery.go const servingParkRecoveryLimit authority C,J4 rev3=PROPOSED-seed; fixed root-side processing limit of 128 retirement records per recovery page 6b949581334f55a7bae1b4579e56ac74810fc3042fdf568fbfa8e6b3c5a2ef11
internal/host/claimservingpark_recovery.go var errServingParkRetagPending authority C,J4 rev3=PROPOSED-seed; fixed pending-retag refusal; startup cannot write the independent claim writer tags 8fa87ebef4defae4fc1733528a76e0cc260711c2f5a664308e5368d69286d45b
internal/host/claimservingpark_recovery.go type servingParkRecoveryPage authority C,J4 rev3=PROPOSED-seed; private bounded recovery results, cursor and incomplete indicator; never a transport selector c2fb4445ed50b5237b72e8a5fff9b1b2f6c9dd1397473eeb2c78a6b8c2804b27
@@ -134,7 +134,7 @@ internal/host/claimservingpark_recovery.go type servingParkRecoveryResult author
internal/host/claimservingpark_recovery.go servingParkLedger.recoveryIDs authority C,J4 rev3=PROPOSED-seed; private rootstate names; sorted bounded retirement page and continuation cursor; malformed record IDs retained for reporting 5b450cd0fb6ae6694bca2f9f75fe972c0d65681981a4caddde7e5c7269108782
internal/host/claimservingpark_recovery.go servingParkLedger.recoverCompleted authority C,J4 rev3=PROPOSED-seed; private completed retirement, exact filesystem deletion and slot references; resume removal, clear completed pending reference and collect only obsolete tombstones after durable replay deadline a2a277c862ec65e8abaf8b93b7eba77a79e538a76d061fb080f37f6c0c6d48f4
internal/host/claimservingpark_recovery.go servingParkRuntime.recoverPending authority C,J4 rev3=PROPOSED-seed; service lifetime cancellation and join; bounded startup results rendered only with indexed host/record/fixed-reason text 0aa3ef7edd0ac2c12b4221907ccd5927e7e3e03a4c2c0b77b669bc10cc49c241
-internal/host/claimservinglegacy.go sameServingLegacyConsumption authority C,J4 rev3=PROPOSED-seed; immutable legacy consumption and retained retirement source identities; compares original evidence while ignoring per-artifact progress and the separate collection flag 1524b2aae315e05734a666f80596bc341a007098a84fedeaf1e32451663d4fd4
+internal/host/claimservinglegacy.go sameServingLegacyConsumption authority C,J4 rev3=PROPOSED-seed; immutable legacy consumption and retained retirement source identities; compares original evidence while ignoring per-artifact progress and the separate collection flag; source boot and persistent directory identity must remain equal to the permanent legacy-consumption record dff50631bc0eac3586c220ea2a131cf233a35b20e3b3f5e8fe35904fcc43c722
internal/host/claimservingpark_recovery_linux.go servingParkRoot.recoverServingPark authority C,J4 rev3=PROPOSED-seed; retained intent, exact current successor, independently rechecked installation, enrollment, receipt and local launch history; resumes original park and completes only existing paired tags; no retry settlement 0839c879792469648ceb62edce21319050351cf4db0a57c1a5220282afb7808f
internal/host/claimservingpark_recovery_linux.go servingParkRoot.recoverServingParks authority C,J4 rev3=PROPOSED-seed; private record page and per-record validation; continues past corrupt or refused hosts and reports incomplete enumeration; completed deletion separated from live host authority; first page recovers namespace under its lock using separate private parent evidence, without selecting a live host fb838aec596eaf1b26069f16006b5b2512284710c9bcebf8abfc65dbc9eb13cf
internal/host/claimservinghandover.go ClaimRootHTTP.servingHandover authority C,J4 rev3=PROPOSED-seed; root-only serving transfer constructor and existing private journal, inventory and registry dependencies 213a1885dd9aa5d0079cd52c4f88c6893ea5b43bfbfd83a67ec2c4697a83265a
@@ -182,11 +182,25 @@ internal/host/claimservingboot_linux.go servingParkFS.checkBoot authority C,J4 r
internal/host/claimservingpark_linux.go type servingParkNamespace authority C,J4 rev3=PROPOSED-seed; durable parent and private-root identities with additive kernel boot scope; an old record is decodable but lacks current mutation authority; optional persistent parent/root proof captured at creation, never inferred for older records e5b695d1869a0565d199267dae6f16ace6090c57a4b3c4bdfe2871fbe81d46df
internal/host/claimservingpark_linux.go type servingParkFS authority C,J4 rev3=PROPOSED-seed; private filesystem dependencies including production kernel boot reader; requests cannot supply boot identity; descriptor-only persistent-handle reader, no open-by-handle operation 0a35b6ac9affb2c9be7577efe9081a54e7c6a74368a9d3502e193a6b29fdd2b2
internal/host/claimservingnamespace_linux.go type servingParkParent authority C,J4 rev3=PROPOSED-seed; root-installation parent identity and additive boot scope; startup never infers an earlier boot from a matching mount number c58afd9dda038b4adb9ebf67fe0a42f1f3dd48f21d11b8f5660bad278e4dfb87
-internal/host/claimservingboot_linux.go validServingParkBootID authority C,J4 rev3=PROPOSED-seed; canonical nonzero kernel boot UUID validation shared by live boot reads and historical topology inspection 76350dcd46b8c66e2e70945267537fb580e15b8da9c80fb158392d317973d3b2
+internal/host/claimservingidentity.go validServingParkBootID authority C,J4 rev3=PROPOSED-seed; canonical nonzero kernel boot UUID validation shared by live boot reads and historical topology inspection 76350dcd46b8c66e2e70945267537fb580e15b8da9c80fb158392d317973d3b2
internal/host/claimservingtopology_linux.go servingParkFS.captureTopology authority C,J4 rev3=PROPOSED-seed; current boot and pinned parent/root descriptors; persistent filesystem identities, handles, owner and mode; rereads descriptor identities and boot before returning creation evidence 169ff4183b9d65770c49aeaa341ad061ee38105e8cdea4ccfb5ee3f57079ec20
internal/host/claimservingtopology_linux.go servingParkFS.inspectRebootTopology authority C,J4 rev3=PROPOSED-seed; private namespace and independently recorded parent, absent quarantine, current nosymfollow mount, persistent filesystem/handle identity and repeated path/permission checks; read-only observation grants no parking or deletion authority file:Read@491,file:Read@946 39d3d1b6e6791937f18f4064e0b523c708c457459adc27be7e1d5bb3870afbbe
internal/host/claimservingtopology_linux.go servingParkPersistentDir authority C,J4 rev3=PROPOSED-seed; descriptor-only Fstat, Fstatfs and NameToHandleAt with AT_EMPTY_PATH; bounded canonical handle and matching statx mount, mode and owner; no caller path, handle opening or permission widening file:Fstat@279,file:Fstatfs@375 2c60437509897dc3d7e861bf554a92cd5c338301b3bfbdee6b5d731ea9d3f7b8
-internal/host/claimservingtopology_linux.go servingParkPersistentDirectory.valid authority C,J4 rev3=PROPOSED-seed; persistent handle format, bounded 1-128 byte handle, filesystem identity, directory type and non-writable group/other mode b161b8aa379bd9822825fb44718679df05d7d7b3a411f14cce2190c38f29a803
-internal/host/claimservingtopology_linux.go type servingParkPersistentDirectory authority C,J4 rev3=PROPOSED-seed; root-recorded filesystem identity, opaque persistent directory handle and owner/mode, independent of numeric mount identity dced817ce21faf3ea261c82c6bf2b8036a74c14a266cd96b48618ce5bc746445
+internal/host/claimservingidentity.go servingParkPersistentDirectory.valid authority C,J4 rev3=PROPOSED-seed; persistent handle format, bounded 1-128 byte handle, filesystem identity, directory type and non-writable group/other mode a40d3a371641b4b51616f86b60c9fd6506cd72c5667df940cfd981ccd3925191
+internal/host/claimservingidentity.go type servingParkPersistentDirectory authority C,J4 rev3=PROPOSED-seed; root-recorded filesystem identity, opaque persistent directory handle and owner/mode, independent of numeric mount identity dced817ce21faf3ea261c82c6bf2b8036a74c14a266cd96b48618ce5bc746445
internal/host/claimservingtopology_linux.go type servingParkRebootObservation authority C,J4 rev3=PROPOSED-seed; read-only old namespace and current boot/topology observation; no effect consumer or transport result accepts it e000b7d132c36a4979c5be443fc87996beafe2a8db320bf978ef1f026a09baa2
internal/host/claimservingtopology_linux.go type servingParkTopology authority C,J4 rev3=PROPOSED-seed; persistent parent/root evidence attached only when root creates and records a new namespace 16a7fce2850bf204ddd67aa10876979be08a5d500a21ea3072e2fc5c1c369586
+internal/host/claimservingidentity.go sameServingParkSource authority C,J4 rev3=PROPOSED-seed; immutable source versus fresh capture; old records compare original identity fields without acquiring new historical evidence 251d4c3de760b413dbbd3a0bdad03ff57560146e20794b9acb723cc142d046bf
+internal/host/claimservingidentity.go servingParkPersistentFile.valid authority C,J4 rev3=PROPOSED-seed; persistent regular-file type and opaque handle validation; no pathname or handle opening 6e071ea46888326a8a5040f486d42b07194c46f2b186fb163d53f60d45f147ba
+internal/host/claimservingidentity.go type servingParkPersistentFile authority C,J4 rev3=PROPOSED-seed; root-recorded regular-file filesystem handle, owner and mode, stored with immutable source evidence 003b704d58342ed25fd8c9cd557a69094115e0f22f70f35bb544e57da35c5ada
+internal/host/claimservingidentity.go validServingParkHandle authority C,J4 rev3=PROPOSED-seed; bounded canonical opaque handle, nonzero filesystem and handle type; pure validation shared by directory and regular-file records 48c0cbdeb5a2173629a1d7414819eea3894403b1f39241f5f49a9717e579466f
+internal/host/claimservingidentity.go validateServingParkPersistentRecord authority C,J4 rev3=PROPOSED-seed; source boot and directory identity, every present or absent file handle and prepared event proof; rejects partial evidence, preserves old record decoding bc7480d85f681f30580779f7d8fa3155d93509b804bd34212b56cc01e4eec69e
+internal/host/claimservingidentity_linux.go servingParkPersistentArtifact authority C,J4 rev3=PROPOSED-seed; no-follow O_PATH file descriptor, Fstatfs, Statx and NameToHandleAt with AT_EMPTY_PATH; name/descriptor device, inode, mount, type, links, allocation, ownership and mode checks; no tenant payload read file:Fstatfs@608,file:Openat@163,file:Statx@1062,file:Statx@347 b3fd1ed9332350bb19d0d34143b3c25ff44a39705207c0034f472690432575db
+internal/host/claimservingidentity_linux.go servingParkStatxMatches authority C,J4 rev3=PROPOSED-seed; recorded file identity versus descriptor or name statx metadata, including single regular-file link and allocated bytes 42ce5eb11aceab2c7f5d1999e91750d57ae668b0467a15b2c87953ca4aa17d63
+internal/host/claimservingpark_records.go type servingParkFile authority C,J4 rev3=PROPOSED-seed; recorded basename, presence, device/inode/mount, type, link count, allocation and progress, with additive persistent file handle; optional persistent filesystem handle for the original file a5524de42a9f8514811326c297b304f9ab4f99b4650f6239895e9207725f30dc
+internal/host/claimservingpark_records.go type servingParkSource authority C,J4 rev3=PROPOSED-seed; immutable source directory and four artifacts; additive capture boot and persistent source identity; absent proof never gains inferred history; optional persistent source-directory handle and capture boot 5acbdb6ed1b1d3f2344ede87fc63652e8deea59e3105499e11ac6d465c1ac848
+internal/host/claimservingreboot_linux.go servingParkFS.inspectRebootRecord authority C,J4 rev3=PROPOSED-seed; repeated read-only per-retirement inspection under namespace lock; unchanged historical record and topology; result is not mutation or agent authority baacf7e1df1d8fd4a09a5f6204597ab51b8765feca9c614ddc74d863a3cf9055
+internal/host/claimservingreboot_linux.go servingParkFS.observeRebootRecord authority C,J4 rev3=PROPOSED-seed; private retirement record and persistent namespace evidence; exact source/event/file handles across boots, bounded event children, per-artifact rename/delete progress; completed parks do not consult live sources file:Readdirnames@2298 4de98aacfd52eff4b1e20395555e352ced86e55ef545d0013febb4b6cc9a3bf1
+internal/host/claimservingreboot_linux.go servingParkFS.rebootDirectory authority C,J4 rev3=PROPOSED-seed; pinned descriptor device/inode, current mount and historical filesystem handle, owner and mode; no effects 2224be87510731990ca620abc0624a80675eca21228e561fa3e540d2ac8273c0
+internal/host/claimservingreboot_linux.go servingParkRebootFileMatches authority C,J4 rev3=PROPOSED-seed; historical file handle and current mount compared to source/event observations; lost rename and unlink replies require original progress; no writes 180ec916768fd2d012e689199963586dabbaa77553cea9d806882d1eacb15a75
+internal/host/claimservingreboot_linux.go type servingParkRebootRecord authority C,J4 rev3=PROPOSED-seed; read-only historical retirement and current source/event/namespace observations; has no effect or transport consumer 2752876f2e40f92a720c17c05e479dbfc7c4910392a1e6dc5ba4dc82b566763c