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 cgroup
4
5import (
6 "errors"
7 "fmt"
8 "os"
9 "os/exec"
10 "path/filepath"
11 "strings"
12 "syscall"
13 "testing"
14 "time"
15)
16
17// These run without a delegated cgroup: a temp directory is not cgroupfs,
18// so they exercise the failure paths. The kernel file covers the rest.
19
20// TestShimRefusesANonCgroupDirectory: the parent's own check (step 2)
21// refuses before anything starts, as a placement error.
22func TestShimRefusesANonCgroupDirectory(t *testing.T) {
23 m := &Manager{root: t.TempDir(), logf: t.Logf}
24 marker := filepath.Join(t.TempDir(), "ran")
25 cmd := exec.Command("/bin/sh", "-c", "touch "+marker)
26 refused, err := m.startViaShim(m.root, cmd, nil)
27 if refused || !isPlacementError(err) {
28 t.Fatalf("refused=%v err=%v, want a placement error", refused, err)
29 }
30 if cmd.Process != nil {
31 t.Fatal("a process was started")
32 }
33 if _, err := os.Stat(marker); err == nil {
34 t.Fatal("tenant code ran")
35 }
36}
37
38// TestShimRefusesAPathOutsideCgroupfs: a cgroup2 directory is not enough;
39// the path must name it under /sys/fs/cgroup/, or the shim could not check
40// where it landed.
41func TestShimRefusesAPathOutsideCgroupfs(t *testing.T) {
42 if _, err := os.Stat("/sys/fs/cgroup/cgroup.controllers"); err != nil {
43 t.Skip("no cgroup2 mount at /sys/fs/cgroup")
44 }
45 m := &Manager{root: "/sys/fs/cgroup", logf: t.Logf}
46 cmd := exec.Command("/bin/true")
47 _, err := m.startViaShim("/sys/fs/cgroup", cmd, nil)
48 if !isPlacementError(err) || cmd.Process != nil {
49 t.Fatalf("err=%v process=%v, want a placement error and no process", err, cmd.Process)
50 }
51}
52
53// TestShimUnavailableWithoutTheHook: a process whose main never ran the
54// hook never starts a shim.
55func TestShimUnavailableWithoutTheHook(t *testing.T) {
56 if !shimHooked.Load() {
57 t.Fatal("TestMain did not run the hook")
58 }
59 shimHooked.Store(false)
60 t.Cleanup(func() { shimHooked.Store(true) })
61 m := &Manager{root: t.TempDir(), logf: t.Logf}
62 cmd := exec.Command("/bin/true")
63 refused, err := m.startViaShim(m.root, cmd, nil)
64 if refused || !isPlacementError(err) {
65 t.Fatalf("refused=%v err=%v, want a placement error", refused, err)
66 }
67 if cmd.Process != nil {
68 t.Fatal("a process was started")
69 }
70}
71
72// TestShimLookPathErrorIsTheCommands: a command whose LookPath failed
73// answers that error, as exec.Cmd.Start would, and starts nothing.
74func TestShimLookPathErrorIsTheCommands(t *testing.T) {
75 m := &Manager{root: t.TempDir(), logf: t.Logf}
76 cmd := exec.Command("carlos-no-such-command-anywhere")
77 if cmd.Err == nil {
78 t.Skip("LookPath found the command")
79 }
80 _, err := m.startViaShim(m.root, cmd, nil)
81 if err != cmd.Err || isPlacementError(err) || cmd.Process != nil {
82 t.Fatalf("err=%v, want the LookPath error %v and no process", err, cmd.Err)
83 }
84}
85
86// TestShimPlacementFailureIsReported runs a real shim (this test binary,
87// through TestMain's hook) against a temp directory, calling the core with
88// a directory fd the prologue would have refused. The shim's own fstatfs
89// check (shim step 4) answers "P".
90func TestShimPlacementFailureIsReported(t *testing.T) {
91 m := &Manager{root: t.TempDir(), logf: t.Logf}
92 f, err := os.Open(m.root)
93 if err != nil {
94 t.Fatal(err)
95 }
96 defer f.Close()
97 marker := filepath.Join(t.TempDir(), "ran")
98 cmd := exec.Command("/bin/sh", "-c", "touch "+marker)
99 _, err = m.startShimCore(m.root, f, "/nowhere", cmd, nil, time.Time{})
100 if !isPlacementError(err) {
101 t.Fatalf("err=%v, want a placement error", err)
102 }
103 if !strings.Contains(err.Error(), "statfs") {
104 t.Fatalf("err=%v, want the shim's statfs refusal", err)
105 }
106 if _, err := os.Stat(fmt.Sprintf("/proc/%d", cmd.Process.Pid)); err == nil {
107 t.Fatal("the shim was not reaped")
108 }
109 if _, err := os.Stat(marker); err == nil {
110 t.Fatal("tenant code ran")
111 }
112 if m.placementsUnder(m.root) != 0 {
113 t.Fatal("the placement is still registered")
114 }
115}
116
117// TestShimBeforeRefusalRegistersNothing: before is asked only once the
118// process is placed, so a start that cannot be placed never asks it, starts
119// nothing of the tenant, and leaves nothing registered. A refusal after a
120// real placement is TestShimKernelBeforeRunsAfterPlacement.
121func TestShimBeforeRefusalRegistersNothing(t *testing.T) {
122 m := &Manager{root: t.TempDir(), logf: t.Logf}
123 asked := 0
124 before := func() error { asked++; return errors.New("not yours any more") }
125 marker := filepath.Join(t.TempDir(), "ran")
126
127 cmd := exec.Command("/bin/sh", "-c", "touch "+marker)
128 refused, err := m.startViaShim(m.root, cmd, before)
129 if refused || !isPlacementError(err) || cmd.Process != nil {
130 t.Fatalf("prologue: refused=%v err=%v process=%v", refused, err, cmd.Process)
131 }
132
133 f, err := os.Open(m.root)
134 if err != nil {
135 t.Fatal(err)
136 }
137 defer f.Close()
138 cmd = exec.Command("/bin/sh", "-c", "touch "+marker)
139 refused, err = m.startShimCore(m.root, f, "/nowhere", cmd, before, time.Time{})
140 if refused || !isPlacementError(err) {
141 t.Fatalf("core: refused=%v err=%v, want a placement error", refused, err)
142 }
143 if asked != 0 {
144 t.Fatalf("before was asked %d times for a process that was never placed", asked)
145 }
146 if _, err := os.Stat(marker); err == nil {
147 t.Fatal("tenant code ran")
148 }
149 if m.placementsUnder(m.root) != 0 {
150 t.Fatal("the placement is still registered")
151 }
152}
153
154// TestShimRefusesNULInEnv: Start refuses an environment entry that holds a
155// NUL (Go issue #56284), where cmd.Environ() would silently drop it. The
156// core answers Start's own error, not a placement error, and starts and
157// registers nothing. With Env nil the NUL can arrive only through Dir, as
158// the PWD Start adds.
159func TestShimRefusesNULInEnv(t *testing.T) {
160 const want = "exec: environment variable contains NUL"
161 for _, c := range []struct {
162 name string
163 env []string
164 dir string
165 }{
166 {"env entry", []string{"A=1", "BAD=x\x00y", "B=2"}, ""},
167 {"env key", []string{"B\x00AD=x"}, ""},
168 {"PWD from dir", nil, "/no\x00where"},
169 } {
170 t.Run(c.name, func(t *testing.T) {
171 marker := filepath.Join(t.TempDir(), "ran")
172 newCmd := func() *exec.Cmd {
173 cmd := exec.Command("/bin/sh", "-c", "touch "+marker)
174 cmd.Env = c.env
175 cmd.Dir = c.dir
176 return cmd
177 }
178 // The control: a direct start answers exactly this.
179 direct := newCmd()
180 if err := direct.Start(); err == nil || err.Error() != want {
181 t.Fatalf("direct Start: err=%v, want %q", err, want)
182 }
183
184 m := &Manager{root: t.TempDir(), logf: t.Logf}
185 f, err := os.Open(m.root)
186 if err != nil {
187 t.Fatal(err)
188 }
189 defer f.Close()
190 cmd := newCmd()
191 refused, err := m.startShimCore(m.root, f, "/nowhere", cmd, nil, time.Time{})
192 if refused || err == nil || err.Error() != want || isPlacementError(err) {
193 t.Fatalf("refused=%v err=%v, want the command's error %q", refused, err, want)
194 }
195 if cmd.Process != nil {
196 t.Fatal("a process was started")
197 }
198 if cmd.Path != "/bin/sh" {
199 t.Fatalf("cmd was rewritten to %s", cmd.Path)
200 }
201 if _, err := os.Stat(marker); err == nil {
202 t.Fatal("tenant code ran")
203 }
204 if m.placementsUnder(m.root) != 0 {
205 t.Fatal("a placement was registered")
206 }
207 })
208 }
209}
210
211func TestShimStatusClassification(t *testing.T) {
212 for _, c := range []struct {
213 in string
214 timedOut bool
215 placed bool
216 }{
217 {"A\n", false, true},
218 {"P procs: permission denied\n", false, false},
219 {"", false, false}, // died before placement
220 {"", true, false}, // deadline before placement
221 {"garbage\n", false, false},
222 {"A", false, false}, // no newline: not the shim's answer
223 } {
224 placed, err := classifyPlacement([]byte(c.in), c.timedOut, shimDeadline)
225 if placed != c.placed {
226 t.Fatalf("classifyPlacement(%q, %v) placed=%v, want %v", c.in, c.timedOut, placed, c.placed)
227 }
228 if !placed && !isPlacementError(err) {
229 t.Fatalf("classifyPlacement(%q, %v) err=%v, want a placement error", c.in, c.timedOut, err)
230 }
231 if placed && err != nil {
232 t.Fatalf("classifyPlacement(%q, %v) err=%v", c.in, c.timedOut, err)
233 }
234 }
235 if _, err := classifyPlacement([]byte("P procs: permission denied\n"), false, shimDeadline); !strings.Contains(err.Error(), "procs: permission denied") {
236 t.Fatalf("the shim's reason is lost: %v", err)
237 }
238 if _, err := classifyPlacement(nil, true, shimDeadline); !strings.Contains(err.Error(), "no answer") {
239 t.Fatalf("the deadline is not named: %v", err)
240 }
241 if _, err := classifyPlacement(nil, false, shimDeadline); !strings.Contains(err.Error(), "exited before placement") {
242 t.Fatalf("the early exit is not named: %v", err)
243 }
244
245 for _, c := range []struct {
246 in string
247 killed bool
248 started bool
249 op string // "" for a placement error, else the *os.PathError's Op
250 errno syscall.Errno
251 }{
252 {"R\n", false, true, "", 0},
253 {"R\n", true, true, "", 0}, // R then killed: may have exec'd, so started
254 {"", false, false, "", 0}, // died after release, before R: placement error
255 {"", true, false, "", 0}, // killed before R: placement error
256 {"E chdir 2\n", false, false, "chdir", syscall.ENOENT},
257 {"R\nE exec 2\n", false, false, "fork/exec", syscall.ENOENT},
258 {"R\nE exec 7\n", false, false, "fork/exec", syscall.E2BIG},
259 {"garbage\n", false, false, "", 0},
260 {"R\ngarbage\n", false, true, "", 0}, // R and no E after it: started
261 {"R", true, true, "", 0}, // R without its newline still counts
262 } {
263 started, err := classifyExec("/bin/app", "/wd", []byte(c.in), c.killed, shimDeadline)
264 if started != c.started {
265 t.Fatalf("classifyExec(%q, %v) started=%v, want %v", c.in, c.killed, started, c.started)
266 }
267 if started {
268 if err != nil {
269 t.Fatalf("classifyExec(%q, %v) err=%v", c.in, c.killed, err)
270 }
271 continue
272 }
273 if c.op == "" {
274 if !isPlacementError(err) {
275 t.Fatalf("classifyExec(%q, %v) err=%v, want a placement error", c.in, c.killed, err)
276 }
277 continue
278 }
279 var pe *os.PathError
280 if !errors.As(err, &pe) || isPlacementError(err) {
281 t.Fatalf("classifyExec(%q, %v) err=%v, want a *os.PathError", c.in, c.killed, err)
282 }
283 wantPath := "/bin/app"
284 if c.op == "chdir" {
285 wantPath = "/wd"
286 }
287 if pe.Op != c.op || pe.Path != wantPath || pe.Err != c.errno {
288 t.Fatalf("classifyExec(%q, %v) = %#v, want Op %q Path %q Err %v", c.in, c.killed, pe, c.op, wantPath, c.errno)
289 }
290 }
291 if _, err := classifyExec("/bin/app", "/wd", nil, false, shimDeadline); !strings.Contains(err.Error(), "exited before exec") {
292 t.Fatalf("the early exit is not named: %v", err)
293 }
294}
295
296// TestShimWaitCapsAtBy: with by unset or later than shimDeadline from now,
297// each wait is shimDeadline, as for every tenant start (Ruling 9); with by
298// sooner, the wait ends at by and the error names what was left of it.
299func TestShimWaitCapsAtBy(t *testing.T) {
300 before := time.Now()
301 d, within := shimWait(time.Time{})
302 if within != shimDeadline || d.Before(before.Add(shimDeadline)) || d.After(time.Now().Add(shimDeadline)) {
303 t.Fatalf("no cap: deadline %v within %v, want shimDeadline from now", d.Sub(before), within)
304 }
305 late := time.Now().Add(shimDeadline + time.Hour)
306 if d, within := shimWait(late); within != shimDeadline || !d.Before(late) {
307 t.Fatalf("a later by: deadline %v within %v, want shimDeadline from now", d, within)
308 }
309 soon := time.Now().Add(time.Second)
310 if d, within := shimWait(soon); !d.Equal(soon) || within <= 0 || within > time.Second {
311 t.Fatalf("a sooner by: deadline %v within %v, want by and at most 1s", d, within)
312 }
313 past := time.Now().Add(-time.Second)
314 if d, within := shimWait(past); !d.Equal(past) || within != 0 {
315 t.Fatalf("a past by: deadline %v within %v, want by and 0", d, within)
316 }
317 if _, err := classifyPlacement(nil, true, 1500*time.Millisecond); !strings.Contains(err.Error(), "within 1.5s") {
318 t.Fatalf("the capped wait is not named: %v", err)
319 }
320 if _, err := classifyExec("/bin/app", "/wd", nil, true, 1500*time.Millisecond); !strings.Contains(err.Error(), "within 1.5s") {
321 t.Fatalf("the capped wait is not named: %v", err)
322 }
323}
324