carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package host
4
5import (
6 "errors"
7 "os"
8 "path/filepath"
9 "syscall"
10 "testing"
11
12 "github.com/carlosframework/platform/internal/instancefs"
13)
14
15// repeatInstall is a crash before the install's journal: the next install
16// runs every write again over what the first left in the instance
17// directory, which the app may have run in since.
18func repeatInstall(t *testing.T, f *installFixture) {
19 t.Helper()
20 if _, err := f.install(); err != nil {
21 t.Fatal(err)
22 }
23 journal, _ := relocationInstallPath(f.r.DataDir, "rel-1")
24 if err := os.Remove(journal); err != nil {
25 t.Fatal(err)
26 }
27}
28
29// decoy is a directory the tenant must never get written or removed into:
30// the box's own data, say. It holds one file the tests check survives.
31func decoy(t *testing.T) string {
32 t.Helper()
33 d := t.TempDir()
34 if err := os.WriteFile(filepath.Join(d, buildHost+".env"), []byte("NOT=the tenant's\n"), 0o600); err != nil {
35 t.Fatal(err)
36 }
37 return d
38}
39
40// untouched fails unless d holds exactly the decoy's own file, unchanged.
41func untouched(t *testing.T, d string) {
42 t.Helper()
43 names, err := os.ReadDir(d)
44 if err != nil {
45 t.Fatal(err)
46 }
47 if len(names) != 1 || names[0].Name() != buildHost+".env" {
48 t.Fatalf("the install wrote into a directory outside the tenant's: %v", names)
49 }
50 if b, err := os.ReadFile(filepath.Join(d, buildHost+".env")); err != nil || string(b) != "NOT=the tenant's\n" {
51 t.Fatalf("the decoy's file was changed: %q %v", b, err)
52 }
53}
54
55// relink moves path aside and puts a symlink to target in its place.
56func relink(t *testing.T, path, target string) string {
57 t.Helper()
58 moved := path + ".moved"
59 if err := os.Rename(path, moved); err != nil {
60 t.Fatal(err)
61 }
62 if err := os.Symlink(target, path); err != nil {
63 t.Fatal(err)
64 }
65 return moved
66}
67
68// A repeated install makes, restores and writes nothing through what the
69// tenant left in its instance directory: a symlink at <instance>/<host>
70// or at the instance directory itself, or a FIFO at <instance>/<host>,
71// refuses the install, never blocks it, and nothing lands outside.
72func TestARepeatedRelocationInstallWritesNothingThroughWhatTheTenantPlanted(t *testing.T) {
73 for _, tc := range []struct {
74 name string
75 plant func(t *testing.T, f *installFixture, d string)
76 want error
77 }{
78 {"a symlink at the files directory", func(t *testing.T, f *installFixture, d string) {
79 relink(t, f.paths().FilesDir, d)
80 }, instancefs.ErrSymlink},
81 // Main's typed inventory refuses this one before any stage B write
82 // (the swap test below reaches the writes themselves).
83 {"a symlinked instance directory", func(t *testing.T, f *installFixture, d string) {
84 relink(t, filepath.Dir(f.paths().FilesDir), d)
85 }, nil},
86 {"a FIFO at the files directory", func(t *testing.T, f *installFixture, _ string) {
87 p := f.paths().FilesDir
88 if err := os.RemoveAll(p); err != nil {
89 t.Fatal(err)
90 }
91 if err := syscall.Mkfifo(p, 0o600); err != nil {
92 t.Fatal(err)
93 }
94 }, nil},
95 } {
96 t.Run(tc.name, func(t *testing.T) {
97 f := newInstallFixture(t, nil)
98 repeatInstall(t, f)
99 d := decoy(t)
100 tc.plant(t, f, d)
101 var err error
102 bounded(t, func() error { _, err = f.install(); return nil })
103 if err == nil || (tc.want != nil && !errors.Is(err, tc.want)) {
104 t.Fatalf("install over a plant: %v; want %v", err, tc.want)
105 }
106 untouched(t, d)
107 if _, ok, _ := f.a.relocationReceipt(f.cfg)(t.Context(), f.read()); ok {
108 t.Fatal("a refused install left a receipt")
109 }
110 })
111 }
112}
113
114// The race the check-then-open left: a symlink renamed in after the
115// install opened the files directory, or after it opened the instance
116// directory's files root and before it writes the env file, is never
117// followed. The restore lands in the directory that was opened; the env
118// write and the env removal refuse a linked instance directory.
119func TestARelocationInstallFollowsNoLinkSwappedInAfterItOpened(t *testing.T) {
120 t.Run("the files directory, between its check and its open", func(t *testing.T) {
121 f := newInstallFixture(t, nil)
122 repeatInstall(t, f)
123 files := f.paths().FilesDir
124 d := decoy(t)
125 swapOnce(t, "checked", func() { relink(t, files, d) })
126 if _, err := f.install(); !errors.Is(err, instancefs.ErrSymlink) {
127 t.Fatalf("install after the files directory was linked: %v", err)
128 }
129 untouched(t, d)
130 })
131 t.Run("the files directory, after its open", func(t *testing.T) {
132 f := newInstallFixture(t, nil)
133 repeatInstall(t, f)
134 files := f.paths().FilesDir
135 if err := os.RemoveAll(filepath.Join(files, "public")); err != nil {
136 t.Fatal(err)
137 }
138 d := decoy(t)
139 var moved string
140 swapOnce(t, "opened", func() { moved = relink(t, files, d) })
141 if _, err := f.install(); err != nil {
142 t.Fatal(err)
143 }
144 untouched(t, d)
145 if _, err := os.Stat(filepath.Join(moved, "public", "index.html")); err != nil {
146 t.Fatalf("the restore did not land in the directory it opened: %v", err)
147 }
148 })
149 for _, tc := range []struct {
150 name string
151 fixture func(t *testing.T) *installFixture
152 }{
153 {"the env write", func(t *testing.T) *installFixture { return newInstallFixture(t, nil) }},
154 {"the env removal", func(t *testing.T) *installFixture { return newInstallFixtureWith(t, buildOptions{NoFiles: true}) }},
155 } {
156 t.Run(tc.name, func(t *testing.T) {
157 f := tc.fixture(t)
158 repeatInstall(t, f)
159 d := decoy(t)
160 swapOnce(t, "opened", func() { relink(t, filepath.Dir(f.paths().FilesDir), d) })
161 if _, err := f.install(); !errors.Is(err, instancefs.ErrSymlink) {
162 t.Fatalf("install after the instance directory was linked: %v", err)
163 }
164 untouched(t, d)
165 })
166 }
167}
168
169// swapOnce runs swap the first time the install or the flush reaches
170// stage on the tenant's ground (tenantRootSeam), then never again.
171func swapOnce(t *testing.T, stage string, swap func()) {
172 t.Helper()
173 done := false
174 prev := tenantRootSeam
175 tenantRootSeam = func(at string) {
176 if at == stage && !done {
177 done = true
178 swap()
179 }
180 }
181 t.Cleanup(func() { tenantRootSeam = prev })
182}
183