carlos / platform Public

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 54544506b4c63487fa1d0a5406c29cbc9a588734 parent 0f38df2
7 files changed, +329 −4
  • internal/host/plan14_generation_journal_root_linux_test.go +128 −0
  • internal/host/plan14_generation_requirements_root_linux_test.go +15 −0
  • internal/host/rootgeneration_inventory.go +1 −1
  • internal/provision/testdata/plan14-root-reads-e.tsv +6 −3
  • internal/rootstate/authorityfile_linux.go +62 −0
  • internal/rootstate/authorityfile_linux_test.go +110 −0
  • internal/rootstate/authorityfile_other.go +7 −0

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	351.943s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.109s
ok  	github.com/carlosframework/platform/cmd/moveprobe	93.216s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.709s
ok  	github.com/carlosframework/platform/internal/activator	233.381s
ok  	github.com/carlosframework/platform/internal/agentsock	0.014s
ok  	github.com/carlosframework/platform/internal/alerting	0.004s
ok  	github.com/carlosframework/platform/internal/analytics	0.019s
ok  	github.com/carlosframework/platform/internal/appconfig	2.334s
ok  	github.com/carlosframework/platform/internal/apperr	0.072s
ok  	github.com/carlosframework/platform/internal/applog	0.135s
ok  	github.com/carlosframework/platform/internal/awsinv	0.004s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.004s
ok  	github.com/carlosframework/platform/internal/boxid	0.534s
?   	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.011s
ok  	github.com/carlosframework/platform/internal/certwarm	0.002s
ok  	github.com/carlosframework/platform/internal/cgroup	0.706s
ok  	github.com/carlosframework/platform/internal/changelog	0.295s
ok  	github.com/carlosframework/platform/internal/clamd	0.056s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	366.025s
?   	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.602s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.009s
ok  	github.com/carlosframework/platform/internal/dbverify	0.004s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.003s
ok  	github.com/carlosframework/platform/internal/docsite	0.004s
ok  	github.com/carlosframework/platform/internal/domains	2.125s
ok  	github.com/carlosframework/platform/internal/doorkey	0.006s
ok  	github.com/carlosframework/platform/internal/economics	0.004s
ok  	github.com/carlosframework/platform/internal/edge	26.108s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.014s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.126s
ok  	github.com/carlosframework/platform/internal/gate	1.662s
ok  	github.com/carlosframework/platform/internal/genselect	0.014s
ok  	github.com/carlosframework/platform/internal/geo	0.015s
ok  	github.com/carlosframework/platform/internal/geoip	0.030s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.152s
ok  	github.com/carlosframework/platform/internal/host	134.938s
ok  	github.com/carlosframework/platform/internal/hostactors	0.392s
ok  	github.com/carlosframework/platform/internal/hostbox	2.433s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.517s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.015s
ok  	github.com/carlosframework/platform/internal/hostexec	4.892s
ok  	github.com/carlosframework/platform/internal/hostfence	0.039s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.554s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.015s
ok  	github.com/carlosframework/platform/internal/hostguard	0.971s
ok  	github.com/carlosframework/platform/internal/hostops	1.444s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.015s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.015s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.434s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.137s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.015s
ok  	github.com/carlosframework/platform/internal/infraview	0.016s
ok  	github.com/carlosframework/platform/internal/instancectl	1.816s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.726s
ok  	github.com/carlosframework/platform/internal/instancefs	0.019s
ok  	github.com/carlosframework/platform/internal/instancekey	0.016s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.023s
ok  	github.com/carlosframework/platform/internal/layout	1.460s
ok  	github.com/carlosframework/platform/internal/lease	9.439s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.023s
ok  	github.com/carlosframework/platform/internal/ledger	0.010s
ok  	github.com/carlosframework/platform/internal/mail	0.158s
ok  	github.com/carlosframework/platform/internal/maintenance	0.077s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.528s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.072s
ok  	github.com/carlosframework/platform/internal/metrics	0.007s
ok  	github.com/carlosframework/platform/internal/moves	21.086s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.780s
ok  	github.com/carlosframework/platform/internal/objstore	0.163s
ok  	github.com/carlosframework/platform/internal/ops	0.155s
ok  	github.com/carlosframework/platform/internal/park	0.195s
ok  	github.com/carlosframework/platform/internal/privatefile	0.003s
ok  	github.com/carlosframework/platform/internal/provision	13.890s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.010s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	4.468s
ok  	github.com/carlosframework/platform/internal/relay	7.243s
ok  	github.com/carlosframework/platform/internal/release	21.514s
ok  	github.com/carlosframework/platform/internal/rollout	3.801s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.004s
ok  	github.com/carlosframework/platform/internal/rootaws	0.007s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.007s
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.303s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	9.907s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.278s
ok  	github.com/carlosframework/platform/internal/status	0.037s
ok  	github.com/carlosframework/platform/internal/steering	0.006s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.227s
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.003s
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.002s
ok  	github.com/carlosframework/platform/internal/vet	1.552s
=== 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 19:23:48 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE7BCE6C013D95, 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
diff --git a/internal/host/plan14_generation_journal_root_linux_test.go b/internal/host/plan14_generation_journal_root_linux_test.go
new file mode 100644
index 000000000..fe0acdbe4
--- /dev/null
+++ b/internal/host/plan14_generation_journal_root_linux_test.go
@@ -0,0 +1,128 @@
+//go:build linux && rootrunner
+
+package host
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/rootstate"
+ "golang.org/x/sys/unix"
+)
+
+func TestPlan14GenerationPrivateJournalRootRunner(t *testing.T) {
+ dir := generationRootRunnerDir(t)
+ private := filepath.Join(dir, "private")
+ if err := os.Mkdir(private, 0700); err != nil {
+ t.Fatal(err)
+ }
+ path := filepath.Join(private, "journals.db")
+ journal, err := hostbox.OpenLaunchJournal(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { journal.Close() })
+ if _, err := hostbox.NewJournalService(journal); err != nil {
+ t.Fatal(err)
+ }
+ check := func(t *testing.T, refuse bool) {
+ t.Helper()
+ reader, err := openGenerationLaunchInventory(path)
+ if reader != nil {
+ defer reader.Close()
+ }
+ if (err != nil) != refuse {
+ t.Fatalf("private journal reader: refuse=%v error=%v", refuse, err)
+ }
+ if err == nil {
+ if _, err := reader.GenerationPayloads(t.Context()); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := reader.GenerationLaunches(t.Context()); err != nil {
+ t.Fatal(err)
+ }
+ }
+ }
+ if f, err := rootstate.OpenTrustedFile(path); err == nil {
+ f.Close()
+ t.Fatal("public-file opener unexpectedly accepted private ancestry")
+ }
+ check(t, false)
+ for _, suffix := range []string{"", "-wal", "-shm"} {
+ for _, attack := range []string{"writable", "service-owned", "symlink", "hardlink", "fifo", "directory"} {
+ t.Run("journal"+suffix+"/"+attack, func(t *testing.T) {
+ leaf := path + suffix
+ info, err := os.Stat(leaf)
+ if err != nil {
+ t.Fatal(err)
+ }
+ must := func(err error) {
+ t.Helper()
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ switch attack {
+ case "writable":
+ must(os.Chmod(leaf, 0666))
+ defer func() { must(os.Chmod(leaf, info.Mode().Perm())) }()
+ case "service-owned":
+ must(os.Chown(leaf, 65534, 65534))
+ defer func() { must(os.Chown(leaf, 0, 0)) }()
+ case "hardlink":
+ must(os.Link(leaf, leaf+".alias"))
+ defer func() { must(os.Remove(leaf + ".alias")) }()
+ default:
+ must(os.Rename(leaf, leaf+".saved"))
+ defer func() {
+ must(os.Remove(leaf))
+ must(os.Rename(leaf+".saved", leaf))
+ }()
+ switch attack {
+ case "symlink":
+ must(os.Symlink(filepath.Base(leaf)+".saved", leaf))
+ case "fifo":
+ must(unix.Mkfifo(leaf, 0600))
+ case "directory":
+ must(os.Mkdir(leaf, 0700))
+ }
+ }
+ check(t, true)
+ })
+ check(t, false)
+ }
+ }
+ for _, attack := range []string{"writable", "service-owned", "symlink"} {
+ t.Run("ancestor/"+attack, func(t *testing.T) {
+ must := func(err error) {
+ t.Helper()
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ switch attack {
+ case "writable":
+ must(os.Chmod(private, 0777))
+ defer func() { must(os.Chmod(private, 0700)) }()
+ case "service-owned":
+ must(os.Chown(private, 65534, 65534))
+ defer func() { must(os.Chown(private, 0, 0)) }()
+ case "symlink":
+ must(os.Rename(private, private+".saved"))
+ must(os.Symlink(private+".saved", private))
+ defer func() {
+ must(os.Remove(private))
+ must(os.Rename(private+".saved", private))
+ }()
+ }
+ check(t, true)
+ })
+ check(t, false)
+ }
+ if err := journal.Close(); err != nil {
+ t.Fatal(err)
+ }
+ check(t, false)
+}
diff --git a/internal/host/plan14_generation_requirements_root_linux_test.go b/internal/host/plan14_generation_requirements_root_linux_test.go
index f9499b1e8..cac272570 100644
--- a/internal/host/plan14_generation_requirements_root_linux_test.go
+++ b/internal/host/plan14_generation_requirements_root_linux_test.go
@@ -116,6 +116,9 @@ func TestPlan14GenerationRequiredArtifactsRootRunner(t *testing.T) {
}
}
write(rootparticipation.Namespace, "census.json", rootparticipation.Census{Schema: 1, Box: "runner", Enrollment: "en", Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true})
+ if info, err := os.Stat("/var/lib/carlos-claims"); err != nil || info.Mode().Perm() != 0700 {
+ t.Fatalf("private journal directory: %v %v", info, err)
+ }
generationRunnerEmptyRegistry(t, dir, uid, gid)
rec := provision.Record{Schema: 1, Host: "retained.example", Account: "bab", App: "shop", Channel: "stable", CreatedAt: time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)}
store, err := openDeployStore(t.Context())
@@ -220,6 +223,18 @@ func TestPlan14GenerationRequiredArtifactsRootRunner(t *testing.T) {
accept := func() {
t.Helper()
if pins, err := pinsUnderRelease(); err != nil || len(pins) != 0 || !certificate().Complete {
+ snapshot, readErr := generationInventoryAt(t.Context(), rootparticipation.Namespace, "/var/lib/carlos-claims/authority/fences", "/var/lib/carlos-claims/journals.db")
+ t.Logf("root inventory: %v", readErr)
+ if readErr == nil {
+ snapshot, readErr = generationRequiredInventory(t.Context(), store, snapshot)
+ t.Logf("required artifacts: %v", readErr)
+ }
+ if readErr == nil {
+ _, readErr = productionGenerationRegistryInventory(t.Context(), snapshot)
+ t.Logf("registry artifacts: %v", readErr)
+ }
+ _, _, kernelErr := productionGenerationKernelCensus(t.Context())
+ t.Logf("kernel census: %v", kernelErr)
t.Fatalf("production required artifacts: %v %v", pins, err)
}
}
diff --git a/internal/host/rootgeneration_inventory.go b/internal/host/rootgeneration_inventory.go
index 567158219..0aea2774c 100644
--- a/internal/host/rootgeneration_inventory.go
+++ b/internal/host/rootgeneration_inventory.go
@@ -284,7 +284,7 @@ func generationInventoryAt(ctx context.Context, censusPath, fencesPath, journalP
func openGenerationLaunchInventory(path string) (*hostbox.LaunchJournal, error) {
for _, suffix := range []string{"", "-wal", "-shm"} {
- file, err := rootstate.OpenTrustedFile(path + suffix)
+ file, err := rootstate.OpenAuthorityFile(path + suffix)
if suffix != "" && errors.Is(err, os.ErrNotExist) {
continue
}
diff --git a/internal/provision/testdata/plan14-root-reads-e.tsv b/internal/provision/testdata/plan14-root-reads-e.tsv
index a81ea634b..2409213e1 100644
--- a/internal/provision/testdata/plan14-root-reads-e.tsv
+++ b/internal/provision/testdata/plan14-root-reads-e.tsv
@@ -54,9 +54,9 @@ internal/release/roothistory_replace.go replacementReconstruction authority E1 S
internal/release/signedhead.go StageSignedHead authority E1 Canonical route is supplied by the root unit verifier. Verifies signed channel, digest ratchet and manifest/artifact before staging; unavailable pin census never authorizes restaging differing bytes. Fixtures: TestPlan14Incident20261006WithLedger and TestPlan14SignedNamedRecoveryAuthority. registry:Account@202,registry:Account@302,registry:Account@403,registry:App@214,registry:App@415,registry:Channel@222,registry:Host@434 ca02f04e55556ba0233dfa0848b0c533a9d9aa06cad6abd33cfd2968b3bd838a
internal/rootstate/link_linux.go ReadTrustedLink authority A,E1 Checks the full root-owned parent chain before descriptor-based live-link reading. Fixture: TestPlan14TrustedLiveLink; privileged acceptance remains required. file:Open@156 90f8e8de4e2c71f07b5d749cbe4bb1c1dfa426576a82138823449f56fc9ec5ab
internal/rootstate/link_linux.go directory.readLink authority A,E1 Pins the symlink inode with O_PATH and checks owner/type/link count before readlinkat. Fixture: TestPlan14TrustedLiveLink. file:Fstat@356,file:Openat@183,file:Readlinkat@554 a8559fe63dce48830916004ecfe7fbbe5d419568c7daf7a3073cf1b5bb12c3d7
-@coverage internal/host fe2dff7143f2b673db504e54c2a75f141207c60855ead4b13bed85ec533450f7
+@coverage internal/host c438d1079a8cf374015b6a3e1a35db5f5425e3527984ab8f24c084330a1582f8
@coverage internal/release 2bb546a86c1ab49b02e424eb0b366b46776857a1249baa2d6fb61ead4209d6bd
-@coverage internal/rootstate 6338f74a6b2059cf77cf478bd6111b37b441eb9d65a309464c378359d4bdac05
+@coverage internal/rootstate a773d6dbc0e4150e0054f7a94d0e1493c88d0d57f4f4530138b3427e2c217bc5
internal/release/roothistory_status.go RootHistory.MigrationStatus authority E1 Exact completion binds the accepted authorization digest from durable private history to enrolled-root signed readback. Pending originals, changed evidence and canonical sources cannot supply retirement completion. Fixtures: TestPlan14HistoryReconstructionCompletion, TestPlan14HistoryRetirementCompletion, TestPlan14ReadReconstructionCompletion, TestPlan14ReadRetirementCompletion, TestPlan14RecoveryRetirementProductionCallback, TestPlan14RootHistoryReconstructionCrash, TestPlan14HistoryRetirementProcessCrash. file:Read@1062,file:Read@3891 f0ab7ad2d2d70f8c22ebfd575f5a1c6931f7eba30ccdba80ad43d0efffc88154
internal/host/legacyrestart.go restartEffectFailed authority B3 E8 merge ruling 5: optional identity failures preserve the same restart outcome; no second restart; real not-up takes main rollback. Fixtures: TestPlan14ERecoveryRestartOutcome, TestPlan14ERecoveryBindingRefusalKeepsPromotedFix, TestPlan14ERecoveryNotUpPrecedesBinding. b68bb65884a14fe0cf78a65f56ded4ea41584bba99af9195827d1ea191b340a9
internal/host/legacyrestart.go legacyRootRestart authority B3,G E9 merge ruling 6: E effects require a census; no-census recovery delegates to main or records evidence only. Fixtures: TestPlan14ERecoveryNoCensus, TestPlan14ERecoveryNoCensusResume, TestR2ERecoveryFlagWithoutCensus, TestPlan14ERecoveryRestartOutcome. Census-backed recovery retains captured authority and outcome precedence. 5fd48683abd82664baa77c2ddc7528f92f557b04586225d64b4a6e77147df931
@@ -302,7 +302,7 @@ internal/host/rootgeneration_inventory.go generationInstallation authority E2 E2
internal/host/rootgeneration_inventory.go generationInventoryDigest authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel witnesses passed on the E27 root runner. E29 physical-payload and installed-unit witnesses passed on the controller runner; E30 required-artifact witness pending. 36ff5a31c6a013ac907d85e59b754cf4088df4600646ba7df177cf14e990177a
internal/host/rootgeneration_inventory.go generationOwner authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel witnesses passed on the E27 root runner. E29 physical-payload and installed-unit witnesses passed on the controller runner; E30 required-artifact witness pending. 69f89153837761f13116937b330ffbf406891299d5580296ea4aade905aa02ce
internal/host/rootgeneration_inventory.go generationReconciler.reconcile authority E2 E30: derive route and ordinary-instance state-container requirements from schema- and shape-validated canonical deployment records bound to retained installation incarnation; include record contents and ETag in both source reads and the certificate digest. Missing, mismatched, omitted or changed evidence refuses without retiring pins. Fixtures: TestPlan14GenerationRequiredArtifacts, TestPlan14GenerationProductionModeGate. Production fixed-path witness TestPlan14GenerationRequiredArtifactsRootRunner is runner-pending. Timer classes, parked artifacts, positive empty-installation evidence and running-tenant containment remain incomplete. d928288910227cc14f158595e44d052436a23c407e89682dd0930ee1011d7e57
-internal/host/rootgeneration_inventory.go openGenerationLaunchInventory authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel witnesses passed on the E27 root runner. E29 physical-payload and installed-unit witnesses passed on the controller runner; E30 required-artifact witness pending. 522445e6d8acbe083d9d8f001719c4dbd3af57c56429e6b73b05c55a3826cb8b
+internal/host/rootgeneration_inventory.go openGenerationLaunchInventory authority E2 E33: census-only generation journal reads accept private root-owned ancestry without chmod or repair. Every ancestor remains descriptor-checked, root-owned and non-writable by group/other; leaves remain single-link regular root-owned files, checked for DB/WAL/SHM. Public-file callers remain unchanged. Fixtures: TestPlan14AuthorityFilePrivateAncestors, TestPlan14AuthorityFileAbsoluteBoundary; TestPlan14GenerationPrivateJournalRootRunner and TestPlan14GenerationRequiredArtifactsRootRunner are runner-pending. Pre-flight 76, 78, 82 and 89. 2b54531e16282caeb0ddbc1dc92b18d6cb2db652589eb3757daefe10daa63a53
internal/host/rootgeneration_inventory.go productionGenerationInventory authority E2 E32: census-only E2 completeness discovers the bounded union of route and timer hosts, including inactive orphan timers, through the service-user read-only parser. Unknown hosts refuse even with an empty root installation list; discovery supplies no authority and never removes a retained obligation. Pre-flight 33, 80, 82, 83, 89 and 90. Fixtures: TestArtifactHosts, TestRemoteReadOnlyWALRegistry, TestPlan14GenerationRegistryRows, TestPlan14GenerationRegistryProductionReader; extended TestPlan14GenerationRequiredArtifactsRootRunner remains runner-pending. 2e0f4082ea9916f06b9dc50854a571274713cd53cc904da31ae5410eb1ee33f9
internal/host/rootgeneration_inventory.go type generationInventory authority E2 E30: derive route and ordinary-instance state-container requirements from schema- and shape-validated canonical deployment records bound to retained installation incarnation; include record contents and ETag in both source reads and the certificate digest. Missing, mismatched, omitted or changed evidence refuses without retiring pins. Fixtures: TestPlan14GenerationRequiredArtifacts, TestPlan14GenerationProductionModeGate. Production fixed-path witness TestPlan14GenerationRequiredArtifactsRootRunner is runner-pending. Timer classes, parked artifacts, positive empty-installation evidence and running-tenant containment remain incomplete. 4de1dc2a2c9261b1e7bbd811d4d78a271d2fc5a37aeca196177f3e9527d0408e
internal/host/rootgeneration_inventory.go type generationReconciler authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel witnesses passed on the E27 root runner. E29 physical-payload and installed-unit witnesses passed on the controller runner; E30 required-artifact witness pending. a979cdff1cd6311cf57d4a93e4501fb8dc2f65772d0013e622175b2a1f610d91
@@ -322,3 +322,6 @@ internal/host/rootgeneration_rows.go generationRegistryInventory authority E2 E3
internal/registry/remote.go serveRemote authority E2 Mixed declaration: E2 adds only the ArtifactHosts read operation; all existing dispatch and write behavior stays legacy. Original immutable legacy fingerprints retained. E32: census-only E2 completeness discovers the bounded union of route and timer hosts, including inactive orphan timers, through the service-user read-only parser. Unknown hosts refuse even with an empty root installation list; discovery supplies no authority and never removes a retained obligation. Pre-flight 33, 80, 82, 83, 89 and 90. Fixtures: TestArtifactHosts, TestRemoteReadOnlyWALRegistry, TestPlan14GenerationRegistryRows, TestPlan14GenerationRegistryProductionReader; extended TestPlan14GenerationRequiredArtifactsRootRunner remains runner-pending. file:Open@332,registry:CallbackError@1712,registry:CallbackFailed@1669,registry:Deadline@1295,registry:Deadline@1369,registry:Method@1022,registry:Method@1043,registry:Method@1070,registry:Method@1604,whole:remoteRequest@1426,whole:remoteRequest@835,whole:remoteRequest@922,whole:remoteResponse@1148,whole:remoteResponse@1194,whole:remoteResponse@1488,whole:remoteResponse@1786,whole:remoteResponse@1822,whole:remoteResponse@392,whole:remoteResponse@431 40254c291f7b1a89b28e39eb1e4ffb3aa5852fd00de076ced1d3f7c48885e6dd
internal/registry/artifacthosts.go Registry.ArtifactHosts authority E2 E32: census-only E2 completeness discovers the bounded union of route and timer hosts, including inactive orphan timers, through the service-user read-only parser. Unknown hosts refuse even with an empty root installation list; discovery supplies no authority and never removes a retained obligation. Pre-flight 33, 80, 82, 83, 89 and 90. Fixtures: TestArtifactHosts, TestRemoteReadOnlyWALRegistry, TestPlan14GenerationRegistryRows, TestPlan14GenerationRegistryProductionReader; extended TestPlan14GenerationRequiredArtifactsRootRunner remains runner-pending. file:Query@196,registry:Hosts@164,registry:remote@59,registry:remote@91,registry:sql@196,whole:remoteClient@61,whole:remoteClient@93 7310bda43df947e7b2d237dce83291dfe15596802d9db404f6ec0bb457cd6540
internal/registry/remote_dispatch.go Registry.dispatchRemote authority E2 Mixed declaration: E2 adds only the ArtifactHosts read operation; all existing dispatch and write behavior stays legacy. Original immutable legacy fingerprints retained. E32: census-only E2 completeness discovers the bounded union of route and timer hosts, including inactive orphan timers, through the service-user read-only parser. Unknown hosts refuse even with an empty root installation list; discovery supplies no authority and never removes a retained obligation. Pre-flight 33, 80, 82, 83, 89 and 90. Fixtures: TestArtifactHosts, TestRemoteReadOnlyWALRegistry, TestPlan14GenerationRegistryRows, TestPlan14GenerationRegistryProductionReader; extended TestPlan14GenerationRequiredArtifactsRootRunner remains runner-pending. registry:A@6070,registry:Account@3533,registry:Accounts@2616,registry:Addr@4110,registry:Addrs@4118,registry:Alias@6118,registry:Alias@6182,registry:Alias@6270,registry:Aliases@5992,registry:Alive@7812,registry:Args@5694,registry:At@5135,registry:At@6201,registry:At@6279,registry:At@7560,registry:Attempts@7582,registry:B@2001,registry:Backing@3929,registry:BuildState@4336,registry:Changed@1809,registry:Changed@2902,registry:Changed@2989,registry:Changed@3078,registry:Changed@3149,registry:Changed@3231,registry:Changed@7492,registry:Channel@3593,registry:Channel@4576,registry:Channel@4980,registry:CheckedAt@5061,registry:Class@4048,registry:DoneBefore@7821,registry:Due@5464,registry:Due@5528,registry:Due@7604,registry:Env@4644,registry:Env@4891,registry:Env@4991,registry:ExcludeHosts@7287,registry:ExcludeHosts@7382,registry:ExpectGood@5260,registry:ExpectOp@2957,registry:Expected@1063,registry:Expected@289,registry:Expected@752,registry:F@1781,registry:F@1842,registry:Fleet@1867,registry:Fleet@2059,registry:Fleet@2141,registry:Fleet@2222,registry:Fleet@2315,registry:FleetBoxes@2276,registry:Fleets@1925,registry:From@3704,registry:From@3765,registry:FromDB@3840,registry:FromOp@3045,registry:FromSock@3850,registry:Hash@4516,registry:Health@4127,registry:Host@272,registry:Host@2730,registry:Host@2771,registry:Host@2857,registry:Host@2933,registry:Host@3019,registry:Host@3111,registry:Host@3183,registry:Host@3275,registry:Host@3525,registry:Host@3585,registry:Host@3643,registry:Host@3696,registry:Host@3757,registry:Host@3832,registry:Host@3921,registry:Host@3975,registry:Host@4040,registry:Host@4102,registry:Host@4180,registry:Host@4255,registry:Host@4328,registry:Host@4393,registry:Host@4454,registry:Host@4508,registry:Host@4636,registry:Host@4690,registry:Host@4751,registry:Host@4813,registry:Host@5043,registry:Host@5127,registry:Host@5184,registry:Host@5252,registry:Host@5341,registry:Host@5401,registry:Host@5456,registry:Host@5520,registry:Host@5635,registry:Host@5686,registry:Host@5753,registry:Host@5828,registry:Host@5891,registry:Host@6488,registry:Host@6635,registry:Host@6923,registry:Host@6987,registry:Host@7046,registry:Host@7129,registry:Host@7212,registry:Host@7443,registry:Host@7528,registry:Host@7660,registry:Host@7741,registry:Host@7904,registry:Host@837,registry:Host@948,registry:Hosts@187,registry:Hosts@4567,registry:Hosts@4882,registry:Hosts@4971,registry:Id@1057,registry:Id@1368,registry:Id@1467,registry:Id@1547,registry:Id@1698,registry:IdValue@2435,registry:Idle@5761,registry:Journal@1075,registry:Journal@1374,registry:Journal@1553,registry:Journal@442,registry:Keep@7920,registry:Kind@7749,registry:Kind@7912,registry:Label@2068,registry:Label@2150,registry:Label@2231,registry:Label@4759,registry:Limit@7278,registry:Limit@7373,registry:MaxRun@5769,registry:Method@151,registry:Ms@7576,registry:Name@1895,registry:Name@2454,registry:Name@6995,registry:Name@7054,registry:Name@7137,registry:Name@7451,registry:Name@7536,registry:Name@7668,registry:Name@845,registry:Name@956,registry:Next@301,registry:NextAt@7594,registry:NextGood@5274,registry:NextValue@1627,registry:NextValue@1725,registry:Now@5962,registry:Now@7271,registry:Now@7366,registry:Now@7459,registry:Now@980,registry:Number64@2542,registry:Number@7709,registry:Number@7780,registry:Number@7865,registry:Observed@365,registry:ObservedValue@643,registry:Old@1620,registry:Old@1718,registry:On@5409,registry:On@5836,registry:On@5899,registry:Op@2875,registry:Op@2951,registry:Op@3119,registry:Op@3191,registry:Op@3283,registry:Owner@280,registry:Owner@377,registry:Owner@660,registry:P@6589,registry:Path@6820,registry:Paths@6517,registry:Prefix@6643,registry:Prev@5199,registry:Registration@7145,registry:Registration@7544,registry:Registration@853,registry:Registration@964,registry:Replica@4462,registry:Replicas@4401,registry:Retirement@1704,registry:Route@2707,registry:Routes@2665,registry:Routes@3309,registry:Routes@5562,registry:Routes@5929,registry:Rows@6931,registry:Seed@5643,registry:Sqid@2446,registry:Sqid@2509,registry:Sqid@2588,registry:Status@2240,registry:Status@2865,registry:Status@2941,registry:Status@3027,registry:Status@3651,registry:Status@5051,registry:Status@6191,registry:Status@7566,registry:Status@7676,registry:Status@869,registry:Steering@4188,registry:StringMap@2352,registry:T@6871,registry:T@747,registry:TValue@2812,registry:Tags@2159,registry:Target@4821,registry:Targets@453,registry:Text@1273,registry:Text@1420,registry:Text@2556,registry:Text@6676,registry:Text@6739,registry:Timer@6959,registry:Timer@704,registry:Timers@7182,registry:Timers@7241,registry:Timers@7330,registry:Times@7954,registry:To@3712,registry:To@3773,registry:ToDB@3862,registry:ToOp@3037,registry:ToSock@3870,registry:TokenHash@2077,registry:Tunnel@4698,registry:Unit@3983,registry:UpdateSettings@4263,registry:Ver@5192,registry:Ver@5349,registry:W@6422,registry:Wildcards@6316,whole:Account@2620,whole:ClaimOwnership@282,whole:ClaimOwnership@379,whole:ClaimOwnership@662,whole:ClaimRow@455,whole:ClaimRowArchive@561,whole:ClaimRowArchive@571,whole:ColdRows@1622,whole:ColdRows@1629,whole:ColdRows@1720,whole:ColdRows@1727,whole:Fleet@1783,whole:Fleet@1844,whole:Fleet@1871,whole:Fleet@1929,whole:FleetBox@2003,whole:FleetBox@2280,whole:GoodBuild@5262,whole:GoodBuild@5276,whole:Route@2669,whole:Route@2711,whole:Route@2814,whole:Route@3313,whole:Route@3468,whole:Route@3474,whole:Route@367,whole:Route@5566,whole:Route@5933,whole:RouteAlias@5996,whole:RouteAlias@6072,whole:RoutePath@6521,whole:RoutePath@6591,whole:Timer@645,whole:Timer@6873,whole:Timer@6933,whole:Timer@6963,whole:Timer@708,whole:Timer@7186,whole:Timer@7245,whole:Timer@7334,whole:Timer@749,whole:WildcardDomain@6320,whole:WildcardDomain@6424 df83a3282714a65c50c067415dfe51c0be4a7e8e384b6b0041875c365cc7f264
+internal/rootstate/authorityfile_linux.go OpenAuthorityFile authority E2 E33: census-only generation journal reads accept private root-owned ancestry without chmod or repair. Every ancestor remains descriptor-checked, root-owned and non-writable by group/other; leaves remain single-link regular root-owned files, checked for DB/WAL/SHM. Public-file callers remain unchanged. Fixtures: TestPlan14AuthorityFilePrivateAncestors, TestPlan14AuthorityFileAbsoluteBoundary; TestPlan14GenerationPrivateJournalRootRunner and TestPlan14GenerationRequiredArtifactsRootRunner are runner-pending. Pre-flight 76, 78, 82 and 89. file:Open@181 39a50bbf5bc21a32c76647548c19af937055158ec583b1620a21645f9391500c
+internal/rootstate/authorityfile_linux.go openAuthorityFile authority E2 E33: census-only generation journal reads accept private root-owned ancestry without chmod or repair. Every ancestor remains descriptor-checked, root-owned and non-writable by group/other; leaves remain single-link regular root-owned files, checked for DB/WAL/SHM. Public-file callers remain unchanged. Fixtures: TestPlan14AuthorityFilePrivateAncestors, TestPlan14AuthorityFileAbsoluteBoundary; TestPlan14GenerationPrivateJournalRootRunner and TestPlan14GenerationRequiredArtifactsRootRunner are runner-pending. Pre-flight 76, 78, 82 and 89. file:Openat@505 c7a9d1950ab15676b18afb750939c3e7e7204d4396be07959a61c97836720f99
+internal/rootstate/authorityfile_other.go OpenAuthorityFile authority E2 E33: census-only generation journal reads accept private root-owned ancestry without chmod or repair. Every ancestor remains descriptor-checked, root-owned and non-writable by group/other; leaves remain single-link regular root-owned files, checked for DB/WAL/SHM. Public-file callers remain unchanged. Fixtures: TestPlan14AuthorityFilePrivateAncestors, TestPlan14AuthorityFileAbsoluteBoundary; TestPlan14GenerationPrivateJournalRootRunner and TestPlan14GenerationRequiredArtifactsRootRunner are runner-pending. Pre-flight 76, 78, 82 and 89. a22c1b26fc03197a6cb312ac5c92bb1038d8fde1df00f444f232a0cd7a54878c
diff --git a/internal/rootstate/authorityfile_linux.go b/internal/rootstate/authorityfile_linux.go
new file mode 100644
index 000000000..d800f2a85
--- /dev/null
+++ b/internal/rootstate/authorityfile_linux.go
@@ -0,0 +1,62 @@
+package rootstate
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/carlosframework/platform/internal/safefile"
+ "golang.org/x/sys/unix"
+)
+
+func OpenAuthorityFile(path string) (*os.File, error) {
+ if !filepath.IsAbs(path) || filepath.Clean(path) != path || path == "/" {
+ return nil, os.ErrPermission
+ }
+ parent, err := os.Open("/")
+ if err != nil {
+ return nil, err
+ }
+ defer parent.Close()
+ return openAuthorityFile(parent, strings.Split(strings.TrimPrefix(path, "/"), "/"), 0)
+}
+
+func openAuthorityFile(parent *os.File, parts []string, owner uint32) (*os.File, error) {
+ if len(parts) == 0 {
+ return nil, os.ErrPermission
+ }
+ fd, err := unix.FcntlInt(parent.Fd(), unix.F_DUPFD_CLOEXEC, 0)
+ if err != nil {
+ return nil, err
+ }
+ defer func() { unix.Close(fd) }()
+ if err := trusted(fd, owner, unix.S_IFDIR); err != nil {
+ return nil, err
+ }
+ for i, name := range parts {
+ if !component(name) {
+ return nil, os.ErrPermission
+ }
+ if i == len(parts)-1 {
+ break
+ }
+ next, err := unix.Openat(fd, name, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
+ if err != nil {
+ return nil, err
+ }
+ unix.Close(fd)
+ fd = next
+ if err := trusted(fd, owner, unix.S_IFDIR); err != nil {
+ return nil, err
+ }
+ }
+ f, err := safefile.OpenFileAt(fd, parts[len(parts)-1], os.O_RDONLY, 0)
+ if err != nil {
+ return nil, err
+ }
+ if err := trusted(int(f.Fd()), owner, unix.S_IFREG); err != nil {
+ f.Close()
+ return nil, err
+ }
+ return f, nil
+}
diff --git a/internal/rootstate/authorityfile_linux_test.go b/internal/rootstate/authorityfile_linux_test.go
new file mode 100644
index 000000000..8a8d0222c
--- /dev/null
+++ b/internal/rootstate/authorityfile_linux_test.go
@@ -0,0 +1,110 @@
+package rootstate
+
+import (
+ "io"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "golang.org/x/sys/unix"
+)
+
+func TestPlan14AuthorityFilePrivateAncestors(t *testing.T) {
+ for _, attack := range []string{"none", "public-leaf-parent", "writable-anchor", "writable-ancestor", "writable-leaf", "ancestor-link", "leaf-link", "hardlink", "fifo", "directory", "wrong-owner", "missing", "relative-component"} {
+ t.Run(attack, func(t *testing.T) {
+ root := t.TempDir()
+ if err := os.Chmod(root, 0700); err != nil {
+ t.Fatal(err)
+ }
+ path := filepath.Join(root, "private", "nested", "journal")
+ if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(path, []byte("retained"), 0600); err != nil {
+ t.Fatal(err)
+ }
+ must := func(err error) {
+ t.Helper()
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ owner := uint32(os.Geteuid())
+ parts := []string{"private", "nested", "journal"}
+ switch attack {
+ case "public-leaf-parent":
+ must(os.Chmod(filepath.Dir(path), 0755))
+ case "writable-anchor":
+ must(os.Chmod(root, 0777))
+ case "writable-ancestor":
+ must(os.Chmod(filepath.Join(root, "private"), 0777))
+ case "writable-leaf":
+ must(os.Chmod(path, 0666))
+ case "ancestor-link":
+ must(os.Rename(filepath.Join(root, "private"), filepath.Join(root, "saved")))
+ must(os.Symlink("saved", filepath.Join(root, "private")))
+ case "leaf-link":
+ must(os.Rename(path, path+".saved"))
+ must(os.Symlink("journal.saved", path))
+ case "hardlink":
+ must(os.Link(path, path+".alias"))
+ case "fifo":
+ must(os.Remove(path))
+ must(unix.Mkfifo(path, 0600))
+ case "directory":
+ must(os.Remove(path))
+ must(os.Mkdir(path, 0700))
+ case "wrong-owner":
+ owner++
+ case "missing":
+ must(os.Remove(path))
+ case "relative-component":
+ parts = []string{"private", "nested", "..", "nested", "journal"}
+ }
+ parent, err := os.Open(root)
+ must(err)
+ defer parent.Close()
+ f, err := openAuthorityFile(parent, parts, owner)
+ if attack != "none" && attack != "public-leaf-parent" {
+ if err == nil {
+ f.Close()
+ t.Fatal("hostile authority file accepted")
+ }
+ return
+ }
+ if err != nil {
+ t.Fatalf("private authority file refused: %v", err)
+ }
+ defer f.Close()
+ raw, err := io.ReadAll(f)
+ if err != nil || string(raw) != "retained" {
+ t.Fatalf("retained bytes: %q %v", raw, err)
+ }
+ })
+ }
+}
+
+func TestPlan14AuthorityFileAbsoluteBoundary(t *testing.T) {
+ for _, path := range []string{"relative", "/etc/../etc/passwd", "/", filepath.Join(t.TempDir(), "missing")} {
+ if f, err := OpenAuthorityFile(path); err == nil {
+ f.Close()
+ t.Fatalf("untrusted path accepted: %q", path)
+ }
+ }
+ t.Run("public-root-owned-file", func(t *testing.T) {
+ for _, path := range []string{"/", "/usr", "/usr/bin", "/usr/bin/true"} {
+ var st unix.Stat_t
+ if err := unix.Lstat(path, &st); err != nil {
+ t.Fatal(err)
+ }
+ if st.Uid != 0 {
+ t.Skipf("root-owned fixture unavailable: %s uid=%d", path, st.Uid)
+ }
+ }
+ f, err := OpenAuthorityFile("/usr/bin/true")
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.Close()
+ })
+}
diff --git a/internal/rootstate/authorityfile_other.go b/internal/rootstate/authorityfile_other.go
new file mode 100644
index 000000000..67e3d1c6e
--- /dev/null
+++ b/internal/rootstate/authorityfile_other.go
@@ -0,0 +1,7 @@
+//go:build !linux
+
+package rootstate
+
+import "os"
+
+func OpenAuthorityFile(string) (*os.File, error) { return nil, os.ErrPermission }