//go:build linux package host import ( "errors" "os" "path/filepath" "syscall" "testing" "github.com/carlosframework/platform/internal/instancefs" ) // repeatInstall is a crash before the install's journal: the next install // runs every write again over what the first left in the instance // directory, which the app may have run in since. func repeatInstall(t *testing.T, f *installFixture) { t.Helper() if _, err := f.install(); err != nil { t.Fatal(err) } journal, _ := relocationInstallPath(f.r.DataDir, "rel-1") if err := os.Remove(journal); err != nil { t.Fatal(err) } } // decoy is a directory the tenant must never get written or removed into: // the box's own data, say. It holds one file the tests check survives. func decoy(t *testing.T) string { t.Helper() d := t.TempDir() if err := os.WriteFile(filepath.Join(d, buildHost+".env"), []byte("NOT=the tenant's\n"), 0o600); err != nil { t.Fatal(err) } return d } // untouched fails unless d holds exactly the decoy's own file, unchanged. func untouched(t *testing.T, d string) { t.Helper() names, err := os.ReadDir(d) if err != nil { t.Fatal(err) } if len(names) != 1 || names[0].Name() != buildHost+".env" { t.Fatalf("the install wrote into a directory outside the tenant's: %v", names) } if b, err := os.ReadFile(filepath.Join(d, buildHost+".env")); err != nil || string(b) != "NOT=the tenant's\n" { t.Fatalf("the decoy's file was changed: %q %v", b, err) } } // relink moves path aside and puts a symlink to target in its place. func relink(t *testing.T, path, target string) string { t.Helper() moved := path + ".moved" if err := os.Rename(path, moved); err != nil { t.Fatal(err) } if err := os.Symlink(target, path); err != nil { t.Fatal(err) } return moved } // A repeated install makes, restores and writes nothing through what the // tenant left in its instance directory: a symlink at / // or at the instance directory itself, or a FIFO at /, // refuses the install, never blocks it, and nothing lands outside. func TestARepeatedRelocationInstallWritesNothingThroughWhatTheTenantPlanted(t *testing.T) { for _, tc := range []struct { name string plant func(t *testing.T, f *installFixture, d string) want error }{ {"a symlink at the files directory", func(t *testing.T, f *installFixture, d string) { relink(t, f.paths().FilesDir, d) }, instancefs.ErrSymlink}, // Main's typed inventory refuses this one before any stage B write // (the swap test below reaches the writes themselves). {"a symlinked instance directory", func(t *testing.T, f *installFixture, d string) { relink(t, filepath.Dir(f.paths().FilesDir), d) }, nil}, {"a FIFO at the files directory", func(t *testing.T, f *installFixture, _ string) { p := f.paths().FilesDir if err := os.RemoveAll(p); err != nil { t.Fatal(err) } if err := syscall.Mkfifo(p, 0o600); err != nil { t.Fatal(err) } }, nil}, } { t.Run(tc.name, func(t *testing.T) { f := newInstallFixture(t, nil) repeatInstall(t, f) d := decoy(t) tc.plant(t, f, d) var err error bounded(t, func() error { _, err = f.install(); return nil }) if err == nil || (tc.want != nil && !errors.Is(err, tc.want)) { t.Fatalf("install over a plant: %v; want %v", err, tc.want) } untouched(t, d) if _, ok, _ := f.a.relocationReceipt(f.cfg)(t.Context(), f.read()); ok { t.Fatal("a refused install left a receipt") } }) } } // The race the check-then-open left: a symlink renamed in after the // install opened the files directory, or after it opened the instance // directory's files root and before it writes the env file, is never // followed. The restore lands in the directory that was opened; the env // write and the env removal refuse a linked instance directory. func TestARelocationInstallFollowsNoLinkSwappedInAfterItOpened(t *testing.T) { t.Run("the files directory, between its check and its open", func(t *testing.T) { f := newInstallFixture(t, nil) repeatInstall(t, f) files := f.paths().FilesDir d := decoy(t) swapOnce(t, "checked", func() { relink(t, files, d) }) if _, err := f.install(); !errors.Is(err, instancefs.ErrSymlink) { t.Fatalf("install after the files directory was linked: %v", err) } untouched(t, d) }) t.Run("the files directory, after its open", func(t *testing.T) { f := newInstallFixture(t, nil) repeatInstall(t, f) files := f.paths().FilesDir if err := os.RemoveAll(filepath.Join(files, "public")); err != nil { t.Fatal(err) } d := decoy(t) var moved string swapOnce(t, "opened", func() { moved = relink(t, files, d) }) if _, err := f.install(); err != nil { t.Fatal(err) } untouched(t, d) if _, err := os.Stat(filepath.Join(moved, "public", "index.html")); err != nil { t.Fatalf("the restore did not land in the directory it opened: %v", err) } }) for _, tc := range []struct { name string fixture func(t *testing.T) *installFixture }{ {"the env write", func(t *testing.T) *installFixture { return newInstallFixture(t, nil) }}, {"the env removal", func(t *testing.T) *installFixture { return newInstallFixtureWith(t, buildOptions{NoFiles: true}) }}, } { t.Run(tc.name, func(t *testing.T) { f := tc.fixture(t) repeatInstall(t, f) d := decoy(t) swapOnce(t, "opened", func() { relink(t, filepath.Dir(f.paths().FilesDir), d) }) if _, err := f.install(); !errors.Is(err, instancefs.ErrSymlink) { t.Fatalf("install after the instance directory was linked: %v", err) } untouched(t, d) }) } } // swapOnce runs swap the first time the install or the flush reaches // stage on the tenant's ground (tenantRootSeam), then never again. func swapOnce(t *testing.T, stage string, swap func()) { t.Helper() done := false prev := tenantRootSeam tenantRootSeam = func(at string) { if at == stage && !done { done = true swap() } } t.Cleanup(func() { tenantRootSeam = prev }) }