Plan 14 integrated: seven more production defects from the extended E2E (not for merge)
Populated-box creation census; private bind-source classification; agent access to root-private retirement state; namespace and mount reuse; persistent identity no longer uses recyclable mount numbers; edge supervisor classification; cross-namespace pin comparison in both generation consumers. Active-box host creation and both isolated seed reboot pairs pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
32 files changed,
+940
−59
internal/host/claimparticipation.go+3 −0internal/host/claimservingpark_recovery_linux.go+11 −1internal/host/claimservingpark_rootrunner_linux_test.go+1 −1internal/host/claimservingreboot_binding.go+1 −1internal/host/claimservingreboot_commit_linux.go+1 −1internal/host/claimservingreboot_linux.go+8 −6internal/host/claimservingreboot_prepare_linux.go+10 −1internal/host/claimservingtopology_linux.go+10 −1internal/host/plan14_lifecycle_diagnostic_root_linux_test.go+51 −0internal/host/plan14_lifecycle_retire_root_linux_test.go+31 −3internal/host/plan14_lifecycle_root_linux_test.go+100 −17internal/host/plan14_lifecycle_trace_root_linux_test.go+37 −1internal/host/plan14_namespace_transition_linux_test.go+120 −0internal/host/plan14_r11_handover_test.go+23 −0internal/host/plan14_remote_cleanup_test.go+35 −0internal/host/plan14_service_supervisor_linux_test.go+27 −0internal/host/plan14_sibling_census_linux_test.go+98 −0internal/host/rootexecution_authority.go+2 −2internal/host/rootexecution_settle.go+1 −1internal/host/rootgeneration_kernel_linux.go+7 −2internal/host/rootgeneration_services_linux.go+20 −2internal/host/rootinventory.go+3 −2internal/host/rootinventory_sibling.go+110 −0internal/hostbox/empty_census.go+13 −0internal/provision/plan14_p3_census_test.go+3 −2internal/provision/testdata/plan14-root-reads-p3.tsv+27 −13internal/release/plan14_generation_boot_test.go+38 −0internal/release/plan14_generation_namespace_test.go+47 −0internal/release/rootgenerationpins.go+1 −1internal/release/rootgenerationproof.go+60 −0internal/rootstate/namespace_recreation_test.go+40 −0internal/rootstate/state.go+1 −1
CI failed — run details
CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok amadan.net/carlos/platform.git/launchctx 0.012s
? amadan.net/carlos/platform.git/launchctx/controlname [no test files]
? amadan.net/carlos/platform.git/launchctx/wireid [no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok github.com/carlosframework/platform 311.287s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 2.802s
ok github.com/carlosframework/platform/cmd/moveprobe 92.179s
ok github.com/carlosframework/platform/cmd/zddprobe 0.671s
ok github.com/carlosframework/platform/internal/activator 251.354s
ok github.com/carlosframework/platform/internal/agentsock 0.014s
ok github.com/carlosframework/platform/internal/alerting 0.007s
ok github.com/carlosframework/platform/internal/analytics 0.021s
ok github.com/carlosframework/platform/internal/appconfig 2.485s
ok github.com/carlosframework/platform/internal/apperr 0.078s
ok github.com/carlosframework/platform/internal/applog 0.153s
ok github.com/carlosframework/platform/internal/awsinv 0.003s
ok github.com/carlosframework/platform/internal/boxchoice 0.005s
ok github.com/carlosframework/platform/internal/boxid 1.330s
? 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.008s
ok github.com/carlosframework/platform/internal/caps 0.012s
ok github.com/carlosframework/platform/internal/certwarm 0.002s
ok github.com/carlosframework/platform/internal/cgroup 0.721s
ok github.com/carlosframework/platform/internal/changelog 0.336s
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 408.743s
? github.com/carlosframework/platform/internal/controlname [no test files]
? github.com/carlosframework/platform/internal/controlsock [no test files]
ok github.com/carlosframework/platform/internal/dbdoor 6.202s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.017s
ok github.com/carlosframework/platform/internal/dbverify 0.011s
ok github.com/carlosframework/platform/internal/dnsrec 0.005s
ok github.com/carlosframework/platform/internal/docsite 0.013s
ok github.com/carlosframework/platform/internal/domains 2.398s
ok github.com/carlosframework/platform/internal/doorkey 0.012s
ok github.com/carlosframework/platform/internal/economics 0.008s
ok github.com/carlosframework/platform/internal/edge 45.007s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.016s
ok github.com/carlosframework/platform/internal/fleetapi 0.149s
ok github.com/carlosframework/platform/internal/gate 1.673s
ok github.com/carlosframework/platform/internal/genselect 0.029s
ok github.com/carlosframework/platform/internal/geo 0.022s
ok github.com/carlosframework/platform/internal/geoip 0.043s
ok github.com/carlosframework/platform/internal/historyreadback 0.174s
ok github.com/carlosframework/platform/internal/host 182.725s
ok github.com/carlosframework/platform/internal/hostactors 0.841s
ok github.com/carlosframework/platform/internal/hostbox 2.703s
ok github.com/carlosframework/platform/internal/hostclaim 0.578s
ok github.com/carlosframework/platform/internal/hostevidence 0.013s
ok github.com/carlosframework/platform/internal/hostexec 4.991s
ok github.com/carlosframework/platform/internal/hostfence 0.038s
ok github.com/carlosframework/platform/internal/hostfiles 10.482s
ok github.com/carlosframework/platform/internal/hostfloor 0.013s
ok github.com/carlosframework/platform/internal/hostguard 1.031s
ok github.com/carlosframework/platform/internal/hostops 1.633s
? 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.013s
ok github.com/carlosframework/platform/internal/hostrelocate 0.467s
ok github.com/carlosframework/platform/internal/hostresolve 0.157s
ok github.com/carlosframework/platform/internal/hostwrite 0.013s
ok github.com/carlosframework/platform/internal/infraview 0.014s
ok github.com/carlosframework/platform/internal/instancectl 5.266s
ok github.com/carlosframework/platform/internal/instanceexec 2.730s
ok github.com/carlosframework/platform/internal/instancefs 0.018s
ok github.com/carlosframework/platform/internal/instancekey 0.018s
ok github.com/carlosframework/platform/internal/instanceupdates 0.029s
2026/10/17 05:28:48 activator: h.example.com hibernated
--- FAIL: TestRawInstancesDirectoryIsNamedOnlyHere (0.34s)
rawdir_test.go:58: ../../internal/host/rootinventory_sibling.go:23 names the raw .instances directory; only internal/layout (and bootstrap's tmpfiles lines) may
FAIL
FAIL github.com/carlosframework/platform/internal/layout 1.567s
ok github.com/carlosframework/platform/internal/lbhealth 0.017s
ok github.com/carlosframework/platform/internal/lease 9.866s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.005s
ok github.com/carlosframework/platform/internal/ledger 0.020s
ok github.com/carlosframework/platform/internal/mail 0.169s
ok github.com/carlosframework/platform/internal/maintenance 0.078s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.539s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.078s
ok github.com/carlosframework/platform/internal/metrics 0.005s
ok github.com/carlosframework/platform/internal/moves 21.188s
ok github.com/carlosframework/platform/internal/moves/archive 0.811s
ok github.com/carlosframework/platform/internal/objstore 0.193s
ok github.com/carlosframework/platform/internal/ops 0.174s
ok github.com/carlosframework/platform/internal/park 0.204s
ok github.com/carlosframework/platform/internal/privatefile 0.004s
ok github.com/carlosframework/platform/internal/provision 36.624s
ok github.com/carlosframework/platform/internal/publicrelease 0.006s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 5.469s
ok github.com/carlosframework/platform/internal/relay 7.266s
ok github.com/carlosframework/platform/internal/release 22.637s
ok github.com/carlosframework/platform/internal/rollout 3.825s
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.058s
ok github.com/carlosframework/platform/internal/rootstate 0.027s
ok github.com/carlosframework/platform/internal/runs 0.565s
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.511s
ok github.com/carlosframework/platform/internal/seeds 0.012s
ok github.com/carlosframework/platform/internal/sim/hostlife 9.931s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 3.521s
ok github.com/carlosframework/platform/internal/status 0.041s
ok github.com/carlosframework/platform/internal/steering 0.006s
ok github.com/carlosframework/platform/internal/stopevidence 0.250s
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.561s
FAIL
make: *** [Makefile:18: test] Error 1
amadan: step gate failed: exit status 2
amadan: job failed at step gate — its output is above, not at the tail of this log
diff --git a/internal/host/claimparticipation.go b/internal/host/claimparticipation.go| index 6b25cc45f..78492f29e 100644 |
| --- a/internal/host/claimparticipation.go |
| +++ b/internal/host/claimparticipation.go |
| @@ -262,6 +262,9 @@ func (p *productionClaimLocal) cleanup(ctx context.Context, o hostclaim.Observed |
| if o.Claim == nil || o.Claim.Holder == nil { |
| return ErrClaimStopInventory |
| } |
| + if !p.legacyParticipation && a.Launches.Remote() && a.Reconcile.Inventory.Remote() { |
| + return ctx.Err() |
| + } |
| return a.Reconcile.Census.CheckLocal(ctx, o, hostbox.RecordSelection{Host: o.Host, Incarnation: o.Claim.Holder.Incarnation}) |
| } |
| func (p *productionClaimLocal) checkLegacy(ctx context.Context, a hostbox.Actor, c *hostclaim.Claim, s hostbox.Selection) error { |
diff --git a/internal/host/claimservingpark_recovery_linux.go b/internal/host/claimservingpark_recovery_linux.go| index ead2b1d13..7c494ec44 100644 |
| --- a/internal/host/claimservingpark_recovery_linux.go |
| +++ b/internal/host/claimservingpark_recovery_linux.go |
| @@ -81,7 +81,17 @@ func (p *servingParkRoot) recoverServingParks(ctx context.Context, after string, |
| if readErr == nil { |
| result.Host = r.Authorization.Successor.Host |
| fs, ok := p.ledger.fs.(*servingParkFS) |
| - if ok && r.Source.BootID != "" && fs.checkBoot(r.Source.BootID) != nil { |
| + parentChanged := false |
| + if ok { |
| + parent, id, e := fs.parent() |
| + if e != nil { |
| + result.Err = e |
| + } else { |
| + parent.Close() |
| + parentChanged = id != r.Source.Parent |
| + } |
| + } |
| + if ok && result.Err == nil && r.Source.BootID != "" && (fs.checkBoot(r.Source.BootID) != nil || parentChanged) { |
| result.Err = p.commitServingReboot(ctx, id) |
| if result.Err == nil { |
| r, result.Err = p.ledger.read(id) |
diff --git a/internal/host/claimservingpark_rootrunner_linux_test.go b/internal/host/claimservingpark_rootrunner_linux_test.go| index af44785bf..2185c6b30 100644 |
| --- a/internal/host/claimservingpark_rootrunner_linux_test.go |
| +++ b/internal/host/claimservingpark_rootrunner_linux_test.go |
| @@ -716,7 +716,7 @@ func servingRunnerPrepareReboot(t *testing.T, fs *servingParkFS, r servingParkRe |
| if err := prepare(); err != nil { |
| t.Fatal("durable reboot preparation", err) |
| } |
| - name := servingRebootPreparationName(a.ID, observe.Topology.BootID) |
| + name := servingNamespacePreparationName(a.ID, observe.Topology) |
| before, err := fs.state.Read(name) |
| if err != nil { |
| t.Fatal(err) |
diff --git a/internal/host/claimservingreboot_binding.go b/internal/host/claimservingreboot_binding.go| index 48d739e54..d1e46ca35 100644 |
| --- a/internal/host/claimservingreboot_binding.go |
| +++ b/internal/host/claimservingreboot_binding.go |
| @@ -26,7 +26,7 @@ func servingParkReadBinding(state rootstate.Store, id string) (*servingParkBootB |
| return nil, err |
| } |
| var b servingParkBootBinding |
| - if servingParkDecode(body, &b) != nil || b.Version != 1 || b.ID != id || !validServingParkBootID(b.BeforeBoot) || !validServingParkBootID(b.BootID) || b.BeforeBoot == b.BootID || b.BeforeParent.Device == 0 || b.BeforeParent.Inode == 0 || b.BeforeParent.Mount == 0 || b.Parent.Device == 0 || b.Parent.Inode != b.BeforeParent.Inode || b.Parent.Mount == 0 { |
| + if servingParkDecode(body, &b) != nil || b.Version != 1 || b.ID != id || !validServingParkBootID(b.BeforeBoot) || !validServingParkBootID(b.BootID) || (b.BeforeBoot == b.BootID && b.BeforeParent.Device != b.Parent.Device) || b.BeforeParent.Device == 0 || b.BeforeParent.Inode == 0 || b.BeforeParent.Mount == 0 || b.Parent.Device == 0 || b.Parent.Inode != b.BeforeParent.Inode || b.Parent.Mount == 0 { |
| return nil, hostbox.ErrChanged |
| } |
| return &b, nil |
diff --git a/internal/host/claimservingreboot_commit_linux.go b/internal/host/claimservingreboot_commit_linux.go| index 5389540f6..34ad5d379 100644 |
| --- a/internal/host/claimservingreboot_commit_linux.go |
| +++ b/internal/host/claimservingreboot_commit_linux.go |
| @@ -22,7 +22,7 @@ func (l *servingParkLedger) commitRebootLocked(ctx context.Context, saved servin |
| if err != nil { |
| return err |
| } |
| - body, err := l.state.Read(servingRebootPreparationName(saved.Authorization.ID, fresh.Observation.Topology.BootID)) |
| + body, err := l.state.Read(servingNamespacePreparationName(saved.Authorization.ID, fresh.Observation.Topology)) |
| if err != nil { |
| return err |
| } |
diff --git a/internal/host/claimservingreboot_linux.go b/internal/host/claimservingreboot_linux.go| index 3493c9f80..85c832a2c 100644 |
| --- a/internal/host/claimservingreboot_linux.go |
| +++ b/internal/host/claimservingreboot_linux.go |
| @@ -56,16 +56,18 @@ func (f *servingParkFS) observeRebootRecord(ctx context.Context, id string) (ser |
| n := topology.Previous |
| // A later retirement binds its parent to namespace history with root-private |
| // persistent evidence. Device/mount numbers constrain only the same boot. |
| - if r.Source.BootID == n.BootID { |
| - if r.Source.Parent != n.Parent { |
| - return out, hostbox.ErrChanged |
| - } |
| - } else if !r.Source.ParentPersistent.valid() || r.Source.ParentPersistent != n.Topology.Parent { |
| + if (r.Source.BootID != n.BootID || r.Source.Parent != n.Parent) && (!r.Source.ParentPersistent.valid() || r.Source.ParentPersistent != n.Topology.Parent) { |
| return out, hostbox.ErrChanged |
| } |
| - if !validServingParkBootID(r.Source.BootID) || r.Source.BootID == topology.BootID || !r.Source.Persistent.valid() || r.Source.Parent.Inode != n.Parent.Inode || r.Source.Persistent.Filesystem != n.Topology.Parent.Filesystem || r.Prepared && (r.Namespace.Inode != n.Root.Inode || r.Namespace.Mount != r.Source.Parent.Mount) { |
| + if !validServingParkBootID(r.Source.BootID) || (r.Source.BootID == topology.BootID && r.Source.Parent.Device != topology.CurrentParent.Device) || !r.Source.Persistent.valid() || r.Source.Parent.Inode != n.Parent.Inode || r.Source.Persistent.Filesystem != n.Topology.Parent.Filesystem || r.Prepared && (r.Namespace.Inode != n.Root.Inode || r.Namespace.Mount != r.Source.Parent.Mount) { |
| return out, hostbox.ErrChanged |
| } |
| + if r.Source.BootID == topology.BootID && r.Source.Parent.Mount == topology.CurrentParent.Mount { |
| + prior, err := servingParkReadBinding(f.state, "retirement-"+id) |
| + if err != nil || prior == nil || prior.BeforeBoot != r.Source.BootID || prior.BeforeParent != r.Source.Parent { |
| + return out, hostbox.ErrChanged |
| + } |
| + } |
| parent, pid, err := f.parent() |
| if err != nil { |
| return out, err |
diff --git a/internal/host/claimservingreboot_prepare_linux.go b/internal/host/claimservingreboot_prepare_linux.go| index 14c1015e2..8c11fe253 100644 |
| --- a/internal/host/claimservingreboot_prepare_linux.go |
| +++ b/internal/host/claimservingreboot_prepare_linux.go |
| @@ -7,6 +7,7 @@ import ( |
| "errors" |
| "os" |
| "reflect" |
| + "strconv" |
| "github.com/carlosframework/platform/internal/hostbox" |
| "github.com/carlosframework/platform/internal/hostclaim" |
| @@ -128,7 +129,7 @@ func (l *servingParkLedger) prepareRebootLocked(ctx context.Context, saved servi |
| if err != nil { |
| return err |
| } |
| - name := servingRebootPreparationName(a.ID, first.Observation.Topology.BootID) |
| + name := servingNamespacePreparationName(a.ID, first.Observation.Topology) |
| body, err := l.state.Read(name) |
| if err == nil { |
| var prior servingParkRebootPreparation |
| @@ -221,3 +222,11 @@ func (p *servingParkRoot) validateServingRebootAuthority(ctx context.Context, o |
| func servingRebootPreparationName(id, boot string) string { |
| return "reboot-" + id + "-" + boot + ".json" |
| } |
| + |
| +func servingNamespacePreparationName(id string, t servingParkRebootObservation) string { |
| + boot := t.BootID |
| + if t.CurrentParent.Mount != t.Previous.Parent.Mount { |
| + boot += "-" + strconv.FormatUint(t.CurrentParent.Mount, 10) |
| + } |
| + return servingRebootPreparationName(id, boot) |
| +} |
diff --git a/internal/host/claimservingtopology_linux.go b/internal/host/claimservingtopology_linux.go| index 320159d56..309887442 100644 |
| --- a/internal/host/claimservingtopology_linux.go |
| +++ b/internal/host/claimservingtopology_linux.go |
| @@ -123,7 +123,7 @@ func (f *servingParkFS) inspectRebootTopology(ctx context.Context) (servingParkR |
| if err != nil { |
| return zero, err |
| } |
| - if servingParkDecode(body, &n) != nil || n.Version != 1 || !validServingParkBootID(n.BootID) || n.BootID == boot || n.BootID != p.BootID || n.Parent != p.Parent || n.Topology == nil || !n.Topology.Parent.valid() || !n.Topology.Root.valid() || n.Root.Device != n.Parent.Device || n.Root.Mount != n.Parent.Mount || n.Root.Inode == 0 { |
| + if servingParkDecode(body, &n) != nil || n.Version != 1 || !validServingParkBootID(n.BootID) || n.BootID != p.BootID || n.Parent != p.Parent || n.Topology == nil || !n.Topology.Parent.valid() || !n.Topology.Root.valid() || n.Root.Device != n.Parent.Device || n.Root.Mount != n.Parent.Mount || n.Root.Inode == 0 { |
| return zero, hostbox.ErrChanged |
| } |
| if _, err := f.state.Read("namespace-quarantine.json"); !errors.Is(err, os.ErrNotExist) { |
| @@ -145,6 +145,15 @@ func (f *servingParkFS) inspectRebootTopology(ctx context.Context) (servingParkR |
| // Device and mount numbers are boot-local. Across boots, require the same |
| // filesystem and persistent handles below, plus unchanged inode numbers. |
| // Both directories must still share the current device and mount. |
| + if n.BootID == boot && pid.Device != n.Parent.Device { |
| + return zero, hostbox.ErrChanged |
| + } |
| + if n.BootID == boot && pid.Mount == n.Parent.Mount { |
| + prior, err := servingParkReadBinding(f.state, "namespace") |
| + if err != nil || prior == nil || prior.BeforeBoot != n.BootID || prior.BeforeParent != n.Parent { |
| + return zero, hostbox.ErrChanged |
| + } |
| + } |
| if pid.Inode != n.Parent.Inode || rid.Device != pid.Device || rid.Inode != n.Root.Inode || rid.Mount != pid.Mount { |
| return zero, hostbox.ErrChanged |
| } |
diff --git a/internal/host/plan14_lifecycle_diagnostic_root_linux_test.go b/internal/host/plan14_lifecycle_diagnostic_root_linux_test.go| index a05ac00aa..763c2a967 100644 |
| --- a/internal/host/plan14_lifecycle_diagnostic_root_linux_test.go |
| +++ b/internal/host/plan14_lifecycle_diagnostic_root_linux_test.go |
| @@ -4,6 +4,8 @@ package host |
| import ( |
| "context" |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostguard" |
| "github.com/carlosframework/platform/internal/release" |
| "github.com/carlosframework/platform/internal/rootparticipation" |
| "github.com/carlosframework/platform/internal/rootstate" |
| @@ -33,9 +35,58 @@ func lifecycleActivationDiagnostic(t *testing.T, ctx context.Context) { |
| t.Log("diagnostic obligations", err) |
| _, err = productionGenerationRegistryInventory(ctx, inv) |
| t.Log("diagnostic registry", err) |
| + if err != nil { |
| + t.Logf("registry snapshot: %+v", inv) |
| + } |
| + lifecycleInventoryDiagnostic(t, ctx, inv, rs) |
| v := generationReconciler{read: productionLocalGenerationInventory, kernel: productionGenerationKernelCensus} |
| _, err = v.reconcile(ctx, rs) |
| return err |
| }) |
| t.Log("diagnostic certificate", err) |
| } |
| + |
| +func lifecycleInventoryDiagnostic(t *testing.T, ctx context.Context, inv generationInventory, rs []release.GenerationReservation) { |
| + physical, err := generationPhysicalPayloads(ctx, inv) |
| + t.Log("physical payload map", err) |
| + if err == nil { |
| + for _, p := range physical { |
| + t.Log("physical payload", p.Inventory.Host, p.Inventory.InstallationStamp, generationPayloadFiles(ctx, dataDir(), []hostbox.GenerationPayload{p})) |
| + } |
| + } |
| + boot, k, err := productionGenerationKernelCensus(context.WithValue(ctx, generationCensusReservationsKey{}, rs)) |
| + t.Log("kernel census", boot, k, err) |
| + for _, v := range inv.Installations { |
| + t.Logf("installation %+v", v) |
| + } |
| + for _, r := range rs { |
| + t.Logf("reservation %+v", r) |
| + } |
| +} |
| + |
| +func lifecycleRetirementDiagnostic(t *testing.T, ctx context.Context, g *release.RootGenerations) { |
| + records, err := g.Reservations(ctx) |
| + t.Log("retirement records", err) |
| + s, err := rootstate.Open("/var/lib/carlos-claims/authority/fences", false, false) |
| + if err != nil { |
| + t.Log(err) |
| + return |
| + } |
| + defer s.Close() |
| + j, err := openGenerationLaunchInventory("/var/lib/carlos-claims/journals.db") |
| + if err != nil { |
| + t.Log(err) |
| + return |
| + } |
| + defer j.Close() |
| + v := generationRetirement{guard: hostguard.Guard{Root: hostguard.NewRootAuthority(s)}, journal: j, kernel: productionGenerationAbsence} |
| + for _, r := range records { |
| + closed, e := v.closure(ctx, r) |
| + t.Log("retirement closure", r.Binding.Target.Host, r.Binding.Target.Stamp, closed, e) |
| + view, e := v.guard.Root.View(ctx, r.Binding.Target.Host, hostguard.Captured{Incarnation: r.Binding.Target.Incarnation, Stamp: r.Binding.Target.Stamp}) |
| + if e == nil && closed { |
| + _, e = productionRetiredGenerationAbsence(ctx, r, view) |
| + t.Log("retirement absence", r.Binding.Target.Host, r.Binding.Target.Stamp, e) |
| + } |
| + } |
| +} |
diff --git a/internal/host/plan14_lifecycle_retire_root_linux_test.go b/internal/host/plan14_lifecycle_retire_root_linux_test.go| index 13ddb4367..746d19ccf 100644 |
| --- a/internal/host/plan14_lifecycle_retire_root_linux_test.go |
| +++ b/internal/host/plan14_lifecycle_retire_root_linux_test.go |
| @@ -128,7 +128,35 @@ func lifecycleVerifyRetiredPins(t *testing.T, store release.Store, key ed25519.P |
| } |
| } |
| } |
| - source := productionRootGenerationPins() |
| + source := productionRootGenerationPins().(generationPinSource) |
| + retire := source.retire |
| + source.retire = func(ctx context.Context, g *release.RootGenerations) error { |
| + err := retire(ctx, g) |
| + t.Log("pins retirement", err) |
| + if err != nil { |
| + lifecycleRetirementDiagnostic(t, ctx, g) |
| + } |
| + return err |
| + } |
| + reconcile := source.reconcile |
| + source.reconcile = func(ctx context.Context, r []release.GenerationReservation) (release.GenerationEvidence, error) { |
| + e, err := reconcile(ctx, r) |
| + t.Log("pins reconciliation", err) |
| + if err != nil { |
| + inv, readErr := generationInventoryAt(ctx, rootparticipation.Namespace, "/var/lib/carlos-claims/authority/fences", "/var/lib/carlos-claims/journals.db") |
| + t.Log("pins raw inventory", readErr) |
| + inv.Handovers, readErr = productionGenerationHandovers(ctx) |
| + t.Log("pins handovers", readErr) |
| + inv.Retirements, readErr = productionGenerationRetirements(ctx) |
| + t.Log("pins retirements", readErr) |
| + _, readErr = generationRetirements(ctx, inv) |
| + t.Log("pins retirement map", readErr) |
| + _, readErr = productionGenerationRegistryInventory(ctx, inv) |
| + t.Log("pins registry", readErr) |
| + lifecycleInventoryDiagnostic(t, ctx, inv, r) |
| + } |
| + return e, err |
| + } |
| lock, err := rootstate.Open(rootReleasePrivate, false, false) |
| lifecycleCheck(t, "release lock", err) |
| defer lock.Close() |
| @@ -150,8 +178,8 @@ func lifecycleVerifyRetiredPins(t *testing.T, store release.Store, key ed25519.P |
| if err != nil { |
| return err |
| } |
| - if pinned { |
| - t.Fatal("retired lifecycle generation remains pinned") |
| + if !pinned { |
| + t.Fatal("active sibling failed to pin the shared generation tree") |
| } |
| return nil |
| })) |
diff --git a/internal/host/plan14_lifecycle_root_linux_test.go b/internal/host/plan14_lifecycle_root_linux_test.go| index 0225c1f37..e74474ac7 100644 |
| --- a/internal/host/plan14_lifecycle_root_linux_test.go |
| +++ b/internal/host/plan14_lifecycle_root_linux_test.go |
| @@ -305,19 +305,32 @@ func lifecycleRun(t *testing.T) { |
| rec = *found |
| } |
| withSibling := phase == "1" || phase == "2" || phase == "crash" |
| - if withSibling && !resume { |
| - sibling := provision.Record{Schema: 1, Host: lifecycleSibling, Account: "bab", App: "sibling", Channel: "stable", CreatedAt: time.Now().UTC()} |
| - completeSibling := lifecycleCreate(t, store, key, actor, pointer, sibling) |
| - completeMain := completeCreate |
| - completeCreate = func() error { |
| - if err := completeMain(); err != nil { |
| - return err |
| - } |
| - return completeSibling() |
| + var completeSibling func() error |
| + createSibling := func() { |
| + if !withSibling || resume { |
| + return |
| + } |
| + if completeSibling == nil { |
| + sibling := provision.Record{Schema: 1, Host: lifecycleSibling, Account: "bab", App: "sibling", Channel: "stable", CreatedAt: time.Now().UTC()} |
| + completeSibling = lifecycleCreate(t, store, key, actor, pointer, sibling) |
| + manifest := release.Manifest{App: "sibling", Version: "v1", Kind: "binary", Artifacts: map[string]release.Artifact{release.AdoptPlatform(): {}}} |
| + lifecycleCheck(t, "ship active sibling", release.Ship(t.Context(), store, "bab", manifest, map[string][]byte{release.AdoptPlatform(): image})) |
| + lifecycleCheck(t, "promote active sibling", release.Promote(t.Context(), store, key, "bab", "sibling", "stable", "v1")) |
| + _, _, e := release.StageSignedHead(t.Context(), store, key.Public().(ed25519.PublicKey), nil, "/opt/carlos", registry.Route{Host: lifecycleSibling, Account: "bab", App: "sibling", Channel: "stable"}) |
| + lifecycleCheck(t, "signed sibling stage", e) |
| + lifecycleCheck(t, "new deployment owner", filepath.Walk(lifecycleHome+"/deployment", func(path string, info os.FileInfo, e error) error { |
| + if e != nil { |
| + return e |
| + } |
| + return os.Chown(path, uid, gid) |
| + })) |
| + } |
| + if err := completeSibling(); errors.Is(err, hostops.ErrPending) { |
| + return |
| + } else { |
| + lifecycleCheck(t, "complete populated-box create", err) |
| } |
| - manifest := release.Manifest{App: "sibling", Version: "v1", Kind: "binary", Artifacts: map[string]release.Artifact{release.AdoptPlatform(): {}}} |
| - lifecycleCheck(t, "ship active sibling", release.Ship(t.Context(), store, "bab", manifest, map[string][]byte{release.AdoptPlatform(): image})) |
| - lifecycleCheck(t, "promote active sibling", release.Promote(t.Context(), store, key, "bab", "sibling", "stable", "v1")) |
| + lifecycleCheck(t, "sibling create ack", os.WriteFile("/run/carlos/lifecycle-sibling-created", []byte("ready"), 0644)) |
| } |
| // Only deployment inputs are service-owned. Root authority remains private. |
| lifecycleCheck(t, "deployment owner", filepath.Walk(lifecycleHome+"/deployment", func(path string, info os.FileInfo, err error) error { |
| @@ -333,14 +346,16 @@ func lifecycleRun(t *testing.T) { |
| out, mountErr = exec.Command("systemctl", "start", "data-instances.mount", "run-carlos-instances.mount", "run-carlos-logs.mount").CombinedOutput() |
| lifecycleCheck(t, string(out), mountErr) |
| recoverControl := lifecycleStartControl(t, server.URL) |
| - lifecycleStartAgent(t, server.URL, completeCreate, func() { |
| + lifecycleStartAgent(t, server.URL, completeCreate, createSibling, func() { |
| _, _, err = release.StageSignedHead(ctx, store, key.Public().(ed25519.PublicKey), nil, "/opt/carlos", registry.Route{Host: rec.Host, Account: rec.Account, App: rec.App, Channel: "stable"}) |
| lifecycleCheck(t, "signed stage", err) |
| lifecycleCheck(t, "point initial version", release.PointLive("/opt/carlos", rec.Host, rec.Account, "v1", rec.App)) |
| - if withSibling { |
| + if withSibling && resume { |
| _, _, err := release.StageSignedHead(ctx, store, key.Public().(ed25519.PublicKey), nil, "/opt/carlos", registry.Route{Host: lifecycleSibling, Account: "bab", App: "sibling", Channel: "stable"}) |
| lifecycleCheck(t, "signed sibling stage", err) |
| - lifecycleCheck(t, "sibling live", release.PointLive("/opt/carlos", lifecycleSibling, "bab", "v1", "sibling")) |
| + if resume { |
| + lifecycleCheck(t, "sibling live", release.PointLive("/opt/carlos", lifecycleSibling, "bab", "v1", "sibling")) |
| + } |
| } |
| }, lifecycleDeleteDriver(t, phase, store, key, actor, pointer), recoverControl) |
| if phase == "1" { |
| @@ -371,7 +386,7 @@ func lifecycleRun(t *testing.T) { |
| } |
| -func lifecycleStartAgent(t *testing.T, endpoint string, completeCreate func() error, stage func(), deletion func(), recoverControl func()) { |
| +func lifecycleStartAgent(t *testing.T, endpoint string, completeCreate func() error, createSibling func(), stage func(), deletion func(), recoverControl func()) { |
| t.Helper() |
| unit, err := unitsFS.ReadFile("units/carlos-edge.service") |
| lifecycleCheck(t, "edge template", err) |
| @@ -426,6 +441,12 @@ func lifecycleStartAgent(t *testing.T, endpoint string, completeCreate func() er |
| recovered = true |
| t.Log("controller and edge restarted after", os.Getenv("PLAN14_LIFECYCLE_CRASH")) |
| } |
| + if _, err := os.Stat("/run/carlos/lifecycle-create-sibling"); err == nil { |
| + createSibling() |
| + } |
| + if _, err := os.Stat("/run/carlos/lifecycle-sibling-installed"); err == nil { |
| + lifecycleCheck(t, "sibling live", release.PointLive("/opt/carlos", lifecycleSibling, "bab", "v1", "sibling")) |
| + } |
| if !created { |
| err := completeCreate() |
| if err == nil { |
| @@ -511,6 +532,23 @@ func lifecycleAgent(t *testing.T) { |
| providers, err := assembly.Build(ctx) |
| lifecycleCheck(t, "agent build", err) |
| lifecycleTraceRegional(t, &providers) |
| + oldPrerequisites := providers.Wake.Prerequisites |
| + providers.Wake.Prerequisites = func(ctx context.Context, rt registry.Route, o hostclaim.Observed, policy activator.StartPolicy) error { |
| + err := oldPrerequisites(ctx, rt, o, policy) |
| + if err != nil && strings.Contains(err.Error(), "no restorable data") { |
| + h := o.Claim.Holder |
| + if assembly.Guard.FenceView != nil { |
| + view, e := assembly.Guard.FenceView(ctx, o.Host, hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp}) |
| + t.Logf("empty recovery root view %+v error=%v", view, e) |
| + for _, r := range view.Receipts { |
| + t.Logf("empty recovery receipt target=%+v body=%s", r.Target, r.Bytes) |
| + } |
| + } |
| + launches, e := assembly.Launches.HostLaunches(ctx, o.Host) |
| + t.Logf("empty recovery launches %+v error=%v", launches, e) |
| + } |
| + return err |
| + } |
| lifecycleTraceWake(t, assembly.Wake) |
| deadline := time.Now().Add(time.Minute) |
| for { |
| @@ -565,7 +603,29 @@ func lifecycleAgent(t *testing.T) { |
| if route == nil { |
| t.Fatal("route disappeared before wake") |
| } |
| - lifecycleWake(t, ctx, providers.Wake, assembly.Wake, *route) |
| + finish := lifecycleLaunch(t, ctx, providers.Wake, assembly.Wake, *route) |
| + if wake == 0 && os.Getenv("PLAN14_LIFECYCLE_PHASE") != "2" && os.Getenv("PLAN14_LIFECYCLE_PHASE") != "" { |
| + lifecycleCheck(t, "request populated-box create", os.WriteFile("/run/carlos/lifecycle-create-sibling", []byte("ready"), 0644)) |
| + until := time.Now().Add(time.Minute) |
| + for { |
| + if _, e := os.Stat("/run/carlos/lifecycle-sibling-created"); e == nil { |
| + break |
| + } |
| + if time.Now().After(until) { |
| + t.Fatal("populated-box creation timeout") |
| + } |
| + lifecycleCheck(t, "populated-box reconcile", provision.Reconcile(ctx, store, reg, cfg)) |
| + time.Sleep(20 * time.Millisecond) |
| + } |
| + lifecycleCheck(t, "provision sibling while main active", provision.Reconcile(ctx, store, reg, cfg)) |
| + sibling, e := reg.Get(lifecycleSibling) |
| + lifecycleCheck(t, "new sibling row", e) |
| + if sibling == nil { |
| + t.Fatal("populated-box creation has no route") |
| + } |
| + lifecycleCheck(t, "sibling installed", os.WriteFile("/run/carlos/lifecycle-sibling-installed", []byte("ready"), 0644)) |
| + } |
| + finish() |
| t.Logf("completed cold wake %d", wake+1) |
| } |
| } |
| @@ -894,6 +954,29 @@ func TestPlan14LifecycleControlProcess(t *testing.T) { |
| } |
| time.Sleep(20 * time.Millisecond) |
| } |
| + if rootHTTP.Root.servingParks != nil { |
| + if p, ok := rootHTTP.Root.servingParks.authority.(*servingParkRoot); ok { |
| + fs := p.ledger.fs.(*servingParkFS) |
| + for _, name := range []string{"namespace-parent.json", "namespace.json", "current-namespace.json"} { |
| + raw, err := p.ledger.state.Read(name) |
| + t.Log("namespace before activation", name, string(raw), err) |
| + } |
| + parent, id, e := fs.parent() |
| + t.Log("namespace current parent", id, e) |
| + if e == nil { |
| + ph, pe := fs.handle(parent) |
| + t.Log("namespace parent proof", ph, pe) |
| + d, did, de := fs.privateDir(parent, ".serving-parked") |
| + t.Log("namespace current root", did, de) |
| + if de == nil { |
| + dh, he := fs.handle(d) |
| + t.Log("namespace root proof", dh, he) |
| + d.Close() |
| + } |
| + parent.Close() |
| + } |
| + } |
| + } |
| startup, finishStartup := context.WithTimeout(ctx, 2*time.Minute) |
| defer finishStartup() |
| if err := activateClaimRoot(ctx, startup, rootHTTP); err != nil { |
diff --git a/internal/host/plan14_lifecycle_trace_root_linux_test.go b/internal/host/plan14_lifecycle_trace_root_linux_test.go| index af0985d7d..1d21ba551 100644 |
| --- a/internal/host/plan14_lifecycle_trace_root_linux_test.go |
| +++ b/internal/host/plan14_lifecycle_trace_root_linux_test.go |
| @@ -8,6 +8,7 @@ import ( |
| "encoding/json" |
| "io" |
| "net/http" |
| + "net/http/httptest" |
| "os/exec" |
| "strings" |
| "sync" |
| @@ -25,6 +26,23 @@ type lifecycleControlTrace struct { |
| t *testing.T |
| } |
| +type lifecycleResponse struct { |
| + http.ResponseWriter |
| + status int |
| + body bytes.Buffer |
| +} |
| + |
| +func (w *lifecycleResponse) Unwrap() http.ResponseWriter { return w.ResponseWriter } |
| +func (w *lifecycleResponse) WriteHeader(status int) { |
| + w.status = status |
| + w.ResponseWriter.WriteHeader(status) |
| +} |
| +func (w *lifecycleResponse) Write(body []byte) (int, error) { |
| + if w.status >= 400 { |
| + w.body.Write(body) |
| + } |
| + return w.ResponseWriter.Write(body) |
| +} |
| func (h lifecycleControlTrace) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| var q hostbox.RoleLaunchRequest |
| if r.URL.Path == "/claim/launch" { |
| @@ -32,7 +50,11 @@ func (h lifecycleControlTrace) ServeHTTP(w http.ResponseWriter, r *http.Request) |
| r.Body = io.NopCloser(bytes.NewReader(raw)) |
| json.Unmarshal(raw, &q) |
| } |
| - h.ClaimRootWithLaunch.ServeHTTP(w, r) |
| + response := &lifecycleResponse{ResponseWriter: w} |
| + h.ClaimRootWithLaunch.ServeHTTP(response, r) |
| + if response.status >= 400 { |
| + h.t.Logf("root RPC %s status=%d reply=%s", r.URL.Path, response.status, response.body.String()) |
| + } |
| if q.Op == hostbox.RoleLaunchStop && q.Reservation != nil { |
| in, err := h.Launch.Journal.Read(r.Context(), q.Reservation.Target) |
| h.t.Logf("stop record err=%v request=%+v record=%+v containment=%v", err, *q.Reservation, in, validatePersistedUnitContainment(r.Context(), in, h.Launch.Units)) |
| @@ -139,3 +161,17 @@ func lifecycleTraceParkState(t *testing.T, p ClaimProviders, o hostclaim.Observe |
| t.Logf("park version=%+v generation=%q known=%v", version, generation, known) |
| } |
| } |
| + |
| +func TestPlan14LifecycleTraceStreams(t *testing.T) { |
| + sink := httptest.NewRecorder() |
| + w := &lifecycleResponse{ResponseWriter: sink} |
| + if _, err := w.Write([]byte("fence")); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := http.NewResponseController(w).Flush(); err != nil { |
| + t.Fatal(err) |
| + } |
| + if !sink.Flushed || sink.Body.String() != "fence" { |
| + t.Fatal("drain frame buffered") |
| + } |
| +} |
diff --git a/internal/host/plan14_namespace_transition_linux_test.go b/internal/host/plan14_namespace_transition_linux_test.go| new file mode 100644 |
| index 000000000..c2fc24bb7 |
| --- /dev/null |
| +++ b/internal/host/plan14_namespace_transition_linux_test.go |
| @@ -0,0 +1,120 @@ |
| +//go:build linux |
| + |
| +package host |
| + |
| +import ( |
| + "os" |
| + "reflect" |
| + "testing" |
| +) |
| + |
| +func TestPlan14ServingSameBootNamespace(t *testing.T) { |
| + f, r := servingRebootRecordFixture(t, "complete") |
| + f.fs.boot = func() (string, error) { return r.Source.BootID, nil } |
| + oldMount := r.Source.Parent.Mount + 123 |
| + for _, d := range []*servingArtifactDirectory{&r.Source.Parent, &r.Source.Directory, &r.Authorization.Directory, &r.Namespace, &r.Event} { |
| + if d.Inode != 0 { |
| + d.Mount = oldMount |
| + } |
| + } |
| + for i := range r.Source.Files { |
| + if r.Source.Files[i].Present { |
| + r.Source.Files[i].Mount = oldMount |
| + } |
| + } |
| + if err := f.ledger.save(r); err != nil { |
| + t.Fatal(err) |
| + } |
| + p, err := f.fs.historicalNamespaceParent() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + p.Parent.Mount = oldMount |
| + if err := f.ledger.write("namespace-parent.json", p); err != nil { |
| + t.Fatal(err) |
| + } |
| + var n servingParkNamespace |
| + body, err := f.disk.Read("namespace.json") |
| + if err != nil || servingParkDecode(body, &n) != nil { |
| + t.Fatal(err) |
| + } |
| + n.Parent.Mount, n.Root.Mount = oldMount, oldMount |
| + if err := f.ledger.write("namespace.json", n); err != nil { |
| + t.Fatal(err) |
| + } |
| + if r.Authorization.Legacy { |
| + c, err := f.ledger.legacyConsumption(r.Authorization.Successor) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if c != nil { |
| + c.Source, c.Authorization = r.Source, r.Authorization |
| + if err := f.ledger.write(servingLegacyKey("legacy", r.Authorization.Successor), c); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + } |
| + historicalLedger := *f.ledger |
| + historicalLedger.historical = true |
| + for i := 0; i < 2; i++ { |
| + if err := historicalLedger.prepareRebootLocked(t.Context(), r, nil); err != nil { |
| + t.Fatal("prepare same-boot namespace", err) |
| + } |
| + if err := historicalLedger.commitRebootLocked(t.Context(), r, nil); err != nil { |
| + t.Fatal("commit same-boot namespace", err) |
| + } |
| + } |
| + if err := f.fs.recoverRecordedNamespace(t.Context()); err != nil { |
| + t.Fatal(err) |
| + } |
| + current, err := f.ledger.read(r.Authorization.ID) |
| + if err != nil || current.Source.Parent.Mount == oldMount { |
| + t.Fatal("binding", err) |
| + } |
| + if err := f.ledger.save(current); err != nil { |
| + t.Fatal(err) |
| + } |
| + historical, err := f.ledger.readHistorical(r.Authorization.ID) |
| + if err != nil || !reflect.DeepEqual(historical, r) { |
| + t.Fatal("history changed", err) |
| + } |
| + if _, err := os.Stat(f.fs.data); err != nil { |
| + t.Fatal(err) |
| + } |
| +} |
| + |
| +func TestPlan14ServingNamespaceMountReuse(t *testing.T) { |
| + f, r := servingRebootRecordFixture(t, "complete") |
| + f.fs.boot = func() (string, error) { return r.Source.BootID, nil } |
| + p, e := f.fs.historicalNamespaceParent() |
| + if e != nil { |
| + t.Fatal(e) |
| + } |
| + stale := p.Parent |
| + stale.Mount += 123 |
| + if e = f.ledger.write("current-namespace.json", servingParkBootBinding{Version: 1, ID: "namespace", BeforeBoot: p.BootID, BootID: p.BootID, BeforeParent: p.Parent, Parent: stale}); e != nil { |
| + t.Fatal(e) |
| + } |
| + if e = f.ledger.write("current-retirement-"+r.Authorization.ID+".json", servingParkBootBinding{Version: 1, ID: "retirement-" + r.Authorization.ID, BeforeBoot: r.Source.BootID, BootID: r.Source.BootID, BeforeParent: r.Source.Parent, Parent: stale}); e != nil { |
| + t.Fatal(e) |
| + } |
| + h := *f.ledger |
| + h.historical = true |
| + if e = h.prepareRebootLocked(t.Context(), r, nil); e != nil { |
| + t.Fatal("prepare reused mount", e) |
| + } |
| + if e = h.commitRebootLocked(t.Context(), r, nil); e != nil { |
| + t.Fatal("commit reused mount", e) |
| + } |
| + if e = f.fs.recoverRecordedNamespace(t.Context()); e != nil { |
| + t.Fatal(e) |
| + } |
| + current, e := f.ledger.read(r.Authorization.ID) |
| + if e != nil || current.Source.Parent != r.Source.Parent { |
| + t.Fatal("return to original mount", e) |
| + } |
| + original, e := f.ledger.readHistorical(r.Authorization.ID) |
| + if e != nil || !reflect.DeepEqual(original, r) { |
| + t.Fatal("history changed", e) |
| + } |
| +} |
diff --git a/internal/host/plan14_r11_handover_test.go b/internal/host/plan14_r11_handover_test.go| index 01c1381af..87d5f65f4 100644 |
| --- a/internal/host/plan14_r11_handover_test.go |
| +++ b/internal/host/plan14_r11_handover_test.go |
| @@ -61,6 +61,29 @@ func TestPlan14ServingHandoverPins(t *testing.T) { |
| if physical[0].Inventory.Entries[0].Identity != physical[1].Inventory.Entries[0].Identity { |
| t.Fatal("predecessor did not follow verified persistent identity") |
| } |
| + t.Run("recreated-namespace", func(t *testing.T) { |
| + raw, _ := json.Marshal(snapshot) |
| + var recreated generationInventory |
| + if err := json.Unmarshal(raw, &recreated); err != nil { |
| + t.Fatal(err) |
| + } |
| + for i, e := range recreated.Payloads[0].Inventory.Entries { |
| + if e.Kind != "path" { |
| + continue |
| + } |
| + current := recreated.Payloads[1].Inventory.Entries[i].Identity |
| + old := current |
| + old.Mount += 100 |
| + recreated.Payloads[0].Inventory.Entries[i].Identity = old |
| + recreated.Handovers[0].Inventory.Entries[i].Identity = old |
| + } |
| + if _, err := generationRegistryInventory(t.Context(), root.Reconcile.Registry, recreated); err != nil { |
| + t.Fatal("recreated namespace transfer", err) |
| + } |
| + if _, err := generationPhysicalPayloads(t.Context(), recreated); err != nil { |
| + t.Fatal("recreated namespace payloads", err) |
| + } |
| + }) |
| original, _ := json.Marshal(snapshot) |
| for _, attack := range []string{"missing journal", "prepared", "successor", "barrier", "receipt", "retired", "path", "row", "revision", "duplicate", "cycle", "old boot"} { |
| t.Run(attack, func(t *testing.T) { |
diff --git a/internal/host/plan14_remote_cleanup_test.go b/internal/host/plan14_remote_cleanup_test.go| new file mode 100644 |
| index 000000000..baa7a5a3d |
| --- /dev/null |
| +++ b/internal/host/plan14_remote_cleanup_test.go |
| @@ -0,0 +1,35 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "os" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + execlease "github.com/carlosframework/platform/internal/hostexec" |
| +) |
| + |
| +func TestPlan14RemoteUnlaunchedCleanup(t *testing.T) { |
| + for _, fault := range []string{"none", "legacy", "read-error", "local-inventory"} { |
| + t.Run(fault, func(t *testing.T) { |
| + f := newRecoveryFixture(t) |
| + launches, inventory := hostbox.RemoteJournals(func(ctx context.Context, q hostbox.JournalRequest) (hostbox.JournalResponse, error) { |
| + if q.Op != "has-host" { |
| + t.Fatalf("unexpected root operation %s", q.Op) |
| + } |
| + if fault == "read-error" { |
| + return hostbox.JournalResponse{}, os.ErrPermission |
| + } |
| + return hostbox.JournalResponse{V: 1, ID: q.ID}, nil |
| + }) |
| + if fault == "local-inventory" { |
| + inventory = nil |
| + } |
| + p := &productionClaimLocal{legacyParticipation: fault == "legacy", assembly: &ClaimAssembly{Wake: &ClaimWakeAssembly{}, Launches: launches, Reconcile: &hostbox.ReconcileAdapter{Inventory: inventory, Census: &hostbox.EmptyInstallationCensus{}}}} |
| + err := p.cleanup(t.Context(), f.read(), execlease.Binding{}) |
| + if (err == nil) != (fault == "none") { |
| + t.Fatal(fault, err) |
| + } |
| + }) |
| + } |
| +} |
diff --git a/internal/host/plan14_service_supervisor_linux_test.go b/internal/host/plan14_service_supervisor_linux_test.go| new file mode 100644 |
| index 000000000..b7163794a |
| --- /dev/null |
| +++ b/internal/host/plan14_service_supervisor_linux_test.go |
| @@ -0,0 +1,27 @@ |
| +//go:build linux |
| + |
| +package host |
| + |
| +import "testing" |
| + |
| +func TestPlan14GenerationServiceSupervisor(t *testing.T) { |
| + for _, tc := range []struct { |
| + name, unit, group, status string |
| + main int |
| + want bool |
| + }{ |
| + {"main", "carlos-edge.service", "/carlos.slice/carlos-edge.service/supervise", "Tgid:\t42\n", 42, true}, |
| + {"other process", "carlos-edge.service", "/carlos.slice/carlos-edge.service/supervise", "Tgid:\t43\n", 42, false}, |
| + {"child group", "carlos-edge.service", "/carlos.slice/carlos-edge.service/supervise/child", "Tgid:\t42\n", 42, false}, |
| + {"other service", "unknown.service", "/carlos.slice/carlos-edge.service/supervise", "Tgid:\t42\n", 42, false}, |
| + {"missing main", "carlos-edge.service", "/carlos.slice/carlos-edge.service/supervise", "Tgid:\t0\n", 0, false}, |
| + {"missing identity", "carlos-edge.service", "/carlos.slice/carlos-edge.service/supervise", "Uid:\t999\n", 42, false}, |
| + {"duplicate", "carlos-edge.service", "/carlos.slice/carlos-edge.service/supervise", "Tgid:\t42\nTgid:\t42\n", 42, false}, |
| + } { |
| + t.Run(tc.name, func(t *testing.T) { |
| + if got := generationServiceSupervisor(tc.unit, tc.group, "/carlos.slice/carlos-edge.service", []byte(tc.status), tc.main); got != tc.want { |
| + t.Fatal(got) |
| + } |
| + }) |
| + } |
| +} |
diff --git a/internal/host/plan14_sibling_census_linux_test.go b/internal/host/plan14_sibling_census_linux_test.go| new file mode 100644 |
| index 000000000..540924e95 |
| --- /dev/null |
| +++ b/internal/host/plan14_sibling_census_linux_test.go |
| @@ -0,0 +1,98 @@ |
| +//go:build linux |
| + |
| +package host |
| + |
| +import ( |
| + "os" |
| + "path/filepath" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| + "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/provision" |
| +) |
| + |
| +func TestPlan14EmptyCensusRootSibling(t *testing.T) { |
| + for _, fault := range []string{"none", "missing-root", "wrong-revision", "rename", "unclassified", "selected-host", "private-alias-substitution", "private-alias-symlink"} { |
| + t.Run(fault, func(t *testing.T) { |
| + f, _, _ := generationLaunchFixture(t) |
| + root := t.TempDir() |
| + rel := filepath.Join("instances", provision.InstanceID(f.target.Host)) |
| + path := filepath.Join(root, rel) |
| + if err := os.MkdirAll(path, 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(filepath.Join(path, "app.db"), []byte("live sibling"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + id, err := hostbox.CaptureInventoryIdentity(root, rel) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + service, err := hostbox.NewJournalService(f.journal) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + in := hostbox.StopInventory{Complete: true, Host: f.target.Host, Incarnation: f.target.Incarnation, InstallationStamp: f.target.Stamp, Box: f.target.Box, Enrollment: f.target.Enrollment, DirectoryRevision: "1", Entries: []hostbox.StopInventoryEntry{{Kind: "path", ManagedRoot: "data", RelativePath: rel, ArtifactID: "state", Ownership: "owner", Materialization: "materialized", Device: id.Device, Inode: id.Inode, Type: 0040000, Identity: id}}} |
| + if err := service.Inventory.WriteStopInventory(t.Context(), in); err != nil { |
| + t.Fatal(err) |
| + } |
| + reg := restartTestRegistry(t) |
| + c := hostbox.EmptyInstallationCensus{Inventory: service.Inventory, Registry: reg, Launches: f.journal, Box: f.target.Box, Enrollment: f.target.Enrollment, DirectoryRevision: "1", Roots: map[string]string{"data": root, "opt": t.TempDir(), "units": t.TempDir()}} |
| + h := ClaimRootHTTP{Launches: f.journal, Roots: c.Roots, Stop: ClaimStopHTTP{Coordinator: ClaimStopCoordinator{Guard: f.root.Guard}}} |
| + c.OtherPath = h.rootSiblingPath |
| + switch fault { |
| + case "missing-root": |
| + h.Stop.Coordinator.Guard.Root = hostguard.NewRootAuthority(&rollbackDisk{dir: t.TempDir()}) |
| + c.OtherPath = h.rootSiblingPath |
| + case "wrong-revision": |
| + if err := service.Inventory.WriteStopInventory(t.Context(), in); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "rename": |
| + if err := os.Rename(path, path+"-old"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Mkdir(path, 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "private-alias-substitution", "private-alias-symlink": |
| + private := filepath.Join(root, ".instances", provision.InstanceID(f.target.Host)) |
| + if err := os.MkdirAll(filepath.Dir(private), 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + if fault == "private-alias-symlink" { |
| + if err := os.Symlink(path, private); err != nil { |
| + t.Fatal(err) |
| + } |
| + } else { |
| + if err := os.Mkdir(private, 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + case "unclassified": |
| + if err := os.WriteFile(filepath.Join(root, "unknown"), []byte("x"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + selected := "new.example" |
| + if fault == "selected-host" { |
| + selected = f.target.Host |
| + } |
| + obs := hostclaim.Observed{Host: selected, Claim: &hostclaim.Claim{Holder: &hostclaim.Holder{Incarnation: "new", Stamp: 1}}} |
| + if fault == "none" { |
| + c.OtherPath = nil |
| + if err := c.CheckLocal(t.Context(), obs, hostbox.RecordSelection{Host: selected, Incarnation: "new"}); err == nil { |
| + t.Fatal("unclassified sibling unexpectedly admitted") |
| + } |
| + c.OtherPath = h.rootSiblingPath |
| + } |
| + err = c.CheckLocal(t.Context(), obs, hostbox.RecordSelection{Host: selected, Incarnation: "new"}) |
| + if (err == nil) != (fault == "none") { |
| + t.Fatal("empty census", fault, err) |
| + } |
| + |
| + }) |
| + } |
| +} |
diff --git a/internal/host/rootexecution_authority.go b/internal/host/rootexecution_authority.go| index 2b5c1c861..f062d6274 100644 |
| --- a/internal/host/rootexecution_authority.go |
| +++ b/internal/host/rootexecution_authority.go |
| @@ -112,7 +112,7 @@ func (h ClaimRootHTTP) executionAuthority(ctx context.Context, q execlease.Execu |
| } |
| if revision == 0 { |
| selected := hostbox.RecordSelection{Host: q.Host, Key: c.Holder.Key, ETag: etag, Incarnation: q.Incarnation, Family: c.Holder.Family} |
| - if err := h.Reconcile.Census.CheckLocal(ctx, obs, selected); err != nil { |
| + if err := h.rootEmptyCensus().CheckLocal(ctx, obs, selected); err != nil { |
| return out, err |
| } |
| } else { |
| @@ -138,7 +138,7 @@ func (h ClaimRootHTTP) executionAuthority(ctx context.Context, q execlease.Execu |
| return out, hostbox.ErrTypedInventory |
| } |
| selected := hostbox.RecordSelection{Host: q.Host, Key: c.Holder.Key, ETag: etag, Incarnation: q.Incarnation, Family: c.Holder.Family} |
| - if err = h.Reconcile.Census.CheckLocal(ctx, obs, selected); err != nil { |
| + if err = h.rootEmptyCensus().CheckLocal(ctx, obs, selected); err != nil { |
| return out, err |
| } |
| out.ParticipationOnly = true |
diff --git a/internal/host/rootexecution_settle.go b/internal/host/rootexecution_settle.go| index e89766c18..e73ed9f2d 100644 |
| --- a/internal/host/rootexecution_settle.go |
| +++ b/internal/host/rootexecution_settle.go |
| @@ -107,7 +107,7 @@ func (h ClaimRootHTTP) cleanupExecution(ctx context.Context, r execlease.Executi |
| if err != nil { |
| return err |
| } |
| - return h.Reconcile.Census.CheckLocal(ctx, o, hostbox.RecordSelection{Host: q.Host, Incarnation: q.Incarnation}) |
| + return h.rootEmptyCensus().CheckLocal(ctx, o, hostbox.RecordSelection{Host: q.Host, Incarnation: q.Incarnation}) |
| } |
| view, err := guard.Root.View(ctx, q.Host, captured) |
| if err != nil { |
diff --git a/internal/host/rootgeneration_kernel_linux.go b/internal/host/rootgeneration_kernel_linux.go| index a9755220e..1c8806aa4 100644 |
| --- a/internal/host/rootgeneration_kernel_linux.go |
| +++ b/internal/host/rootgeneration_kernel_linux.go |
| @@ -598,10 +598,11 @@ func generationCurrentFiles(b release.GenerationBinding, open func(string) (*os. |
| defer d.Close() |
| for _, item := range []struct { |
| file *os.File |
| + path string |
| identity *rootstate.FileIdentity |
| proof rootstate.FileIdentity |
| }{ |
| - {f, &b.Executable, b.ExecutableProof}, {d, &b.TreeIdentity, b.TreeProof}, |
| + {f, b.Path, &b.Executable, b.ExecutableProof}, {d, b.Tree, &b.TreeIdentity, b.TreeProof}, |
| } { |
| current, err := rootstate.Identity(item.file) |
| if err != nil { |
| @@ -619,7 +620,7 @@ func generationCurrentFiles(b release.GenerationBinding, open func(string) (*os. |
| } |
| } else { |
| observed, err := rootstate.PersistentIdentity(item.file) |
| - if err != nil || !item.proof.Same(observed) { |
| + if err != nil || !generationFileProof(item.path, item.proof, observed) { |
| return b, os.ErrPermission |
| } |
| } |
| @@ -658,3 +659,7 @@ func generationTree(ctx context.Context, b release.GenerationBinding) (map[roots |
| }) |
| return refs, err |
| } |
| + |
| +func generationFileProof(path string, before, after rootstate.FileIdentity) bool { |
| + return release.GenerationFileProof(path, before, after) |
| +} |
diff --git a/internal/host/rootgeneration_services_linux.go b/internal/host/rootgeneration_services_linux.go| index 785e028d7..bbe9f81ea 100644 |
| --- a/internal/host/rootgeneration_services_linux.go |
| +++ b/internal/host/rootgeneration_services_linux.go |
| @@ -80,7 +80,8 @@ func generationServiceReader(ctx context.Context) func(string, []byte) (bool, er |
| } |
| units[unit] = s |
| } |
| - if !generationServiceGroup(group, s.group) { |
| + supervisor := generationServiceSupervisor(unit, group, s.group, status, s.main) |
| + if !generationServiceGroup(group, s.group) && !supervisor { |
| return false, os.ErrPermission |
| } |
| name, err := os.Readlink(path + "/exe") |
| @@ -101,7 +102,8 @@ func generationServiceReader(ctx context.Context) func(string, []byte) (bool, er |
| } |
| } |
| mainGroup, err := generationRead("/proc/" + strconv.Itoa(s.main) + "/cgroup") |
| - if err != nil || !generationServiceGroup(strings.TrimPrefix(strings.TrimSpace(string(mainGroup)), "0::"), s.group) { |
| + mainPath := strings.TrimPrefix(strings.TrimSpace(string(mainGroup)), "0::") |
| + if err != nil || (!generationServiceGroup(mainPath, s.group) && !(supervisor && mainPath == group)) { |
| return false, os.ErrPermission |
| } |
| return true, nil |
| @@ -170,3 +172,19 @@ func generationServiceExecutable(task, name string) (rootstate.FileIdentity, err |
| fd = next |
| } |
| } |
| + |
| +func generationServiceSupervisor(unit, group, base string, status []byte, main int) bool { |
| + if unit != "carlos-edge.service" || base == "" || filepath.Clean(base) != base || group != base+"/supervise" || main <= 0 { |
| + return false |
| + } |
| + tgid := "" |
| + for _, line := range strings.Split(string(status), "\n") { |
| + if value, ok := strings.CutPrefix(line, "Tgid:"); ok { |
| + if tgid != "" { |
| + return false |
| + } |
| + tgid = strings.TrimSpace(value) |
| + } |
| + } |
| + return tgid == strconv.Itoa(main) |
| +} |
diff --git a/internal/host/rootinventory.go b/internal/host/rootinventory.go| index 97aeec343..219ab5e42 100644 |
| --- a/internal/host/rootinventory.go |
| +++ b/internal/host/rootinventory.go |
| @@ -243,12 +243,13 @@ func (h ClaimRootHTTP) commitRootInventoryPending(ctx context.Context, q hostbox |
| if (!firstLanding && len(in.Entries) != 0) || rootFirstInstallation(observed.Claim, actor) != nil { |
| return hostbox.ErrChanged |
| } |
| + census := h.rootEmptyCensus() |
| selected := hostbox.RecordSelection{Host: in.Host, Key: holder.Key, ETag: q.RecordETag, Incarnation: in.Incarnation, Family: holder.Family} |
| if firstLanding && currentRow != nil && priorRoot.Registration == installation.Registration { |
| - if err := h.Reconcile.Census.CheckRootRegisteredRow(ctx, observed, selected, currentRow.ClaimOwnership); err != nil { |
| + if err := census.CheckRootRegisteredRow(ctx, observed, selected, currentRow.ClaimOwnership); err != nil { |
| return err |
| } |
| - } else if err := h.Reconcile.Census.CheckLocal(ctx, observed, selected); err != nil { |
| + } else if err := census.CheckLocal(ctx, observed, selected); err != nil { |
| return err |
| } |
| } else { |
diff --git a/internal/host/rootinventory_sibling.go b/internal/host/rootinventory_sibling.go| new file mode 100644 |
| index 000000000..fac0248fc |
| --- /dev/null |
| +++ b/internal/host/rootinventory_sibling.go |
| @@ -0,0 +1,110 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "os" |
| + "path/filepath" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/provision" |
| + "github.com/carlosframework/platform/internal/release" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| +) |
| + |
| +func (h ClaimRootHTTP) rootEmptyCensus() *hostbox.EmptyInstallationCensus { |
| + census := *h.Reconcile.Census |
| + census.OtherPath = h.rootSiblingPath |
| + return &census |
| +} |
| + |
| +func (h ClaimRootHTTP) rootSiblingPath(ctx context.Context, selected, root, rel string) (bool, error) { |
| + parent := filepath.ToSlash(filepath.Dir(rel)) |
| + if !(root == "data" && (parent == "instances" || parent == ".instances") || root == "opt" && (parent == "live" || parent == "previous")) { |
| + return false, nil |
| + } |
| + payloads, err := h.Launches.GenerationPayloads(ctx) |
| + if err != nil { |
| + return false, err |
| + } |
| + for _, p := range payloads { |
| + in := p.Inventory |
| + if in.Host == selected { |
| + continue |
| + } |
| + candidate := root == "data" && (rel == filepath.ToSlash(filepath.Join("instances", provision.InstanceID(in.Host))) || rel == filepath.ToSlash(filepath.Join(".instances", provision.InstanceID(in.Host)))) || root == "opt" && (rel == "live/"+in.Host || rel == "previous/"+in.Host) |
| + if !candidate { |
| + continue |
| + } |
| + own, err := h.Stop.Coordinator.Guard.Root.Installation(ctx, in.Host, hostguard.Captured{Incarnation: in.Incarnation, Stamp: in.InstallationStamp}) |
| + if err != nil { |
| + return false, err |
| + } |
| + if own.Stamp != in.InstallationStamp { |
| + continue |
| + } |
| + if !in.Complete || own.Host != in.Host || own.Box != in.Box || own.Enrollment != in.Enrollment || own.InventoryRevision != p.Revision || own.Record != provision.RecordKey(own.Account, own.App, own.Host) { |
| + return false, os.ErrPermission |
| + } |
| + if root == "data" { |
| + for _, e := range in.Entries { |
| + if e.Kind == "path" && e.ManagedRoot == root && e.RelativePath == filepath.ToSlash(filepath.Join("instances", provision.InstanceID(in.Host))) && e.Materialization == "materialized" { |
| + err := generationPayloadFiles(ctx, h.Roots[root], []hostbox.GenerationPayload{{Inventory: hostbox.StopInventory{Entries: []hostbox.StopInventoryEntry{e}}}}) |
| + if err == nil && rel != e.RelativePath { |
| + err = rootSiblingAlias(h.Roots[root], rel, e.RelativePath) |
| + } |
| + return err == nil, err |
| + } |
| + } |
| + return false, nil |
| + } |
| + state, err := rootstate.Open(release.RootGenerationsDir, false, false) |
| + if err != nil { |
| + return false, err |
| + } |
| + pins, err := (&release.RootGenerations{Store: state}).Reservations(ctx) |
| + state.Close() |
| + if err != nil { |
| + return false, err |
| + } |
| + target, err := os.Readlink(filepath.Join(h.Roots[root], rel)) |
| + if err != nil { |
| + return false, err |
| + } |
| + for _, pin := range pins { |
| + b := pin.Binding |
| + if b.Target.Host == own.Host && b.Target.Incarnation == own.Incarnation && b.Target.Box == own.Box && b.Target.Enrollment == own.Enrollment && b.Binding.Account == own.Account && b.Binding.App == own.App && b.Record == own.Record && target == b.Path && target == release.VersionPath(h.Roots[root], own.Account, b.Version, own.App) { |
| + return true, nil |
| + } |
| + } |
| + return false, os.ErrPermission |
| + } |
| + return false, nil |
| +} |
| + |
| +func rootSiblingAlias(root, alias, public string) error { |
| + a, err := hostbox.CaptureInventoryIdentity(root, alias) |
| + if err != nil { |
| + return err |
| + } |
| + b, err := hostbox.CaptureInventoryIdentity(root, public) |
| + if err != nil { |
| + return err |
| + } |
| + ar, err := filepath.Rel(a.MountPoint, filepath.Join(root, alias)) |
| + if err != nil || !filepath.IsLocal(ar) { |
| + return os.ErrPermission |
| + } |
| + br, err := filepath.Rel(b.MountPoint, filepath.Join(root, public)) |
| + if err != nil || !filepath.IsLocal(br) { |
| + return os.ErrPermission |
| + } |
| + if a.Boot != b.Boot || a.Device != b.Device || a.MountNamespace != b.MountNamespace || filepath.Join(a.MountRoot, ar) != filepath.Join(b.MountRoot, br) { |
| + return os.ErrPermission |
| + } |
| + b.MountRoot, b.MountPoint, b.Mount = a.MountRoot, a.MountPoint, a.Mount |
| + if !a.Same(b) { |
| + return os.ErrPermission |
| + } |
| + return nil |
| +} |
diff --git a/internal/hostbox/empty_census.go b/internal/hostbox/empty_census.go| index ea6c9a236..f6c15e96e 100644 |
| --- a/internal/hostbox/empty_census.go |
| +++ b/internal/hostbox/empty_census.go |
| @@ -28,6 +28,7 @@ type EmptyInstallationCensus struct { |
| // fences; excluding that directory here never assigns its contents to an |
| // installation artifact inventory. |
| SharedPaths map[string][]string |
| + OtherPath func(context.Context, string, string, string) (bool, error) |
| } |
| // CheckHeld is the hold-only path's local check (region HA R5-3; Astra |
| @@ -205,6 +206,18 @@ func (c EmptyInstallationCensus) checkLocal(ctx context.Context, obs hostclaim.O |
| if censusNamesHost(rel, selected.Host) { |
| return ErrTypedInventory |
| } |
| + if c.OtherPath != nil { |
| + known, err := c.OtherPath(ctx, selected.Host, name, rel) |
| + if err != nil { |
| + return err |
| + } |
| + if known { |
| + if d.IsDir() { |
| + return filepath.SkipDir |
| + } |
| + return nil |
| + } |
| + } |
| if rule, ok := rules[rel]; ok { |
| return rule.check(path, d) |
| } |
diff --git a/internal/provision/plan14_p3_census_test.go b/internal/provision/plan14_p3_census_test.go| index 3fec3e018..85cce5b77 100644 |
| --- a/internal/provision/plan14_p3_census_test.go |
| +++ b/internal/provision/plan14_p3_census_test.go |
| @@ -53,9 +53,10 @@ func plan14P3WriteCensusInventory() []censusEntry { |
| kept = append(kept, e) |
| } |
| return append(kept, |
| + censusEntry{"internal/host/rootinventory_sibling.go", "ClaimRootHTTP.rootSiblingPath", "key-site", "key:RecordKey,lit:instances", "0ead075a56726415017bbbb5db140025b54faf3af387309c4ef86b8bd0b934a4"}, |
| censusEntry{"internal/hostclaim/claim.go", "Claim.Validate", "key-site", "lit:instances", "ffeb10f34954f5b4c8566ee1d66a89a99eb303a8611e4671696dd312c5296c29"}, |
| censusEntry{"internal/hostclaim/retainedloss.go", "validateRetainedAdmissionLosses", "reference", "lit:instances", "b32ddaed69411feaa06c45e84287fb1c09b0168498231a5386e3acd937d335e0"}, |
| - censusEntry{"internal/host/rootexecution_authority.go", "ClaimRootHTTP.executionAuthority", "key-site", "key:RecordKey,lit:instances", "cca1bc79b5efe35afbda9df8b61e46ae2644bf99ed5234da3ae8d88fcc4a872d"}, |
| + censusEntry{"internal/host/rootexecution_authority.go", "ClaimRootHTTP.executionAuthority", "key-site", "key:RecordKey,lit:instances", "d4c3831803029792b9bdc5d1a98049fe31a4cbca24dc3f09aac768533cd8ffee"}, |
| censusEntry{"internal/host/claimproduction.go", "productionClaimRegional.View", "reference", "lit:instances", "bcefe9f8fd6420e795ef95a6170fbad1030e29f2261bcc2d677f76b6f6d577ce"}, |
| censusEntry{"internal/host/claimordinaryparks.go", "productionClaimRegional.ordinaryHome", "reference", "lit:instances", "fa96f75f8af5fd1e3ba3dff81e27ac0fb7c6029663472292d698bfddaec99fe8"}, |
| censusEntry{"internal/host/rootgeneration_inventory.go", "generationReconciler.reconcile", "key-site", "key:RecordKey", "da4df99dcbcec0cd62025c8c50e8f6be19e64ac990034002bcad1a6b8332bcb6"}, |
| @@ -64,7 +65,7 @@ func plan14P3WriteCensusInventory() []censusEntry { |
| censusEntry{"internal/host/proofbinding.go", "proofBindingSource.binding", "key-site", "key:RecordKey", "20ed721cbcaccb6ab813a0d317542a43fe057544811920ba43ac6431c3e73000"}, |
| censusEntry{"internal/host/proofbinding.go", "proofBindingSource.unitBinding", "key-site", "key:RecordKey", "82e214d1cf66d29d0900f5c1ce9cfe0a2f31c070da4bf1497ef3fd5273c09aad"}, |
| censusEntry{"internal/host/proofnamed.go", "proofBindingSource.record", "key-site", "key:RecordKey", "97f1501b9543e36c6a231170425c5c79c7c22489500aadf119e064abc1abde93"}, |
| - censusEntry{"internal/host/rootinventory.go", "ClaimRootHTTP.commitRootInventoryPending", "key-site", "key:RecordKey", "18fcea255a124e5c970c32d943c960836602dd12d9f00669583d8b4bd72fe7e6"}, |
| + censusEntry{"internal/host/rootinventory.go", "ClaimRootHTTP.commitRootInventoryPending", "key-site", "key:RecordKey", "b00707c1c5a3b1f3b51fd573fbb09a176bda8a055e64232b9044b7c78640ba51"}, |
| censusEntry{"internal/host/rootgenerations_launch.go", "rootGenerationSource.resolve", "key-site", "key:RecordKey", "3cd7c8132e9c451c983392f4d1cc676ddc14b761c90dec3e631d54ae800f9951"}, |
| censusEntry{"internal/host/rootupdates.go", "rootUpdateSource.resolve", "key-site", "key:RecordKey", "6c5c447d2abd518a941979dcd1748ee028f3b555ce077fad849c02be0c77c2fa"}, |
| censusEntry{"internal/host/rootmovesfloor.go", "rootMovesFloors.refresh", "key-site", "key:writeRecord", "cb70a1eefbb4f8f243090270f23141c9c9a773ee278af002c79a6c3ef6b7c3b7"}, |
diff --git a/internal/provision/testdata/plan14-root-reads-p3.tsv b/internal/provision/testdata/plan14-root-reads-p3.tsv| index 90ee7bb68..8b99c5914 100644 |
| --- a/internal/provision/testdata/plan14-root-reads-p3.tsv |
| +++ b/internal/provision/testdata/plan14-root-reads-p3.tsv |
| @@ -203,9 +203,9 @@ internal/hostguard/rootmigration.go RootAuthority.CompleteMigrationInventory aut |
| @coverage internal/edgesfile e0a7dd2795beedde4d17389b96b562f3f9438ad8505ea19a62d1e9c4b921aefb |
| @coverage internal/host/units bf6ae516012701e0ca41908167e4efd60dd3c2c24e9af1555dec077e8be44461 |
| @coverage internal/hostfloor 696e5ea4fa37b924b68e8e262235b99298ee19342d875213ba8209e5287dd3c6 |
| -@coverage internal/release 1c219907db7ac4ab5d7f5e416c37cd623045e5b5ef759f121ab327effa4017c3 |
| +@coverage internal/release 82473b1d79cb321075c98d392fae4d6ca9e036f5c2ecd38a849235d881dda6ae |
| @coverage internal/rootparticipation 7798d05a815af052b9b40d4ff381834a58b2740082e8f673c173e34cb3ce4f22 |
| -@coverage internal/rootstate 292c259ffe1e7144029af096cc39691d6239b2dbc5f01583d76d2e3f4653b2cb |
| +@coverage internal/rootstate 1d77c02742516922b16847943e885dda8ee218e350de039aa5f177a8c20f2d29 |
| @coverage internal/safefile 7030f312f7a60487381f0dbae2cdecaaa83625b639b6af15104f912592550816 |
| internal/host/rootgeneration_kernel_linux.go generationKernel.absent authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=bounded task and persistent generation inspection retains boot and process start checks; refusal diagnostics identify the failing task file:Readlink@312 9aee837e918fbc1cc6f16bbf8e6b5652a700cf0c767d99150cac3e87bb03d2a3 |
| internal/host/rootgenerations_launch.go productionRootGenerations authority E2,E3 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=connects the census root generation source to independent runtime failure and served settlement;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures file:Open@386 e5a3f19159348abed074b2e2c01f9e611e47783395e5f3d063b7c1a657145a92 |
| @@ -221,7 +221,7 @@ internal/release/adopt.go adoptHost authority B3,E1,E2,E3,E4,J1 E16: root first- |
| internal/release/adopt.go settingsTarget authority E3 Settings target rejects foreign consumed decisions and forces root-owned settings rebind to the canonical tenant.;rev3=PROPOSED-p3;run=8;identity=carlos adopt uid 0; legacy agent pass uid carlos;basis=census branch delegates target selection to root inputs before any legacy consumed, good-version or build-state read registry:App@2977,registry:App@3077,registry:App@3362,registry:BuildState@2680,registry:GoodVersion@3387,registry:Host@1313,registry:Host@1723,registry:Host@2241,registry:Host@2488,registry:Host@2777,registry:Host@3373,registry:Host@3785,registry:Host@429,registry:Host@4440,registry:Host@4628,registry:Host@702,registry:UpdateSettings@556,whole:Route@1187,whole:Route@2277,whole:Route@2284,whole:Route@2842,whole:Route@2849,whole:Route@301,whole:Route@495,whole:Route@513,whole:Route@658 420982c2cbde642ae3008b61f93dc514defb527c05b20203864de8d052621599 |
| internal/release/adopt.go type passConfig authority E2,E3 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree.;rev3=PROPOSED-p3;run=8;identity=carlos adopt uid 0; legacy agent pass uid carlos;basis=stores the typed census-only root update provider and per-pass resolved input authority 7a405d8b9a24ccb4c9f09f91ca468f74a1e9b4a8b960b430dc3a6290a6b310cd |
| internal/release/rootgenerationpins.go RootGenerationPins.StagedPins authority E2 rev3=PROPOSED-p3;identity=root release retention and adoption uid 0;basis=checks the recorded boot before any legacy file identity comparison; cross-boot release pins require persistent executable and tree evidence;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures 0bcf3e831097fd3d1e2f3a58fd1e0707f98e04dc058fbea1f923b073c3691d50 |
| -internal/release/rootgenerationpins.go generationFileIdentity authority E2 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree.;rev3=PROPOSED-p3;run=8;identity=root release retention uid 0;basis=persistent filesystem and handle evidence supersedes boot-local device equality for new root generation records;rev3=PROPOSED-p3;run=8;identity=root release retention uid 0;basis=persistent filesystem and handle evidence supersedes boot-local device equality for new root generation records;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures file:Lstat@659,file:Open@785 38404f3b434b829ad551344259dcf9e7cadac906dcda262ca1eb4bd9d95ff824 |
| +internal/release/rootgenerationpins.go generationFileIdentity authority E2 rev3=PROPOSED-p3;run=25;identity=root generation activation, retirement and release readers uid 0;basis=both generation consumers require the same persistent filesystem/handle/inode/type/owner/mode and filesystem-relative source path across private and host mount layouts; same-boot device stays exact; root-owned ancestry remains independently checked by each opener file:Lstat@681,file:Open@807 fb5a8d7a801a2451e108d8f16933e3e17693989665345596c7c21ae082d3047d |
| internal/release/rootgenerations.go type GenerationBinding authority E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. PROPOSED E47: root-owned immutable unit/shape binding, reserved launch placement and exact invocation stop; retained generation binding carries the canonical unit. Default unit paths remain unchanged. Production named-route wiring and unit/env/timer retirement are not complete.;rev3=PROPOSED-p3;run=8;identity=root generation journal uid 0;basis=additive persistent executable and tree fields preserve historical device and inode records;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures fffc440df59bd9833eaaaaa609db352eb99f15f2938e36228ef78c78c91b8fe7 |
| internal/release/rootupdates.go RootUpdateInputs.routes authority E3,J1 rev3=PROPOSED-p3;run=8;identity=carlos adopt uid 0;basis=uses registry hostname only as selector and replaces protected dispatch fields with independently resolved root inputs registry:Host@308,registry:Host@365,registry:Host@382,registry:Host@481,whole:Route@244,whole:Route@273,whole:Route@368,whole:Route@499,whole:Route@512,whole:Route@520,whole:Route@538 c75f2735786a9185baf8c54ea14a6c2adbe387a84cfec92585faba2163d95a67 |
| internal/release/rootupdates.go RootUpdateInputs.target authority E3 rev3=PROPOSED-p3;run=8;identity=carlos adopt uid 0;basis=re-resolves canonical route before selection and persists root request decision before permitting adoption registry:Account@747,registry:Account@857,registry:App@759,registry:App@872,registry:Channel@767,registry:Host@248,registry:Host@341,registry:UpdateSettings@467,whole:Route@364,whole:Route@382,whole:Route@470,whole:Route@571,whole:Route@581 cb946ee06c8c53e26c13bb910f9a6d9c526b593a8b0d4fed7be1f42275faa727 |
| @@ -253,7 +253,7 @@ internal/host/rootgeneration_parked.go generationRetirements authority E2 rev3=P |
| internal/host/rootgeneration_payload_linux.go generationPayloadFiles authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=verifies exact retained persistent file identity; on a cross-boot ancestry refusal permits only the configured carlos owner at the managed root itself, retaining root-owned protected ancestors and refusing writable roots 58e62ff06eba73b12b0e892d862a338dafb62170a47a0abc7e92989f5f11b0d7 |
| internal/host/rootgenerations_launch.go rootGenerationSource.reserve authority E2,E3 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=writes root runtime intent after the generation reservation and before launch under release and host exclusion b09d7390279bcb302d83c282a517680f43c44013e07fc490f37bc798fd1703ba |
| internal/host/rootgenerations_launch.go type generationLaunchPermit authority E2,E3 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=retains the root runtime completion and close callbacks for the reserved launch 964b8f1ea86c1a85f8a60d5d87168d71375e9c835e2c64859267dc822265c135 |
| -internal/host/rootinventory.go ClaimRootHTTP.commitRootInventoryPending authority E2 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=canonicalizes persistent evidence under root exclusion while preserving the original request digest and durable replay;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures file:Read@1454,file:Read@3494,registry:Account@4858,registry:Account@4873,registry:App@4894,registry:App@4905,registry:ClaimIncarnation@4364,registry:ClaimOwner@4414,registry:ClaimOwner@5544,registry:ClaimOwnership@4956,registry:ClaimOwnership@4978,registry:ClaimOwnership@5860,registry:ClaimOwnership@8042,registry:ClaimRowID@4578,registry:ClaimRowID@4655,registry:ClaimStamp@4392,registry:DBPath@4922,registry:DBPath@4936,registry:Host@4828,registry:Host@4840,registry:Host@8518,registry:Name@8541,whole:ClaimOwnership@4960,whole:ClaimOwnership@4988,whole:ClaimOwnership@5870,whole:ClaimOwnership@8053,whole:Route@10308,whole:Route@3686,whole:Route@4069,whole:Route@4085,whole:Route@4140,whole:Route@4791,whole:Route@4804,whole:Route@4813,whole:Route@5051,whole:Route@5283,whole:Route@6117,whole:Route@6253,whole:Route@6477,whole:Route@7886,whole:Route@9573,whole:Route@9634,whole:Timer@8526,whole:Timer@8549 18fcea255a124e5c970c32d943c960836602dd12d9f00669583d8b4bd72fe7e6 |
| +internal/host/rootinventory.go ClaimRootHTTP.commitRootInventoryPending authority E2 rev3=PROPOSED-p3;run=25;identity=root instance-control uid 0;basis=new-host empty census classifies sibling state only by exact root installation and payload revision with persistent identity, and sibling live links only by matching root generation reservation; unknown and selected-host artifacts refuse file:Read@1454,file:Read@3494,registry:Account@4858,registry:Account@4873,registry:App@4894,registry:App@4905,registry:ClaimIncarnation@4364,registry:ClaimOwner@4414,registry:ClaimOwner@5544,registry:ClaimOwnership@4956,registry:ClaimOwnership@4978,registry:ClaimOwnership@5860,registry:ClaimOwnership@8063,registry:ClaimRowID@4578,registry:ClaimRowID@4655,registry:ClaimStamp@4392,registry:DBPath@4922,registry:DBPath@4936,registry:Host@4828,registry:Host@4840,registry:Host@8527,registry:Name@8550,whole:ClaimOwnership@4960,whole:ClaimOwnership@4988,whole:ClaimOwnership@5870,whole:ClaimOwnership@8074,whole:Route@10317,whole:Route@3686,whole:Route@4069,whole:Route@4085,whole:Route@4140,whole:Route@4791,whole:Route@4804,whole:Route@4813,whole:Route@5051,whole:Route@5283,whole:Route@6117,whole:Route@6253,whole:Route@6477,whole:Route@7919,whole:Route@9582,whole:Route@9643,whole:Timer@8535,whole:Timer@8558 b00707c1c5a3b1f3b51fd573fbb09a176bda8a055e64232b9044b7c78640ba51 |
| internal/host/rootinventory.go ClaimRootHTTP.verifyInventoryPath authority C,J1 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=uses checked persistent evidence for protected installation entries; explicit legacy fixtures preserve their same-boot check;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures file:Lstat@315 2ee5519ed6e8ff27bcfdb5db7748a71f4f331a271d84d7688835798792824a78 |
| internal/host/rootinventory_identity.go ClaimRootHTTP.bindInventoryPaths authority C,E2,J1 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=discards all caller-supplied persistent evidence and captures or revalidates it independently against the retained root inventory;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=discards caller proof, captures new objects independently, retains immutable prior proof, and verifies cross-boot ancestry through the shared inventory verifier;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=requires the proposed boot to equal the independently observed boot for new materialization, discards caller filesystem proof, captures new objects independently, retains immutable prior proof, and verifies cross-boot ancestry through the shared inventory verifier 73c94e6482dafa576ac607ba4a54a83921febf0f2092361e88a259a884db75d7 |
| internal/host/rootinventory_state_linux.go ClaimRoleAssembly.prepareStateInventory authority C,J1 rev3=PROPOSED-p3;identity=carlos agent claim lifecycle uid carlos; final authorization remains in root;basis=uses root-supplied persistent state identity for remote inventory reuse while keeping agent proposals non-authoritative;rev3=PROPOSED-p3;run=11;identity=carlos agent uid carlos; root validates final inventory and launch;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures file:Fstatat@1313,file:Fstatat@2304,file:Open@799,file:Openat@1086 c5355a26fa146eb0ddb4cc7c1e536de5781e171d52363a0be38ffb2074651a53 |
| @@ -290,8 +290,8 @@ internal/caps/converge.go Restart authority E4,J4 rev3=PROPOSED-p3;run=10;identi |
| @coverage internal/caps 0502b0ee74b47a1a7b37e538e1c1c0ff30b50d08b5ec7e780b7429bf094c5eb6 |
| internal/caps/rootauthority.go Ops.authorizeMutation authority E4,J4 rev3=PROPOSED-p3;run=10;identity=root adopt uid 0;basis=refuses legacy unit capability mutation on any retained census or unreadable census; root configuration stays fixed; absence preserves legacy operations file:Lstat@55 5549b2317e66dda41d73b23216bfca7b18ab1814607140794eb40124feae3618 |
| internal/host/claimactivation_preflight.go claimActivationPreflight authority G,J4 rev3=PROPOSED-p3;run=13;identity=agent uid carlos; root migration/adopt/control uid 0;basis=service-owned registry contributes refusal only; claimActivationVerified separately checks canonical records and root retained unit state registry:Backing@280,registry:Unit@260,whole:Route@246 e5713a34359330ef311f31c47e0342f8322471243a7aba61fdecc751a53f34b4 |
| -internal/host/rootgeneration_kernel_linux.go generationCurrentFiles authority E2 PROPOSED E51: Root reopens the canonical executable through trusted ancestry and its containing tree; persistent proof rebinds only the transient scanner copy to current devices. Historical pins without proof refuse across boots.;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=uses E persistent executable and tree proof; legacy numeric tuples require a nonempty equal current boot, never unknown or other-boot numbers file:Open@219 1c1a5ebdd68091af35b8e26cf07d20e623b54e7109a64cffd5a55017fd721fa4 |
| -internal/rootstate/state.go FileIdentity.Same authority E2 PROPOSED E51: Persistent equality requires filesystem, handle, inode, ownership, mode and mount topology; device/mount numbers compare only on the same boot. PROPOSED E51: Persistent equality requires filesystem, handle, inode, ownership, mode and mount topology; device/mount numbers compare only on the same boot. PROPOSED E51 clarification: within one boot devices remain exact; mount IDs compare within one mount namespace. A root namespace clone still requires identical filesystem, handle, inode, ownership and mount topology.;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=requires boot and persistent filesystem, handle, inode, mode, ownership and attachment proof; same-boot device identity remains exact, mount numbers are local to the mount namespace; missing boot never grants equality 478f64ca9e22e9ed33b655541ccb487289405a1b817b3789a6771650d3d0b7b4 |
| +internal/host/rootgeneration_kernel_linux.go generationCurrentFiles authority E2 rev3=PROPOSED-p3;run=25;identity=root retirement and activation readers uid 0;basis=private controller and host mounts may expose the same canonical generation path; require equal filesystem-relative source path plus persistent filesystem/handle/inode/type/owner/mode and same-boot device, never a pathname alone file:Open@219 93510e08a5530962857316b1024a169891d2406c29a80df9b8e45d2c0785c9a6 |
| +internal/rootstate/state.go FileIdentity.Same authority E2 rev3=PROPOSED-p3;run=25;identity=root claim, adoption or recovery controller uid 0;basis=retained filesystem, handle, inode, owner, mode and mount source/path remain exact; same-boot devices remain exact; mount numbers and namespace inode names are observations rather than durable object identity, including when a restarted private namespace reports the same namespace name 750c3a84f35450314ddca65fedbb9beb622d1c96b4e833c3a7d8d4e88dbfec1e |
| @removed internal/host/rootgeneration_kernel_linux.go generationCurrentBinding A,E2,G census-ruling-J4 |
| @remove internal/host/rootgeneration_kernel_linux.go generationRetainedFiles |
| @removed internal/host/rootgeneration_kernel_linux.go generationRetainedFiles A,E2,G census-ruling-J4 |
| @@ -346,7 +346,7 @@ internal/host/claimtoken_directory.go ensureClaimLaunchToken authority E2 rev3=P |
| internal/host/rootexecution_client.go productionClaimLocal.acknowledgeOperation authority C rev3=PROPOSED-p3;run=18;identity=agent uid carlos;basis=publishes only the required participant own active root-client tuple under claim CAS; checks host incarnation operation box enrollment and leaves existing acknowledgement immutable 12c251703e5b06937af6c993068b45dd41dd8304a2b1d543eaa213dc56cfb4d9 |
| internal/hostbox/census_vendor_linux.go censusVendorUnitLink authority G,C rev3=PROPOSED-p3;run=18;identity=root migration/control uid 0; agent census uid carlos;basis=classifies only root-owned trusted systemd links to checked regular vendor units; rejects CARLOS names, untrusted ancestry, foreign targets and nested link paths; non-Linux stub only refuses file:EvalSymlinks@867 c4d781b5c4ed60bd16e7d20db119ba6029c039fe60bbacba0219c407deb3ee0d |
| internal/hostbox/census_vendor_other.go censusVendorUnitLink authority G,C rev3=PROPOSED-p3;run=18;identity=root migration/control uid 0; agent census uid carlos;basis=classifies only root-owned trusted systemd links to checked regular vendor units; rejects CARLOS names, untrusted ancestry, foreign targets and nested link paths; non-Linux stub only refuses 0b72813762dc54469d67e63f8e9f7e29fd2dec51855cbdbf82899a340ad6b275 |
| -internal/hostbox/empty_census.go EmptyInstallationCensus.checkLocal authority C,G rev3=PROPOSED-p3;run=18;identity=root control uid 0; agent census uid carlos;basis=permits exact selected zero-stamp creating participation without installation authority; known trusted OS unit links no longer make a fresh installation appear occupied; host-scoped and unknown artifacts still refuse file:Exec@1153,file:Lstat@2914,file:WalkDir@3161,registry:ClaimOwnership@2173,registry:Host@2120,whole:ClaimOwnership@2177,whole:ClaimOwnership@2195,whole:Route@2108,whole:Timer@2337 0e7b667d0b7acf5c93dbc5f3f7aec1254a7cf70778d322cc751762074c738085 |
| +internal/hostbox/empty_census.go EmptyInstallationCensus.checkLocal authority C,G rev3=PROPOSED-p3;run=25;identity=root instance-control uid 0; agent fallback uid carlos remains strict;basis=new-host empty census classifies sibling state only by exact root installation and payload revision with persistent identity, and sibling live links only by matching root generation reservation; unknown and selected-host artifacts refuse file:Exec@1153,file:Lstat@2914,file:WalkDir@3161,registry:ClaimOwnership@2173,registry:Host@2120,whole:ClaimOwnership@2177,whole:ClaimOwnership@2195,whole:Route@2108,whole:Timer@2337 13faf3934b6e2a1f843fc96bc2f62de779e88ca1dc93e07b2a02354c5c76eab8 |
| internal/hostbox/reconcile.go Reconciler.installWithWork authority C rev3=PROPOSED-p3;run=18;identity=agent uid carlos;basis=settles participation before activation and inventory creation; existing selected write checks still revalidate before each materialization file:Exec@1240,file:Prepare@1679,file:Prepare@1710,file:Read@392 d0ef9a8b5e5231acd18599287b4a44c0c934f8f9df68e6b799e3dbbadab5133d |
| internal/hostbox/reconcile.go type Reconciler authority C rev3=PROPOSED-p3;run=18;identity=agent uid carlos;basis=production preparation callback receives the observed claim before inventory activation; it grants no write by itself and failures stop installation file:Prepare@109 03c51a76fcfea2c8aef269488a9f69096713723a5c79fc96ceb813a9cafd8e0e |
| internal/hostexec/participation.go PreinstallationParticipant authority D rev3=PROPOSED-p3;run=18;identity=root execution authorizer uid 0;basis=admits participation only for the exact sole selected creating executor with zero stamp/session and no writer; does not grant installation or launch authority file:Exec@1412,file:Exec@1684,file:Exec@276,file:Exec@507 2107f8a06975dfaccb7b6f1395dee2db670a5895532e8f01988b0b7ce2036a97 |
| @@ -365,8 +365,8 @@ internal/hostguard/recoveryfence.go Guard.PendingStopFences authority C,J4 rev3= |
| internal/hostguard/recoveryfence.go Guard.pendingFences authority C,J2,J4,J1 rev3=PROPOSED-p3;run=19;identity=root;basis=reads root-owned records under host lock and keeps exact barrier classification separate from ordinary stop recovery; legacy behavior retained 20be9535fd54587a54e3027f5388581ae5a64e5fcfced5a6031ffc9a4fc76a3b |
| internal/host/claimroleassembly_linux.go verifiedClaimExecutable authority C,G,J4 rev3=PROPOSED-p3;run=19;identity=agent uid carlos;basis=pins root-owned executable bytes and keeps the verified descriptor while supplying its absolute path to the root launcher file:Fstat@1122,file:Fstat@579,file:OpenFile@376,file:Read@949 7a7e9af520e4157e60ae93d12c3268a629a9b0bd1e83b263cdd27103cf2e227c |
| internal/host/claimroleassembly_linux.go claimExecutableName authority C,G,J4 rev3=PROPOSED-p3;run=19;identity=agent uid carlos;basis=duplicates the existing descriptor with CLOEXEC and its validated absolute name; never reopens a potentially replaced path e887880731a16c9e7934aef33a08169b34e7236f6d3b85a195602d35f8b69107 |
| -@coverage internal/host 60676a836eb61ff244de77b770fd0baa49bb5b8554a0558b96e5d78481fb7fb6 |
| -@coverage internal/hostbox 98d6c395a50aa809e0a63c0fa2ccf4463dde57ee89eab90dff79a243f245305f |
| +@coverage internal/host b81380b5d103ae9aef7caeb8ee7aae0604be9d19c418aaf0324ee80fff5497d2 |
| +@coverage internal/hostbox 0b6ddb07bc7ef62f23e402757155825e8d098eb47ec9288c09b8754bb1632e56 |
| @coverage internal/hostguard ca664497969680d692172abda403b6a5d6664f8d7c8bc16a6440f02dd8426be6 |
| internal/host/claimstop.go ClaimStopCoordinator.FenceAndStop authority C,J4 rev3=PROPOSED-p3;run=20;identity=root instance-control uid 0;basis=authenticated session stop retains root fence before independent unjournaled absence check; proves no journaled installation, containment or open files before bypassing nonexistent writer closure; normal drain remains for recorded launches; no-census unchanged file:Read@1610 58147f3abbf278037fc660392aeec0b6e76b667e3fe8435845ebaedc62d2ead1 |
| internal/host/claimstop_recovery.go ClaimStopCoordinator.RecoverStopped authority C,J4 rev3=PROPOSED-p3;run=20;identity=root instance-control uid 0;basis=pending exact root fence and exact installation inventory permit recovery through independent absence verification when no launch journal was created a91b528d9f9259612c767bf0a94e0ff7664294668f29f584a681a1a0f656ad90 |
| @@ -417,14 +417,14 @@ internal/host/claimordinaryparks.go productionClaimRegional.authorizeOrdinaryRet |
| internal/park/sweep.go protected authority C,E2,J4 rev3=PROPOSED-p3;run=21;identity=claim regional worker service UID under root stop or signed retirement evidence;basis=ordinary view distinguishes independently retained file versions from database generation pins; only explicit file namespace pins receive per-version protection af0ffc56bb49f87738a94350c4c92305a8b922ede0587bc8fad4bb79b10f20d0 |
| internal/park/remote.go Engine.selected authority C,E2,J4 rev3=PROPOSED-p3;run=21;identity=claim regional worker service UID under root stop or signed retirement evidence;basis=ordinary generation retirement excludes exact live file versions from fresh selection and revalidates on manifest replay; replay selection mismatch refuses deletion 43dd4b71ef97868599cf56560a458026ac77bada65f75b314630c436e57112ca |
| @coverage internal/park b611c74d7f75c985a1546f0b79b2f7db79312a3d3d2450981c5184c04c8e324e |
| -internal/host/rootexecution_authority.go ClaimRootHTTP.executionAuthority authority C,E2,J4 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=independently reads canonical claim and record before root registration; a valid preinstallation over an exactly retired root hostname returns the deferred-reinstall refusal without mutating retained evidence; retained move-source authority remains separate file:Exec@1657,file:Read@553 cca1bc79b5efe35afbda9df8b61e46ae2644bf99ed5234da3ae8d88fcc4a872d |
| -internal/host/rootexecution_settle.go ClaimRootHTTP.cleanupExecution authority C,E2,J4 rev3=PROPOSED-p3;run=22;identity=root execution broker uid 0;basis=canonical root stop receipt and irreversible retired installation plus closed launch journal discharge already-completed cleanup after cgroups vanish; live sessions retain fresh kernel checks and remote claim settlement remains mandatory file:Read@2928,file:Read@4251 3a7091afec06f61182a80bd59475d5005296ac1ac06a1e9c9ab6d48bd9a24810 |
| +internal/host/rootexecution_authority.go ClaimRootHTTP.executionAuthority authority C,E2,J4 rev3=PROPOSED-p3;run=25;identity=root instance-control uid 0;basis=new-host empty census classifies sibling state only by exact root installation and payload revision with persistent identity, and sibling live links only by matching root generation reservation; unknown and selected-host artifacts refuse file:Exec@1657,file:Read@553 d4c3831803029792b9bdc5d1a98049fe31a4cbca24dc3f09aac768533cd8ffee |
| +internal/host/rootexecution_settle.go ClaimRootHTTP.cleanupExecution authority C,E2,J4 rev3=PROPOSED-p3;run=25;identity=root instance-control uid 0;basis=new-host empty census classifies sibling state only by exact root installation and payload revision with persistent identity, and sibling live links only by matching root generation reservation; unknown and selected-host artifacts refuse file:Read@2928,file:Read@4252 2aeade2e9ab8cf2782b5f5c1b054504310e73dc1139747f38539014941fcd9e0 |
| internal/hostclaim/delete.go ToParking authority C,E2,J4 rev3=PROPOSED-p3;run=21;identity=production console or local regional worker using authenticated operation;basis=console ordinary delete transition under claim CAS; sets the previously empty ordinary deletion phase to park so exact root stop tuples can retire it; existing nonempty phases retained 9d5d3cf3fd1255e4a3cda50cedcd65197f65155ea4bfa57d50670ebad0e5492a |
| internal/hostbox/renewing.go renewingHeld.Release authority C,E2,J4 rev3=PROPOSED-p3;run=21;identity=production console or local regional worker using authenticated operation;basis=regional park worker release of held lease; cancels work immediately then issues exact conditional lease deletion under detached request bounded cleanup context; grants no new work 0cfd55e1a0f40851dd27be382187fc01cfab5dc2bca54262966f6a66a2799e39 |
| @coverage internal/hostclaim 5fc93a54af58686466e4f5f61f42aed8bb3e7d75038ecf48dc5faafa9e3ee83c |
| internal/host/claimstop_producers.go ClaimProducers.DrainClaim authority C,E2,J4 rev3=PROPOSED-p3;run=21;identity=authenticated local writer controller answering root stop;basis=only a fully joined and compacted drain may acknowledge a new fence for the same exact writer; unfinished drains and foreign tuples refuse; producer admission remains closed a3633b7f153cf84f565d3d6d19db1e38eba5f578de57b9a65900fd74a837c5cf |
| internal/hostbox/typedpark_path.go TypedParker.ParkedArtifactRoot authority C,J2,J4 rev3=PROPOSED-p3;run=21;identity=root retirement or generation verifier uid 0;basis=only configured root or recorded strict descendant mount containing artifact; malformed or unrelated mount paths refuse;basis=recorded mount relocation only for CapturePersistent root-protected artifacts; legacy destinations unchanged e4fe742ac8e421856f66729d888e81f828f8679fee6df93a310e0948d34cca48 |
| -internal/host/claimparticipation.go productionClaimLocal.cleanup authority C,D rev3=PROPOSED-p3;run=22;identity=agent uid carlos;basis=census-only exact retired-installation discharge precedes stale-session stop; authenticated root retirement receipt permits local close, root broker independently checks cleanup and settlement d77ed95879a026037604ce9024dc9c614d5d36597b26ff2503d6858d41c0da71 |
| +internal/host/claimparticipation.go productionClaimLocal.cleanup authority C,D rev3=PROPOSED-p3;run=25;identity=agent uid carlos;basis=agent closes admission and consults root launch inventory; with no recorded launch the remote client defers the filesystem census to RootBroker settlement, whose cleanupExecution independently repeats the root census; legacy and local-journal paths retain their original checks 192f8d91f40338dd304cef77e0394d971685e15142917ba1e70d606c28a6d859 |
| internal/host/claimparticipation_retired.go ClaimAssembly.completedRetirementDischarge authority C,D rev3=PROPOSED-p3;run=22;identity=agent uid carlos;basis=authenticated root fence view and exact canonical retirement receipt bind host incarnation stamp session operation box and enrollment; missing or substituted receipt refuses discharge and root settlement retains kernel checks 75b827cf499f524ead84c6824c328f7ac702854c53a8570dfdc0df0bbc168df8 |
| internal/hostclaim/delete.go FreeAfterPark authority C rev3=PROPOSED-p3;run=22;identity=authenticated console or lifecycle worker through existing claim CAS;basis=conditional holder removal after all parking obligations finish; preserves native loss evidence atomically before removal and keeps the signed-declaration archive gate 76f88d4f0f3bab8094a9426d8d809d8c8b25d8aede5915aaa43f78b0767eaea7 |
| internal/hostclaim/retainedloss.go retainNativeAdmissionLosses authority C rev3=PROPOSED-p3;run=22;identity=authenticated console or lifecycle worker through existing claim CAS;basis=copies exact native admission evidence and original holder identity into claim-retained state; does not certify losses or relax signed-declaration archival 278b599c5e822d63a2c50bb2e0ba563371564cbdfb9c05531d7efe223aa26d3f |
| @@ -459,7 +459,7 @@ internal/host/rootgeneration_kernel_linux.go type generationKernel authority E2 |
| internal/host/rootgeneration_kernel_other.go productionGenerationAbsence authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=checks recorded launch targets and controller absence then inspects all tasks with independently verified service classification de47d997019075326e1d5513d7576390d085566fd69edc35efd00f067fb1db24 |
| internal/host/rootgeneration_retired_linux.go generationRetiredController authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=for an exact retired isolated installation, an old controller may still serve another host only after its boot/start identity and all thread references to this target generation are independently checked 793dfb40f339824ca3aeb9ceecfde69d8b63452eadf1a8abda734722401f1b79 |
| internal/host/rootgeneration_services_linux.go generationServiceGroup authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=accepts exact systemd cgroup or root-owned non-writable descendants only; delegated service-owned subgroups cannot impersonate platform or system services file:Lstat@281 b82ebaadda3ee8a36a66d69c36dda2551329197b903c09875b41fc0bbff44b38 |
| -internal/host/rootgeneration_services_linux.go generationServiceReader authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=checks kernel cgroup against systemd ControlGroup and MainPID, root-owned unit and drop-ins, and root-protected executable inode; nonroot executable must match ExecStart; managed tenant groups and unexplained service subgroups refuse file:CommandContext@919,file:EvalSymlinks@1633,file:Readlink@2131 38c4656a7fca9e16865418b835cc8344c3eff99899713a0095d6a51ad4daa6ef |
| +internal/host/rootgeneration_services_linux.go generationServiceReader authority E2 rev3=PROPOSED-p3;run=25;identity=root generation kernel reader uid 0;basis=systemd main PID and trusted unit/executable identify only the edge main process threads in its exact supervise subgroup; unknown child processes and other services remain refused; does not exempt descriptor or generation-reference checks file:CommandContext@919,file:EvalSymlinks@1633,file:Readlink@2228 a25956a39883aa0e392068f715a99d7d86e993c9ec6c2fa7df6e3d5bed07964f |
| internal/hostguard/rootretired_host.go RootAuthority.HostRetired authority C rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=refusal-only host-locked read of the exact retained installation, inactive stamp and retired tuple; quarantine, pending installation and barrier refuse; no evidence is rewritten e9e93474a629358ab5dda2d845b2571e913d5a7ec14beb544851f55690b17b1b |
| internal/host/rootgeneration_services_linux.go generationServiceExecutable authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=opens the kernel-selected process root and checks each executable-path component with descriptor-relative no-symlink traversal, root ownership and no group/world writes; final single-link regular inode must equal the running executable; private controller mounts cannot hide a legitimate host executable file:Fstat@576,file:Open@243,file:Openat@1123 b0380d86d61198c94f826dc48df3d46f1352dd983279b75387dbbe2b51cca59f |
| internal/host/rootgeneration_payload_other.go generationPayloadFiles authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=verifies exact retained persistent file identity; on a cross-boot ancestry refusal permits only the configured carlos owner at the managed root itself, retaining root-owned protected ancestors and refusing writable roots candidate:Kind@183,candidate:Kind@204 182189d8409113d671cdc76fc5d2d73e7056057bca230d546e65300c428b2c18 |
| @@ -471,3 +471,17 @@ internal/hostbox/inventory_identity_other.go VerifyInventoryIdentity authority E |
| internal/host/rootgeneration_census_linux.go generationCensusRetry authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=restarts the entire independent kernel census at most three times only when a process disappears; unknown tasks and permission failures refuse immediately, and no partial census is accepted 3cfd54f395ed7e77b15c01b76567de0188573603aafc4e7ec72f0c3cfb9d512f |
| internal/host/rootgeneration_requirements.go generationLocalObligations authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=requires retained route and state artifacts; a current route-only installation without predecessors or any launch is unlaunched only after exact root FreshInstallation proof and physical state-directory absence, recomputed on every read file:Lstat@3018 1a5c493c0a6383df880d4d6e730f6d01822559cab804b57ac445e36bb4535f3c |
| internal/host/rootgeneration_requirements.go generationUnlaunched authority E2 rev3=PROPOSED-p3;run=24;identity=root claim/adopt/update controller uid 0;basis=requires absence of every root launch for this hostname, selects the earliest retained stamp of this same incarnation, and performs one durable unconsumed-creation read; any read error refuses; empty installations keep their existing path; consumers independently validate transfers and physical state 8041de414e9b90f044066c4cad51e7e8b43dea858a4e2ae6eb6f22418466a720 |
| +internal/host/rootinventory_sibling.go ClaimRootHTTP.rootSiblingPath authority C,G rev3=PROPOSED-p3;run=25;identity=root claim, adoption or recovery controller uid 0;basis=classifies only another root-recorded installation with an exact payload revision and persistent directory identity; private bind source must map to the same filesystem path and object; live/previous links must name a retained root generation for the same host, incarnation, box, account and app file:Open@1911,file:Readlink@2161 0ead075a56726415017bbbb5db140025b54faf3af387309c4ef86b8bd0b934a4 |
| +internal/host/claimservingreboot_binding.go servingParkReadBinding authority C,G rev3=PROPOSED-p3;run=25;identity=root instance-control uid 0;basis=persistent filesystem, handle, inode, owner and topology evidence authorizes mount-namespace rebinding; same-boot device substitution refuses; original retirement identities stay immutable; preparation is keyed by the observed mount and repeat reads precede each binding write file:Read@111 e15d6fa67724d7dc031c33cd72c0e167d36de7bd78af15954248700b44dcd3cb |
| +internal/host/claimservingreboot_commit_linux.go servingParkLedger.commitRebootLocked authority C,G rev3=PROPOSED-p3;run=25;identity=root instance-control uid 0;basis=persistent filesystem, handle, inode, owner and topology evidence authorizes private-namespace rebinding; device substitution within a boot refuses; immutable source identities and repeated observations constrain namespace and record commits file:Read@246 0bcb3022797dbc3bbf3330633b55cd2b797a8e02122c7f1abcc287f7234c1922 |
| +internal/host/claimservingreboot_linux.go servingParkFS.observeRebootRecord authority C,G rev3=PROPOSED-p3;run=25;identity=root instance-control uid 0;basis=persistent filesystem, handle, inode, owner and topology evidence authorizes mount-namespace rebinding; same-boot device substitution refuses; original retirement identities stay immutable; preparation is keyed by the observed mount and repeat reads precede each binding write file:Readdirnames@3147 2f806025a2a308f545188aa2dbc6d8cd2f178d51711ba68beb16ef4da94aa746 |
| +internal/host/claimservingreboot_prepare_linux.go servingParkLedger.prepareRebootLocked authority C,G rev3=PROPOSED-p3;run=25;identity=root instance-control uid 0;basis=persistent filesystem, handle, inode, owner and topology evidence authorizes private-namespace rebinding; device substitution within a boot refuses; immutable source identities and repeated observations constrain namespace and record commits file:Read@621 ce54be50398b3a3eaede02d240e9a9c58515168f5195bd498b788041658ae6a5 |
| +internal/host/claimservingtopology_linux.go servingParkFS.inspectRebootTopology authority C,G rev3=PROPOSED-p3;run=25;identity=root instance-control uid 0;basis=persistent filesystem, handle, inode, owner and topology evidence authorizes mount-namespace rebinding; same-boot device substitution refuses; original retirement identities stay immutable; preparation is keyed by the observed mount and repeat reads precede each binding write file:Read@501,file:Read@936 9d4612c414ea82f4f20723bb0e2dbca8963e93ca377b159527a39ea861e59870 |
| +internal/host/claimservingpark_recovery_linux.go servingParkRoot.recoverServingParks authority C,G rev3=PROPOSED-p3;run=25;identity=root instance-control uid 0;basis=persistent filesystem, handle, inode, owner and topology evidence authorizes private-namespace rebinding; device substitution within a boot refuses; immutable source identities and repeated observations constrain namespace and record commits b1a49869990546ea6205c69db52c45d4f35fa747f3a1e1780c9b548c4efdd68f |
| +internal/host/rootinventory_sibling.go rootSiblingAlias authority C,G rev3=PROPOSED-p3;run=25;identity=root instance-control uid 0;basis=compares the fixed public and private instance paths through no-symlink capture; requires equal current boot/device/namespace, identical filesystem path under the mount sources, persistent handle, inode, mode and ownership 1f48a064a1a54fbfd5969aa48f12f3a47711070eba5e91fcc287d950121c4608 |
| +internal/host/rootinventory_sibling.go ClaimRootHTTP.rootEmptyCensus authority C,G rev3=PROPOSED-p3;run=25;identity=root instance-control uid 0;basis=copies the configured census and adds root-only sibling classification for execution admission, settlement and inventory registration; no agent-owned registry authority ca59fff522b539f198e6aa8c56cac120014c0edfb072bb6d1044760fec13cb47 |
| +internal/host/claimservingreboot_prepare_linux.go servingNamespacePreparationName authority C,G rev3=PROPOSED-p3;run=25;identity=root instance-control uid 0;basis=keys durable preparation by boot and observed mount so repeated same-boot controller namespace recovery cannot collide with a prior preparation; all authority and persistent observations remain checked by the prepare/commit callers 01de715e16cb00830f17999f497b1c85a202f236609f5306aeed5840001a44a0 |
| +internal/host/rootgeneration_kernel_linux.go generationFileProof authority E2,G rev3=PROPOSED-p3;run=25;identity=root generation activation, retirement and release readers uid 0;basis=both generation consumers require the same persistent filesystem/handle/inode/type/owner/mode and filesystem-relative source path across private and host mount layouts; same-boot device stays exact; root-owned ancestry remains independently checked by each opener 5ab08e86bf9dcaef527b0751068dbf838f869115c630ead17c0bc866b47a17d5 |
| +internal/host/rootgeneration_services_linux.go generationServiceSupervisor authority E2,G rev3=PROPOSED-p3;run=25;identity=root generation kernel reader uid 0;basis=systemd main PID and trusted unit/executable identify only the edge main process threads in its exact supervise subgroup; unknown child processes and other services remain refused; does not exempt descriptor or generation-reference checks 62fcbfa690841ecc68ed935cae404a75b0d4f3b533af871af64a9ff19586d85b |
| +internal/release/rootgenerationproof.go GenerationFileProof authority E2 rev3=PROPOSED-p3;run=25;identity=root generation activation, retirement and release readers uid 0;basis=both generation consumers require the same persistent filesystem/handle/inode/type/owner/mode and filesystem-relative source path across private and host mount layouts; same-boot device stays exact; root-owned ancestry remains independently checked by each opener 1f61004a6273ecd4b92d5cdb31b06d4e1cf1ff57f668131ea9b2d0c57b5b4f02 |
| +internal/release/rootgenerationproof.go generationProofUnescape authority E2 rev3=PROPOSED-p3;run=25;identity=root generation activation, retirement and release readers uid 0;basis=both generation consumers require the same persistent filesystem/handle/inode/type/owner/mode and filesystem-relative source path across private and host mount layouts; same-boot device stays exact; root-owned ancestry remains independently checked by each opener 744d84125ad095053c32cdbfe83632b284d222ada237fa3dca5b5c9f5f307656 |
diff --git a/internal/release/plan14_generation_boot_test.go b/internal/release/plan14_generation_boot_test.go| index 362c2edbd..49bdf8057 100644 |
| --- a/internal/release/plan14_generation_boot_test.go |
| +++ b/internal/release/plan14_generation_boot_test.go |
| @@ -3,6 +3,7 @@ |
| package release |
| import ( |
| + "golang.org/x/sys/unix" |
| "os" |
| "path/filepath" |
| "testing" |
| @@ -37,3 +38,40 @@ func TestPlan14P3GenerationLegacyPinBootBoundary(t *testing.T) { |
| } |
| } |
| } |
| + |
| +func TestPlan14GenerationPinsPrivateMount(t *testing.T) { |
| + const path = "/usr/bin/true" |
| + for _, p := range []string{"/", "/usr", "/usr/bin", path} { |
| + var st unix.Stat_t |
| + if err := unix.Lstat(p, &st); err != nil { |
| + t.Fatal(err) |
| + } |
| + if st.Uid != 0 { |
| + t.Skipf("root-owned fixture unavailable: %s uid=%d", p, st.Uid) |
| + } |
| + } |
| + f, err := rootstate.OpenAuthorityFile(path) |
| + if os.IsNotExist(err) { |
| + t.Skip("system executable unavailable") |
| + } |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer f.Close() |
| + id, err := rootstate.Identity(f) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + proof, err := rootstate.PersistentIdentity(f) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + proof.MountRoot, proof.MountPoint = "/usr/bin", "/usr/bin" |
| + if err = generationFileIdentity(path, id, proof, proof.Boot); err != nil { |
| + t.Fatal("private mount proof refused by pins", err) |
| + } |
| + proof.MountRoot = "/substituted" |
| + if err = generationFileIdentity(path, id, proof, proof.Boot); err == nil { |
| + t.Fatal("substituted source accepted") |
| + } |
| +} |
diff --git a/internal/release/plan14_generation_namespace_test.go b/internal/release/plan14_generation_namespace_test.go| new file mode 100644 |
| index 000000000..3c2576bf1 |
| --- /dev/null |
| +++ b/internal/release/plan14_generation_namespace_test.go |
| @@ -0,0 +1,47 @@ |
| +//go:build linux |
| + |
| +package release |
| + |
| +import ( |
| + "github.com/carlosframework/platform/internal/rootstate" |
| + "testing" |
| +) |
| + |
| +func TestPlan14GenerationNamespaceProof(t *testing.T) { |
| + path := "/opt/carlos/versions/v1/lifecycle" |
| + before := rootstate.FileIdentity{Boot: "boot", Device: 66305, Inode: 131086, Filesystem: "ext4:volume", Handle: "file-handle", HandleType: 1, Mode: 0100755, MountRoot: "/opt/carlos", MountPoint: "/opt/carlos", MountNamespace: "private", Mount: 306} |
| + after := before |
| + after.MountRoot, after.MountPoint, after.MountNamespace, after.Mount = "/", "/", "host", 127 |
| + if before.Same(after) { |
| + t.Fatal("fixture must cross mount layouts") |
| + } |
| + if !GenerationFileProof(path, before, after) { |
| + t.Fatal("same file through host mount refused") |
| + } |
| + for _, field := range []string{"source", "point", "device", "filesystem", "handle", "inode", "owner", "mode", "missing"} { |
| + bad := after |
| + switch field { |
| + case "source": |
| + bad.MountRoot = "/other" |
| + case "point": |
| + bad.MountPoint = "/opt" |
| + case "device": |
| + bad.Device++ |
| + case "filesystem": |
| + bad.Filesystem = "other" |
| + case "handle": |
| + bad.Handle = "other" |
| + case "inode": |
| + bad.Inode++ |
| + case "owner": |
| + bad.UID++ |
| + case "mode": |
| + bad.Mode++ |
| + case "missing": |
| + bad.Handle = "" |
| + } |
| + if GenerationFileProof(path, before, bad) { |
| + t.Fatal("substitution accepted", field) |
| + } |
| + } |
| +} |
diff --git a/internal/release/rootgenerationpins.go b/internal/release/rootgenerationpins.go| index a9bb46a42..e4b504b42 100644 |
| --- a/internal/release/rootgenerationpins.go |
| +++ b/internal/release/rootgenerationpins.go |
| @@ -85,7 +85,7 @@ func generationFileIdentity(path string, expected, proof rootstate.FileIdentity, |
| if err != nil { |
| return err |
| } |
| - if !proof.Same(actual) { |
| + if !GenerationFileProof(path, proof, actual) { |
| return os.ErrPermission |
| } |
| return nil |
diff --git a/internal/release/rootgenerationproof.go b/internal/release/rootgenerationproof.go| new file mode 100644 |
| index 000000000..a9701475e |
| --- /dev/null |
| +++ b/internal/release/rootgenerationproof.go |
| @@ -0,0 +1,60 @@ |
| +package release |
| + |
| +import ( |
| + "github.com/carlosframework/platform/internal/rootstate" |
| + "os" |
| + "path/filepath" |
| + "strings" |
| +) |
| + |
| +func GenerationFileProof(path string, before, after rootstate.FileIdentity) bool { |
| + if before.Same(after) { |
| + return true |
| + } |
| + source := func(id rootstate.FileIdentity) (string, bool) { |
| + root, e1 := generationProofUnescape(id.MountRoot) |
| + point, e2 := generationProofUnescape(id.MountPoint) |
| + if e1 != nil || e2 != nil || !filepath.IsAbs(root) || !filepath.IsAbs(point) || filepath.Clean(root) != root || filepath.Clean(point) != point { |
| + return "", false |
| + } |
| + rel, err := filepath.Rel(point, path) |
| + if err != nil || !filepath.IsLocal(rel) { |
| + return "", false |
| + } |
| + return filepath.Join(root, rel), true |
| + } |
| + a, ok1 := source(before) |
| + b, ok2 := source(after) |
| + if !ok1 || !ok2 || a != b { |
| + return false |
| + } |
| + after.MountRoot, after.MountPoint = before.MountRoot, before.MountPoint |
| + return before.Same(after) |
| +} |
| + |
| +func generationProofUnescape(s string) (string, error) { |
| + var out strings.Builder |
| + for i := 0; i < len(s); i++ { |
| + if s[i] != '\\' { |
| + out.WriteByte(s[i]) |
| + continue |
| + } |
| + if i+3 >= len(s) { |
| + return "", os.ErrPermission |
| + } |
| + switch s[i : i+4] { |
| + case "\\040": |
| + out.WriteByte(' ') |
| + case "\\011": |
| + out.WriteByte('\t') |
| + case "\\012": |
| + out.WriteByte('\n') |
| + case "\\134": |
| + out.WriteByte('\\') |
| + default: |
| + return "", os.ErrPermission |
| + } |
| + i += 3 |
| + } |
| + return out.String(), nil |
| +} |
diff --git a/internal/rootstate/namespace_recreation_test.go b/internal/rootstate/namespace_recreation_test.go| new file mode 100644 |
| index 000000000..6e68fcfed |
| --- /dev/null |
| +++ b/internal/rootstate/namespace_recreation_test.go |
| @@ -0,0 +1,40 @@ |
| +package rootstate |
| + |
| +import "testing" |
| + |
| +func TestPlan14PersistentNamespaceRecreation(t *testing.T) { |
| + before := FileIdentity{Device: 66305, Inode: 655373, Boot: "same-boot", Filesystem: "ext4:volume", Handle: "directory-handle", HandleType: 1, Mount: 384, MountRoot: "/data/.instances", MountPoint: "/data/instances", MountNamespace: "mnt:[4026532470]", UID: 999, GID: 989, Mode: 0040700} |
| + after := before |
| + after.Mount = 385 |
| + if !before.Same(after) { |
| + t.Fatal("same persistent object after namespace recreation refused") |
| + } |
| + for _, field := range []string{"device", "filesystem", "handle", "inode", "owner", "group", "mode", "root", "point", "unknown"} { |
| + bad := after |
| + switch field { |
| + case "device": |
| + bad.Device++ |
| + case "filesystem": |
| + bad.Filesystem = "other" |
| + case "handle": |
| + bad.Handle = "other" |
| + case "inode": |
| + bad.Inode++ |
| + case "owner": |
| + bad.UID++ |
| + case "group": |
| + bad.GID++ |
| + case "mode": |
| + bad.Mode++ |
| + case "root": |
| + bad.MountRoot = "/other" |
| + case "point": |
| + bad.MountPoint = "/other" |
| + case "unknown": |
| + bad.Handle = "" |
| + } |
| + if before.Same(bad) { |
| + t.Fatal("substitution accepted", field) |
| + } |
| + } |
| +} |
diff --git a/internal/rootstate/state.go b/internal/rootstate/state.go| index c9d213198..d7ef96069 100644 |
| --- a/internal/rootstate/state.go |
| +++ b/internal/rootstate/state.go |
| @@ -65,5 +65,5 @@ func (a FileIdentity) Same(b FileIdentity) bool { |
| if a.Inode != b.Inode || a.Filesystem != b.Filesystem || a.Handle != b.Handle || a.HandleType != b.HandleType || a.UID != b.UID || a.GID != b.GID || a.Mode != b.Mode || a.MountRoot != b.MountRoot || a.MountPoint != b.MountPoint { |
| return false |
| } |
| - return a.Boot != b.Boot || (a.Device == b.Device && (a.MountNamespace != b.MountNamespace || a.Mount == b.Mount)) |
| + return a.Boot != b.Boot || a.Device == b.Device |
| } |