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 "context"
7 "errors"
8 "fmt"
9 "io/fs"
10 "maps"
11 "os"
12 "path/filepath"
13 "slices"
14 "syscall"
15 "testing"
16
17 "golang.org/x/sys/unix"
18)
19
20// floorDirsOpt is an opt directory with system/ and system/guard at 0755,
21// as guard-arm leaves them, reached with no symlink on the way. The tests
22// walk from opt itself as the base, owned by the test's own uid and made
23// 0755 whatever the test's umask: the real ancestors of a scratch directory
24// are not the test's.
25func floorDirsOpt(t *testing.T) string {
26 t.Helper()
27 opt, err := filepath.EvalSymlinks(t.TempDir())
28 if err != nil {
29 t.Fatal(err)
30 }
31 guard := systemGuardDir(opt)
32 if err := os.MkdirAll(guard, 0o755); err != nil {
33 t.Fatal(err)
34 }
35 for _, dir := range []string{opt, filepath.Join(opt, "system"), guard} {
36 if err := os.Chmod(dir, 0o755); err != nil {
37 t.Fatal(err)
38 }
39 }
40 return opt
41}
42
43func floorDirMode(t *testing.T, dir string) os.FileMode {
44 t.Helper()
45 st, err := os.Lstat(dir)
46 if err != nil {
47 t.Fatal(err)
48 }
49 return st.Mode()
50}
51
52// journal-migrate runs from the window's umask 077 root shell. Both public
53// floor directories come out 0755, whether they are missing, left 0700 by
54// an earlier attempt, or left by an earlier build's interrupted
55// mkdirAllExact (mkdir done under the umask, chmod never reached).
56func TestRootFloorDirsIgnoreUmask(t *testing.T) {
57 old := syscall.Umask(0o077)
58 defer syscall.Umask(old)
59 uid, gid := uint32(os.Getuid()), uint32(os.Getgid())
60 for name, earlier := range map[string]func(opt string) error{
61 "missing": func(string) error { return nil },
62 "moves 0700": func(opt string) error { return os.Mkdir(rootFloorDir(opt), 0o700) },
63 "seeds 0700": func(opt string) error { return os.Mkdir(rootSeedsFloorDir(opt), 0o700) },
64 "interrupted": func(opt string) error { return os.MkdirAll(rootSeedsFloorDir(opt), 0o755) }, // an earlier build's mkdirAllExact mkdir, under 077
65 "both 0700": func(opt string) error {
66 return errors.Join(os.Mkdir(rootFloorDir(opt), 0o700), os.Mkdir(rootSeedsFloorDir(opt), 0o700))
67 },
68 } {
69 t.Run(name, func(t *testing.T) {
70 opt := floorDirsOpt(t)
71 if err := earlier(opt); err != nil {
72 t.Fatal(err)
73 }
74 if err := prepareRootFloorDirsOwned(opt, opt, uid, gid); err != nil {
75 t.Fatal(err)
76 }
77 for _, dir := range []string{rootFloorDir(opt), rootSeedsFloorDir(opt), systemGuardDir(opt)} {
78 if m := floorDirMode(t, dir); !m.IsDir() || m.Perm() != 0o755 {
79 t.Fatalf("%s is %v, want a 0755 directory", dir, m)
80 }
81 }
82 })
83 }
84}
85
86// The repair touches only a real directory with the expected owner: a
87// symlink (at the directory or on its way), a file, or another owner is
88// refused and left as it was.
89func TestRootFloorDirsRefuseWhatTheyDoNotOwn(t *testing.T) {
90 uid, gid := uint32(os.Getuid()), uint32(os.Getgid())
91 for name, c := range map[string]struct {
92 setup func(t *testing.T, opt string) string // returns the path whose mode must not change
93 uid, gid uint32
94 want []error // the refusal is one of these, so it is this case's own
95 }{
96 "symlink": {setup: func(t *testing.T, opt string) string {
97 target := filepath.Join(opt, "elsewhere")
98 if err := os.Mkdir(target, 0o700); err != nil {
99 t.Fatal(err)
100 }
101 if err := os.Symlink(target, rootFloorDir(opt)); err != nil {
102 t.Fatal(err)
103 }
104 return target
105 }, uid: uid, gid: gid, want: []error{unix.ELOOP, unix.ENOTDIR}},
106 "symlink on the way": {setup: func(t *testing.T, opt string) string {
107 real := filepath.Join(opt, "realguard")
108 if err := os.Rename(systemGuardDir(opt), real); err != nil {
109 t.Fatal(err)
110 }
111 if err := os.Mkdir(filepath.Join(real, "moves"), 0o700); err != nil {
112 t.Fatal(err)
113 }
114 if err := os.Symlink(real, systemGuardDir(opt)); err != nil {
115 t.Fatal(err)
116 }
117 return filepath.Join(real, "moves")
118 }, uid: uid, gid: gid, want: []error{unix.ELOOP, unix.ENOTDIR}},
119 "file": {setup: func(t *testing.T, opt string) string {
120 if err := os.WriteFile(rootFloorDir(opt), nil, 0o600); err != nil {
121 t.Fatal(err)
122 }
123 return rootFloorDir(opt)
124 }, uid: uid, gid: gid, want: []error{unix.ENOTDIR}},
125 // Unprivileged, every directory here is the test's, so another
126 // owner refuses at the base, before the leaf; the leaf's own owner
127 // check is the root runner's (the Foreign rerun witness).
128 "other owner": {setup: func(t *testing.T, opt string) string {
129 if err := os.Mkdir(rootFloorDir(opt), 0o700); err != nil {
130 t.Fatal(err)
131 }
132 return rootFloorDir(opt)
133 }, uid: uid + 1, gid: gid, want: []error{os.ErrPermission}},
134 "other group": {setup: func(t *testing.T, opt string) string {
135 if err := os.Mkdir(rootFloorDir(opt), 0o700); err != nil {
136 t.Fatal(err)
137 }
138 return rootFloorDir(opt)
139 }, uid: uid, gid: gid + 1, want: []error{os.ErrPermission}},
140 } {
141 t.Run(name, func(t *testing.T) {
142 opt := floorDirsOpt(t)
143 left := c.setup(t, opt)
144 before := floorDirMode(t, left)
145 err := prepareRootFloorDirsOwned(opt, opt, c.uid, c.gid)
146 if !slices.ContainsFunc(c.want, func(w error) bool { return errors.Is(err, w) }) {
147 t.Fatalf("repair = %v, want a refusal with one of %v", err, c.want)
148 }
149 if after := floorDirMode(t, left); after != before {
150 t.Fatalf("%s changed from %v to %v", left, before, after)
151 }
152 })
153 }
154 // Production's owner is root: unprivileged, the test's own directories
155 // are another owner's, so prepareRootFloorDirs refuses them untouched.
156 if os.Geteuid() != 0 {
157 opt := floorDirsOpt(t)
158 if err := os.Mkdir(rootFloorDir(opt), 0o700); err != nil {
159 t.Fatal(err)
160 }
161 if err := prepareRootFloorDirs(opt); !errors.Is(err, os.ErrPermission) {
162 t.Fatalf("prepareRootFloorDirs over a non-root directory = %v", err)
163 }
164 if m := floorDirMode(t, rootFloorDir(opt)); m.Perm() != 0o700 {
165 t.Fatalf("refused directory is now %v", m)
166 }
167 }
168}
169
170// floorTree records every path under roots, with its mode, owner and, for
171// a symlink, its target, so a refusal can be shown to have changed nothing.
172func floorTree(t *testing.T, roots ...string) map[string]string {
173 t.Helper()
174 out := map[string]string{}
175 for _, root := range roots {
176 err := filepath.WalkDir(root, func(path string, _ fs.DirEntry, err error) error {
177 if err != nil {
178 return err
179 }
180 st, err := os.Lstat(path)
181 if err != nil {
182 return err
183 }
184 sys := st.Sys().(*syscall.Stat_t)
185 v := fmt.Sprintf("%v %d:%d", st.Mode(), sys.Uid, sys.Gid)
186 if st.Mode()&os.ModeSymlink != 0 {
187 target, err := os.Readlink(path)
188 if err != nil {
189 return err
190 }
191 v += " -> " + target
192 }
193 out[path] = v
194 return nil
195 })
196 if err != nil {
197 t.Fatal(err)
198 }
199 }
200 return out
201}
202
203// Fix round 2: the repair checks every directory from its base to the
204// floor directory's parent before it makes or changes anything. A symlink
205// or a missing directory on the way, an ancestor writable by group or
206// others, or one with another owner refuses, whether the floor directory
207// is missing (nothing is made, in either tree) or left 0700 (it stays so).
208func TestRootFloorDirsCheckAncestryFirst(t *testing.T) {
209 uid, gid := uint32(os.Getuid()), uint32(os.Getgid())
210 elsewhere := func(t *testing.T) string {
211 t.Helper()
212 dir, err := filepath.EvalSymlinks(t.TempDir())
213 if err != nil {
214 t.Fatal(err)
215 }
216 return dir
217 }
218 moves0700 := func(t *testing.T, opt string) {
219 t.Helper()
220 if err := os.Mkdir(rootFloorDir(opt), 0o700); err != nil {
221 t.Fatal(err)
222 }
223 }
224 for name, c := range map[string]struct {
225 // setup returns any tree outside opt that must not change either.
226 setup func(t *testing.T, opt string) []string
227 uid, gid uint32
228 want []error // the refusal is one of these, so it is this case's own
229 }{
230 "symlinked guard, moves missing": {setup: func(t *testing.T, opt string) []string {
231 target := elsewhere(t)
232 if err := os.Remove(systemGuardDir(opt)); err != nil {
233 t.Fatal(err)
234 }
235 if err := os.Symlink(target, systemGuardDir(opt)); err != nil {
236 t.Fatal(err)
237 }
238 return []string{target}
239 }, uid: uid, gid: gid, want: []error{unix.ELOOP, unix.ENOTDIR}},
240 "symlinked system, moves missing": {setup: func(t *testing.T, opt string) []string {
241 target := elsewhere(t)
242 if err := os.Mkdir(filepath.Join(target, "guard"), 0o755); err != nil {
243 t.Fatal(err)
244 }
245 if err := os.RemoveAll(filepath.Join(opt, "system")); err != nil {
246 t.Fatal(err)
247 }
248 if err := os.Symlink(target, filepath.Join(opt, "system")); err != nil {
249 t.Fatal(err)
250 }
251 return []string{target}
252 }, uid: uid, gid: gid, want: []error{unix.ELOOP, unix.ENOTDIR}},
253 "guard missing": {setup: func(t *testing.T, opt string) []string {
254 if err := os.Remove(systemGuardDir(opt)); err != nil {
255 t.Fatal(err)
256 }
257 return nil
258 }, uid: uid, gid: gid, want: []error{unix.ENOENT}},
259 "group-writable system, moves 0700": {setup: func(t *testing.T, opt string) []string {
260 moves0700(t, opt)
261 if err := os.Chmod(filepath.Join(opt, "system"), 0o775); err != nil {
262 t.Fatal(err)
263 }
264 return nil
265 }, uid: uid, gid: gid, want: []error{os.ErrPermission}},
266 "world-writable guard, moves missing": {setup: func(t *testing.T, opt string) []string {
267 if err := os.Chmod(systemGuardDir(opt), 0o777); err != nil {
268 t.Fatal(err)
269 }
270 return nil
271 }, uid: uid, gid: gid, want: []error{os.ErrPermission}},
272 "world-writable base, moves 0700": {setup: func(t *testing.T, opt string) []string {
273 moves0700(t, opt)
274 if err := os.Chmod(opt, 0o757); err != nil {
275 t.Fatal(err)
276 }
277 return nil
278 }, uid: uid, gid: gid, want: []error{os.ErrPermission}},
279 // Every ancestor is another owner's; the floor directory would be
280 // made by the test's own uid, so a create before the ancestry check
281 // shows in the tree.
282 "other-owned ancestors, moves missing": {setup: func(t *testing.T, opt string) []string {
283 return nil
284 }, uid: uid + 1, gid: gid, want: []error{os.ErrPermission}},
285 } {
286 t.Run(name, func(t *testing.T) {
287 opt := floorDirsOpt(t)
288 others := c.setup(t, opt)
289 roots := append([]string{opt}, others...)
290 before := floorTree(t, roots...)
291 err := prepareRootFloorDirsOwned(opt, opt, c.uid, c.gid)
292 if err == nil {
293 t.Fatal("repair accepted it")
294 }
295 if !slices.ContainsFunc(c.want, func(w error) bool { return errors.Is(err, w) }) {
296 t.Fatalf("refused with %v, want one of %v", err, c.want)
297 }
298 if after := floorTree(t, roots...); !maps.Equal(before, after) {
299 t.Fatalf("the refused repair changed the tree:\nbefore %v\nafter %v", before, after)
300 }
301 for _, root := range others {
302 if _, err := os.Lstat(filepath.Join(root, "moves")); !errors.Is(err, os.ErrNotExist) {
303 t.Fatal("moves made through the symlink", err)
304 }
305 }
306 })
307 }
308 // The repair's own argument checks: a floor directory not beneath the
309 // base, or the base itself, refuses before any open.
310 opt := floorDirsOpt(t)
311 for _, base := range []string{rootFloorDir(opt), filepath.Join(opt, "elsewhere"), "relative"} {
312 if err := repairRootFloorDir(base, rootFloorDir(opt), uid, gid); !errors.Is(err, os.ErrPermission) {
313 t.Fatalf("base %s: %v", base, err)
314 }
315 }
316 if _, err := os.Lstat(rootFloorDir(opt)); !errors.Is(err, os.ErrNotExist) {
317 t.Fatal("moves made", err)
318 }
319}
320
321// The migration repairs the floor directories under its lock and quiesce,
322// before guardReady's first floor read; a refused repair stops it there,
323// with no census and no record written.
324func TestRootMigrationPreparesFloorsBeforeGuardReady(t *testing.T) {
325 m, _, _ := migrationFixture(t)
326 var order []string
327 quiesce := m.quiesce
328 m.quiesce = func(ctx context.Context) (func(), error) { order = append(order, "quiesce"); return quiesce(ctx) }
329 m.prepareFloors = func(context.Context) error { order = append(order, "prepareFloors"); return nil }
330 m.guardReady = func(context.Context) error { order = append(order, "guardReady"); return nil }
331 observe := m.observe
332 m.observe = func(ctx context.Context) (rootMigrationSnapshot, error) {
333 order = append(order, "observe")
334 return observe(ctx)
335 }
336 if err := m.run(t.Context()); err != nil {
337 t.Fatal(err)
338 }
339 if len(order) < 4 || !slices.Equal(order[:4], []string{"quiesce", "prepareFloors", "guardReady", "observe"}) {
340 t.Fatalf("order = %v", order)
341 }
342
343 m, _, _ = migrationFixture(t)
344 guarded := false
345 m.prepareFloors = func(context.Context) error { return os.ErrPermission }
346 m.guardReady = func(context.Context) error { guarded = true; return nil }
347 if err := m.run(t.Context()); !errors.Is(err, os.ErrPermission) || guarded {
348 t.Fatalf("refused repair: run = %v, guardReady ran = %v", err, guarded)
349 }
350 if _, err := m.execution.Read("census.json"); !os.IsNotExist(err) {
351 t.Fatal("census before the floor repair", err)
352 }
353 if _, err := m.state.Read("migration.json"); !os.IsNotExist(err) {
354 t.Fatal("migration record before the floor repair", err)
355 }
356
357 m, _, _ = migrationFixture(t)
358 m.prepareFloors = nil
359 if err := m.run(t.Context()); !errors.Is(err, errRootMigrationBusy) {
360 t.Fatalf("no floor repair wired: run = %v", err)
361 }
362}
363