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>
2 files changed,
+50
−2
internal/host/movesland_finalfix_test.go+38 −0internal/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: |