WIP files seed checkpoint 17 (not for merge)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 files changed,
+731
−14
internal/host/claimservingdisplaced_linux.go+136 −0internal/host/claimservingdisplaced_linux_test.go+398 −0internal/host/claimservingdisplaced_rootrunner_linux_test.go+152 −0internal/host/claimservingpark_linux.go+24 −5internal/host/claimservingpark_records.go+4 −0internal/host/claimservingquarantine_linux_test.go+4 −0internal/provision/testdata/plan14-root-reads-seed.tsv+13 −9
CI passed — run details
CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok amadan.net/carlos/platform.git/launchctx 0.011s
? amadan.net/carlos/platform.git/launchctx/controlname [no test files]
? amadan.net/carlos/platform.git/launchctx/wireid [no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok github.com/carlosframework/platform 350.904s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 3.163s
ok github.com/carlosframework/platform/cmd/moveprobe 93.418s
ok github.com/carlosframework/platform/cmd/zddprobe 0.707s
ok github.com/carlosframework/platform/internal/activator 234.023s
ok github.com/carlosframework/platform/internal/agentsock 0.018s
ok github.com/carlosframework/platform/internal/alerting 0.006s
ok github.com/carlosframework/platform/internal/analytics 0.030s
ok github.com/carlosframework/platform/internal/appconfig 2.408s
ok github.com/carlosframework/platform/internal/apperr 0.081s
ok github.com/carlosframework/platform/internal/applog 0.147s
ok github.com/carlosframework/platform/internal/awsinv 0.005s
ok github.com/carlosframework/platform/internal/boxchoice 0.006s
ok github.com/carlosframework/platform/internal/boxid 0.908s
? github.com/carlosframework/platform/internal/boxid/boxidtest [no test files]
? github.com/carlosframework/platform/internal/boxid/internal/signtest [no test files]
ok github.com/carlosframework/platform/internal/boxvolumes 0.010s
ok github.com/carlosframework/platform/internal/caps 0.014s
ok github.com/carlosframework/platform/internal/certwarm 0.005s
ok github.com/carlosframework/platform/internal/cgroup 0.708s
ok github.com/carlosframework/platform/internal/changelog 0.296s
ok github.com/carlosframework/platform/internal/clamd 0.055s
? github.com/carlosframework/platform/internal/cliflag [no test files]
ok github.com/carlosframework/platform/internal/console 369.677s
? 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.560s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.012s
ok github.com/carlosframework/platform/internal/dbverify 0.005s
ok github.com/carlosframework/platform/internal/dnsrec 0.004s
ok github.com/carlosframework/platform/internal/docsite 0.005s
ok github.com/carlosframework/platform/internal/domains 2.172s
ok github.com/carlosframework/platform/internal/doorkey 0.007s
ok github.com/carlosframework/platform/internal/economics 0.004s
ok github.com/carlosframework/platform/internal/edge 26.394s
? 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.126s
ok github.com/carlosframework/platform/internal/gate 1.687s
ok github.com/carlosframework/platform/internal/genselect 0.020s
ok github.com/carlosframework/platform/internal/geo 0.011s
ok github.com/carlosframework/platform/internal/geoip 0.032s
ok github.com/carlosframework/platform/internal/host 155.460s
ok github.com/carlosframework/platform/internal/hostactors 0.594s
ok github.com/carlosframework/platform/internal/hostbox 2.245s
ok github.com/carlosframework/platform/internal/hostclaim 0.532s
ok github.com/carlosframework/platform/internal/hostevidence 0.013s
ok github.com/carlosframework/platform/internal/hostexec 4.845s
ok github.com/carlosframework/platform/internal/hostfence 0.032s
ok github.com/carlosframework/platform/internal/hostfiles 10.468s
ok github.com/carlosframework/platform/internal/hostfloor 0.013s
ok github.com/carlosframework/platform/internal/hostguard 1.022s
ok github.com/carlosframework/platform/internal/hostops 1.446s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.013s
ok github.com/carlosframework/platform/internal/hostquarantine 0.014s
ok github.com/carlosframework/platform/internal/hostrelocate 0.476s
ok github.com/carlosframework/platform/internal/hostresolve 0.166s
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.811s
ok github.com/carlosframework/platform/internal/instanceexec 2.714s
ok github.com/carlosframework/platform/internal/instancefs 0.013s
ok github.com/carlosframework/platform/internal/instancekey 0.015s
ok github.com/carlosframework/platform/internal/instanceupdates 0.021s
ok github.com/carlosframework/platform/internal/layout 1.447s
ok github.com/carlosframework/platform/internal/lease 9.485s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.018s
ok github.com/carlosframework/platform/internal/ledger 0.027s
ok github.com/carlosframework/platform/internal/mail 0.182s
ok github.com/carlosframework/platform/internal/maintenance 0.088s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.515s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.080s
ok github.com/carlosframework/platform/internal/metrics 0.005s
ok github.com/carlosframework/platform/internal/moves 21.200s
ok github.com/carlosframework/platform/internal/moves/archive 0.757s
ok github.com/carlosframework/platform/internal/objstore 0.194s
ok github.com/carlosframework/platform/internal/ops 0.162s
ok github.com/carlosframework/platform/internal/park 0.201s
ok github.com/carlosframework/platform/internal/privatefile 0.006s
ok github.com/carlosframework/platform/internal/provision 11.690s
ok github.com/carlosframework/platform/internal/publicrelease 0.008s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 4.059s
ok github.com/carlosframework/platform/internal/relay 7.234s
ok github.com/carlosframework/platform/internal/release 19.997s
ok github.com/carlosframework/platform/internal/rollout 3.652s
ok github.com/carlosframework/platform/internal/rolloutwords 0.003s
ok github.com/carlosframework/platform/internal/rootaws 0.008s
ok github.com/carlosframework/platform/internal/rootparticipation 0.006s
ok github.com/carlosframework/platform/internal/rootstate 0.026s
ok github.com/carlosframework/platform/internal/s3compat 0.005s
ok github.com/carlosframework/platform/internal/safefile 0.004s
ok github.com/carlosframework/platform/internal/schedule 2.275s
ok github.com/carlosframework/platform/internal/sim/hostlife 8.947s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 3.937s
ok github.com/carlosframework/platform/internal/status 0.041s
ok github.com/carlosframework/platform/internal/steering 0.005s
ok github.com/carlosframework/platform/internal/stopevidence 0.255s
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.003s
ok github.com/carlosframework/platform/internal/unixdial 0.002s
ok github.com/carlosframework/platform/internal/updatewords 0.004s
ok github.com/carlosframework/platform/internal/usage 0.004s
ok github.com/carlosframework/platform/internal/version 0.001s
ok github.com/carlosframework/platform/internal/vet 1.562s
=== 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.021s
=== RUN TestT4StorageProbeMinio
2026/10/14 20:29:52 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE7F6961D64AAC, 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.021s
minio contract: PASS
Intent
Human
<task-notification> <task-id>b59e39h10</task-id> <tool-use-id>toolu_01YAqRsiNMb5sKpNRhSAP6bU</tool-use-id> <output-file>/home/paulca/.cache/tmp/claude-1001/-home-paulca-amadan-net-carlos-platform/6387394d-0b76-4de7-bbb4-d725fe24b08c/tasks/b59e39h10.output</output-file> <status>completed</status> <summary>Background command "Wait for seed build run 17" completed (exit code 0)</summary> </task-notification>
diff --git a/internal/host/claimservingdisplaced_linux.go b/internal/host/claimservingdisplaced_linux.go| new file mode 100644 |
| index 000000000..b1791a950 |
| --- /dev/null |
| +++ b/internal/host/claimservingdisplaced_linux.go |
| @@ -0,0 +1,136 @@ |
| +//go:build linux |
| + |
| +package host |
| + |
| +import ( |
| + "errors" |
| + "io" |
| + "os" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "golang.org/x/sys/unix" |
| +) |
| + |
| +const servingParkRootSearchLimit = 128 |
| + |
| +// The namespace lock covers this lookup. Only the current root or the prior |
| +// root in a completed quarantine can contain an authorized event. Quarantine |
| +// retention itself never grants access to the obstruction's children. |
| +func (f *servingParkFS) eventRoot(r *servingParkRecord) (*os.File, servingParkNamespace, error) { |
| + root, n, err := f.namespace() |
| + if err != nil { |
| + return nil, n, err |
| + } |
| + if n.Parent != r.Source.Parent { |
| + root.Close() |
| + return nil, n, hostbox.ErrChanged |
| + } |
| + if r.Namespace == (servingArtifactDirectory{}) || r.Namespace == n.Root { |
| + // An old lost-rmdir reply cannot identify which namespace is absent. |
| + // Preserve it until independent evidence resolves that ambiguity. |
| + if r.Namespace == (servingArtifactDirectory{}) && r.EventRemoveIntent { |
| + body, e := f.state.Read("namespace-quarantine.json") |
| + if e == nil { |
| + var q servingParkQuarantine |
| + if servingParkDecode(body, &q) != nil || q.validate() != nil || q.Previous != nil { |
| + root.Close() |
| + return nil, n, hostbox.ErrChanged |
| + } |
| + } else if !errors.Is(e, os.ErrNotExist) { |
| + root.Close() |
| + return nil, n, e |
| + } |
| + } |
| + return root, n, nil |
| + } |
| + root.Close() |
| + return f.displacedRoot(r) |
| +} |
| + |
| +func (f *servingParkFS) displacedRoot(r *servingParkRecord) (*os.File, servingParkNamespace, error) { |
| + var n servingParkNamespace |
| + body, err := f.state.Read("namespace-quarantine.json") |
| + if err != nil { |
| + return nil, n, err |
| + } |
| + var q servingParkQuarantine |
| + if servingParkDecode(body, &q) != nil || q.validate() != nil || !q.Complete || q.Previous == nil || q.Parent != r.Source.Parent || r.Namespace != (servingArtifactDirectory{}) && r.Namespace != q.Previous.Root { |
| + return nil, n, hostbox.ErrChanged |
| + } |
| + n = *q.Previous |
| + parent, id, err := f.parent() |
| + if err != nil { |
| + return nil, n, err |
| + } |
| + defer parent.Close() |
| + var st unix.Stat_t |
| + if err := unix.Fstat(int(parent.Fd()), &st); err != nil || id != n.Parent || st.Mode&0022 != 0 { |
| + return nil, n, hostbox.ErrChanged |
| + } |
| + // Bound both memory and directory work. A larger parent remains blocked; |
| + // no partial search is interpreted as absence or permission to replace. |
| + names, err := parent.Readdirnames(servingParkRootSearchLimit + 1) |
| + if len(names) > servingParkRootSearchLimit || err != nil && !errors.Is(err, io.EOF) { |
| + return nil, n, hostbox.ErrChanged |
| + } |
| + var found *os.File |
| + defer func() { |
| + if found != nil { |
| + found.Close() |
| + } |
| + }() |
| + for _, name := range names { |
| + var s unix.Statx_t |
| + err := unix.Statx(int(parent.Fd()), name, unix.AT_SYMLINK_NOFOLLOW, unix.STATX_BASIC_STATS|unix.STATX_MNT_ID, &s) |
| + if errors.Is(err, unix.ENOENT) { |
| + continue |
| + } |
| + if err != nil { |
| + return nil, n, err |
| + } |
| + candidate := servingArtifactDirectory{Device: unix.Mkdev(s.Dev_major, s.Dev_minor), Inode: s.Ino, Mount: s.Mnt_id} |
| + if s.Mask&unix.STATX_MNT_ID == 0 || s.Mode&unix.S_IFMT != unix.S_IFDIR || candidate != n.Root { |
| + continue |
| + } |
| + d, actual, err := f.privateDir(parent, name) |
| + if err != nil { |
| + return nil, n, err |
| + } |
| + if actual != n.Root || found != nil { |
| + d.Close() |
| + return nil, n, hostbox.ErrChanged |
| + } |
| + found = d |
| + } |
| + if found == nil { |
| + return nil, n, hostbox.ErrChanged |
| + } |
| + check, current, err := f.parent() |
| + if err != nil { |
| + return nil, n, err |
| + } |
| + check.Close() |
| + if current != n.Parent { |
| + return nil, n, hostbox.ErrChanged |
| + } |
| + root := found |
| + found = nil |
| + return root, n, nil |
| +} |
| + |
| +// Upgrade old records only after event identity has been proved. Persist the |
| +// root before deletion so an absent event remains unambiguous on replay. |
| +func (f *servingParkFS) recordEventRoot(r *servingParkRecord, root *os.File, save func() error) error { |
| + id, err := servingParkDirID(root) |
| + if err != nil { |
| + return err |
| + } |
| + if r.Namespace != (servingArtifactDirectory{}) { |
| + if r.Namespace != id { |
| + return hostbox.ErrChanged |
| + } |
| + return nil |
| + } |
| + r.Namespace = id |
| + return save() |
| +} |
diff --git a/internal/host/claimservingdisplaced_linux_test.go b/internal/host/claimservingdisplaced_linux_test.go| new file mode 100644 |
| index 000000000..b57b96fe8 |
| --- /dev/null |
| +++ b/internal/host/claimservingdisplaced_linux_test.go |
| @@ -0,0 +1,398 @@ |
| +//go:build linux |
| + |
| +package host |
| + |
| +import ( |
| + "bytes" |
| + "errors" |
| + "fmt" |
| + "os" |
| + "path/filepath" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/provision" |
| + "golang.org/x/sys/unix" |
| +) |
| + |
| +func displacedParkFixture(t *testing.T, oldRecord bool) (*servingParkDiskFixture, servingParkRecord, servingParkQuarantine, string) { |
| + t.Helper() |
| + f := newServingParkDiskFixture(t) |
| + f.put("", "last tenant writes") |
| + f.put("-wal", "last WAL") |
| + f.put("-shm", "last SHM") |
| + a, stops := f.evidence(10, true, "missing") |
| + if _, err := f.park(a, stops); err != nil { |
| + t.Fatal(err) |
| + } |
| + f.complete(a) |
| + r, err := f.ledger.read(a.ID) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if oldRecord { |
| + r.Namespace = servingArtifactDirectory{} |
| + if err := f.ledger.save(r); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + path := filepath.Join(f.data, "instances", ".serving-parked") |
| + saved := path + "-saved" |
| + if err := os.Rename(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("quarantine", err) |
| + } |
| + return f, r, q, saved |
| +} |
| + |
| +func TestServingDisplacedParkReplacementAndExpiry(t *testing.T) { |
| + for _, old := range []bool{false, true} { |
| + for _, replacement := range []bool{false, true} { |
| + t.Run(fmt.Sprintf("old=%v/replacement=%v", old, replacement), func(t *testing.T) { |
| + f, r, q, saved := displacedParkFixture(t, old) |
| + before, err := f.disk.Read("retirement-" + r.Authorization.ID + ".json") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := f.fs.CheckParked(&r); err != nil { |
| + t.Fatal("known displaced park inaccessible", err) |
| + } |
| + after, err := f.disk.Read("retirement-" + r.Authorization.ID + ".json") |
| + if err != nil || !bytes.Equal(before, after) { |
| + t.Fatal("inspection wrote authority", err) |
| + } |
| + f.put("", "current tenant writes") |
| + if replacement { |
| + next, stops := f.evidence(11, false, "started") |
| + if _, err := f.park(next, stops); err != nil { |
| + t.Fatal("replacement", err) |
| + } |
| + f.complete(next) |
| + body, err := os.ReadFile(f.path(next, "")) |
| + if err != nil || string(body) != "current tenant writes" { |
| + t.Fatal("replacement database", err) |
| + } |
| + } else { |
| + f.now = r.ReapAfter |
| + if err := f.ledger.withNamespace(t.Context(), func() error { return f.ledger.reapLocked(t.Context(), r.Authorization.ID) }); err != nil { |
| + t.Fatal("expiry", err) |
| + } |
| + body, err := os.ReadFile(provision.NestedDBPath(f.data, f.base.Host)) |
| + if err != nil || string(body) != "current tenant writes" { |
| + t.Fatal("expiry touched live database", err) |
| + } |
| + } |
| + got, err := f.ledger.read(r.Authorization.ID) |
| + if err != nil || !got.Closed || !got.EventRemoved || got.Namespace != q.Previous.Root || !got.ReapAfter.Equal(r.ReapAfter) { |
| + t.Fatal("closed record lost identity or deadline", err) |
| + } |
| + if _, err := os.Stat(filepath.Join(saved, r.Authorization.ID)); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatal("old event remains", err) |
| + } |
| + if err := f.ledger.recoverCompleted(t.Context(), r.Authorization.ID); err != nil { |
| + t.Fatal("closed replay", err) |
| + } |
| + quarantinePreserved(t, f, q) |
| + }) |
| + } |
| + } |
| +} |
| + |
| +func TestServingDisplacedParkRetainsUnprovenIdentities(t *testing.T) { |
| + for _, fault := range []string{"parent", "parent-mode", "root", "root-mode", "symlink", "nested", "event", "extra-child", "hardlink", "file", "quarantine", "limit"} { |
| + t.Run(fault, func(t *testing.T) { |
| + f, r, q, saved := displacedParkFixture(t, false) |
| + event := filepath.Join(saved, r.Authorization.ID) |
| + db := filepath.Join(event, r.Authorization.Successor.Host+".db") |
| + keep := db |
| + switch fault { |
| + case "parent": |
| + parent := filepath.Dir(saved) |
| + if err := os.Rename(parent, parent+"-saved"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Mkdir(parent, 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + keep = filepath.Join(parent+"-saved", filepath.Base(saved), r.Authorization.ID, filepath.Base(db)) |
| + case "parent-mode": |
| + if err := os.Chmod(filepath.Dir(saved), 0777); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "root", "symlink", "nested": |
| + outside := filepath.Join(f.data, "retained") |
| + if fault == "nested" { |
| + outside = filepath.Join(filepath.Dir(saved), "container", "retained") |
| + if err := os.Mkdir(filepath.Dir(outside), 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + if err := os.Rename(saved, outside); err != nil { |
| + t.Fatal(err) |
| + } |
| + keep = filepath.Join(outside, r.Authorization.ID, filepath.Base(db)) |
| + if fault == "symlink" { |
| + if err := os.Symlink(outside, saved); err != nil { |
| + t.Fatal(err) |
| + } |
| + } else if err := os.Mkdir(saved, 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "root-mode": |
| + if err := os.Chmod(saved, 0755); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "event": |
| + if err := os.Rename(event, event+"-saved"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Mkdir(event, 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + keep = filepath.Join(event+"-saved", filepath.Base(db)) |
| + case "extra-child": |
| + if err := os.WriteFile(filepath.Join(event, "unknown"), []byte("unrecorded"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "hardlink": |
| + if err := os.Link(db, filepath.Join(f.data, "other-link")); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "file": |
| + if err := os.Rename(db, db+"-saved"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(db, []byte("victim"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + keep = db + "-saved" |
| + case "quarantine": |
| + if err := f.disk.Write("namespace-quarantine.json", []byte("{}")); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "limit": |
| + entries, err := os.ReadDir(filepath.Dir(saved)) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + // Exactly one over the bound: the limited read includes the |
| + // genuine root regardless of the filesystem's enumeration order. |
| + for i := len(entries); i < servingParkRootSearchLimit+1; i++ { |
| + if err := os.Mkdir(filepath.Join(filepath.Dir(saved), fmt.Sprintf("unknown-%d", i)), 0000); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + } |
| + before, err := f.disk.Read("retirement-" + r.Authorization.ID + ".json") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + f.now = r.ReapAfter |
| + if err := f.ledger.withNamespace(t.Context(), func() error { return f.ledger.reapLocked(t.Context(), r.Authorization.ID) }); err == nil { |
| + t.Fatal("unproven identity deleted") |
| + } |
| + got, err := os.ReadFile(keep) |
| + if err != nil || string(got) != "last tenant writes" { |
| + t.Fatal("retained bytes changed", err) |
| + } |
| + // Deletion intent may be recorded, but no file progress is permitted. |
| + after, err := f.ledger.read(r.Authorization.ID) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, file := range after.Source.Files { |
| + if file.DeleteIntent || file.Deleted { |
| + t.Fatal("unsafe deletion progress") |
| + } |
| + } |
| + if after.Closed || after.EventRemoveIntent || len(before) == 0 { |
| + t.Fatal("unsafe closure") |
| + } |
| + if fault != "parent" { |
| + quarantinePreserved(t, f, q) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestServingDisplacedParkRecoveryWriteBoundaries(t *testing.T) { |
| + for _, fault := range []string{"root-before", "root-after", "unlink", "rmdir"} { |
| + t.Run(fault, func(t *testing.T) { |
| + f, r, q, saved := displacedParkFixture(t, true) |
| + f.now = r.ReapAfter |
| + fired := false |
| + check := func(name string, body []byte) error { |
| + var next servingParkRecord |
| + if !fired && name == "retirement-"+r.Authorization.ID+".json" && servingParkDecode(body, &next) == nil && next.Namespace.Inode != 0 { |
| + fired = true |
| + return unix.ENOSPC |
| + } |
| + return nil |
| + } |
| + if fault == "root-before" { |
| + f.ledger.state = quarantineWriteFault{Store: f.disk, before: check} |
| + } |
| + if fault == "root-after" { |
| + f.ledger.state = quarantineWriteFault{Store: f.disk, after: check} |
| + } |
| + if fault == "unlink" || fault == "rmdir" { |
| + f.fs.unlink = func(fd int, name string, flags int) error { |
| + if err := unix.Unlinkat(fd, name, flags); err != nil { |
| + return err |
| + } |
| + if !fired && (fault == "unlink" && flags == 0 || fault == "rmdir" && flags == unix.AT_REMOVEDIR) { |
| + fired = true |
| + return unix.ENOSPC |
| + } |
| + return nil |
| + } |
| + } |
| + reap := func() error { |
| + return f.ledger.withNamespace(t.Context(), func() error { return f.ledger.reapLocked(t.Context(), r.Authorization.ID) }) |
| + } |
| + if err := reap(); !errors.Is(err, unix.ENOSPC) || !fired { |
| + t.Fatal("missing boundary", err) |
| + } |
| + if fault == "root-before" || fault == "root-after" { |
| + body, err := os.ReadFile(filepath.Join(saved, r.Authorization.ID, r.Authorization.Successor.Host+".db")) |
| + if err != nil || string(body) != "last tenant writes" { |
| + t.Fatal("deleted before durable root", err) |
| + } |
| + } |
| + f.ledger.state = f.disk |
| + fs := newServingParkFS(f.data, f.disk) |
| + fs.owner, fs.mount = f.fs.owner, f.fs.mount |
| + f.fs, f.ledger.fs = fs, fs |
| + if err := f.ledger.recoverCompleted(t.Context(), r.Authorization.ID); err != nil { |
| + t.Fatal("recovery", err) |
| + } |
| + got, err := f.ledger.read(r.Authorization.ID) |
| + if err != nil || !got.Closed || !got.EventRemoved || got.Namespace != q.Previous.Root { |
| + t.Fatal("incomplete recovered deletion", err) |
| + } |
| + quarantinePreserved(t, f, q) |
| + }) |
| + } |
| +} |
| + |
| +func TestServingDisplacedParkCannotAdoptEventUnderUnknownRoot(t *testing.T) { |
| + for _, old := range []bool{false, true} { |
| + t.Run(fmt.Sprint(old), func(t *testing.T) { |
| + f, r, q, saved := displacedParkFixture(t, old) |
| + outside := filepath.Join(f.data, "known-root") |
| + if err := os.Rename(saved, outside); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Mkdir(saved, 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Rename(filepath.Join(outside, r.Authorization.ID), filepath.Join(saved, r.Authorization.ID)); err != nil { |
| + t.Fatal(err) |
| + } |
| + f.now = r.ReapAfter |
| + if err := f.ledger.withNamespace(t.Context(), func() error { return f.ledger.reapLocked(t.Context(), r.Authorization.ID) }); err == nil { |
| + t.Fatal("event identity authorized an unknown root") |
| + } |
| + body, err := os.ReadFile(filepath.Join(saved, r.Authorization.ID, r.Authorization.Successor.Host+".db")) |
| + if err != nil || string(body) != "last tenant writes" { |
| + t.Fatal("unknown root lost known event", err) |
| + } |
| + quarantinePreserved(t, f, q) |
| + }) |
| + } |
| +} |
| + |
| +func TestServingDisplacedParkPendingRenameResumesOriginalSlot(t *testing.T) { |
| + f := newServingParkDiskFixture(t) |
| + f.put("", "interrupted database") |
| + f.put("-wal", "interrupted WAL") |
| + a, stops := f.evidence(10, true, "missing") |
| + 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 unix.EIO |
| + } |
| + if _, err := f.park(a, stops); !errors.Is(err, unix.EIO) { |
| + t.Fatal("missing interrupted rename", err) |
| + } |
| + r, err := f.ledger.read(a.ID) |
| + if err != nil || r.Complete || r.Parked { |
| + t.Fatal("pending record", err) |
| + } |
| + 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) |
| + } |
| + f.fs.rename = unix.Renameat2 |
| + q, err := quarantineRecover(t, f, r.Source.Parent) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := f.park(a, stops); err != nil { |
| + t.Fatal("pending replay", err) |
| + } |
| + f.complete(a) |
| + slot, err := f.ledger.slot(a) |
| + if err != nil || slot.ID != a.ID || slot.Pending != "" { |
| + t.Fatal("recovery changed slot", err) |
| + } |
| + for suffix, want := range map[string]string{"": "interrupted database", "-wal": "interrupted WAL"} { |
| + body, err := os.ReadFile(filepath.Join(path+"-saved", a.ID, a.Successor.Host+".db"+suffix)) |
| + if err != nil || string(body) != want { |
| + t.Fatal("pending recovery lost bytes", err) |
| + } |
| + } |
| + quarantinePreserved(t, f, q) |
| +} |
| + |
| +func TestServingDisplacedParkOldAbsentEventRemainsAmbiguous(t *testing.T) { |
| + f, r, q, saved := displacedParkFixture(t, true) |
| + // Model an older deletion whose rmdir reply was lost before the root was |
| + // pinned. Neither namespace's absence supplies the missing identity. |
| + r.Deleting, r.Cause, r.EventRemoveIntent = true, "expiry", true |
| + for i := range r.Source.Files { |
| + item := &r.Source.Files[i] |
| + if item.Present { |
| + item.DeleteIntent, item.Deleted = true, true |
| + if err := os.Remove(filepath.Join(saved, r.Authorization.ID, item.Name)); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + } |
| + if err := os.Remove(filepath.Join(saved, r.Authorization.ID)); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := f.ledger.save(r); err != nil { |
| + t.Fatal(err) |
| + } |
| + before, err := f.disk.Read("retirement-" + r.Authorization.ID + ".json") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := f.ledger.recoverCompleted(t.Context(), r.Authorization.ID); err == nil { |
| + t.Fatal("old absence accepted as root identity") |
| + } |
| + after, err := f.disk.Read("retirement-" + r.Authorization.ID + ".json") |
| + if err != nil || !bytes.Equal(before, after) { |
| + t.Fatal("ambiguous recovery wrote progress", err) |
| + } |
| + if _, err := os.Stat(filepath.Join(saved, r.Authorization.ID)); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatal("ambiguous event recreated", err) |
| + } |
| + quarantinePreserved(t, f, q) |
| +} |
diff --git a/internal/host/claimservingdisplaced_rootrunner_linux_test.go b/internal/host/claimservingdisplaced_rootrunner_linux_test.go| new file mode 100644 |
| index 000000000..b38902638 |
| --- /dev/null |
| +++ b/internal/host/claimservingdisplaced_rootrunner_linux_test.go |
| @@ -0,0 +1,152 @@ |
| +//go:build linux && rootrunner |
| + |
| +package host |
| + |
| +import ( |
| + "errors" |
| + "os" |
| + "path/filepath" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/rootstate" |
| + "golang.org/x/sys/unix" |
| +) |
| + |
| +// Production filesystem and private state, with fixture retirement authority. |
| +// This does not exercise an installed root service or authenticated retirement. |
| +func TestRootServingParkDisplacedRoot(t *testing.T) { |
| + servingRootRunner(t) |
| + for _, fault := range []string{"", "old-record", "lost-rmdir-reply", "submount", "unknown-root-with-event"} { |
| + t.Run(fault, func(t *testing.T) { |
| + root, err := os.MkdirTemp("/var/lib", "carlos-rootrunner-displaced-") |
| + 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) |
| + } |
| + }) |
| + r := servingRunnerPark(t, data, state) |
| + r.Complete = true // completion is a fixture, not claim-writer evidence |
| + if fault == "old-record" { |
| + r.Namespace = servingArtifactDirectory{} |
| + } |
| + fs := newServingParkFS(data, state) |
| + ledger := &servingParkLedger{state: state} |
| + save := func() error { return ledger.write("fixture.json", r) } |
| + if err := save(); err != nil { |
| + t.Fatal(err) |
| + } |
| + path := filepath.Join(data, "instances", ".serving-parked") |
| + saved := path + "-saved" |
| + if err := os.Rename(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("retained unknown bytes"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + q, err := fs.recoverNamespace(t.Context(), r.Source.Parent) |
| + if err != nil || q.Previous == nil { |
| + t.Fatal("quarantine", err) |
| + } |
| + if fault == "submount" { |
| + if err := unix.Mount(saved, saved, "", unix.MS_BIND, ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + t.Cleanup(func() { |
| + if err := unix.Unmount(saved, 0); err != nil { |
| + t.Error(err) |
| + } |
| + }) |
| + } |
| + if fault == "unknown-root-with-event" { |
| + // Move the genuine event into an unrecorded root while keeping its |
| + // inode. Event identity alone must not grant parent authority. |
| + outside := filepath.Join(data, "instances", "container") |
| + if err := os.Mkdir(outside, 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Rename(saved, filepath.Join(outside, "known")); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Mkdir(saved, 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Rename(filepath.Join(outside, "known", r.Authorization.ID), filepath.Join(saved, r.Authorization.ID)); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + if fault == "submount" || fault == "unknown-root-with-event" { |
| + writes := 0 |
| + r.Deleting, r.Cause = true, "expiry" |
| + if err := fs.Delete(&r, func() error { writes++; return nil }); err == nil || writes != 0 { |
| + t.Fatal("unproven root accepted", writes, err) |
| + } |
| + for suffix, want := range map[string]string{"": "database", "-wal": "WAL", "-shm": "SHM"} { |
| + body, err := os.ReadFile(filepath.Join(saved, r.Authorization.ID, r.Authorization.Successor.Host+".db"+suffix)) |
| + if err != nil || string(body) != want { |
| + t.Fatal("refusal lost bytes", err) |
| + } |
| + } |
| + } else { |
| + if err := fs.CheckParked(&r); err != nil { |
| + t.Fatal("displaced root", err) |
| + } |
| + r.Deleting, r.Cause = true, "expiry" |
| + if err := save(); err != nil { |
| + t.Fatal(err) |
| + } |
| + if fault == "lost-rmdir-reply" { |
| + fs.unlink = func(fd int, name string, flags int) error { |
| + if err := unix.Unlinkat(fd, name, flags); err != nil { |
| + return err |
| + } |
| + if flags == unix.AT_REMOVEDIR { |
| + return unix.EIO |
| + } |
| + return nil |
| + } |
| + if err := fs.Delete(&r, save); !errors.Is(err, unix.EIO) { |
| + t.Fatal("missing lost reply", err) |
| + } |
| + body, err := state.Read("fixture.json") |
| + if err != nil || servingParkDecode(body, &r) != nil { |
| + t.Fatal("retained progress", err) |
| + } |
| + fs = newServingParkFS(data, state) |
| + } |
| + if err := fs.Delete(&r, save); err != nil { |
| + t.Fatal("delete recorded event", err) |
| + } |
| + if !r.EventRemoved || r.Namespace != q.Previous.Root { |
| + t.Fatal("missing recorded root or completion") |
| + } |
| + if err := fs.Delete(&r, save); err != nil { |
| + t.Fatal("replay", err) |
| + } |
| + if _, err := os.Stat(filepath.Join(saved, r.Authorization.ID)); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatal("event remains", err) |
| + } |
| + } |
| + body, err := os.ReadFile(filepath.Join(filepath.Dir(path), q.name(), "unknown")) |
| + if err != nil || string(body) != "retained unknown bytes" { |
| + t.Fatal("quarantine changed", err) |
| + } |
| + }) |
| + } |
| +} |
diff --git a/internal/host/claimservingpark_linux.go b/internal/host/claimservingpark_linux.go| index 538d5d2fc..46a9bd802 100644 |
| --- a/internal/host/claimservingpark_linux.go |
| +++ b/internal/host/claimservingpark_linux.go |
| @@ -267,7 +267,7 @@ func (f *servingParkFS) Capture(a servingParkAuthorization) (servingParkSource, |
| } |
| func (f *servingParkFS) event(r *servingParkRecord) (*os.File, *os.File, error) { |
| - root, n, err := f.namespace() |
| + root, n, err := f.eventRoot(r) |
| if err != nil { |
| return nil, nil, err |
| } |
| @@ -276,6 +276,19 @@ func (f *servingParkFS) event(r *servingParkRecord) (*os.File, *os.File, error) |
| return nil, nil, hostbox.ErrChanged |
| } |
| event, id, err := f.privateDir(root, r.Authorization.ID) |
| + // Older records did not pin their namespace. Their exact event must still |
| + // exist before they can be associated with the prior recorded root. |
| + if (err != nil || id != r.Event) && r.Prepared && r.Namespace == (servingArtifactDirectory{}) { |
| + if event != nil { |
| + event.Close() |
| + } |
| + root.Close() |
| + root, n, err = f.displacedRoot(r) |
| + if err != nil { |
| + return nil, nil, err |
| + } |
| + event, id, err = f.privateDir(root, r.Authorization.ID) |
| + } |
| if err != nil { |
| root.Close() |
| return nil, nil, err |
| @@ -296,7 +309,10 @@ func (f *servingParkFS) Prepare(r *servingParkRecord, save func() error) error { |
| } |
| defer root.Close() |
| defer event.Close() |
| - return f.sync(int(root.Fd())) |
| + if err := f.sync(int(root.Fd())); err != nil { |
| + return err |
| + } |
| + return f.recordEventRoot(r, root, save) |
| } |
| root, n, err := f.namespace() |
| if err != nil { |
| @@ -329,7 +345,7 @@ func (f *servingParkFS) Prepare(r *servingParkRecord, save func() error) error { |
| if err = f.sync(int(root.Fd())); err != nil { |
| return err |
| } |
| - r.Event, r.Prepared = id, true |
| + r.Event, r.Prepared, r.Namespace = id, true, n.Root |
| return save() |
| } |
| @@ -452,6 +468,9 @@ func (f *servingParkFS) Delete(r *servingParkRecord, save func() error) error { |
| return hostbox.ErrChanged |
| } |
| } |
| + if err := f.recordEventRoot(r, root, save); err != nil { |
| + return err |
| + } |
| for i := range r.Source.Files { |
| item := &r.Source.Files[i] |
| if !item.Present { |
| @@ -572,7 +591,7 @@ func (f *servingParkFS) removeEvent(r *servingParkRecord, save func() error) err |
| return hostbox.ErrChanged |
| } |
| } |
| - root, n, err := f.namespace() |
| + root, n, err := f.eventRoot(r) |
| if err != nil { |
| return err |
| } |
| @@ -624,7 +643,7 @@ func (f *servingParkFS) removedEvent(r *servingParkRecord) error { |
| if !r.EventRemoveIntent || !r.EventRemoved { |
| return hostbox.ErrChanged |
| } |
| - root, n, err := f.namespace() |
| + root, n, err := f.eventRoot(r) |
| if err != nil { |
| return err |
| } |
diff --git a/internal/host/claimservingpark_records.go b/internal/host/claimservingpark_records.go| index 584991273..691e807df 100644 |
| --- a/internal/host/claimservingpark_records.go |
| +++ b/internal/host/claimservingpark_records.go |
| @@ -42,6 +42,7 @@ type servingParkSource struct { |
| } |
| type servingParkRecord struct { |
| + Namespace servingArtifactDirectory `json:",omitempty"` |
| LegacyEvidence *servingLegacyEvidence `json:",omitempty"` |
| Continuations []servingParkContinuation `json:",omitempty"` |
| EventRemoveIntent, EventRemoved bool `json:",omitempty"` |
| @@ -194,6 +195,9 @@ func validateServingParkRecord(r servingParkRecord) error { |
| if r.Prepared && (r.Event.Inode == 0 || r.Event.Device != r.Source.Directory.Device || r.Event.Mount != r.Source.Directory.Mount) { |
| return hostbox.ErrChanged |
| } |
| + if r.Namespace != (servingArtifactDirectory{}) && (!r.Prepared || r.Namespace.Inode == 0 || r.Namespace.Device != r.Source.Parent.Device || r.Namespace.Mount != r.Source.Parent.Mount || r.Namespace == r.Source.Parent || r.Namespace == r.Event) { |
| + return hostbox.ErrChanged |
| + } |
| if r.Parked { |
| if !r.Prepared || r.ParkedAt.IsZero() || !r.ReapAfter.Equal(r.ParkedAt.Add(7*24*time.Hour)) { |
| return hostbox.ErrChanged |
diff --git a/internal/host/claimservingquarantine_linux_test.go b/internal/host/claimservingquarantine_linux_test.go| index aa65e9467..7514e9472 100644 |
| --- a/internal/host/claimservingquarantine_linux_test.go |
| +++ b/internal/host/claimservingquarantine_linux_test.go |
| @@ -555,6 +555,10 @@ func TestServingParkQuarantinePreservesBlockedSlot(t *testing.T) { |
| if err := os.Rename(path, path+"-saved"); err != nil { |
| t.Fatal(err) |
| } |
| + // Finding the genuine root does not authorize an unknown event child. |
| + if err := os.WriteFile(filepath.Join(path+"-saved", a.ID, "unknown"), []byte("retain"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| if err := os.Mkdir(path, 0700); err != nil { |
| t.Fatal(err) |
| } |
diff --git a/internal/provision/testdata/plan14-root-reads-seed.tsv b/internal/provision/testdata/plan14-root-reads-seed.tsv| index a7f7680dc..50794cdbd 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 16 overlay; pending Paul’s census ruling. |
| +# Files seed checkpoint 17 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. |
| @@ -36,10 +36,10 @@ internal/host/claimservingpark_linux.go sameServingParkFile authority C,J4 rev3= |
| internal/host/claimservingpark_linux.go servingParkDirID authority C,J4 rev3=PROPOSED-seed; descriptor statx directory type, device, inode and mount identity file:Statx@102 3480dad6d65fedcc17e678df38cf13352ad3c4a8a9d0a09a395973d92b98094e |
| internal/host/claimservingpark_linux.go servingParkFS.Capture authority C,J4 rev3=PROPOSED-seed; canonical nested source directory, four fixed basenames, source mount and bounded generation-marker bytes through the captured descriptor file:Fstat@920,file:Openat@694,file:ReadAll@1138 c3a9348ddd9548f3fcfdcd45abc42b6a758a6399ed34e92481785e26c110763d |
| internal/host/claimservingpark_linux.go servingParkFS.CheckParked authority C,J4 rev3=PROPOSED-seed; all recorded event identities, unexpected children and source absence before root completion file:Readdirnames@1266 ac675e110e1493ab1ec69359696d462228f1e1265923c2c0d471111851a2c645 |
| -internal/host/claimservingpark_linux.go servingParkFS.Delete authority C,J4 rev3=PROPOSED-seed; recorded event and artifact identities; validates complete child set before journaled unlink, then removes only the recorded empty event with durable intent and progress file:Readdirnames@402 9242121cefd1767725b160092a248a8f60d952e5093b6270ea71dfc377b5300c |
| +internal/host/claimservingpark_linux.go servingParkFS.Delete authority C,J4 rev3=PROPOSED-seed; recorded event and complete artifact set before journaled unlink; persist proven namespace identity before old-record deletion; remove only the recorded empty event file:Readdirnames@402 7fa2b1a5f32a7c67eceb48e6eee5adaef44fbb0c065e05a21fcd60391f40ca34 |
| internal/host/claimservingpark_linux.go servingParkFS.Move authority C,J4 rev3=PROPOSED-seed; recorded source/destination inodes and explicit absences; no-replace same-mount rename, post-rename identity and both parent fsyncs before progress 6e43ae58b6a02d167135fabdde7ade06502511c5237b313e6ca6c2a8e9a2c8d0 |
| -internal/host/claimservingpark_linux.go servingParkFS.Prepare authority C,J4 rev3=PROPOSED-seed; recorded private namespace and empty event 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.Prepare authority C,J4 rev3=PROPOSED-seed; recorded private namespace and empty event; persist namespace and event identities together; old prepared events gain a namespace only after exact identity proof file:Prepare@24,file:Readdirnames@967 1fddbfb85e6381113db4807f41890dac36a104997094a139408eca75b4572eab |
| +internal/host/claimservingpark_linux.go servingParkFS.event authority C,J4 rev3=PROPOSED-seed; current or proven displaced namespace and exact private event mount/device/inode; old records may fall back only to the previous root recorded before quarantine 625ed442442a9b07c57392f72dea64e9e91eba65d2e2ef0e9899275756336788 |
| internal/host/claimservingpark_linux.go servingParkFS.namespace authority C,J4 rev3=PROPOSED-seed; private rootstate namespace and quarantine completion records, effective parent mount and root-owned park directory; pending quarantine blocks namespace creation and use; unknown roots refused without adoption 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 |
| @@ -64,7 +64,7 @@ internal/host/claimservingpark_records.go servingParkLedger.slot authority C,J4 |
| internal/host/claimservingpark_records.go servingParkLedger.withNamespace authority C,J4 rev3=PROPOSED-seed; private rootstate namespace lock and cancellation before invoking root authority; composed with root host authority through the typed journal service b7cfae44fdb7de9ef8130f5581710ae3a1a526bb059b4f616650172490f5c8ac |
| internal/host/claimservingpark_records.go servingParkLedger.write authority C,J4 rev3=PROPOSED-seed; serialized private record sent only to the supplied rootstate store 0635d2c27871e67079119a0f2e3034e817b765d0fe3a47e7d9bdc66fb2779571 |
| internal/host/claimservingpark_records.go type servingParkFilesystem authority C,J4 rev3=PROPOSED-seed; private filesystem effect interface over root-recorded retirement identities; no wire or public request fields file:Prepare@180 b93f0e2af16b66aee1ff71294e215eeba9e661fe696c917c982d9ebc2dc682c7 |
| -internal/host/claimservingpark_records.go validateServingParkRecord authority C,J4 rev3=PROPOSED-seed; version, immutable tuples, exact serving receipt bytes, source identities, per-file progress, disposition, completion and retention deadline invariants; bounded immutable continuation history and exact predecessor stops a4715a0b91f5573ceaef4af9d2f4492924f7ab079cf11a41f2e99b6b0db2a87d |
| +internal/host/claimservingpark_records.go validateServingParkRecord authority C,J4 rev3=PROPOSED-seed; version, immutable tuples, receipts, source and artifact identities, progress, disposition and retention deadline; additive namespace must be prepared, distinct from event/parent and on the same mount e9223b996c4682efc30032c8b9306d1cf903b26f8749104a98ab07151801fac2 |
| internal/host/claimservingparksource_linux.go servingParkStateDirectory authority C,J4 rev3=PROPOSED-seed; root installation/inventory snapshots and authoritative serving observation; exact account/app/record, revision/enrollment, canonical nested path, ownership and descriptor identity; root acquires these inputs under the host lock through the typed journal service ec4a121330c5c8f27343f21af0bbe458cced885d4754e96c0d87b12d9b4a8f35 |
| internal/hostguard/servingretirement.go Guard.WithServingRetirement authority C,J4 rev3=PROPOSED-seed; fresh serving claim and root installation/fence snapshot under root and shared host locks; refuses pending, quarantined, foreign, retired or already stopped successors; permits an older completed predecessor 3d105c50be7e613777055f561321efadf77fe823d8dc3263a60f2c1211b71108 |
| internal/host/claimservingparkroot_linux.go servingParkRoot.withAuthority authority C,J4 rev3=PROPOSED-seed; live edge peer reauthentication under namespace and host locks before census, inventory and final claim checks; root request entry refuses nil connections d3582f0760b4459721b3e6040cda738b105c1da19cc5d0746d04fea26e313b5c |
| @@ -75,7 +75,7 @@ internal/host/claimservingparkroot_linux.go validateServingRootHistory authority |
| internal/host/claimservingpark_records.go servingParkRecord.activeAuthorization authority C,J4 rev3=PROPOSED-seed; last persisted continuation authorization or immutable original authorization; no source or deadline changes fe168fe52317e45ce70c3e97a9bc2c52d2409328ab4844a8d2a34fc4babba36d |
| internal/host/claimservingpark_records.go servingParkLedger.continueLocked authority C,J4 rev3=PROPOSED-seed; namespace/host-protected slot and record locks; pending original retirement, absent source and retained parked identities before durable bounded continuation 87f297a924f7f153624304233218d8b5548a016fc7dbf0f7cb8dedb789324009 |
| internal/host/claimservingpark_records.go servingParkLedger.replaceLocked authority C,J4 rev3=PROPOSED-seed; completed incumbent record under its record lock; resumes its recorded deletion cause before slot replacement 3e67316e959c9a901c32a2a61fda4e6ea5e92fe51bdb7fdb5f5c53d9d5960aad |
| -@coverage internal/host 5895ba21eb875fa6744e6c3316480aa8bf313c9853b0f85ec32003d6113149b5 |
| +@coverage internal/host f07985c10ad6616354369c033f884b465971caff7a688823fe6bee61af6ed2b4 |
| @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 |
| @@ -124,9 +124,9 @@ internal/host/claimwakestop.go ClaimAssembly.collectCapturedStop authority C,G,J |
| internal/hostbox/restore.go loadPinned authority C,J2,J4 rev3=PROPOSED-seed; exact selected LTX versions and original trailer/pin proof; validated bytes normalized for Litestream while retaining original database checksum for final comparison file:ReadAll@662 1e2b22c7c501d31cd1f2cb3be0fe419fa7e25afde87c6ecb43ca85edb7dbc3a4 |
| internal/host/claimretire_transport.go ClaimRootHTTP.RecoverPending authority C,J4 rev3=PROPOSED-seed; startup serving handover recovery before ordinary cold and root recovery; old destructive serving intents refuse 627b40daecf11e1a6a6c9f92aeb3ebcf226131da6dfa584ed85559c8b9b5ff30 |
| internal/host/claimservingparkroot_linux.go servingParkRoot.withRootInstallation authority C,J4 rev3=PROPOSED-seed; fresh claim, root installation and directory inventory, eligible enrollment, paired tags, census gate and post-input claim reread under namespace then host guard; startup grants no new identity file:Read@2113 8060d3a2628f86257c3600c986242e0d4957ee918214a89655af828dd3ddf93b |
| -internal/host/claimservingpark_linux.go servingParkFS.removeEvent authority C,J4 rev3=PROPOSED-seed; recorded empty event directory identity and absence; journaled removal intent before descriptor-relative rmdir, root parent fsync and durable progress; no recursion file:Readdirnames@789 b493696d2f16425ddc2714184b407acbaf13ef62c93f28c3d4ac8dc7012607c9 |
| -internal/host/claimservingpark_linux.go servingParkFS.removedEvent authority C,J4 rev3=PROPOSED-seed; recorded namespace parent and absence of previously removed event; replacement names refuse completion 792218fc7bfb4ac7475e7c694f72489812db8591493218f8ee1c9545cb813566 |
| -internal/host/claimservingpark_records.go type servingParkRecord authority C,J4 rev3=PROPOSED-seed; private versioned retirement shape; additive empty-event deletion intent and completion bits checked alongside artifact deletion progress bfa0992f8494ebae3850471f2377e5d1e08a0a6bdd825bca6a80458d40a2abbe |
| +internal/host/claimservingpark_linux.go servingParkFS.removeEvent authority C,J4 rev3=PROPOSED-seed; recorded current or displaced namespace and empty event; durable removal intent, descriptor-relative rmdir and fsync; ambiguous old-root absence refuses file:Readdirnames@790 66aa36321bc2bf3245daec35032783bb164cb21fe9b47620fbec71fd4b56cb89 |
| +internal/host/claimservingpark_linux.go servingParkFS.removedEvent authority C,J4 rev3=PROPOSED-seed; absence within the exact recorded current or displaced root; root replacement and ambiguous pre-upgrade absence refuse d4100944fa7e41b83578839a91df3e546fa069f2c2558b9e8216b53fd329306d |
| +internal/host/claimservingpark_records.go type servingParkRecord authority C,J4 rev3=PROPOSED-seed; private versioned retirement shape; additive namespace device/inode/mount alongside event deletion progress; old records remain readable e0e65b59eef4d478bdc0abab79fb4fe2db62047387582485e6c8bcecf5d2d31b |
| internal/host/claimservingpark_recovery.go const servingParkRecoveryLimit authority C,J4 rev3=PROPOSED-seed; fixed root-side processing limit of 128 retirement records per recovery page 6b949581334f55a7bae1b4579e56ac74810fc3042fdf568fbfa8e6b3c5a2ef11 |
| internal/host/claimservingpark_recovery.go var errServingParkRetagPending authority C,J4 rev3=PROPOSED-seed; fixed pending-retag refusal; startup cannot write the independent claim writer tags 8fa87ebef4defae4fc1733528a76e0cc260711c2f5a664308e5368d69286d45b |
| internal/host/claimservingpark_recovery.go type servingParkRecoveryPage authority C,J4 rev3=PROPOSED-seed; private bounded recovery results, cursor and incomplete indicator; never a transport selector c2fb4445ed50b5237b72e8a5fff9b1b2f6c9dd1397473eeb2c78a6b8c2804b27 |
| @@ -170,3 +170,7 @@ internal/host/claimservingquarantine_linux.go servingParkQuarantine.unrecordedNa |
| 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 |
| +internal/host/claimservingdisplaced_linux.go const servingParkRootSearchLimit authority C,J4 rev3=PROPOSED-seed; fixed bound of 128 immediate parent entries; overflow refuses before opening candidate contents or deleting artifacts f7767fc1ea8f855aa6ed5e145e5b576c061c374876b57291f7182ebbdd899595 |
| +internal/host/claimservingdisplaced_linux.go servingParkFS.eventRoot authority C,J4 rev3=PROPOSED-seed; current namespace and recorded retirement root; selects only current or independently recorded prior namespace; ambiguous old absent-event records refuse file:Read@562 7c47b53f88059eea930d483465570fc7205b3cec27a118472071ca4d6df7d5e7 |
| +internal/host/claimservingdisplaced_linux.go servingParkFS.displacedRoot authority C,J4 rev3=PROPOSED-seed; completed private quarantine and prior namespace identity; no-follow bounded parent listing/statx and exact root-owned directory open; same device/inode/mount and safe parent recheck; no unknown child traversal file:Fstat@649,file:Read@144,file:Readdirnames@939,file:Statx@1259 762661ee74aaed30ca1147160a995786ad3a55200f6ffd25106125799ff0c74d |
| +internal/host/claimservingdisplaced_linux.go servingParkFS.recordEventRoot authority C,J4 rev3=PROPOSED-seed; descriptor identity of an already verified event root; persist old-record namespace before first deletion; existing namespace must match exactly b4febb444cd09cd5ed45fc616d172966038d71026024c89dfce9e8c530435756 |