WIP files seed checkpoint 14 (not for merge)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5 files changed,
+755
−3
internal/host/claimservingpark_linux.go+6 −0internal/host/claimservingquarantine_linux.go+266 −0internal/host/claimservingquarantine_linux_test.go+347 −0internal/host/claimservingquarantine_rootrunner_linux_test.go+126 −0internal/provision/testdata/plan14-root-reads-seed.tsv+10 −3
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 365.963s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 3.405s
ok github.com/carlosframework/platform/cmd/moveprobe 93.296s
ok github.com/carlosframework/platform/cmd/zddprobe 0.708s
ok github.com/carlosframework/platform/internal/activator 234.449s
ok github.com/carlosframework/platform/internal/agentsock 0.022s
ok github.com/carlosframework/platform/internal/alerting 0.005s
ok github.com/carlosframework/platform/internal/analytics 0.027s
ok github.com/carlosframework/platform/internal/appconfig 2.322s
ok github.com/carlosframework/platform/internal/apperr 0.080s
ok github.com/carlosframework/platform/internal/applog 0.136s
ok github.com/carlosframework/platform/internal/awsinv 0.007s
ok github.com/carlosframework/platform/internal/boxchoice 0.008s
ok github.com/carlosframework/platform/internal/boxid 0.774s
? 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.014s
ok github.com/carlosframework/platform/internal/caps 0.012s
ok github.com/carlosframework/platform/internal/certwarm 0.003s
ok github.com/carlosframework/platform/internal/cgroup 0.694s
ok github.com/carlosframework/platform/internal/changelog 0.289s
ok github.com/carlosframework/platform/internal/clamd 0.056s
? github.com/carlosframework/platform/internal/cliflag [no test files]
ok github.com/carlosframework/platform/internal/console 368.570s
? 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.951s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.011s
ok github.com/carlosframework/platform/internal/dbverify 0.004s
ok github.com/carlosframework/platform/internal/dnsrec 0.004s
ok github.com/carlosframework/platform/internal/docsite 0.007s
ok github.com/carlosframework/platform/internal/domains 2.130s
ok github.com/carlosframework/platform/internal/doorkey 0.009s
ok github.com/carlosframework/platform/internal/economics 0.005s
ok github.com/carlosframework/platform/internal/edge 26.252s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.008s
ok github.com/carlosframework/platform/internal/fleetapi 0.125s
ok github.com/carlosframework/platform/internal/gate 1.660s
ok github.com/carlosframework/platform/internal/genselect 0.017s
ok github.com/carlosframework/platform/internal/geo 0.014s
ok github.com/carlosframework/platform/internal/geoip 0.032s
ok github.com/carlosframework/platform/internal/host 150.329s
ok github.com/carlosframework/platform/internal/hostactors 0.635s
ok github.com/carlosframework/platform/internal/hostbox 2.428s
ok github.com/carlosframework/platform/internal/hostclaim 0.513s
ok github.com/carlosframework/platform/internal/hostevidence 0.014s
ok github.com/carlosframework/platform/internal/hostexec 4.919s
ok github.com/carlosframework/platform/internal/hostfence 0.036s
ok github.com/carlosframework/platform/internal/hostfiles 10.425s
ok github.com/carlosframework/platform/internal/hostfloor 0.015s
ok github.com/carlosframework/platform/internal/hostguard 1.017s
ok github.com/carlosframework/platform/internal/hostops 1.497s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.015s
ok github.com/carlosframework/platform/internal/hostquarantine 0.013s
ok github.com/carlosframework/platform/internal/hostrelocate 0.445s
ok github.com/carlosframework/platform/internal/hostresolve 0.134s
ok github.com/carlosframework/platform/internal/hostwrite 0.013s
ok github.com/carlosframework/platform/internal/infraview 0.013s
ok github.com/carlosframework/platform/internal/instancectl 1.834s
ok github.com/carlosframework/platform/internal/instanceexec 2.738s
ok github.com/carlosframework/platform/internal/instancefs 0.014s
ok github.com/carlosframework/platform/internal/instancekey 0.018s
ok github.com/carlosframework/platform/internal/instanceupdates 0.023s
ok github.com/carlosframework/platform/internal/layout 1.481s
ok github.com/carlosframework/platform/internal/lease 9.457s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.018s
ok github.com/carlosframework/platform/internal/ledger 0.031s
ok github.com/carlosframework/platform/internal/mail 0.164s
ok github.com/carlosframework/platform/internal/maintenance 0.081s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.567s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.086s
ok github.com/carlosframework/platform/internal/metrics 0.005s
ok github.com/carlosframework/platform/internal/moves 21.096s
ok github.com/carlosframework/platform/internal/moves/archive 0.793s
ok github.com/carlosframework/platform/internal/objstore 0.200s
ok github.com/carlosframework/platform/internal/ops 0.163s
ok github.com/carlosframework/platform/internal/park 0.201s
ok github.com/carlosframework/platform/internal/privatefile 0.005s
ok github.com/carlosframework/platform/internal/provision 11.775s
ok github.com/carlosframework/platform/internal/publicrelease 0.007s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 4.024s
ok github.com/carlosframework/platform/internal/relay 7.234s
ok github.com/carlosframework/platform/internal/release 19.831s
ok github.com/carlosframework/platform/internal/rollout 3.665s
ok github.com/carlosframework/platform/internal/rolloutwords 0.003s
ok github.com/carlosframework/platform/internal/rootaws 0.009s
ok github.com/carlosframework/platform/internal/rootparticipation 0.007s
ok github.com/carlosframework/platform/internal/rootstate 0.025s
ok github.com/carlosframework/platform/internal/s3compat 0.005s
ok github.com/carlosframework/platform/internal/safefile 0.004s
ok github.com/carlosframework/platform/internal/schedule 2.300s
ok github.com/carlosframework/platform/internal/sim/hostlife 9.543s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 3.983s
ok github.com/carlosframework/platform/internal/status 0.041s
ok github.com/carlosframework/platform/internal/steering 0.006s
ok github.com/carlosframework/platform/internal/stopevidence 0.345s
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.005s
ok github.com/carlosframework/platform/internal/unixdial 0.004s
ok github.com/carlosframework/platform/internal/updatewords 0.005s
ok github.com/carlosframework/platform/internal/usage 0.004s
ok github.com/carlosframework/platform/internal/version 0.003s
ok github.com/carlosframework/platform/internal/vet 1.584s
=== 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.023s
=== RUN TestT4StorageProbeMinio
2026/10/14 19:34:03 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE7C5DAFB95F22, 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.023s
minio contract: PASS
diff --git a/internal/host/claimservingpark_linux.go b/internal/host/claimservingpark_linux.go| index 78f07a7a2..538d5d2fc 100644 |
| --- a/internal/host/claimservingpark_linux.go |
| +++ b/internal/host/claimservingpark_linux.go |
| @@ -125,6 +125,9 @@ func (f *servingParkFS) namespace() (*os.File, servingParkNamespace, error) { |
| defer parent.Close() |
| b, err := f.state.Read("namespace.json") |
| if errors.Is(err, os.ErrNotExist) { |
| + if err = f.checkNamespaceQuarantine(n); err != nil { |
| + return nil, n, err |
| + } |
| // EEXIST is deliberately not adoption authority for a service-owned name. |
| if err = unix.Mkdirat(int(parent.Fd()), ".serving-parked", 0700); err != nil { |
| return nil, n, err |
| @@ -154,6 +157,9 @@ func (f *servingParkFS) namespace() (*os.File, servingParkNamespace, error) { |
| if servingParkDecode(b, &n) != nil || n.Version != 1 || n.Parent != id { |
| return nil, n, hostbox.ErrChanged |
| } |
| + if err = f.checkNamespaceQuarantine(n); err != nil { |
| + return nil, n, err |
| + } |
| d, rid, err := f.privateDir(parent, ".serving-parked") |
| if err != nil { |
| return nil, n, err |
diff --git a/internal/host/claimservingquarantine_linux.go b/internal/host/claimservingquarantine_linux.go| new file mode 100644 |
| index 000000000..b986b9813 |
| --- /dev/null |
| +++ b/internal/host/claimservingquarantine_linux.go |
| @@ -0,0 +1,266 @@ |
| +//go:build linux |
| + |
| +package host |
| + |
| +import ( |
| + "context" |
| + "crypto/rand" |
| + "encoding/hex" |
| + "errors" |
| + "io" |
| + "os" |
| + "reflect" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "golang.org/x/sys/unix" |
| +) |
| + |
| +// One obstruction may be retained per namespace. A second replacement needs |
| +// independent investigation; retries cannot accumulate quarantine directories. |
| +type servingParkQuarantine struct { |
| + Version int |
| + ID string |
| + Parent, Obstruction servingArtifactDirectory |
| + Previous *servingParkNamespace `json:",omitempty"` |
| + Moved bool |
| + Fresh servingArtifactDirectory |
| + Complete bool |
| +} |
| + |
| +func (q servingParkQuarantine) name() string { |
| + return ".serving-parked-quarantine-" + q.ID |
| +} |
| + |
| +func (q servingParkQuarantine) validate() error { |
| + id, err := hex.DecodeString(q.ID) |
| + if err != nil || len(id) != 16 || hex.EncodeToString(id) != q.ID || q.Version != 1 || q.Parent.Inode == 0 || q.Parent.Device == 0 || q.Parent.Mount == 0 || q.Obstruction.Inode == 0 || q.Obstruction.Device != q.Parent.Device || q.Obstruction.Mount != q.Parent.Mount { |
| + return hostbox.ErrChanged |
| + } |
| + if q.Previous != nil && (q.Previous.Version != 1 || q.Previous.Parent != q.Parent || q.Previous.Root.Inode == 0 || q.Previous.Root.Device != q.Parent.Device || q.Previous.Root.Mount != q.Parent.Mount || q.Previous.Root == q.Obstruction) { |
| + return hostbox.ErrChanged |
| + } |
| + if q.Fresh != (servingArtifactDirectory{}) && (!q.Moved || q.Fresh.Inode == 0 || q.Fresh.Device != q.Parent.Device || q.Fresh.Mount != q.Parent.Mount || q.Fresh == q.Obstruction || q.Previous != nil && q.Fresh == q.Previous.Root) { |
| + return hostbox.ErrChanged |
| + } |
| + if q.Complete && (!q.Moved || q.Fresh.Inode == 0) { |
| + return hostbox.ErrChanged |
| + } |
| + return nil |
| +} |
| + |
| +// Publishing namespace.json can succeed before quarantine completion. Keep |
| +// ordinary parking out until that final record is durable, including after a |
| +// lost reply, so recovery can still require an empty replacement root. |
| +func (f *servingParkFS) checkNamespaceQuarantine(n servingParkNamespace) error { |
| + body, err := f.state.Read("namespace-quarantine.json") |
| + if errors.Is(err, os.ErrNotExist) { |
| + return nil |
| + } |
| + if err != nil { |
| + return err |
| + } |
| + var q servingParkQuarantine |
| + if servingParkDecode(body, &q) != nil || q.validate() != nil || !q.Complete || n.Parent != q.Parent || n.Root != q.Fresh { |
| + return hostbox.ErrChanged |
| + } |
| + return nil |
| +} |
| + |
| +// This is an internal mechanism, not a request handler. The caller supplies a |
| +// parent identity from independent root installation evidence and holds the |
| +// namespace lock. No caller path, child listing or quarantine deletion exists. |
| +func (f *servingParkFS) recoverNamespace(ctx context.Context, expected servingArtifactDirectory) (servingParkQuarantine, error) { |
| + var q servingParkQuarantine |
| + if ctx == nil { |
| + return q, hostbox.ErrUnsupported |
| + } |
| + if err := ctx.Err(); err != nil { |
| + return q, err |
| + } |
| + parent, pid, err := f.parent() |
| + if err != nil { |
| + return q, err |
| + } |
| + defer parent.Close() |
| + var st unix.Stat_t |
| + if err = unix.Fstat(int(parent.Fd()), &st); err != nil { |
| + return q, err |
| + } |
| + if pid != expected || st.Mode&0022 != 0 { |
| + return q, hostbox.ErrChanged |
| + } |
| + ledger := servingParkLedger{state: f.state} |
| + save := func() error { |
| + if err := ctx.Err(); err != nil { |
| + return err |
| + } |
| + if err := q.validate(); err != nil { |
| + return err |
| + } |
| + return ledger.write("namespace-quarantine.json", q) |
| + } |
| + body, err := f.state.Read("namespace-quarantine.json") |
| + if err == nil { |
| + if servingParkDecode(body, &q) != nil || q.validate() != nil || q.Parent != pid { |
| + return q, hostbox.ErrChanged |
| + } |
| + } else if errors.Is(err, os.ErrNotExist) { |
| + q.Version, q.Parent = 1, pid |
| + body, err = f.state.Read("namespace.json") |
| + if err == nil { |
| + var prior servingParkNamespace |
| + if servingParkDecode(body, &prior) != nil || prior.Version != 1 || prior.Parent != pid { |
| + return q, hostbox.ErrChanged |
| + } |
| + q.Previous = &prior |
| + } else if !errors.Is(err, os.ErrNotExist) { |
| + return q, err |
| + } |
| + obstruction, err := servingParkOpenDir(parent, ".serving-parked") |
| + if err != nil { |
| + return q, err |
| + } |
| + q.Obstruction, err = servingParkDirID(obstruction) |
| + obstruction.Close() |
| + if err != nil { |
| + return q, err |
| + } |
| + var nonce [16]byte |
| + if _, err = rand.Read(nonce[:]); err != nil { |
| + return q, err |
| + } |
| + q.ID = hex.EncodeToString(nonce[:]) |
| + if err = save(); err != nil { |
| + return q, err |
| + } |
| + } else { |
| + return q, err |
| + } |
| + // A private record is necessary but not sufficient: it must still describe |
| + // the installed namespace, or the exact replacement recorded by this run. |
| + body, err = f.state.Read("namespace.json") |
| + if err == nil { |
| + var current servingParkNamespace |
| + if servingParkDecode(body, ¤t) != nil { |
| + return q, hostbox.ErrChanged |
| + } |
| + old := q.Previous != nil && reflect.DeepEqual(current, *q.Previous) |
| + fresh := q.Fresh.Inode != 0 && current == (servingParkNamespace{Version: 1, Parent: q.Parent, Root: q.Fresh}) |
| + if !old && !fresh || q.Complete && !fresh { |
| + return q, hostbox.ErrChanged |
| + } |
| + } else if !errors.Is(err, os.ErrNotExist) || q.Previous != nil || q.Complete { |
| + return q, err |
| + } |
| + if !q.Moved { |
| + if err = f.moveQuarantine(ctx, parent, q); err != nil { |
| + return q, err |
| + } |
| + q.Moved = true |
| + if err = save(); err != nil { |
| + return q, err |
| + } |
| + } |
| + if err = f.checkQuarantine(parent, q); err != nil { |
| + return q, err |
| + } |
| + if q.Fresh.Inode == 0 { |
| + if err = ctx.Err(); err != nil { |
| + return q, err |
| + } |
| + // EEXIST is never proof that a root created before a crash is ours. |
| + // If creation lost its identity record, retain both directories and |
| + // refuse further recovery rather than adopt or quarantine repeatedly. |
| + if err = unix.Mkdirat(int(parent.Fd()), ".serving-parked", 0700); err != nil { |
| + return q, err |
| + } |
| + fresh, id, err := f.privateDir(parent, ".serving-parked") |
| + if err != nil { |
| + return q, err |
| + } |
| + fresh.Close() |
| + q.Fresh = id |
| + if err = save(); err != nil { |
| + return q, err |
| + } |
| + } |
| + fresh, id, err := f.privateDir(parent, ".serving-parked") |
| + if err != nil { |
| + return q, err |
| + } |
| + defer fresh.Close() |
| + if id != q.Fresh { |
| + return q, hostbox.ErrChanged |
| + } |
| + // Recheck the effective mount/path before publishing the replacement. |
| + check, current, err := f.parent() |
| + if err != nil { |
| + return q, err |
| + } |
| + check.Close() |
| + if current != q.Parent { |
| + return q, hostbox.ErrChanged |
| + } |
| + if q.Complete { |
| + return q, ctx.Err() |
| + } |
| + // No retirement can use this root before publication. Even a directory |
| + // with the right owner and mode is refused if it already contains data. |
| + names, err := fresh.Readdirnames(1) |
| + if len(names) != 0 || err != nil && !errors.Is(err, io.EOF) { |
| + return q, hostbox.ErrChanged |
| + } |
| + if err = ctx.Err(); err != nil { |
| + return q, err |
| + } |
| + if err = f.sync(int(parent.Fd())); err != nil { |
| + return q, err |
| + } |
| + if err = ledger.write("namespace.json", servingParkNamespace{Version: 1, Parent: q.Parent, Root: q.Fresh}); err != nil { |
| + return q, err |
| + } |
| + q.Complete = true |
| + return q, save() |
| +} |
| + |
| +func (f *servingParkFS) checkQuarantine(parent *os.File, q servingParkQuarantine) error { |
| + d, err := servingParkOpenDir(parent, q.name()) |
| + if err != nil { |
| + return err |
| + } |
| + defer d.Close() |
| + id, err := servingParkDirID(d) |
| + if err != nil || id != q.Obstruction { |
| + return hostbox.ErrChanged |
| + } |
| + return nil |
| +} |
| + |
| +func (f *servingParkFS) moveQuarantine(ctx context.Context, parent *os.File, q servingParkQuarantine) error { |
| + if err := ctx.Err(); err != nil { |
| + return err |
| + } |
| + source, err := servingParkOpenDir(parent, ".serving-parked") |
| + if errors.Is(err, os.ErrNotExist) { |
| + // The rename may have succeeded before its reply or fsync was lost. |
| + if err = f.checkQuarantine(parent, q); err != nil { |
| + return err |
| + } |
| + return f.sync(int(parent.Fd())) |
| + } |
| + if err != nil { |
| + return err |
| + } |
| + defer source.Close() |
| + id, err := servingParkDirID(source) |
| + if err != nil || id != q.Obstruction { |
| + return hostbox.ErrChanged |
| + } |
| + if err = f.rename(int(parent.Fd()), ".serving-parked", int(parent.Fd()), q.name(), unix.RENAME_NOREPLACE); err != nil { |
| + return err |
| + } |
| + if err = f.checkQuarantine(parent, q); err != nil { |
| + return err |
| + } |
| + return f.sync(int(parent.Fd())) |
| +} |
diff --git a/internal/host/claimservingquarantine_linux_test.go b/internal/host/claimservingquarantine_linux_test.go| new file mode 100644 |
| index 000000000..21e7d0b3b |
| --- /dev/null |
| +++ b/internal/host/claimservingquarantine_linux_test.go |
| @@ -0,0 +1,347 @@ |
| +//go:build linux |
| + |
| +package host |
| + |
| +import ( |
| + "context" |
| + "errors" |
| + "os" |
| + "path/filepath" |
| + "reflect" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/provision" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| + "golang.org/x/sys/unix" |
| +) |
| + |
| +func quarantineFixture(t *testing.T) (*servingParkDiskFixture, servingArtifactDirectory, string) { |
| + t.Helper() |
| + f := newServingParkDiskFixture(t) |
| + parent, id, err := f.fs.parent() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + parent.Close() |
| + path := filepath.Join(f.data, "instances", ".serving-parked") |
| + if err := os.Mkdir(path, 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(filepath.Join(path, "unknown"), []byte("keep these bytes"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + return f, id, path |
| +} |
| + |
| +func quarantineRecover(t *testing.T, f *servingParkDiskFixture, expected servingArtifactDirectory) (servingParkQuarantine, error) { |
| + t.Helper() |
| + var q servingParkQuarantine |
| + err := f.ledger.withNamespace(t.Context(), func() error { |
| + var err error |
| + q, err = f.fs.recoverNamespace(t.Context(), expected) |
| + return err |
| + }) |
| + return q, err |
| +} |
| + |
| +func quarantinePreserved(t *testing.T, f *servingParkDiskFixture, q servingParkQuarantine) { |
| + t.Helper() |
| + path := filepath.Join(f.data, "instances", q.name()) |
| + body, err := os.ReadFile(filepath.Join(path, "unknown")) |
| + if err != nil || string(body) != "keep these bytes" { |
| + t.Fatal("quarantine lost data", err) |
| + } |
| + d, err := os.Open(path) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + id, err := servingParkDirID(d) |
| + d.Close() |
| + if err != nil || id != q.Obstruction { |
| + t.Fatal("quarantine changed identity", err) |
| + } |
| +} |
| + |
| +func TestServingParkQuarantineUnknownRoot(t *testing.T) { |
| + f, parent, path := quarantineFixture(t) |
| + // Contents include a symlink to a victim and an unreadable subtree. The |
| + // recovery only renames the outer directory and never inspects either. |
| + victim := filepath.Join(t.TempDir(), "victim") |
| + if err := os.WriteFile(victim, []byte("victim"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Symlink(victim, filepath.Join(path, "link")); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Mkdir(filepath.Join(path, "unreadable"), 0000); err != nil { |
| + t.Fatal(err) |
| + } |
| + q, err := quarantineRecover(t, f, parent) |
| + if err != nil || !q.Complete || q.Previous != nil { |
| + t.Fatalf("quarantine: %+v %v", q, err) |
| + } |
| + quarantinePreserved(t, f, q) |
| + if body, err := os.ReadFile(victim); err != nil || string(body) != "victim" { |
| + t.Fatal("quarantine followed victim", err) |
| + } |
| + before, err := f.disk.Read("namespace-quarantine.json") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + f.fs = newServingParkFS(f.data, f.disk) |
| + f.fs.owner, f.fs.mount = uint32(os.Geteuid()), func(string) (bool, error) { return true, nil } |
| + f.ledger.fs = f.fs |
| + for i := 0; i < 3; i++ { |
| + again, err := quarantineRecover(t, f, parent) |
| + if err != nil || !reflect.DeepEqual(again, q) { |
| + t.Fatal("quarantine replay", err) |
| + } |
| + } |
| + after, err := f.disk.Read("namespace-quarantine.json") |
| + if err != nil || string(after) != string(before) { |
| + t.Fatal("replay changed quarantine record", err) |
| + } |
| + // A fresh host can use the empty namespace. Unknown bytes do not become |
| + // retirement records and survive an ordinary expiry pass. |
| + f.put("", "current database") |
| + a, stops := f.evidence(10, true, "missing") |
| + r, err := f.park(a, stops) |
| + if err != nil { |
| + t.Fatal("park after quarantine", err) |
| + } |
| + f.complete(a) |
| + f.now = r.ReapAfter |
| + if err := f.ledger.withNamespace(t.Context(), func() error { return f.ledger.reapLocked(t.Context(), a.ID) }); err != nil { |
| + t.Fatal(err) |
| + } |
| + quarantinePreserved(t, f, q) |
| + // Only one quarantine is allowed. A later replacement is retained in |
| + // place for investigation, never adopted or moved to another quarantine. |
| + if err := os.Rename(path, path+"-saved"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Mkdir(path, 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := quarantineRecover(t, f, parent); err == nil { |
| + t.Fatal("replaced root accepted") |
| + } |
| + names, err := os.ReadDir(filepath.Dir(path)) |
| + if err != nil || len(names) != 4 { // instance, fresh obstruction, saved root, quarantine |
| + t.Fatalf("unexpected recovery directories: %v %v", names, err) |
| + } |
| +} |
| + |
| +type quarantineWriteFault struct { |
| + rootstate.Store |
| + before, after func(string, []byte) error |
| +} |
| + |
| +func (s quarantineWriteFault) Write(name string, body []byte) error { |
| + if s.before != nil { |
| + if err := s.before(name, body); err != nil { |
| + return err |
| + } |
| + } |
| + if err := s.Store.Write(name, body); err != nil { |
| + return err |
| + } |
| + if s.after != nil { |
| + return s.after(name, body) |
| + } |
| + return nil |
| +} |
| + |
| +func TestServingParkQuarantineResumesDurableBoundaries(t *testing.T) { |
| + for _, boundary := range []string{"intent", "rename", "sync", "moved", "fresh", "namespace", "complete"} { |
| + t.Run(boundary, func(t *testing.T) { |
| + f, parent, _ := quarantineFixture(t) |
| + crash := errors.New("interrupted quarantine") |
| + f.fs.state = quarantineWriteFault{Store: f.disk, after: func(name string, body []byte) error { |
| + var q servingParkQuarantine |
| + if name == "namespace-quarantine.json" { |
| + if err := servingParkDecode(body, &q); err != nil { |
| + return err |
| + } |
| + if boundary == "intent" && !q.Moved || boundary == "moved" && q.Moved && q.Fresh.Inode == 0 || boundary == "fresh" && q.Fresh.Inode != 0 && !q.Complete || boundary == "complete" && q.Complete { |
| + return crash |
| + } |
| + } |
| + if boundary == "namespace" && name == "namespace.json" { |
| + return crash |
| + } |
| + return nil |
| + }} |
| + if boundary == "rename" { |
| + f.fs.rename = func(a int, b string, c int, d string, flags uint) error { |
| + if err := unix.Renameat2(a, b, c, d, flags); err != nil { |
| + return err |
| + } |
| + return crash |
| + } |
| + } |
| + if boundary == "sync" { |
| + f.fs.sync = func(int) error { return crash } |
| + } |
| + if _, err := quarantineRecover(t, f, parent); !errors.Is(err, crash) { |
| + t.Fatal("missing interruption", err) |
| + } |
| + if boundary != "complete" { |
| + if root, _, err := f.fs.namespace(); err == nil { |
| + root.Close() |
| + t.Fatal("unfinished quarantine allowed ordinary parking") |
| + } |
| + } |
| + f.fs.state, f.fs.rename, f.fs.sync = f.disk, unix.Renameat2, unix.Fsync |
| + q, err := quarantineRecover(t, f, parent) |
| + if err != nil || !q.Complete { |
| + t.Fatalf("recovery: %+v %v", q, err) |
| + } |
| + quarantinePreserved(t, f, q) |
| + }) |
| + } |
| +} |
| + |
| +func TestServingParkQuarantineRefusesUnrecordedFreshRoot(t *testing.T) { |
| + f, parent, _ := quarantineFixture(t) |
| + f.fs.state = quarantineWriteFault{Store: f.disk, before: func(name string, body []byte) error { |
| + var q servingParkQuarantine |
| + if name == "namespace-quarantine.json" && servingParkDecode(body, &q) == nil && q.Fresh.Inode != 0 { |
| + return unix.ENOSPC |
| + } |
| + return nil |
| + }} |
| + q, err := quarantineRecover(t, f, parent) |
| + if !errors.Is(err, unix.ENOSPC) { |
| + t.Fatal("expected full-disk fault", err) |
| + } |
| + f.fs.state = f.disk |
| + if _, err := quarantineRecover(t, f, parent); !errors.Is(err, unix.EEXIST) { |
| + t.Fatal("adopted fresh root without durable identity", err) |
| + } |
| + if _, err := f.disk.Read("namespace.json"); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatal("published an unrecorded root", err) |
| + } |
| + quarantinePreserved(t, f, q) |
| +} |
| + |
| +func TestServingParkQuarantineRefusesUnsafeAuthority(t *testing.T) { |
| + for _, fault := range []string{"parent", "mode", "symlink", "mount", "cancel", "corrupt", "known-root"} { |
| + t.Run(fault, func(t *testing.T) { |
| + f, parent, path := quarantineFixture(t) |
| + switch fault { |
| + case "parent": |
| + parent.Inode++ |
| + case "mode": |
| + if err := os.Chmod(filepath.Dir(path), 0777); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "symlink": |
| + if err := os.Rename(path, path+"-saved"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Symlink(path+"-saved", path); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "mount": |
| + f.fs.mount = func(string) (bool, error) { return false, nil } |
| + case "corrupt": |
| + if err := f.disk.Write("namespace.json", []byte("{}")); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "known-root": |
| + d, err := os.Open(path) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + id, err := servingParkDirID(d) |
| + d.Close() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := f.ledger.write("namespace.json", servingParkNamespace{Version: 1, Parent: parent, Root: id}); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + var err error |
| + if fault == "cancel" { |
| + ctx, cancel := context.WithCancel(t.Context()) |
| + cancel() |
| + _, err = f.fs.recoverNamespace(ctx, parent) |
| + } else { |
| + _, err = quarantineRecover(t, f, parent) |
| + } |
| + if err == nil { |
| + t.Fatal("unsafe quarantine accepted") |
| + } |
| + if _, err := f.disk.Read("namespace-quarantine.json"); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatal("unsafe authority persisted intent", err) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestServingParkQuarantinePreservesBlockedSlot(t *testing.T) { |
| + f := newServingParkDiskFixture(t) |
| + f.put("", "old served database") |
| + a, stops := f.evidence(10, true, "missing") |
| + r, err := f.park(a, stops) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + f.complete(a) |
| + path := filepath.Join(f.data, "instances", ".serving-parked") |
| + if err := os.Rename(path, path+"-saved"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Mkdir(path, 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(filepath.Join(path, "unknown"), []byte("keep these bytes"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + q, err := quarantineRecover(t, f, r.Source.Parent) |
| + if err != nil || q.Previous == nil { |
| + t.Fatal("recorded-root recovery", err) |
| + } |
| + quarantinePreserved(t, f, q) |
| + f.put("", "new served database") |
| + next, receipts := f.evidence(11, false, "started") |
| + if _, err := f.park(next, receipts); err == nil { |
| + t.Fatal("unresolved slot replaced") |
| + } |
| + slot, err := f.ledger.slot(a) |
| + if err != nil || slot.ID != a.ID { |
| + t.Fatal("blocked slot changed", err) |
| + } |
| + body, err := os.ReadFile(filepath.Join(path+"-saved", a.ID, a.Successor.Host+".db")) |
| + if err != nil || string(body) != "old served database" { |
| + t.Fatal("old park changed", err) |
| + } |
| + body, err = os.ReadFile(provision.NestedDBPath(f.data, f.base.Host)) |
| + if err != nil || string(body) != "new served database" { |
| + t.Fatal("blocked replacement changed live source", err) |
| + } |
| + f.base.Host, f.target.Host = "other.example", "other.example" |
| + if err := os.MkdirAll(filepath.Dir(provision.NestedDBPath(f.data, f.base.Host)), 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + f.put("", "other host database") |
| + other, otherStops := f.evidence(20, true, "missing") |
| + if _, err := f.park(other, otherStops); err != nil { |
| + t.Fatal("blocked host prevented unrelated park", err) |
| + } |
| + f.complete(other) |
| + quarantinePreserved(t, f, q) |
| + if _, err := f.disk.Read("retirement-" + q.ID + ".json"); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatal("quarantine became deletion authority", err) |
| + } |
| + if err := q.validate(); err != nil { |
| + t.Fatal(err) |
| + } |
| + q.Fresh = q.Obstruction |
| + if !errors.Is(q.validate(), hostbox.ErrChanged) { |
| + t.Fatal("quarantine reused as fresh root") |
| + } |
| +} |
diff --git a/internal/host/claimservingquarantine_rootrunner_linux_test.go b/internal/host/claimservingquarantine_rootrunner_linux_test.go| new file mode 100644 |
| index 000000000..438a355c4 |
| --- /dev/null |
| +++ b/internal/host/claimservingquarantine_rootrunner_linux_test.go |
| @@ -0,0 +1,126 @@ |
| +//go:build linux && rootrunner |
| + |
| +package host |
| + |
| +import ( |
| + "errors" |
| + "os" |
| + "path/filepath" |
| + "reflect" |
| + "testing" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/rootstate" |
| + "golang.org/x/sys/unix" |
| +) |
| + |
| +// Parent authority is a fixture. UID, mount checks, descriptor-relative rename |
| +// and the private store are production; no agent or running root service is used. |
| +func TestRootServingParkQuarantine(t *testing.T) { |
| + servingRootRunner(t) |
| + for _, fault := range []string{"", "lost-rename-reply", "submount", "symlink"} { |
| + t.Run(fault, func(t *testing.T) { |
| + root, err := os.MkdirTemp("/var/lib", "carlos-rootrunner-quarantine-") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + t.Cleanup(func() { |
| + if err := os.RemoveAll(root); err != nil { |
| + t.Error(err) |
| + } |
| + }) |
| + data := filepath.Join(root, "data") |
| + servingRunnerMount(t, data) |
| + state, err := rootstate.Open(filepath.Join(root, "authority"), true, false) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + t.Cleanup(func() { |
| + if err := state.Close(); err != nil { |
| + t.Error(err) |
| + } |
| + }) |
| + fs := newServingParkFS(data, state) |
| + p, parent, err := fs.parent() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + p.Close() |
| + path := filepath.Join(data, "instances", ".serving-parked") |
| + if err := os.Mkdir(path, 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(filepath.Join(path, "unknown"), []byte("retained unknown bytes"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + if fault == "submount" { |
| + if err := unix.Mount(path, path, "", unix.MS_BIND, ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + t.Cleanup(func() { |
| + if err := unix.Unmount(path, 0); err != nil { |
| + t.Error(err) |
| + } |
| + }) |
| + } |
| + if fault == "symlink" { |
| + if err := os.Rename(path, path+"-saved"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Symlink(path+"-saved", path); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + ledger := servingParkLedger{state: state, fs: fs, now: time.Now} |
| + recover := func() (servingParkQuarantine, error) { |
| + var q servingParkQuarantine |
| + err := ledger.withNamespace(t.Context(), func() error { var err error; q, err = fs.recoverNamespace(t.Context(), parent); return err }) |
| + return q, err |
| + } |
| + if fault == "lost-rename-reply" { |
| + fs.rename = func(a int, b string, c int, d string, flags uint) error { |
| + if err := unix.Renameat2(a, b, c, d, flags); err != nil { |
| + return err |
| + } |
| + return unix.EIO |
| + } |
| + if _, err := recover(); !errors.Is(err, unix.EIO) { |
| + t.Fatal("lost reply", err) |
| + } |
| + fs.rename = unix.Renameat2 |
| + } |
| + q, err := recover() |
| + if fault == "submount" || fault == "symlink" { |
| + if err == nil { |
| + t.Fatal("unsafe obstruction accepted") |
| + } |
| + if _, err := state.Read("namespace-quarantine.json"); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatal("unsafe quarantine intent", err) |
| + } |
| + return |
| + } |
| + if err != nil || !q.Complete { |
| + t.Fatalf("quarantine: %+v %v", q, err) |
| + } |
| + if q.Obstruction.Mount != parent.Mount || q.Fresh.Mount != parent.Mount { |
| + t.Fatal("quarantine crossed bind mount") |
| + } |
| + body, err := os.ReadFile(filepath.Join(data, "instances", q.name(), "unknown")) |
| + if err != nil || string(body) != "retained unknown bytes" { |
| + t.Fatal("quarantine changed bytes", err) |
| + } |
| + fs = newServingParkFS(data, state) |
| + if replay, err := recover(); err != nil || !reflect.DeepEqual(q, replay) { |
| + t.Fatal("fresh-adapter replay", err) |
| + } |
| + r := servingRunnerPark(t, data, state) |
| + if err := fs.CheckParked(&r); err != nil { |
| + t.Fatal("park after quarantine", err) |
| + } |
| + body, err = os.ReadFile(filepath.Join(data, "instances", q.name(), "unknown")) |
| + if err != nil || string(body) != "retained unknown bytes" { |
| + t.Fatal("park touched quarantine", err) |
| + } |
| + }) |
| + } |
| +} |
diff --git a/internal/provision/testdata/plan14-root-reads-seed.tsv b/internal/provision/testdata/plan14-root-reads-seed.tsv| index 6525f6fad..7736e0b23 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 12 overlay; pending Paul’s census ruling. |
| +# Files seed checkpoint 14 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. |
| @@ -40,7 +40,7 @@ internal/host/claimservingpark_linux.go servingParkFS.Delete authority C,J4 rev3 |
| 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 before persisting event identity; existing unrecorded namespace is refused file:Prepare@24,file:Readdirnames@890 2480401a7da95a24a7557be0b8adff67394689abb418e36d0fafecdb8c1d827b |
| internal/host/claimservingpark_linux.go servingParkFS.event authority C,J4 rev3=PROPOSED-seed; recorded namespace parent and private event mount/device/inode identities 8e2ce5bf2f5931e702b847d664bd5cff5fbf90c71ed74b5a2d52ca06d9e0da7c |
| -internal/host/claimservingpark_linux.go servingParkFS.namespace authority C,J4 rev3=PROPOSED-seed; private rootstate namespace record, effective parent mount and root-owned park directory; unknown roots refused without adoption file:Read@211 788a7186623346d4a623c2924cf33f2797b40515b6c42db8154bd38168cc8cd4 |
| +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 file:Read@211 ac3dafd74eeab38012de643179c320d63a023d100a4617321a9e017c1249c9af |
| 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 |
| @@ -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 3b69facca859268158f6e90dc5ff978225d200725716332ae0235ba94a415a5c |
| +@coverage internal/host dfdcf02e2dafb0586ecf40884a5f4265fbbd3c0fcd26498ce8851c957a24a942 |
| @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 |
| @@ -154,3 +154,10 @@ internal/hostguard/coldhandover.go Guard.BeginColdHandover authority C,J4 rev3=P |
| internal/hostguard/coldhandover.go Guard.BeginServingHandover authority C,J4 rev3=PROPOSED-seed; serving-only root installation and empty-operation checks; session-zero authorization remains in root callback bd953e2794294c5638161eff30fd743000fbd467d6ea37df734668021d8f6c02 |
| internal/hostguard/coldhandover.go Guard.beginColdHandover authority C,J4 rev3=PROPOSED-seed; fresh claim and root installation under host locks; exact completed stop for admitted predecessors, validated no-process path for session zero, and durable successor barrier 6bdeec90f4322aa0824557cd35950af428b25889e15badeb4b017ccbbf0aae70 |
| internal/host/claimservinghandover.go servingPriorRevision authority C,J4 rev3=PROPOSED-seed; canonical recorded directory revision bounded by the current authenticated enrollment revision; never rewrites retained stop evidence 8007e188e2ad600cf36d8eed0fb5adb591f08f52be52285c85db348e0780cb84 |
| +internal/host/claimservingquarantine_linux.go servingParkFS.checkQuarantine authority C,J4 rev3=PROPOSED-seed; no-follow descriptor identity of the root-chosen quarantine basename; does not inspect children or authorize deletion fcc7e95f30091eec5a6bab8a81b21512096763a0129d98db00e5de276bb3d38f |
| +internal/host/claimservingquarantine_linux.go servingParkFS.moveQuarantine authority C,J4 rev3=PROPOSED-seed; recorded obstruction device, inode and mount before and after same-mount NOREPLACE rename; lost-reply replay requires exact destination identity 06ca904d7d5f8fa9541f4a9a1513de52d465fecaeae2b6d5249b6af4dd61722c |
| +internal/host/claimservingquarantine_linux.go servingParkFS.recoverNamespace authority C,J4 rev3=PROPOSED-seed; independently supplied root parent identity, effective nosymfollow mount and parent mode; strict namespace/quarantine records, bounded one-obstruction recovery and empty fresh-root publication; internal mechanism not bound to production startup file:Fstat@388,file:Read@1051,file:Read@1610,file:Read@1951,file:Read@783,file:Readdirnames@3896 3b883470739156ec88096f95b448c3e3f1c609191bd85b493af500a9a1e19ff5 |
| +internal/host/claimservingquarantine_linux.go servingParkQuarantine.name authority C,J4 rev3=PROPOSED-seed; root-generated 128-bit quarantine identity converted to a fixed basename; no caller pathname 08554ada2338c42eb0c53d7b39709583f6af12e51f5a005f42dc3bd44906b4ee |
| +internal/host/claimservingquarantine_linux.go servingParkQuarantine.validate authority C,J4 rev3=PROPOSED-seed; private quarantine shape, same-mount parent/obstruction/fresh identities and monotonic phase relationships; retention only f7fa1711feac1ebe57f0a821dcbcdb90564506ebcea7a6e267ec5943a852fac2 |
| +internal/host/claimservingquarantine_linux.go type servingParkQuarantine authority C,J4 rev3=PROPOSED-seed; private versioned quarantine intent with previous namespace and exact obstruction/fresh-root identities; no artifact deletion authority 0098723e5b7bbbede18d6485a9ffb7766995c31b25fdec78997d914e6b5d18fe |
| +internal/host/claimservingquarantine_linux.go servingParkFS.checkNamespaceQuarantine authority C,J4 rev3=PROPOSED-seed; strict private quarantine completion record and exact published namespace identity; ordinary parking cannot use a replacement before durable quarantine completion file:Read@95 4f6fa5659078157960b790e456b6af3f55de26494ae5ff6231efc5532e2c8677 |