carlos / platform Public

WIP files seed checkpoint 33 (not for merge)

Boot-aware handover identity: equal device numbers across boots no longer skip persistent verification; audited for the same fast path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev f9b6f1fddd35679d3a2018615468dd0a61a41211 parent 7bf183e
16 files changed, +265 −34
  • internal/host/claimfilesseed_integration_linux_test.go +3 −3
  • internal/host/claimfilesseed_reboot_rootrunner_linux_test.go +3 −0
  • internal/host/claimfilesseed_renumber_rootrunner_linux_test.go +1 −0
  • internal/host/claimfilesseed_restore_linux_test.go +7 −0
  • internal/host/claimhandover_recovery_test.go +7 −2
  • internal/host/claimservinghandover_linux_test.go +13 −6
  • internal/host/claimservinghandover_reboot_linux.go +7 −1
  • internal/host/claimservinghandover_reboot_linux_test.go +120 −4
  • internal/host/claimservinghandover_reboot_other_test.go +11 −0
  • internal/host/claimservingparkroot_linux_test.go +5 −1
  • internal/host/claimservingpeer_rootrunner_linux_test.go +0 −1
  • internal/host/claimservingretire_linux_test.go +6 −2
  • internal/hostbox/servinghandover.go +5 −7
  • internal/hostbox/servinghandover_linux_test.go +71 −1
  • internal/provision/plan9_b8_census_test.go +1 −1
  • internal/provision/testdata/plan14-root-reads-seed.tsv +5 −5

CI cancelled — superseded by a newer push — run details

CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok  	amadan.net/carlos/platform.git/launchctx	0.011s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m

amadan: job cancelled — a newer push superseded it
diff --git a/internal/host/claimfilesseed_integration_linux_test.go b/internal/host/claimfilesseed_integration_linux_test.go
index 6b6057683..fc5416e83 100644
--- a/internal/host/claimfilesseed_integration_linux_test.go
+++ b/internal/host/claimfilesseed_integration_linux_test.go
@@ -245,10 +245,10 @@ func TestFilesSeedReportsSkippedPaths(t *testing.T) {
func TestFilesSeedPreparationChangedController(t *testing.T) {
witness := filesSeedLiveWitness{configure: func(*ClaimWakeAssembly) {}, serve: func(*testing.T, *servingPreparation, hostclaim.Observed, activator.ClaimStart) {}}
witness.prepare = func(p *servingPreparation) {
- p.local.BootID, p.local.ControllerID, p.local.PID, p.local.ProcessStart = "new-boot", "new-controller", 88, "new-start"
- p.f.f.peer = StopPeer{UID: 2001, PID: 88, ProcessStart: "new-start", BootID: "new-boot", ControllerID: "new-controller"}
+ p.local.ControllerID, p.local.PID, p.local.ProcessStart = "new-controller", 88, "new-start"
+ p.f.f.peer = StopPeer{UID: 2001, PID: 88, ProcessStart: "new-start", BootID: p.local.BootID, ControllerID: "new-controller"}
p.f.p.root.Stop.Coordinator.CarlosUID = 2001
- p.wake.Retries.BootID, p.wake.Retries.ControllerID = "new-boot", "new-controller"
+ p.wake.Retries.BootID, p.wake.Retries.ControllerID = p.local.BootID, "new-controller"
if len(p.wake.pending) != 0 {
t.Fatal("rebuilding active fixture")
}
diff --git a/internal/host/claimfilesseed_reboot_rootrunner_linux_test.go b/internal/host/claimfilesseed_reboot_rootrunner_linux_test.go
index 5035026bc..a308ae29a 100644
--- a/internal/host/claimfilesseed_reboot_rootrunner_linux_test.go
+++ b/internal/host/claimfilesseed_reboot_rootrunner_linux_test.go
@@ -233,6 +233,9 @@ func TestRootRebootFilesSeedTwoHostPhase2(t *testing.T) {
if strings.Contains(t.Name(), "Renumber") && entry.Device == uint64(current.Dev) {
t.Fatal("real filesystem device did not renumber")
}
+ if !strings.Contains(t.Name(), "Renumber") && (entry.Device != uint64(current.Dev) || entry.Inode != current.Ino) {
+ t.Fatal("unchanged-number reboot control changed identity")
+ }
t.Logf("reboot directory host=%s recordedDevice=%d currentDevice=%d recordedInode=%d currentInode=%d", savedHost.Host, entry.Device, current.Dev, entry.Inode, current.Ino)
}
}
diff --git a/internal/host/claimfilesseed_renumber_rootrunner_linux_test.go b/internal/host/claimfilesseed_renumber_rootrunner_linux_test.go
index 3249a63e3..9b3121677 100644
--- a/internal/host/claimfilesseed_renumber_rootrunner_linux_test.go
+++ b/internal/host/claimfilesseed_renumber_rootrunner_linux_test.go
@@ -101,6 +101,7 @@ func seedRunnerRenumberDisk(t *testing.T) {
func TestRootServingHandoverDirectoryProof(t *testing.T) {
servingRootRunner(t)
TestServingHandoverRebootDirectoryProof(t)
+ TestServingHandoverRebootEqualNumbers(t)
}
func TestRootServingParkRetainedDoesNotBlockRetirement(t *testing.T) {
diff --git a/internal/host/claimfilesseed_restore_linux_test.go b/internal/host/claimfilesseed_restore_linux_test.go
index 57a8c0b00..cd45329ea 100644
--- a/internal/host/claimfilesseed_restore_linux_test.go
+++ b/internal/host/claimfilesseed_restore_linux_test.go
@@ -53,6 +53,13 @@ type servingPreparation struct {
func newServingPreparation(t *testing.T, f *servingRootFixture, boot, controller string, configure ...func(*ClaimWakeAssembly)) *servingPreparation {
t.Helper()
+ if boot == "boot" {
+ var err error
+ boot, err = f.p.ledger.fs.(*servingParkFS).currentBoot()
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
p := &servingPreparation{f: f, local: claimStopLocal{Actor: f.actor, BootID: boot, ControllerID: controller, PID: 77, ProcessStart: controller + "-start"}}
f.f.peer = StopPeer{UID: 1001, PID: p.local.PID, ProcessStart: p.local.ProcessStart, BootID: boot, ControllerID: controller}
launchFixture := plan9D5Fixture(t)
diff --git a/internal/host/claimhandover_recovery_test.go b/internal/host/claimhandover_recovery_test.go
index c2e52e469..991b6e0d9 100644
--- a/internal/host/claimhandover_recovery_test.go
+++ b/internal/host/claimhandover_recovery_test.go
@@ -95,7 +95,11 @@ func TestClaimColdHandoverAfterControllerRecoveryReachesNextWake(t *testing.T) {
f := newRecoveryFixture(t)
// Production reads claims and publishes evidence in the same bucket.
f.evidence = f.claims
- former := f.controller("boot-1", "controller-1", 4101, "start-1")
+ recordedBoot, parkRuntime := servingRecoveryFilesystem(t, f.guard.DataDir)
+ if boot == "boot-1" {
+ boot = recordedBoot
+ }
+ former := f.controller(recordedBoot, "controller-1", 4101, "start-1")
admitted := former.admit(t, f)
f.revision = 8
restarted := f.controller(boot, "controller-2", 4202, "start-2")
@@ -116,7 +120,7 @@ func TestClaimColdHandoverAfterControllerRecoveryReachesNextWake(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- if receipt.Target.ControllerID != "controller-1" || receipt.Target.BootID != "boot-1" || receipt.Target.DirectoryRevision != "7" {
+ if receipt.Target.ControllerID != "controller-1" || receipt.Target.BootID != recordedBoot || receipt.Target.DirectoryRevision != "7" {
t.Fatalf("recovered tuple was rewritten: %+v", receipt.Target)
}
base := t.TempDir()
@@ -184,6 +188,7 @@ func TestClaimColdHandoverAfterControllerRecoveryReachesNextWake(t *testing.T) {
coordinator := f.root(recoveryStopDrain{})
coordinator.Guard = rootGuard
root := ClaimRootHTTP{Launches: f.journal, Stop: ClaimStopHTTP{Coordinator: coordinator, Authenticate: func(context.Context, net.Conn) (StopPeer, error) { return f.peer, nil }}, Roots: map[string]string{"data": f.guard.DataDir}, Reconcile: hostbox.ReconcileAdapter{Box: "box-1", Enrollment: "enrollment-1", Registry: reg, Inventory: inventory, Retirement: hostbox.TypedRetirement{JournalDir: filepath.Join(base, "retire")}}}
+ root.servingParks = parkRuntime
root.RequireNewWork = func(context.Context) error { return nil }
root.InventoryActor = func(context.Context) (hostbox.Actor, error) {
actor := restarted.local.Actor
diff --git a/internal/host/claimservinghandover_linux_test.go b/internal/host/claimservinghandover_linux_test.go
index 30bba16f1..32cf7bc8a 100644
--- a/internal/host/claimservinghandover_linux_test.go
+++ b/internal/host/claimservinghandover_linux_test.go
@@ -5,6 +5,7 @@ package host
import (
"context"
"encoding/json"
+ "net"
"os"
"path/filepath"
"reflect"
@@ -109,7 +110,7 @@ func TestServingHandoverThenPrelaunchParkReplay(t *testing.T) {
if err != nil || installed != (hostguard.Captured{Incarnation: f.current.Incarnation, Stamp: f.current.Stamp}) {
t.Fatalf("installation: %+v %v", installed, err)
}
- target := recoveryTuple(f.f.read(), claimStopLocal{Actor: f.actor, BootID: "boot", ControllerID: "controller"})
+ target := recoveryTuple(f.f.read(), claimStopLocal{Actor: f.actor, BootID: f.receipt.Target.BootID, ControllerID: "controller"})
if err := f.f.journal.Prelaunch(t.Context(), hostbox.LaunchInventory{Target: target, ControllerPID: 77, ProcessStart: "edge-start", NoState: true, Groups: []hostbox.LaunchGroup{{RelativePath: "carlos/next", Device: 1, Inode: 99}}}); err != nil {
t.Fatal(err)
}
@@ -204,8 +205,13 @@ func runServingColdHandoverBoundary(t *testing.T, mount func(*testing.T, string)
func TestServingHandoverLegacy(t *testing.T) {
f, _, coordinator := newServingLegacyRootSetup(t, false)
root := servingHandoverRoot(t, f)
+ boot, err := servingParkBootID()
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.f.peer.BootID = boot
o := f.f.read()
- req := StopRequest{Region: f.actor.Region, Host: o.Host, Incarnation: o.Claim.Holder.Incarnation, InstallationStamp: o.Claim.Holder.Stamp, Box: f.actor.Box, Enrollment: f.actor.Enrollment, BootID: "boot", ControllerID: "controller", ExpectedClaimETag: o.ETag, ExpectedDirectoryRevision: "7"}
+ req := StopRequest{Region: f.actor.Region, Host: o.Host, Incarnation: o.Claim.Holder.Incarnation, InstallationStamp: o.Claim.Holder.Stamp, Box: f.actor.Box, Enrollment: f.actor.Enrollment, BootID: boot, ControllerID: "controller", ExpectedClaimETag: o.ETag, ExpectedDirectoryRevision: "7"}
if _, err := coordinator.FenceAndStop(t.Context(), req); err != nil {
t.Fatal(err)
}
@@ -282,7 +288,7 @@ func TestServingHandoverRefusesForeignAuthority(t *testing.T) {
t.Fatal(err)
}
case "prelaunch":
- target := recoveryTuple(f.f.read(), claimStopLocal{Actor: f.actor, BootID: "boot", ControllerID: "controller"})
+ target := recoveryTuple(f.f.read(), claimStopLocal{Actor: f.actor, BootID: f.receipt.Target.BootID, ControllerID: "controller"})
if err = f.f.journal.Prelaunch(t.Context(), hostbox.LaunchInventory{Target: target, ControllerPID: 77, ProcessStart: "edge-start", NoState: true, Groups: []hostbox.LaunchGroup{{RelativePath: "carlos/next", Device: 1, Inode: 99}}}); err != nil {
t.Fatal(err)
}
@@ -315,10 +321,11 @@ func TestServingHandoverRefusesForeignAuthority(t *testing.T) {
func TestServingStoppedWriterSecondHandover(t *testing.T) {
f := newServingRootFixture(t, true)
root := servingHandoverRoot(t, f)
- f.f.peer = StopPeer{UID: 1001, PID: 77, ProcessStart: "edge-start", BootID: "boot", ControllerID: "controller"}
+ f.f.peer = StopPeer{UID: 1001, PID: 77, ProcessStart: "edge-start", BootID: f.receipt.Target.BootID, ControllerID: "controller"}
coordinator := f.f.root(nil)
coordinator.Evidence, coordinator.Inventory = f.f.claims, root.Journals.Inventory
root.Stop.Coordinator = coordinator
+ root.Stop.Authenticate = func(context.Context, net.Conn) (StopPeer, error) { return f.f.peer, nil }
f.p.root.Stop.Coordinator = coordinator
socket := servingRootSocket(t, root)
transfer := coldHandoverWith(handoverRecoveryIdentity(t, f.actor), socket, func(context.Context, hostclaim.Observed) (hostguard.FenceReceipt, error) {
@@ -327,7 +334,7 @@ func TestServingStoppedWriterSecondHandover(t *testing.T) {
if err := transfer(t.Context(), f.predecessor, f.f.read()); err != nil {
t.Fatal(err)
}
- target := recoveryTuple(f.f.read(), claimStopLocal{Actor: f.actor, BootID: "boot", ControllerID: "controller"})
+ target := recoveryTuple(f.f.read(), claimStopLocal{Actor: f.actor, BootID: f.receipt.Target.BootID, ControllerID: "controller"})
if err := f.f.journal.Prelaunch(t.Context(), hostbox.LaunchInventory{Target: target, ControllerPID: 77, ProcessStart: "edge-start", NoState: true, Groups: []hostbox.LaunchGroup{{RelativePath: "carlos/next", Device: 1, Inode: 99}}}); err != nil {
t.Fatal(err)
}
@@ -353,7 +360,7 @@ func TestServingStoppedWriterSecondHandover(t *testing.T) {
t.Fatal(err)
}
o := f.f.read()
- req := StopRequest{Region: f.actor.Region, Host: o.Host, Incarnation: f.current.Incarnation, InstallationStamp: f.current.Stamp, WriterSession: f.current.Session, Box: f.actor.Box, Enrollment: f.actor.Enrollment, BootID: "boot", ControllerID: "controller", ExpectedClaimETag: o.ETag, ExpectedDirectoryRevision: "7"}
+ req := StopRequest{Region: f.actor.Region, Host: o.Host, Incarnation: f.current.Incarnation, InstallationStamp: f.current.Stamp, WriterSession: f.current.Session, Box: f.actor.Box, Enrollment: f.actor.Enrollment, BootID: f.receipt.Target.BootID, ControllerID: "controller", ExpectedClaimETag: o.ETag, ExpectedDirectoryRevision: "7"}
drain := stopDrainFunc(func(_ context.Context, req StopRequest, fence string) (DrainAcknowledgement, error) {
return DrainClaimAcknowledgement(fence, req.ControllerID, "edge-start", nil), nil
})
diff --git a/internal/host/claimservinghandover_reboot_linux.go b/internal/host/claimservinghandover_reboot_linux.go
index 527f2de6f..2852c6718 100644
--- a/internal/host/claimservinghandover_reboot_linux.go
+++ b/internal/host/claimservinghandover_reboot_linux.go
@@ -27,9 +27,15 @@ func (h ClaimRootHTTP) verifyServingHandoverDirectory(ctx context.Context, prior
return hostbox.ErrChanged
}
boot, err := fs.currentBoot()
- if err != nil || prior.BootID == boot || !validServingParkBootID(prior.BootID) {
+ if err != nil || !validServingParkBootID(prior.BootID) {
return hostbox.ErrChanged
}
+ if prior.BootID == boot {
+ if actual.Dev != e.Device {
+ return hostbox.ErrChanged
+ }
+ return ctx.Err()
+ }
slot, err := l.slot(servingParkAuthorization{Successor: servingArtifactIdentity{Host: prior.Host, Box: prior.Box}})
if err != nil {
return err
diff --git a/internal/host/claimservinghandover_reboot_linux_test.go b/internal/host/claimservinghandover_reboot_linux_test.go
index f23c6df88..f2a5d8040 100644
--- a/internal/host/claimservinghandover_reboot_linux_test.go
+++ b/internal/host/claimservinghandover_reboot_linux_test.go
@@ -3,8 +3,14 @@
package host
import (
+ "context"
+ "database/sql"
+ "errors"
+ "fmt"
+ "github.com/carlosframework/platform/internal/hostclaim"
"os"
"path/filepath"
+ "reflect"
"testing"
"github.com/carlosframework/platform/internal/hostbox"
@@ -12,7 +18,7 @@ import (
)
func TestServingHandoverRebootDirectoryProof(t *testing.T) {
- for _, fault := range []string{"", "same-boot", "foreign-host", "inode", "handle", "mount", "missing-record"} {
+ for _, fault := range []string{"", "same-boot", "same-boot-renumber", "missing-boot", "boot-read", "foreign-host", "inode", "handle", "mount", "missing-record"} {
t.Run(fault, func(t *testing.T) {
f, r := servingRebootPreparationFixture(t, "complete")
if err := f.p.commitServingReboot(t.Context(), r.Authorization.ID); err != nil {
@@ -20,11 +26,18 @@ func TestServingHandoverRebootDirectoryProof(t *testing.T) {
}
fs := f.p.ledger.fs.(*servingParkFS)
prior := hostguard.FenceTarget{Host: r.Authorization.Successor.Host, Box: r.Authorization.Successor.Box, Incarnation: r.Authorization.Successor.Incarnation, Stamp: r.Authorization.Successor.Stamp, BootID: r.Source.BootID}
- e := hostbox.StopInventoryEntry{Kind: "path", ManagedRoot: "data", RelativePath: "instances/" + prior.Host, Device: r.Source.Directory.Device + 1, Inode: r.Source.Directory.Inode, Type: 0040000}
+ e := hostbox.StopInventoryEntry{Kind: "path", ManagedRoot: "data", RelativePath: "instances/" + prior.Host, Device: r.Source.Directory.Device, Inode: r.Source.Directory.Inode, Type: 0040000}
a := hostbox.Artifact{Dev: r.Source.Directory.Device, Ino: e.Inode, Type: e.Type}
switch fault {
- case "same-boot":
+ case "same-boot", "same-boot-renumber":
prior.BootID = servingOtherBoot
+ if fault == "same-boot-renumber" {
+ e.Device++
+ }
+ case "missing-boot":
+ prior.BootID = ""
+ case "boot-read":
+ fs.boot = func() (string, error) { return "", os.ErrPermission }
case "foreign-host":
prior.Host = "foreign.example"
case "inode":
@@ -54,9 +67,112 @@ func TestServingHandoverRebootDirectoryProof(t *testing.T) {
}
h := servingTransportRoot(t, f)
err := h.verifyServingHandoverDirectory(t.Context(), prior, e, a)
- if (err == nil) != (fault == "") {
+ if (err == nil) != (fault == "" || fault == "same-boot") {
t.Fatalf("fault %q: %v", fault, err)
}
})
}
}
+
+func TestServingHandoverRebootEqualNumbers(t *testing.T) {
+ for _, changed := range []bool{false, true} {
+ t.Run(fmt.Sprint(changed), func(t *testing.T) {
+ f := newServingRootFixture(t, true)
+ root := servingHandoverRoot(t, f)
+ next := func() hostbox.ColdSuccessor {
+ return hostbox.ColdSuccessor{Host: f.current.Host, Incarnation: f.current.Incarnation, Box: f.actor.Box, Enrollment: f.actor.Enrollment, DirectoryRevision: "7", ETag: f.f.read().ETag, Generation: f.current.Generation, Stamp: f.current.Stamp, Session: f.current.Session}
+ }
+ if err := root.servingHandover().Run(t.Context(), f.receipt.Target, next()); err != nil {
+ t.Fatal(err)
+ }
+ a, err := f.park()
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.tags(f.current.Stamp)
+ if err := f.p.completeServingDatabasePark(t.Context(), f.conn, a.ID, f.current); err != nil {
+ t.Fatal(err)
+ }
+ prior := servingStop(t, f.f, f.f.read(), f.receipt.Target.BootID).Target
+ o := f.f.read()
+ admitted, err := hostclaim.Update(t.Context(), f.f.claims, o, hostclaim.AdmitWriter(hostclaim.Admission{ObservedSession: f.current.Session, ObservedGeneration: f.current.Generation, Box: f.actor.Box, Region: f.actor.Region, PredecessorRegion: f.actor.Region, Mode: "cold", NewGeneration: "G3"}))
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.current, err = servingWriterIdentity(admitted, f.actor.Box, f.actor.Region)
+ if err != nil {
+ t.Fatal(err)
+ }
+ fs := f.p.ledger.fs.(*servingParkFS)
+ fs.boot = func() (string, error) { return servingOtherBoot, nil }
+ if prior.BootID == servingOtherBoot {
+ t.Fatal("boots must differ")
+ }
+ if err := f.p.commitServingReboot(t.Context(), a.ID); err != nil {
+ t.Fatal(err)
+ }
+ if changed {
+ handle := fs.handle
+ fs.handle = func(d *os.File) (servingParkPersistentDirectory, error) {
+ p, err := handle(d)
+ if filepath.Base(d.Name()) == prior.Host {
+ p.Handle += "00"
+ }
+ return p, err
+ }
+ }
+ h := root.servingHandover()
+ verify := h.VerifyDirectory
+ calls := 0
+ h.VerifyDirectory = func(ctx context.Context, p hostguard.FenceTarget, e hostbox.StopInventoryEntry, a hostbox.Artifact) error {
+ calls++
+ if a.Dev != e.Device || a.Ino != e.Inode || a.Type != e.Type {
+ t.Fatal("numeric identities must be equal")
+ }
+ return verify(ctx, p, e, a)
+ }
+ before, err := h.Registry.Get(prior.Host)
+ if err != nil {
+ t.Fatal(err)
+ }
+ n := next()
+ err = h.Run(t.Context(), prior, n)
+ if changed {
+ if !errors.Is(err, hostbox.ErrChanged) {
+ t.Fatal("changed persistent evidence accepted", err)
+ }
+ after, err := h.Registry.Get(prior.Host)
+ if err != nil || !reflect.DeepEqual(before, after) {
+ t.Fatal("registry changed", err)
+ }
+ if _, err := h.Inventory.ReadInstallation(t.Context(), n.Host, n.Incarnation, n.Stamp); !errors.Is(err, sql.ErrNoRows) {
+ t.Fatal("successor inventory written", err)
+ }
+ } else {
+ if err != nil {
+ t.Fatal("unchanged filesystem refused", err)
+ }
+ if err := h.Run(t.Context(), prior, n); err != nil {
+ t.Fatal("replay", err)
+ }
+ if calls < 5 {
+ t.Fatal("missing boundary verification", calls)
+ }
+ }
+ if calls == 0 {
+ t.Fatal("persistent verifier bypassed")
+ }
+ })
+ }
+}
+
+func servingRecoveryFilesystem(t *testing.T, data string) (string, *servingParkRuntime) {
+ t.Helper()
+ disk := &rollbackDisk{dir: t.TempDir()}
+ fs := newServingParkFS(data, disk)
+ boot, err := fs.currentBoot()
+ if err != nil {
+ t.Fatal(err)
+ }
+ return boot, &servingParkRuntime{authority: &servingParkRoot{ledger: &servingParkLedger{state: disk, fs: fs}}}
+}
diff --git a/internal/host/claimservinghandover_reboot_other_test.go b/internal/host/claimservinghandover_reboot_other_test.go
new file mode 100644
index 000000000..97d45422e
--- /dev/null
+++ b/internal/host/claimservinghandover_reboot_other_test.go
@@ -0,0 +1,11 @@
+//go:build !linux
+
+package host
+
+import "testing"
+
+func servingRecoveryFilesystem(t *testing.T, _ string) (string, *servingParkRuntime) {
+ t.Helper()
+ t.Skip("serving directory verification requires Linux")
+ return "", nil
+}
diff --git a/internal/host/claimservingparkroot_linux_test.go b/internal/host/claimservingparkroot_linux_test.go
index 4b0b588e7..6ab192cb1 100644
--- a/internal/host/claimservingparkroot_linux_test.go
+++ b/internal/host/claimservingparkroot_linux_test.go
@@ -101,7 +101,11 @@ func newServingRootFixture(t *testing.T, launched bool) *servingRootFixture {
InventoryActor: func(context.Context) (hostbox.Actor, error) { return fx.actor, nil }, RequireNewWork: func(context.Context) error { return nil }}
fx.install(first)
fx.tags(fx.prior.Stamp)
- fx.receipt = servingStop(t, f, f.read())
+ boot, err := fs.currentBoot()
+ if err != nil {
+ t.Fatal(err)
+ }
+ fx.receipt = servingStop(t, f, f.read(), boot)
target := fx.receipt.Target
if err := f.journal.Prelaunch(t.Context(), hostbox.LaunchInventory{Target: target, ControllerPID: 77, ProcessStart: "edge-start", NoState: true, Groups: []hostbox.LaunchGroup{{RelativePath: "carlos/parent", Device: 1, Inode: 2}}}); err != nil {
t.Fatal(err)
diff --git a/internal/host/claimservingpeer_rootrunner_linux_test.go b/internal/host/claimservingpeer_rootrunner_linux_test.go
index ca10ef57a..b22aaab39 100644
--- a/internal/host/claimservingpeer_rootrunner_linux_test.go
+++ b/internal/host/claimservingpeer_rootrunner_linux_test.go
@@ -389,7 +389,6 @@ func runServingParkPeerNamespace(t *testing.T, handover, writer bool, namespace
return nil
}
h := servingTransportRoot(t, f)
- h.servingParks.authority = servingPeerParkTrace{servingParkAuthority: h.servingParks.authority, root: f.p, t: t}
path := filepath.Join(root, "root.sock")
listener, err := net.Listen("unix", path)
if err != nil {
diff --git a/internal/host/claimservingretire_linux_test.go b/internal/host/claimservingretire_linux_test.go
index 743262d4f..1c3189306 100644
--- a/internal/host/claimservingretire_linux_test.go
+++ b/internal/host/claimservingretire_linux_test.go
@@ -159,9 +159,13 @@ func servingAdmit(t *testing.T, f *recoveryFixture, in *ClaimLaunchInputs, gener
}
return admitted
}
-func servingStop(t *testing.T, f *recoveryFixture, o hostclaim.Observed) hostguard.FenceReceipt {
+func servingStop(t *testing.T, f *recoveryFixture, o hostclaim.Observed, boots ...string) hostguard.FenceReceipt {
t.Helper()
- target := recoveryTuple(o, claimStopLocal{Actor: hostbox.Actor{Box: "box-1", Region: "ie", Enrollment: "enrollment", Revision: 7}, BootID: "boot", ControllerID: "controller"})
+ boot := "boot"
+ if len(boots) != 0 {
+ boot = boots[0]
+ }
+ target := recoveryTuple(o, claimStopLocal{Actor: hostbox.Actor{Box: "box-1", Region: "ie", Enrollment: "enrollment", Revision: 7}, BootID: boot, ControllerID: "controller"})
target.FenceID, target.ClaimGen = "stop-"+o.Claim.Holder.Generation, o.Claim.Gen
if _, err := f.guard.BeginFence(t.Context(), target); err != nil {
t.Fatal(err)
diff --git a/internal/hostbox/servinghandover.go b/internal/hostbox/servinghandover.go
index 5414a5e5a..983c007c2 100644
--- a/internal/hostbox/servinghandover.go
+++ b/internal/hostbox/servinghandover.go
@@ -253,13 +253,11 @@ func (h ServingHandover) directory(ctx context.Context, prior hostguard.FenceTar
if a.Ino != e.Inode || a.Type != e.Type {
return a, ErrChanged
}
- if a.Dev != e.Device {
- if h.VerifyDirectory == nil {
- return a, ErrChanged
- }
- if err := h.VerifyDirectory(ctx, prior, e, a); err != nil {
- return a, err
- }
+ if h.VerifyDirectory == nil {
+ return a, ErrChanged
+ }
+ if err := h.VerifyDirectory(ctx, prior, e, a); err != nil {
+ return a, err
}
return a, nil
}
diff --git a/internal/hostbox/servinghandover_linux_test.go b/internal/hostbox/servinghandover_linux_test.go
index 34a0b7008..ccc114113 100644
--- a/internal/hostbox/servinghandover_linux_test.go
+++ b/internal/hostbox/servinghandover_linux_test.go
@@ -4,6 +4,7 @@ package hostbox
import (
"context"
+ "database/sql"
"errors"
"fmt"
"os"
@@ -104,7 +105,12 @@ func servingHandoverFixture(t *testing.T, deviceDelta ...uint64) (ServingHandove
return ErrChanged
}
return nil
- }}, Validate: func(_ context.Context, c *hostclaim.Claim, p hostguard.FenceTarget, _ ColdSuccessor, _ StopInventory, i hostguard.RootInstallation) error {
+ }}, VerifyDirectory: func(_ context.Context, p hostguard.FenceTarget, e StopInventoryEntry, a Artifact) error {
+ if p.BootID != "boot" || a.Dev != e.Device {
+ return ErrChanged
+ }
+ return nil
+ }, Validate: func(_ context.Context, c *hostclaim.Claim, p hostguard.FenceTarget, _ ColdSuccessor, _ StopInventory, i hostguard.RootInstallation) error {
if p != prior || i != installation || !reflect.DeepEqual(c, successor.Claim) {
return ErrChanged
}
@@ -244,3 +250,67 @@ func TestServingHandoverRebindsOnlyProvenDirectory(t *testing.T) {
})
}
}
+
+func TestServingHandoverEqualNumbersRequireProof(t *testing.T) {
+ for _, boundary := range []string{"initial", "intent", "barrier", "inventory-write", "complete-journal"} {
+ t.Run(boundary, func(t *testing.T) {
+ h, p, n := servingHandoverFixture(t)
+ changed := boundary == "initial"
+ calls := 0
+ h.VerifyDirectory = func(_ context.Context, _ hostguard.FenceTarget, e StopInventoryEntry, a Artifact) error {
+ calls++
+ if e.Device != a.Dev || e.Inode != a.Ino || e.Type != a.Type {
+ t.Fatal("numeric identities differ")
+ }
+ if changed {
+ return ErrChanged
+ }
+ return nil
+ }
+ if boundary == "inventory-write" {
+ validate := h.Validate
+ h.Validate = func(ctx context.Context, c *hostclaim.Claim, p hostguard.FenceTarget, n ColdSuccessor, in StopInventory, i hostguard.RootInstallation) error {
+ err := validate(ctx, c, p, n, in, i)
+ if calls == 3 {
+ changed = true
+ }
+ return err
+ }
+ } else if boundary != "initial" {
+ h.checkpoint = func(point string) error {
+ if point == boundary {
+ return errors.New("interrupted")
+ }
+ return nil
+ }
+ if h.Run(t.Context(), p, n) == nil {
+ t.Fatal("interruption missed")
+ }
+ h.checkpoint = nil
+ changed = true
+ }
+ before, err := h.Registry.Get(p.Host)
+ if err != nil {
+ t.Fatal(err)
+ }
+ inventory, invErr := h.Inventory.ReadInstallation(t.Context(), n.Host, n.Incarnation, n.Stamp)
+ if err := h.Run(t.Context(), p, n); !errors.Is(err, ErrChanged) {
+ t.Fatal("unproven directory accepted", err)
+ }
+ if calls == 0 {
+ t.Fatal("proof skipped")
+ }
+ after, err := h.Registry.Get(p.Host)
+ if err != nil || !reflect.DeepEqual(before, after) {
+ t.Fatal("registry changed", err)
+ }
+ got, gotErr := h.Inventory.ReadInstallation(t.Context(), n.Host, n.Incarnation, n.Stamp)
+ if !reflect.DeepEqual(inventory, got) || !errors.Is(gotErr, invErr) {
+ t.Fatal("inventory changed", gotErr)
+ }
+ if boundary != "complete-journal" && !errors.Is(gotErr, sql.ErrNoRows) {
+ t.Fatal("successor inventory exists", gotErr)
+ }
+ })
+ }
+}
diff --git a/internal/provision/plan9_b8_census_test.go b/internal/provision/plan9_b8_census_test.go
index ba5efc8dd..b05b4084d 100644
--- a/internal/provision/plan9_b8_census_test.go
+++ b/internal/provision/plan9_b8_census_test.go
@@ -479,7 +479,7 @@ var plan9B8CensusInventory = []censusEntry{
{"internal/host/claimservingmaintenance_linux.go", "claimServingRoom", "key-site", "lit:instances", "ce0cb25c53adfd3dba7344a98049f44fc3f0b3e633b22ad37299dea81503e403"},
// rev3=PROPOSED-seed: local instances mount/state references; no deployment record writer. Root authority remains in the seed overlay.
{"internal/host/claimservingpark_linux.go", "servingParkFS.parent", "key-site", "lit:instances", "1d241a7d446bbbade6e460efd911863f8a5246b8ac1d47b2548034e3feb2f216"},
- {"internal/host/claimservinghandover_reboot_linux.go", "ClaimRootHTTP.verifyServingHandoverDirectory", "key-site", "lit:instances", "19e8315f54850af3a5ddc04ac6e2e9d9035b2392c9eae7c22e05000d956b7e1e"},
+ {"internal/host/claimservinghandover_reboot_linux.go", "ClaimRootHTTP.verifyServingHandoverDirectory", "key-site", "lit:instances", "03e6aa32c980b6fc3512c88bbf5e93f18e84606c36357810bede2b7d3df25b12"},
{"internal/host/claimservingparksource_linux.go", "servingParkStateDirectory", "key-site", "lit:instances", "ec4a121330c5c8f27343f21af0bbe458cced885d4754e96c0d87b12d9b4a8f35"},
{"internal/host/rootinventory.go", "ClaimRootHTTP.recoverRootInventory", "key-site", "key:RecordKey", "10d09b5ab6d07d316437233c9a65bf0bcdfe1083db4d0769fa8303b255dfd0ac"},
{"internal/host/proofnamed.go", "proofBindingSource.record", "key-site", "key:RecordKey", "10a99ff4b355907831a904010b7019d480ce93f2f595e99e076d3c702d10ca2b"},
diff --git a/internal/provision/testdata/plan14-root-reads-seed.tsv b/internal/provision/testdata/plan14-root-reads-seed.tsv
index a3dc77643..54b4c9a74 100644
--- a/internal/provision/testdata/plan14-root-reads-seed.tsv
+++ b/internal/provision/testdata/plan14-root-reads-seed.tsv
@@ -302,7 +302,7 @@ internal/hostbox/servinghandover.go ServingHandover.Recover authority C,J4 rev3=
internal/hostbox/servinghandover.go ServingHandover.Run authority C,J4 rev3=PROPOSED-seed; FS-R11/run31; root; original stopped predecessor evidence remains immutable; successful persistent directory proof stores current device in successor inventory registry:Account@2154,registry:Addr@2373,registry:Addrs@2391,registry:App@2198,registry:Backing@2410,registry:Canary@2467,registry:ClaimOwner@5615,registry:ClaimOwnership@2841,registry:ClaimOwnership@3057,registry:ClaimOwnership@3090,registry:ClaimRowID@3120,registry:ClaimRowID@5686,registry:ClaimRowID@5860,registry:DBPath@2286,registry:Kind@2128,registry:Name@3174,registry:Name@5825,registry:Platform@2451,registry:Replica@2481,registry:Route@2841,registry:Route@3090,registry:Route@5615,registry:Route@5686,registry:Socket@2234,registry:Timers@3039,registry:Timers@3057,registry:Timers@3120,registry:Timers@3174,registry:Timers@5768,registry:Tunnel@2431,registry:Unit@2355,whole:ClaimOwnership@2853,whole:ClaimOwnership@3073,whole:ClaimOwnership@3102,whole:ColdRows@3514,whole:ColdRows@3522,whole:ColdRows@3530,whole:ColdRows@3537,whole:ColdRows@5617,whole:ColdRows@5688,whole:ColdRows@5770,whole:ColdRows@6351,whole:ColdRows@6361,whole:Route@2114,whole:Route@2643,whole:Route@2651,whole:Route@2701,whole:Route@2709,whole:Route@2847,whole:Route@3096,whole:Route@5623,whole:Route@5694,whole:Timer@2656,whole:Timer@2664,whole:Timer@2714,whole:Timer@2752,whole:Timer@3045,whole:Timer@3063,whole:Timer@3126,whole:Timer@3180,whole:Timer@5776 bc5399896f9509e846062aba6bbefcaff5f9b4712a400a9b3f2473966d680b3f
internal/hostbox/servinghandover.go ServingHandover.at authority C,J4 rev3=PROPOSED-seed; in-process interruption seam; nil in production 0178e5fcfb2cc194fb4f33f0f230b52849ddeb87a4b7c4e6cfb640e8b2f00b8a
internal/hostbox/servinghandover.go ServingHandover.validate authority C,J4 rev3=PROPOSED-seed; FS-R11/run31; root; unchanged inventory and stopped predecessor plus directory inode/type and boot-aware device proof file:Exec@258,registry:ClaimIncarnation@1906,registry:ClaimOwner@1379,registry:ClaimOwner@1980,registry:ClaimOwnership@1874,registry:ClaimRowID@2015,registry:ClaimStamp@1949,registry:Name@1860,registry:Registration@2052,registry:Route@1033,registry:Route@1379,registry:Timers@1806,registry:Timers@2249,whole:ClaimOwnership@1880,whole:ColdRows@1035,whole:ColdRows@1381,whole:ColdRows@1808,whole:ColdRows@2251,whole:Route@1042,whole:Route@1388,whole:Timer@1815,whole:Timer@2258 5935f65fef167c9e5c2265fb7fe5f25148f6aaa25f90c46ae970de9ad8fd7c0d
-internal/hostbox/servinghandover.go type ServingHandover authority C,J4 rev3=PROPOSED-seed; FS-R11/run31; root; adds fail-closed directory verification callback, used only on changed device d92c342005a7d2a240716c60bb56d55a742bb5a54757e45b642c3c5625b99eb8
+internal/hostbox/servinghandover.go type ServingHandover authority C,J4 rev3=PROPOSED-seed; FS-R11/run31; root; fail-closed directory verification callback invoked for every directory observation d92c342005a7d2a240716c60bb56d55a742bb5a54757e45b642c3c5625b99eb8
internal/hostbox/servingpark.go LaunchJournal.RootSessionLaunches authority C,J4 rev3=PROPOSED-seed; root-local persisted launch records for exact host/incarnation/stamp/session/box/enrollment; refuses agent journal transport cbbb9378bc53ac499972bed5fb654e05856a9afd7256d55323a717140ffcba18
internal/hostbox/servingpark_service.go JournalService.ExecuteServingPark authority C,J4 rev3=PROPOSED-seed; exact journal request and authenticated enrollment; durable pending receipt before filesystem effects, completed reply after effects; uncertain outcomes retain request df93855480924b39cf49edbeedd136799a7e327860ee4dec7742796d3d428333
internal/hostbox/servingpark_service.go JournalService.prepareServingPark authority C,J4 rev3=PROPOSED-seed; SQLite peer, digest, reply and retention boundary; exact completed replay, bounded request age and clock-rollback rejection before callback; existing bounded receipt pruning file:QueryRowContext@1050,file:QueryRowContext@331 83f1bae59de484a9d1cae09ce8e359aa173634c091203ea3ce4f6e8639e20d5e
@@ -325,8 +325,8 @@ system.go runSystem authority B,C,D,F,J4 rev3=PROPOSED-seed; command arguments,
system.go systemVerb authority B,C,D,F,J4 rev3=PROPOSED-seed; command arguments, claim flags, service identity and root delegation capability before constructing agent-mode resources; one-shot skips Configure, Start, duties and listeners; close resources only after private runtime joins ee25513ed72f9b8338d95682c4c76b4ced26ec1f060528fc5804da9afbfbfd83
@coverage . bf3d70b6a2c5d769a3c580c9d5511231499b788d9c08a542895de8381e5a39c9
@coverage internal/activator 9bc3f0f89cc3902c34917f1c6b2f86a778cec0bed499e5ea783fac9d93c95a15
-@coverage internal/host 6753f47be8c2d263dd1fd116fb112a39a6184fe8400b26046a74f9462c15e1af
-@coverage internal/hostbox 86337ee9d1f7a4bdd15e331cac712c55d39da4cc26a7c013573a7a9535fe3975
+@coverage internal/host 53fdcf5a89a9fb4e81ce6d04d330fa1d035c1ca3e982b1a61358079c65b2eb76
+@coverage internal/hostbox 8830f7182eed4547c79c8b3b6ccbae071b2675eb0ce86196063aff64a4879405
@coverage internal/hostfiles a38dbcd9789ba4acd237e88c834f2b2bfca9847b0d43c32b0116dc8996d7a142
@coverage internal/hostguard a721142f4f3ac5fa17b6b1e2ccfc66ca809cfdcbf2733fdc00fbd0efaa90aeb6
internal/host/rootexecution_recovery.go recoverRootExecutionBeforeUpdate authority D rev3=PROPOSED-seed; extracted update recovery ordering; root instance-control identity; broker durable controller state then best-effort local park and retirement journals; retained park failures cannot gate update eb97b162b0d712dd2fe8971984353ebc10efe1142077a3c2531781dd969cbbf5
@@ -352,10 +352,10 @@ internal/host/claimrestore_observation.go claimRestoreObservation.snapshot autho
internal/host/claimrestore_observation.go sameClaimObservation authority C,J4 rev3=PROPOSED-seed; FS-R11/run31; agent; exact ETag and complete claim comparison; same writer alone is insufficient 460d2a7e29552be9aa2d5a90a962b8f1fea6ba7b494239a4d35f43d4efb9462b
internal/host/claimrestore_observation.go type claimRestoreObservation authority C,J4 rev3=PROPOSED-seed; FS-R11/run31; agent; tracks only Prepare and proven own CAS results for Materialize 60f66cb892d1a4a43b7168c07ce3049eac30c7a1c4a018af646c38350114dd55
internal/host/claimretire_local.go claimArtifactRetirement.Retire authority C,G,J4 rev3=PROPOSED-seed; FS-R11/run31; agent; existing guarded db retirement, reports its actual before/after CAS to the runtime file:Read@900 d724a762d208822fe5e3c19d7ee03df0f911e1e7e20c05597702b42e57581bcc
-internal/host/claimservinghandover_reboot_linux.go ClaimRootHTTP.verifyServingHandoverDirectory authority C,J4 rev3=PROPOSED-seed; FS-R11/run31; root; reads private completed retirement and slot, current boot and bound namespace; exact host/inode/type and persistent filesystem/handle against current authorized mount before rebinding device; no deletion authority 19e8315f54850af3a5ddc04ac6e2e9d9035b2392c9eae7c22e05000d956b7e1e
+internal/host/claimservinghandover_reboot_linux.go ClaimRootHTTP.verifyServingHandoverDirectory authority C,J4 rev3=PROPOSED-seed; run33; root; numeric device identity only with valid same-boot stop evidence; every cross-boot transfer requires historical persistent directory evidence even when device numbers match 03e6aa32c980b6fc3512c88bbf5e93f18e84606c36357810bede2b7d3df25b12
internal/host/claimservinghandover_reboot_other.go ClaimRootHTTP.verifyServingHandoverDirectory authority C,J4 rev3=PROPOSED-seed; FS-R11/run31; root; refuses unsupported non-Linux proof without reads 0932178d6251804b979c84d0131cfbc9f8bec2aa73d88256e77eee9d1526e68a
internal/host/claimwakeassembly.go type ClaimLaunchInputs authority C,J4 rev3=PROPOSED-seed; FS-R11/run31; agent; private callback carries own Restore CAS receipt without granting claim authority 25fd2977645bf4382c54aedef246b915eb63768da3c3d48be862797013182fcf
-internal/hostbox/servinghandover.go ServingHandover.directory authority C,J4 rev3=PROPOSED-seed; FS-R11/run31; root; descriptor-relative capture requires exact inode/type; device mismatch additionally requires production root-private persistent proof a6e6c6faabb9f74c4229bafec2473a66769dc7010b15653d0e3bfa66112ca69e
+internal/hostbox/servinghandover.go ServingHandover.directory authority C,J4 rev3=PROPOSED-seed; run33; root; every directory capture invokes boot-aware verification, including validation, replay and successor inventory write 9bdb9d021846b705e833fe07be1b30727e7ee0f7fc69cca293ffebc05c5681ba
internal/park/local.go Local.RetireSuperseded legacy accepted-legacy accepted-legacy;rev3=PROPOSED-seed; census-ruling-FS-R11; agent; existing guarded retirement remains legacy; returns its actual CAS before/after observations rather than substituting a reread file:Read@2127 e2245dfcfebbf206ccfbd6be5dc937a03639614af2a085f4deeb186413ab7a02
@legacy 79007308de276cec4b99a8c01183456d9a93733db3f3781dc51f08ea9385b101 internal/activator/servingretry.go ServingRetries.Wrap legacy accepted-legacy accepted-legacy;rev3=PROPOSED-seed; census-ruling-FS-R11;census-ruling-FS-R6; agent; existing route/input authority remains legacy; reports exact successful retry CAS to the runtime, which refuses unrelated changes file:Prepare@128,file:Prepare@4325,file:Prepare@4455,file:Read@2888,file:Read@3531,file:Read@5054,file:Read@6084,file:Read@6768,whole:Route@1212,whole:Route@1550,whole:Route@1648,whole:Route@1948,whole:Route@2360,whole:Route@4473,whole:Route@5421,whole:Route@6634,whole:Route@876,whole:Route@954 e6ba56e2fc95197e5b765e2aa7e78d8366a351a5cb98e4720d000bb9975c3a34
@legacy 68f5b655f5909a4e2f24d1357fd19c86c60b0a89a43846940cb7f72158e90181 internal/activator/servingretry.go ServingRetries.settle legacy accepted-legacy accepted-legacy;rev3=PROPOSED-seed; census-ruling-FS-R11; agent; existing claim selection remains legacy; returns exact successful retry settlement CAS through optional callback file:Read@264,file:Read@786 4d51344dfa9560d7bf5f5569efd5e00f902beb840e26afef6c1c2504e6f22fea