carlos / platform Public

host: the guarded database install refuses a symlink or other non-regular file at <dbPath>.gen, dangling or not (final re-review residual)

ReadGenerationMarker follows a symlink and reads a dangling one as absent,
so WriteGen would have replaced a .gen symlink the landing did not write.
The guarded block now Lstat-checks .gen first; DBPath (regularFile) and
the WAL paths were already Lstat-checked, and a non-regular DBPath is now
the same errLandingChanged refusal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev f417182cb1dea1b96eb1558929721a16e6c6df63 parent 8107bfb
2 files changed, +50 −2
  • internal/host/movesland_finalfix_test.go +38 −0
  • internal/host/moveslandinstall.go +12 −2

CI failed — run details

diff --git a/internal/host/movesland_finalfix_test.go b/internal/host/movesland_finalfix_test.go
index 232a6774b..5e85ba952 100644
--- a/internal/host/movesland_finalfix_test.go
+++ b/internal/host/movesland_finalfix_test.go
@@ -244,6 +244,44 @@ func TestLandingInstallRefusesASuccessor(t *testing.T) {
}
notRestored(t, h)
})
+ // A symlink at a destination is never this landing's, dangling or not:
+ // it is not read through (a dangling one would look absent) and never
+ // replaced (final re-review residual).
+ for _, tc := range []struct{ name, suffix string }{{"a dangling marker symlink", ".gen"}, {"a dangling database symlink", ""}, {"a dangling WAL symlink", "-wal"}} {
+ t.Run(tc.name, func(t *testing.T) {
+ h := newLandingHarness(t)
+ h.flip(t)
+ link, target := dbPath(h)+tc.suffix, filepath.Join(h.tdir, "elsewhere", "nothing")
+ h.landFault = func(p string) error {
+ if p == "before-db-verify" {
+ if err := os.Symlink(target, link); err != nil {
+ t.Fatal(err)
+ }
+ }
+ return nil
+ }
+ h.land(t)
+ if !containsString(h.landHits, "before-db-verify") {
+ t.Fatalf("the install never observed: %v", h.landHits)
+ }
+ fi, err := os.Lstat(link)
+ if err != nil || fi.Mode()&os.ModeSymlink == 0 {
+ t.Fatalf("%s is no longer the symlink the landing did not write: %v %v", link, fi, err)
+ }
+ if got, err := os.Readlink(link); err != nil || got != target {
+ t.Fatalf("%s points at %q (%v), want %q", link, got, err, target)
+ }
+ if _, err := os.Lstat(target); !errors.Is(err, os.ErrNotExist) {
+ t.Fatalf("something was written through the symlink: %v", err)
+ }
+ if tc.suffix != "" {
+ if _, err := os.Lstat(dbPath(h)); !errors.Is(err, os.ErrNotExist) {
+ t.Fatalf("installed beside a symlink the landing did not write: %v", err)
+ }
+ }
+ notRestored(t, h)
+ })
+ }
t.Run("a successor marker", func(t *testing.T) {
h := newLandingHarness(t)
h.flip(t)
diff --git a/internal/host/moveslandinstall.go b/internal/host/moveslandinstall.go
index 6d4a08df0..4a1be3cfe 100644
--- a/internal/host/moveslandinstall.go
+++ b/internal/host/moveslandinstall.go
@@ -537,6 +537,16 @@ func (l *landingRun) restore(ctx context.Context, obs hostclaim.Observed) (sha s
}
}
err = l.guarded(ctx, captured, provision.LandConfigured, func() error {
+ // Every destination is Lstat-checked, never read through: a symlink
+ // or any other non-regular file at <dbPath>.gen, DBPath or the WAL
+ // paths, dangling or not, is not this landing's and is never
+ // replaced (final re-review residual). ReadGenerationMarker follows
+ // a symlink and reads a dangling one as absent.
+ if fi, err := os.Lstat(dbPath + ".gen"); err == nil && !fi.Mode().IsRegular() {
+ return fmt.Errorf("%w: %s.gen is not a regular file (%s)", errLandingChanged, dbPath, fi.Mode())
+ } else if err != nil && !errors.Is(err, os.ErrNotExist) {
+ return err
+ }
gen, err := hostbox.ReadGenerationMarker(dbPath + ".gen")
if err != nil {
return fmt.Errorf("read %s.gen: %w", dbPath, err)
@@ -544,9 +554,9 @@ func (l *landingRun) restore(ctx context.Context, obs hostclaim.Observed) (sha s
if gen != "" && gen != id {
return fmt.Errorf("%w: %s.gen names generation %s", errLandingChanged, dbPath, gen)
}
- now, err := regularFile(dbPath)
+ now, err := regularFile(dbPath) // Lstat: a non-regular DBPath is refused
if err != nil {
- return err
+ return fmt.Errorf("%w: %v", errLandingChanged, err)
}
switch {
case now: