WIP files seed checkpoint 16 (not for merge)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 files changed,
+491
−43
internal/host/claimservingnamespace_linux_test.go+8 −2internal/host/claimservingpeer_rootrunner_linux_test.go+33 −10internal/host/claimservingquarantine_linux.go+89 −22internal/host/claimservingquarantine_linux_test.go+261 −1internal/host/claimservingquarantine_rootrunner_linux_test.go+89 −1internal/provision/testdata/plan14-root-reads-seed.tsv+11 −7
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 348.853s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 3.042s
ok github.com/carlosframework/platform/cmd/moveprobe 92.287s
ok github.com/carlosframework/platform/cmd/zddprobe 0.704s
ok github.com/carlosframework/platform/internal/activator 234.318s
ok github.com/carlosframework/platform/internal/agentsock 0.017s
ok github.com/carlosframework/platform/internal/alerting 0.004s
ok github.com/carlosframework/platform/internal/analytics 0.024s
ok github.com/carlosframework/platform/internal/appconfig 2.324s
ok github.com/carlosframework/platform/internal/apperr 0.086s
ok github.com/carlosframework/platform/internal/applog 0.163s
ok github.com/carlosframework/platform/internal/awsinv 0.008s
ok github.com/carlosframework/platform/internal/boxchoice 0.005s
ok github.com/carlosframework/platform/internal/boxid 0.598s
? 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.009s
ok github.com/carlosframework/platform/internal/caps 0.020s
ok github.com/carlosframework/platform/internal/certwarm 0.004s
ok github.com/carlosframework/platform/internal/cgroup 0.702s
ok github.com/carlosframework/platform/internal/changelog 0.309s
ok github.com/carlosframework/platform/internal/clamd 0.057s
? github.com/carlosframework/platform/internal/cliflag [no test files]
ok github.com/carlosframework/platform/internal/console 365.829s
? github.com/carlosframework/platform/internal/controlname [no test files]
? github.com/carlosframework/platform/internal/controlsock [no test files]
ok github.com/carlosframework/platform/internal/dbdoor 6.397s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.010s
ok github.com/carlosframework/platform/internal/dbverify 0.009s
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.119s
ok github.com/carlosframework/platform/internal/doorkey 0.009s
ok github.com/carlosframework/platform/internal/economics 0.008s
ok github.com/carlosframework/platform/internal/edge 26.061s
? 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.127s
ok github.com/carlosframework/platform/internal/gate 1.659s
ok github.com/carlosframework/platform/internal/genselect 0.018s
ok github.com/carlosframework/platform/internal/geo 0.019s
ok github.com/carlosframework/platform/internal/geoip 0.033s
ok github.com/carlosframework/platform/internal/host 147.708s
ok github.com/carlosframework/platform/internal/hostactors 0.366s
ok github.com/carlosframework/platform/internal/hostbox 2.299s
ok github.com/carlosframework/platform/internal/hostclaim 0.551s
ok github.com/carlosframework/platform/internal/hostevidence 0.014s
ok github.com/carlosframework/platform/internal/hostexec 4.800s
ok github.com/carlosframework/platform/internal/hostfence 0.034s
ok github.com/carlosframework/platform/internal/hostfiles 10.465s
ok github.com/carlosframework/platform/internal/hostfloor 0.015s
ok github.com/carlosframework/platform/internal/hostguard 0.970s
ok github.com/carlosframework/platform/internal/hostops 1.470s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.014s
ok github.com/carlosframework/platform/internal/hostquarantine 0.014s
ok github.com/carlosframework/platform/internal/hostrelocate 0.479s
ok github.com/carlosframework/platform/internal/hostresolve 0.131s
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.815s
ok github.com/carlosframework/platform/internal/instanceexec 2.721s
ok github.com/carlosframework/platform/internal/instancefs 0.018s
ok github.com/carlosframework/platform/internal/instancekey 0.018s
ok github.com/carlosframework/platform/internal/instanceupdates 0.021s
ok github.com/carlosframework/platform/internal/layout 1.427s
ok github.com/carlosframework/platform/internal/lease 9.482s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.024s
ok github.com/carlosframework/platform/internal/ledger 0.029s
ok github.com/carlosframework/platform/internal/mail 0.188s
ok github.com/carlosframework/platform/internal/maintenance 0.083s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.529s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.072s
ok github.com/carlosframework/platform/internal/metrics 0.008s
ok github.com/carlosframework/platform/internal/moves 21.082s
ok github.com/carlosframework/platform/internal/moves/archive 0.782s
ok github.com/carlosframework/platform/internal/objstore 0.187s
ok github.com/carlosframework/platform/internal/ops 0.202s
ok github.com/carlosframework/platform/internal/park 0.205s
ok github.com/carlosframework/platform/internal/privatefile 0.004s
ok github.com/carlosframework/platform/internal/provision 11.148s
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.072s
ok github.com/carlosframework/platform/internal/relay 7.226s
ok github.com/carlosframework/platform/internal/release 19.588s
ok github.com/carlosframework/platform/internal/rollout 3.632s
ok github.com/carlosframework/platform/internal/rolloutwords 0.003s
ok github.com/carlosframework/platform/internal/rootaws 0.007s
ok github.com/carlosframework/platform/internal/rootparticipation 0.008s
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.293s
ok github.com/carlosframework/platform/internal/sim/hostlife 9.157s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 3.515s
ok github.com/carlosframework/platform/internal/status 0.038s
ok github.com/carlosframework/platform/internal/steering 0.008s
ok github.com/carlosframework/platform/internal/stopevidence 0.333s
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.004s
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.540s
=== 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.024s
=== RUN TestT4StorageProbeMinio
2026/10/14 20:10:36 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE7E5C56894C80, 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.024s
minio contract: PASS
diff --git a/internal/host/claimservingnamespace_linux_test.go b/internal/host/claimservingnamespace_linux_test.go| index 5dc77f2ca..577f81a35 100644 |
| --- a/internal/host/claimservingnamespace_linux_test.go |
| +++ b/internal/host/claimservingnamespace_linux_test.go |
| @@ -101,7 +101,7 @@ func TestServingNamespaceParentRequiresIndependentInstallation(t *testing.T) { |
| } |
| func TestServingNamespaceStartupUsesRecordedParent(t *testing.T) { |
| - for _, boundary := range []string{"parent", "quarantine-rename", "fresh", "complete"} { |
| + for _, boundary := range []string{"parent", "quarantine-rename", "fresh-before", "fresh", "complete"} { |
| t.Run(boundary, func(t *testing.T) { |
| f := newServingRootFixture(t, true) |
| fs := f.p.ledger.fs.(*servingParkFS) |
| @@ -112,7 +112,13 @@ func TestServingNamespaceStartupUsesRecordedParent(t *testing.T) { |
| if err := os.WriteFile(filepath.Join(path, "unknown"), []byte("retained"), 0600); err != nil { |
| t.Fatal(err) |
| } |
| - fs.state = quarantineWriteFault{Store: f.disk, after: func(name string, body []byte) error { |
| + fs.state = quarantineWriteFault{Store: f.disk, before: func(name string, body []byte) error { |
| + var q servingParkQuarantine |
| + if boundary == "fresh-before" && name == "namespace-quarantine.json" && servingParkDecode(body, &q) == nil && q.Fresh.Inode != 0 { |
| + return unix.EIO |
| + } |
| + return nil |
| + }, after: func(name string, body []byte) error { |
| var q servingParkQuarantine |
| if boundary == "parent" && name == "namespace-parent.json" { |
| return unix.EIO |
diff --git a/internal/host/claimservingpeer_rootrunner_linux_test.go b/internal/host/claimservingpeer_rootrunner_linux_test.go| index f1c027148..87ea29b68 100644 |
| --- a/internal/host/claimservingpeer_rootrunner_linux_test.go |
| +++ b/internal/host/claimservingpeer_rootrunner_linux_test.go |
| @@ -200,14 +200,18 @@ func TestRootServingParkAuthenticatedHandover(t *testing.T) { |
| func runServingParkAuthenticatedPeer(t *testing.T, handover, writer bool) { |
| t.Helper() |
| - runServingParkPeerNamespace(t, handover, writer, false) |
| + runServingParkPeerNamespace(t, handover, writer, "") |
| } |
| func TestRootServingNamespaceStartup(t *testing.T) { |
| - runServingParkPeerNamespace(t, true, false, true) |
| + runServingParkPeerNamespace(t, true, false, "parent") |
| } |
| -func runServingParkPeerNamespace(t *testing.T, handover, writer, namespace bool) { |
| +func TestRootServingNamespaceUnrecordedStartup(t *testing.T) { |
| + runServingParkPeerNamespace(t, true, false, "fresh") |
| +} |
| + |
| +func runServingParkPeerNamespace(t *testing.T, handover, writer bool, namespace string) { |
| t.Helper() |
| servingRootRunner(t) |
| pid1, err := os.ReadFile("/proc/1/comm") |
| @@ -498,7 +502,7 @@ func runServingParkPeerNamespace(t *testing.T, handover, writer, namespace bool) |
| if err != nil || actual.ControllerPID != next.PID || actual.ProcessStart != next.ProcessStart || len(actual.Groups) != 1 || actual.Groups[0] != pre.Launch.Groups[0] { |
| t.Fatalf("prelaunch not retained: %+v %v", actual, err) |
| } |
| - if namespace { |
| + if namespace != "" { |
| fs := f.p.ledger.fs.(*servingParkFS) |
| parkPath := filepath.Join(fs.data, "instances", ".serving-parked") |
| if err := os.Mkdir(parkPath, 0700); err != nil { |
| @@ -507,18 +511,28 @@ func runServingParkPeerNamespace(t *testing.T, handover, writer, namespace bool) |
| if err := os.WriteFile(filepath.Join(parkPath, "unknown"), []byte("retained unknown bytes"), 0600); err != nil { |
| t.Fatal(err) |
| } |
| - // Only the reply after the real private parent write is lost. Its |
| - // identity comes from production authenticated installation checks. |
| - fs.state = quarantineWriteFault{Store: state, after: func(name string, _ []byte) error { |
| - if name == "namespace-parent.json" { |
| + // Parent evidence comes from authenticated installation checks. Lose |
| + // either its reply or the later fresh-root identity write. |
| + fs.state = quarantineWriteFault{Store: state, before: func(name string, body []byte) error { |
| + var q servingParkQuarantine |
| + if namespace == "fresh" && name == "namespace-quarantine.json" && servingParkDecode(body, &q) == nil && q.Fresh.Inode != 0 { |
| + return unix.ENOSPC |
| + } |
| + return nil |
| + }, after: func(name string, _ []byte) error { |
| + if namespace == "parent" && name == "namespace-parent.json" { |
| return unix.EIO |
| } |
| return nil |
| }} |
| command(q, true) |
| fs.state = state |
| - if _, err := state.Read("namespace-quarantine.json"); !errors.Is(err, os.ErrNotExist) { |
| - t.Fatal("lost parent reply did not stop before quarantine", err) |
| + if namespace == "parent" { |
| + if _, err := state.Read("namespace-quarantine.json"); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatal("lost parent reply did not stop before quarantine", err) |
| + } |
| + } else if err := os.WriteFile(filepath.Join(parkPath, "uncertain"), []byte("unrecorded bytes"), 0600); err != nil { |
| + t.Fatal(err) |
| } |
| before := f.f.read() |
| restarted := *f.p |
| @@ -537,6 +551,15 @@ func runServingParkPeerNamespace(t *testing.T, handover, writer, namespace bool) |
| if quarantine.Parent.Mount != quarantine.Obstruction.Mount || quarantine.Parent.Mount != quarantine.Fresh.Mount { |
| t.Fatal("startup crossed the bind mount") |
| } |
| + if namespace == "fresh" { |
| + if !quarantine.UnrecordedMoved || quarantine.Unrecorded.Mount != quarantine.Parent.Mount || quarantine.Unrecorded == quarantine.Fresh { |
| + t.Fatal("startup adopted unrecorded root") |
| + } |
| + body, err = os.ReadFile(filepath.Join(filepath.Dir(parkPath), quarantine.unrecordedName(), "uncertain")) |
| + if err != nil || string(body) != "unrecorded bytes" { |
| + t.Fatal("startup changed unrecorded bytes", err) |
| + } |
| + } |
| body, err = os.ReadFile(filepath.Join(filepath.Dir(parkPath), quarantine.name(), "unknown")) |
| if err != nil || string(body) != "retained unknown bytes" || !reflect.DeepEqual(before, f.f.read()) { |
| t.Fatal("startup changed unknown bytes or claim", err) |
diff --git a/internal/host/claimservingquarantine_linux.go b/internal/host/claimservingquarantine_linux.go| index b986b9813..d2a29dfa0 100644 |
| --- a/internal/host/claimservingquarantine_linux.go |
| +++ b/internal/host/claimservingquarantine_linux.go |
| @@ -15,14 +15,16 @@ import ( |
| "golang.org/x/sys/unix" |
| ) |
| -// One obstruction may be retained per namespace. A second replacement needs |
| -// independent investigation; retries cannot accumulate quarantine directories. |
| +// Retain one obstruction and, if creation loses its identity write, one |
| +// unrecorded replacement. Further replacements require investigation. |
| type servingParkQuarantine struct { |
| Version int |
| ID string |
| Parent, Obstruction servingArtifactDirectory |
| Previous *servingParkNamespace `json:",omitempty"` |
| Moved bool |
| + Unrecorded servingArtifactDirectory `json:",omitempty"` |
| + UnrecordedMoved bool `json:",omitempty"` |
| Fresh servingArtifactDirectory |
| Complete bool |
| } |
| @@ -31,6 +33,10 @@ func (q servingParkQuarantine) name() string { |
| return ".serving-parked-quarantine-" + q.ID |
| } |
| +func (q servingParkQuarantine) unrecordedName() string { |
| + return ".serving-parked-unrecorded-" + 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 { |
| @@ -42,6 +48,12 @@ func (q servingParkQuarantine) validate() error { |
| 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.Unrecorded != (servingArtifactDirectory{}) && (!q.Moved || q.Unrecorded.Inode == 0 || q.Unrecorded.Device != q.Parent.Device || q.Unrecorded.Mount != q.Parent.Mount || q.Unrecorded == q.Obstruction || q.Unrecorded == q.Fresh || q.Previous != nil && q.Unrecorded == q.Previous.Root || q.Fresh.Inode != 0 && !q.UnrecordedMoved) { |
| + return hostbox.ErrChanged |
| + } |
| + if q.UnrecordedMoved && q.Unrecorded.Inode == 0 { |
| + return hostbox.ErrChanged |
| + } |
| if q.Complete && (!q.Moved || q.Fresh.Inode == 0) { |
| return hostbox.ErrChanged |
| } |
| @@ -165,22 +177,12 @@ func (f *servingParkFS) recoverNamespace(ctx context.Context, expected servingAr |
| 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 { |
| + if err = f.createQuarantineReplacement(ctx, parent, &q, save); err != nil { |
| return q, err |
| } |
| - fresh.Close() |
| - q.Fresh = id |
| - if err = save(); err != nil { |
| + } |
| + if q.UnrecordedMoved { |
| + if err = f.checkRetainedDirectory(parent, q.unrecordedName(), q.Unrecorded); err != nil { |
| return q, err |
| } |
| } |
| @@ -224,26 +226,34 @@ func (f *servingParkFS) recoverNamespace(ctx context.Context, expected servingAr |
| } |
| func (f *servingParkFS) checkQuarantine(parent *os.File, q servingParkQuarantine) error { |
| - d, err := servingParkOpenDir(parent, q.name()) |
| + return f.checkRetainedDirectory(parent, q.name(), q.Obstruction) |
| +} |
| + |
| +func (f *servingParkFS) checkRetainedDirectory(parent *os.File, name string, expected servingArtifactDirectory) error { |
| + d, err := servingParkOpenDir(parent, name) |
| if err != nil { |
| return err |
| } |
| defer d.Close() |
| id, err := servingParkDirID(d) |
| - if err != nil || id != q.Obstruction { |
| + if err != nil || id != expected { |
| return hostbox.ErrChanged |
| } |
| return nil |
| } |
| func (f *servingParkFS) moveQuarantine(ctx context.Context, parent *os.File, q servingParkQuarantine) error { |
| + return f.moveRetainedDirectory(ctx, parent, q.name(), q.Obstruction) |
| +} |
| + |
| +func (f *servingParkFS) moveRetainedDirectory(ctx context.Context, parent *os.File, name string, expected servingArtifactDirectory) 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 { |
| + if err = f.checkRetainedDirectory(parent, name, expected); err != nil { |
| return err |
| } |
| return f.sync(int(parent.Fd())) |
| @@ -253,14 +263,71 @@ func (f *servingParkFS) moveQuarantine(ctx context.Context, parent *os.File, q s |
| } |
| defer source.Close() |
| id, err := servingParkDirID(source) |
| - if err != nil || id != q.Obstruction { |
| + if err != nil || id != expected { |
| return hostbox.ErrChanged |
| } |
| - if err = f.rename(int(parent.Fd()), ".serving-parked", int(parent.Fd()), q.name(), unix.RENAME_NOREPLACE); err != nil { |
| + if err = f.rename(int(parent.Fd()), ".serving-parked", int(parent.Fd()), name, unix.RENAME_NOREPLACE); err != nil { |
| return err |
| } |
| - if err = f.checkQuarantine(parent, q); err != nil { |
| + if err = f.checkRetainedDirectory(parent, name, expected); err != nil { |
| return err |
| } |
| return f.sync(int(parent.Fd())) |
| } |
| + |
| +// EEXIST grants retention only. Persist the unexpected identity before moving |
| +// it, and never inspect its contents. One spare name bounds crash recovery even |
| +// if the service owner keeps replacing the root or private writes keep failing. |
| +func (f *servingParkFS) createQuarantineReplacement(ctx context.Context, parent *os.File, q *servingParkQuarantine, save func() error) error { |
| + for { |
| + if err := ctx.Err(); err != nil { |
| + return err |
| + } |
| + if q.Unrecorded.Inode != 0 { |
| + if !q.UnrecordedMoved { |
| + if err := f.moveRetainedDirectory(ctx, parent, q.unrecordedName(), q.Unrecorded); err != nil { |
| + return err |
| + } |
| + q.UnrecordedMoved = true |
| + if err := save(); err != nil { |
| + return err |
| + } |
| + } |
| + if err := f.checkRetainedDirectory(parent, q.unrecordedName(), q.Unrecorded); err != nil { |
| + return err |
| + } |
| + } |
| + err := unix.Mkdirat(int(parent.Fd()), ".serving-parked", 0700) |
| + if errors.Is(err, unix.EEXIST) && q.Unrecorded.Inode == 0 { |
| + d, err := servingParkOpenDir(parent, ".serving-parked") |
| + if err != nil { |
| + return err |
| + } |
| + id, err := servingParkDirID(d) |
| + d.Close() |
| + if err != nil { |
| + return err |
| + } |
| + q.Unrecorded = id |
| + if err := save(); err != nil { |
| + return err |
| + } |
| + continue |
| + } |
| + if err != nil { |
| + return err |
| + } |
| + fresh, id, err := f.privateDir(parent, ".serving-parked") |
| + if err != nil { |
| + return err |
| + } |
| + fresh.Close() |
| + q.Fresh = id |
| + // Directory durability precedes the private identity record. Otherwise |
| + // a reboot could retain the record while losing its directory entry. |
| + if err := f.sync(int(parent.Fd())); err != nil { |
| + return err |
| + } |
| + return save() |
| + } |
| +} |
diff --git a/internal/host/claimservingquarantine_linux_test.go b/internal/host/claimservingquarantine_linux_test.go| index 21e7d0b3b..aa65e9467 100644 |
| --- a/internal/host/claimservingquarantine_linux_test.go |
| +++ b/internal/host/claimservingquarantine_linux_test.go |
| @@ -154,7 +154,7 @@ func (s quarantineWriteFault) Write(name string, body []byte) error { |
| } |
| func TestServingParkQuarantineResumesDurableBoundaries(t *testing.T) { |
| - for _, boundary := range []string{"intent", "rename", "sync", "moved", "fresh", "namespace", "complete"} { |
| + for _, boundary := range []string{"intent", "rename", "sync", "moved", "fresh-sync", "fresh", "namespace", "complete"} { |
| t.Run(boundary, func(t *testing.T) { |
| f, parent, _ := quarantineFixture(t) |
| crash := errors.New("interrupted quarantine") |
| @@ -184,6 +184,21 @@ func TestServingParkQuarantineResumesDurableBoundaries(t *testing.T) { |
| if boundary == "sync" { |
| f.fs.sync = func(int) error { return crash } |
| } |
| + if boundary == "fresh-sync" { |
| + f.fs.sync = func(fd int) error { |
| + if _, err := os.Stat(filepath.Join(f.data, "instances", ".serving-parked")); err == nil { |
| + return crash |
| + } |
| + return unix.Fsync(fd) |
| + } |
| + 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 { |
| + t.Fatal("fresh identity saved before successful parent sync") |
| + } |
| + return nil |
| + }} |
| + } |
| if _, err := quarantineRecover(t, f, parent); !errors.Is(err, crash) { |
| t.Fatal("missing interruption", err) |
| } |
| @@ -216,6 +231,11 @@ func TestServingParkQuarantineRefusesUnrecordedFreshRoot(t *testing.T) { |
| if !errors.Is(err, unix.ENOSPC) { |
| t.Fatal("expected full-disk fault", err) |
| } |
| + // One unknown replacement can be retained. A second lost identity write |
| + // exhausts that bounded recovery; neither directory becomes authority. |
| + if _, err := quarantineRecover(t, f, parent); !errors.Is(err, unix.ENOSPC) { |
| + t.Fatal("expected second 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) |
| @@ -226,6 +246,246 @@ func TestServingParkQuarantineRefusesUnrecordedFreshRoot(t *testing.T) { |
| quarantinePreserved(t, f, q) |
| } |
| +func quarantineUnrecordedFixture(t *testing.T) (*servingParkDiskFixture, servingArtifactDirectory, servingArtifactDirectory) { |
| + t.Helper() |
| + f, parent, path := 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 |
| + }} |
| + if _, err := quarantineRecover(t, f, parent); !errors.Is(err, unix.ENOSPC) { |
| + t.Fatal("missing creation interruption", err) |
| + } |
| + f.fs.state = f.disk |
| + 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 := os.WriteFile(filepath.Join(path, "uncertain"), []byte("never adopt or delete"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Symlink("/unavailable/victim", filepath.Join(path, "link")); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Mkdir(filepath.Join(path, "unreadable"), 0000); err != nil { |
| + t.Fatal(err) |
| + } |
| + return f, parent, id |
| +} |
| + |
| +func TestServingParkQuarantineRetainsUnrecordedReplacement(t *testing.T) { |
| + for _, boundary := range []string{"", "intent-before", "intent-after", "rename", "sync", "moved-before", "moved-after", "fresh-after", "namespace", "complete"} { |
| + t.Run(boundary, func(t *testing.T) { |
| + f, parent, unrecorded := quarantineUnrecordedFixture(t) |
| + crash := errors.New("interrupted replacement retention") |
| + writeFault := func(after bool) func(string, []byte) error { |
| + return func(name string, body []byte) error { |
| + var q servingParkQuarantine |
| + if name == "namespace-quarantine.json" && servingParkDecode(body, &q) == nil && q.Unrecorded.Inode != 0 { |
| + if boundary == "intent-before" && !after && !q.UnrecordedMoved || boundary == "intent-after" && after && !q.UnrecordedMoved || boundary == "moved-before" && !after && q.UnrecordedMoved || boundary == "moved-after" && after && q.UnrecordedMoved || boundary == "fresh-after" && after && q.Fresh.Inode != 0 || boundary == "complete" && after && q.Complete { |
| + return crash |
| + } |
| + } |
| + if boundary == "namespace" && after && name == "namespace.json" { |
| + return crash |
| + } |
| + return nil |
| + } |
| + } |
| + f.fs.state = quarantineWriteFault{Store: f.disk, before: writeFault(false), after: writeFault(true)} |
| + 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 } |
| + } |
| + q, err := quarantineRecover(t, f, parent) |
| + if boundary != "" && !errors.Is(err, crash) || boundary == "" && err != nil { |
| + t.Fatal("unexpected interruption", 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 |
| + q, err = quarantineRecover(t, f, parent) |
| + if err != nil || !q.Complete || !q.UnrecordedMoved || q.Unrecorded != unrecorded || q.Fresh == unrecorded { |
| + t.Fatalf("replacement recovery: %+v %v", q, err) |
| + } |
| + quarantinePreserved(t, f, q) |
| + body, err := os.ReadFile(filepath.Join(f.data, "instances", q.unrecordedName(), "uncertain")) |
| + if err != nil || string(body) != "never adopt or delete" { |
| + t.Fatal("lost unrecorded contents", err) |
| + } |
| + before, err := f.disk.Read("namespace-quarantine.json") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + for i := 0; i < 3; i++ { |
| + if replay, err := quarantineRecover(t, f, parent); err != nil || !reflect.DeepEqual(replay, q) { |
| + t.Fatal("completed replacement replay", err) |
| + } |
| + } |
| + after, err := f.disk.Read("namespace-quarantine.json") |
| + if err != nil || string(before) != string(after) { |
| + t.Fatal("completed replay wrote authority", err) |
| + } |
| + names, err := os.ReadDir(filepath.Join(f.data, "instances")) |
| + if err != nil || len(names) != 4 { |
| + t.Fatalf("unbounded recovery directories: %v %v", names, err) |
| + } |
| + if boundary == "" { |
| + f.put("", "new database") |
| + a, stops := f.evidence(10, true, "missing") |
| + r, err := f.park(a, stops) |
| + if err != nil { |
| + t.Fatal(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) |
| + body, err := os.ReadFile(filepath.Join(f.data, "instances", q.unrecordedName(), "uncertain")) |
| + if err != nil || string(body) != "never adopt or delete" { |
| + t.Fatal("reaper touched retained replacement", err) |
| + } |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestServingParkQuarantineValidatesUnrecordedPhases(t *testing.T) { |
| + f, parent, _ := quarantineUnrecordedFixture(t) |
| + q, err := quarantineRecover(t, f, parent) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, fault := range []string{"unmoved", "missing", "device", "mount", "inode", "obstruction", "fresh", "previous"} { |
| + t.Run(fault, func(t *testing.T) { |
| + broken := q |
| + switch fault { |
| + case "unmoved": |
| + broken.UnrecordedMoved = false |
| + case "missing": |
| + broken.Unrecorded = servingArtifactDirectory{} |
| + case "device": |
| + broken.Unrecorded.Device++ |
| + case "mount": |
| + broken.Unrecorded.Mount++ |
| + case "inode": |
| + broken.Unrecorded.Inode = 0 |
| + case "obstruction": |
| + broken.Unrecorded = q.Obstruction |
| + case "fresh": |
| + broken.Unrecorded = q.Fresh |
| + case "previous": |
| + broken.Previous = &servingParkNamespace{Version: 1, Parent: parent, Root: q.Unrecorded} |
| + } |
| + if !errors.Is(broken.validate(), hostbox.ErrChanged) { |
| + t.Fatal("accepted invalid retained replacement", fault) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestServingParkQuarantineUnrecordedIdentityRefusals(t *testing.T) { |
| + for _, fault := range []string{"parent", "symlink", "destination", "recorded-replacement", "cancel", "retained-replacement"} { |
| + t.Run(fault, func(t *testing.T) { |
| + f, parent, _ := quarantineUnrecordedFixture(t) |
| + path := filepath.Join(f.data, "instances", ".serving-parked") |
| + f.fs.state = quarantineWriteFault{Store: f.disk, after: func(name string, body []byte) error { |
| + var q servingParkQuarantine |
| + if name == "namespace-quarantine.json" && servingParkDecode(body, &q) == nil && q.Unrecorded.Inode != 0 && !q.UnrecordedMoved { |
| + return unix.EIO |
| + } |
| + return nil |
| + }} |
| + q, err := quarantineRecover(t, f, parent) |
| + if !errors.Is(err, unix.EIO) { |
| + t.Fatal("missing retention intent", err) |
| + } |
| + f.fs.state = f.disk |
| + switch fault { |
| + case "parent": |
| + parent.Inode++ |
| + case "symlink", "recorded-replacement": |
| + if err := os.Rename(path, path+"-saved"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if fault == "symlink" { |
| + err = os.Symlink(path+"-saved", path) |
| + } else { |
| + err = os.Mkdir(path, 0700) |
| + } |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + case "destination": |
| + if err := os.Mkdir(filepath.Join(filepath.Dir(path), q.unrecordedName()), 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "retained-replacement": |
| + q, err = quarantineRecover(t, f, parent) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + retained := filepath.Join(filepath.Dir(path), q.unrecordedName()) |
| + if err := os.Rename(retained, retained+"-saved"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Mkdir(retained, 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + beforeNames, err := os.ReadDir(filepath.Dir(path)) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + beforeRecord, err := f.disk.Read("namespace-quarantine.json") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + 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 replacement accepted") |
| + } |
| + afterNames, err := os.ReadDir(filepath.Dir(path)) |
| + if err != nil || len(afterNames) != len(beforeNames) { |
| + t.Fatal("refusal changed retained directory names", err) |
| + } |
| + for i := range beforeNames { |
| + if beforeNames[i].Name() != afterNames[i].Name() { |
| + t.Fatal("refusal moved a replacement directory") |
| + } |
| + } |
| + afterRecord, err := f.disk.Read("namespace-quarantine.json") |
| + if err != nil || string(beforeRecord) != string(afterRecord) { |
| + t.Fatal("refusal changed retention authority", 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) { |
diff --git a/internal/host/claimservingquarantine_rootrunner_linux_test.go b/internal/host/claimservingquarantine_rootrunner_linux_test.go| index 438a355c4..dae54e436 100644 |
| --- a/internal/host/claimservingquarantine_rootrunner_linux_test.go |
| +++ b/internal/host/claimservingquarantine_rootrunner_linux_test.go |
| @@ -17,8 +17,17 @@ import ( |
| // 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) { |
| + runRootServingParkQuarantine(t, []string{"", "lost-rename-reply", "submount", "symlink"}) |
| +} |
| + |
| +func TestRootServingParkUnrecordedReplacement(t *testing.T) { |
| + runRootServingParkQuarantine(t, []string{"lost-fresh-record", "lost-retention-reply", "unrecorded-submount", "repeated-full-disk"}) |
| +} |
| + |
| +func runRootServingParkQuarantine(t *testing.T, faults []string) { |
| + t.Helper() |
| servingRootRunner(t) |
| - for _, fault := range []string{"", "lost-rename-reply", "submount", "symlink"} { |
| + for _, fault := range faults { |
| t.Run(fault, func(t *testing.T) { |
| root, err := os.MkdirTemp("/var/lib", "carlos-rootrunner-quarantine-") |
| if err != nil { |
| @@ -89,7 +98,77 @@ func TestRootServingParkQuarantine(t *testing.T) { |
| } |
| fs.rename = unix.Renameat2 |
| } |
| + var unrecorded servingArtifactDirectory |
| + if fault == "lost-fresh-record" || fault == "lost-retention-reply" || fault == "unrecorded-submount" || fault == "repeated-full-disk" { |
| + fs.state = quarantineWriteFault{Store: state, 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 |
| + }} |
| + if _, err := recover(); !errors.Is(err, unix.ENOSPC) { |
| + t.Fatal("missing fresh identity write failure", err) |
| + } |
| + d, err := os.Open(path) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + unrecorded, err = servingParkDirID(d) |
| + d.Close() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(filepath.Join(path, "uncertain"), []byte("unrecorded bytes"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + if fault == "repeated-full-disk" { |
| + if _, err := recover(); !errors.Is(err, unix.ENOSPC) { |
| + t.Fatal("missing second full disk failure", err) |
| + } |
| + } |
| + fs.state = state |
| + if fault == "lost-retention-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("missing retention reply failure", err) |
| + } |
| + } |
| + fs = newServingParkFS(data, state) |
| + if fault == "unrecorded-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) |
| + } |
| + }) |
| + } |
| + } |
| q, err := recover() |
| + if fault == "unrecorded-submount" || fault == "repeated-full-disk" { |
| + if err == nil { |
| + t.Fatal("unrecorded replacement accepted", fault) |
| + } |
| + if _, err := state.Read("namespace.json"); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatal("unsafe namespace published", err) |
| + } |
| + retained := path |
| + if fault == "repeated-full-disk" { |
| + retained = filepath.Join(filepath.Dir(path), q.unrecordedName()) |
| + } |
| + body, err := os.ReadFile(filepath.Join(retained, "uncertain")) |
| + if err != nil || string(body) != "unrecorded bytes" { |
| + t.Fatal("refusal lost unrecorded bytes", err) |
| + } |
| + return |
| + } |
| if fault == "submount" || fault == "symlink" { |
| if err == nil { |
| t.Fatal("unsafe obstruction accepted") |
| @@ -105,6 +184,15 @@ func TestRootServingParkQuarantine(t *testing.T) { |
| if q.Obstruction.Mount != parent.Mount || q.Fresh.Mount != parent.Mount { |
| t.Fatal("quarantine crossed bind mount") |
| } |
| + if unrecorded.Inode != 0 { |
| + if q.Unrecorded != unrecorded || !q.UnrecordedMoved || q.Fresh == unrecorded { |
| + t.Fatal("unrecorded root became namespace authority") |
| + } |
| + body, err := os.ReadFile(filepath.Join(filepath.Dir(path), q.unrecordedName(), "uncertain")) |
| + if err != nil || string(body) != "unrecorded bytes" { |
| + t.Fatal("retention changed unrecorded bytes", err) |
| + } |
| + } |
| 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) |
diff --git a/internal/provision/testdata/plan14-root-reads-seed.tsv b/internal/provision/testdata/plan14-root-reads-seed.tsv| index 66e209653..a7f7680dc 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 15 overlay; pending Paul’s census ruling. |
| +# Files seed checkpoint 16 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. |
| @@ -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 203b52f5900b66476f3ecc3cad8b8ea2526ceee7df31b8bae6652eb718158b5d |
| +@coverage internal/host 5895ba21eb875fa6744e6c3316480aa8bf313c9853b0f85ec32003d6113149b5 |
| @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,15 +154,19 @@ 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 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 bbf7d5600fc6fcf0c60d2b7c9e83d8ac57f9edaca3367c9c6e3fadffab303d8d |
| +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 12801fc3c906d28c41919fed8eb425dd01a480c42c7cd51653f686dc1f76a794 |
| +internal/host/claimservingquarantine_linux.go servingParkFS.recoverNamespace authority C,J4 rev3=PROPOSED-seed; independently recorded parent, effective mount and strict private quarantine records; retains one obstruction plus one unrecorded replacement; completed replay is read-only; no content deletion file:Fstat@388,file:Read@1051,file:Read@1610,file:Read@1951,file:Read@783,file:Readdirnames@3579 b157e0e79b663f5af04a01b3b8a5e3ab807d1fff78cb3f6ec4028f17f976ce5c |
| 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 servingParkQuarantine.validate authority C,J4 rev3=PROPOSED-seed; private quarantine shape and phase checks; original, prior, unrecorded and fresh directories must have distinct same-mount identities; retention only 86901fcae59cac1b613b2953723fb2fe79c6f38f88903a28d166ef4e0936200f |
| +internal/host/claimservingquarantine_linux.go type servingParkQuarantine authority C,J4 rev3=PROPOSED-seed; private versioned quarantine intent; additive unrecorded-replacement identity and rename progress bounded to one spare; no artifact deletion authority cc8ec82f24c038110d21a141003ba0093b57921fb3f92d426ad8c2cab2109687 |
| 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 |
| internal/host/claimservingnamespace_linux.go servingParkFS.namespaceParent authority A,C,J4 rev3=PROPOSED-seed; strict private parent record with nonzero device, inode and mount; missing authority differs from corrupt state file:Read@107 0b2be4cfd1acf24c2e031cbd6d7bb013832db708031a4b107987b9c652269ebe |
| internal/host/claimservingnamespace_linux.go servingParkFS.recordNamespaceParent authority A,C,J4 rev3=PROPOSED-seed; root-validated source directory under namespace and host locks; current nosymfollow parent and safe permissions; existing parent, namespace and quarantine evidence constrain first capture and replay file:Fstat@473,file:Read@1037 1f1d439eaab06b7d93822a96a962ba2d71ac0b22cdd35972a65a3919c4e3dd06 |
| internal/host/claimservingnamespace_linux.go servingParkFS.recoverRecordedNamespace authority A,C,J4 rev3=PROPOSED-seed; separate private parent identity, current mount and permissions, namespace and quarantine records; no parent capture at startup; bounded quarantine retention without artifact deletion file:Fstat@362,file:Read@588,file:Read@777 0e0774ff8b7532889f0995f9c5c5ecdb6a4bfa7e10619f7806f76299ada44198 |
| internal/host/claimservingnamespace_linux.go servingParkRoot.prepareServingNamespace authority C,J4 rev3=PROPOSED-seed; root-authorized retirement identity and existing record; full source/disposition validation precedes first parent write and bounded quarantine recovery; final ledger capture revalidates source 6411305a91f27caf14805823eb57d36b0549beef39541324e1f814c922ec5cfe |
| internal/host/claimservingpark_records.go servingParkLedger.capture authority C,J4 rev3=PROPOSED-seed; no-follow source snapshot, root launch/stop disposition, legacy marker and empty-bootstrap refusal, and complete retirement-record validation; read-only prerequisite before parent or retirement writes e3f3c2c919d96ae0a02fe0ac1a4e8694851dce7b73b3a9968f93f4445f3f35b3 |
| +internal/host/claimservingquarantine_linux.go servingParkQuarantine.unrecordedName authority C,J4 rev3=PROPOSED-seed; fixed retention basename from the existing private quarantine nonce; no caller path or new nonce on retry e037771d8e939a426c26104c10b92a2b205376d14574a92015f6f5a4bff90375 |
| +internal/host/claimservingquarantine_linux.go servingParkFS.checkRetainedDirectory authority C,J4 rev3=PROPOSED-seed; no-follow descriptor identity of a retained directory; exact mount, device and inode; never reads children 8d3d2b44f7b8f08e7a19c1741a9979d7181cc76156be8ffbf43190a43c67de9d |
| +internal/host/claimservingquarantine_linux.go servingParkFS.moveRetainedDirectory authority C,J4 rev3=PROPOSED-seed; pinned source and retained destination identities around no-replace same-mount rename and parent sync; lost reply accepts only the recorded destination 8e341682ab62bfe67680fbc3a478fb78d257444a3b9bde23daa98b7a13d1ada5 |
| +internal/host/claimservingquarantine_linux.go servingParkFS.createQuarantineReplacement authority C,J4 rev3=PROPOSED-seed; unrecorded root directory identity captured only for retention; one durable bounded retention intent, identity-checked move, new mkdir and parent fsync before fresh identity publication; repeated loss refuses cfbc39cc48eb37af01c972c2c2309100cfc1cff38f95addc0b7e2ded40ac46da |