carlos / platform Public

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>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 941ba4fe7b033cbf73a8608701d5cbcae6808358 parent 595c0db
32 files changed, +940 −59
  • internal/host/claimparticipation.go +3 −0
  • internal/host/claimservingpark_recovery_linux.go +11 −1
  • internal/host/claimservingpark_rootrunner_linux_test.go +1 −1
  • internal/host/claimservingreboot_binding.go +1 −1
  • internal/host/claimservingreboot_commit_linux.go +1 −1
  • internal/host/claimservingreboot_linux.go +8 −6
  • internal/host/claimservingreboot_prepare_linux.go +10 −1
  • internal/host/claimservingtopology_linux.go +10 −1
  • internal/host/plan14_lifecycle_diagnostic_root_linux_test.go +51 −0
  • internal/host/plan14_lifecycle_retire_root_linux_test.go +31 −3
  • internal/host/plan14_lifecycle_root_linux_test.go +100 −17
  • internal/host/plan14_lifecycle_trace_root_linux_test.go +37 −1
  • internal/host/plan14_namespace_transition_linux_test.go +120 −0
  • internal/host/plan14_r11_handover_test.go +23 −0
  • internal/host/plan14_remote_cleanup_test.go +35 −0
  • internal/host/plan14_service_supervisor_linux_test.go +27 −0
  • internal/host/plan14_sibling_census_linux_test.go +98 −0
  • internal/host/rootexecution_authority.go +2 −2
  • internal/host/rootexecution_settle.go +1 −1
  • internal/host/rootgeneration_kernel_linux.go +7 −2
  • internal/host/rootgeneration_services_linux.go +20 −2
  • internal/host/rootinventory.go +3 −2
  • internal/host/rootinventory_sibling.go +110 −0
  • internal/hostbox/empty_census.go +13 −0
  • internal/provision/plan14_p3_census_test.go +3 −2
  • internal/provision/testdata/plan14-root-reads-p3.tsv +27 −13
  • internal/release/plan14_generation_boot_test.go +38 −0
  • internal/release/plan14_generation_namespace_test.go +47 −0
  • internal/release/rootgenerationpins.go +1 −1
  • internal/release/rootgenerationproof.go +60 −0
  • internal/rootstate/namespace_recreation_test.go +40 −0
  • internal/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
}