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 "context"
7 "errors"
8 "fmt"
9 "os"
10 "os/exec"
11 "path/filepath"
12 "strings"
13 "testing"
14 "time"
15)
16
17// The arm-time placement check (zdd readiness 3, Task 3). The ungated tests
18// run anywhere; the kernel ones need CARLOS_SHIM_KERNEL=1 inside a unit with
19// Delegate=yes, as shim_kernel_linux_test.go's do:
20//
21// systemd-run --user --wait --pipe --collect -p Delegate=yes \
22// -E TMPDIR=/tmp/k/zddr3 -E CARLOS_SHIM_KERNEL=1 -E CARLOS_GEN_KERNEL=1 \
23// cgroup.test -test.run 'VerifyPlacement' -test.v -test.count=1
24
25func assertNoPlacementCheckDir(t *testing.T, root string) {
26 t.Helper()
27 if _, err := os.Lstat(filepath.Join(root, placementCheckName)); !errors.Is(err, os.ErrNotExist) {
28 t.Fatalf("%s left behind under %s (%v)", placementCheckName, root, err)
29 }
30}
31
32func TestVerifyPlacementNilManager(t *testing.T) {
33 var m *Manager
34 if err := m.VerifyPlacement(context.Background()); !errors.Is(err, ErrNoDelegation) {
35 t.Fatalf("nil manager: %v, want ErrNoDelegation", err)
36 }
37}
38
39func TestVerifyPlacementWithoutTheShim(t *testing.T) {
40 prev := shimHooked.Load()
41 shimHooked.Store(false)
42 t.Cleanup(func() { shimHooked.Store(prev) })
43 root := t.TempDir()
44 m := &Manager{root: root, logf: t.Logf}
45 err := m.VerifyPlacement(context.Background())
46 if err == nil || !strings.Contains(err.Error(), "shim") {
47 t.Fatalf("no shim: %v, want an error naming the shim", err)
48 }
49 assertNoPlacementCheckDir(t, root)
50}
51
52// A manager over a directory that is no cgroup answers an error and leaves
53// nothing behind, a leftover from an earlier run included.
54func TestVerifyPlacementOverATempRootFails(t *testing.T) {
55 root := t.TempDir()
56 if err := os.Mkdir(filepath.Join(root, placementCheckName), 0o755); err != nil {
57 t.Fatal(err)
58 }
59 m := &Manager{root: root, logf: t.Logf}
60 err := m.VerifyPlacement(context.Background())
61 if err == nil {
62 t.Fatal("a temp directory passed the placement check")
63 }
64 t.Logf("temp root: %v", err)
65 assertNoPlacementCheckDir(t, root)
66 if m.placementsUnder(root) != 0 {
67 t.Fatal("a placement is still registered")
68 }
69}
70
71// TestVerifyPlacementKernelChild is the probe that never exits: it sleeps
72// when it finds itself in the placement check's cgroup, and otherwise (a
73// normal test run) returns at once.
74func TestVerifyPlacementKernelChild(t *testing.T) {
75 raw, err := os.ReadFile("/proc/self/cgroup")
76 if err != nil {
77 return
78 }
79 rel, ok := ParseSelfCgroup(string(raw))
80 if !ok || filepath.Base(rel) != placementCheckName {
81 return
82 }
83 time.Sleep(time.Minute)
84}
85
86// awaitProbe judges a probe that has already exited by its own exit, never
87// by which of its two channels the select saw first: with ctx already done
88// and the probe a zombie, an exit 0 passes and an exit 1 fails as the
89// probe's failure, every time. One still running is killed, reaped, and
90// fails on ctx's error. Ungated: plain processes, no cgroup.
91func TestAwaitProbeJudgesAnExitedProbeByItsExit(t *testing.T) {
92 done, cancel := context.WithCancel(context.Background())
93 cancel()
94 for i := 0; i < 50; i++ {
95 ok := exitedProbe(t, "/bin/true")
96 if err := awaitProbe(done, ok); err != nil {
97 t.Fatalf("round %d: a probe that exited 0 before ctx ended: %v, want nil", i, err)
98 }
99 bad := exitedProbe(t, "/bin/false")
100 err := awaitProbe(done, bad)
101 if err == nil || errors.Is(err, context.Canceled) || !strings.Contains(err.Error(), "the probe failed: exit status 1") {
102 t.Fatalf("round %d: a probe that exited 1 before ctx ended: %v, want its own exit", i, err)
103 }
104 }
105 hung := exec.Command("/bin/sleep", "60")
106 if err := hung.Start(); err != nil {
107 t.Fatal(err)
108 }
109 err := awaitProbe(done, hung)
110 if !errors.Is(err, context.Canceled) || !strings.Contains(err.Error(), "did not exit") {
111 t.Fatalf("a running probe with ctx done: %v, want ctx's error", err)
112 }
113 if hung.ProcessState == nil {
114 t.Fatal("the killed probe was not reaped")
115 }
116}
117
118// exitedProbe starts path and returns once the process has exited and is
119// a zombie, not yet reaped.
120func exitedProbe(t *testing.T, path string) *exec.Cmd {
121 t.Helper()
122 cmd := exec.Command(path)
123 if err := cmd.Start(); err != nil {
124 t.Fatal(err)
125 }
126 stat := fmt.Sprintf("/proc/%d/stat", cmd.Process.Pid)
127 for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(time.Millisecond) {
128 raw, err := os.ReadFile(stat)
129 if err != nil {
130 t.Fatal(err)
131 }
132 // The state follows the parenthesised command name.
133 if i := strings.LastIndexByte(string(raw), ')'); i >= 0 && strings.HasPrefix(string(raw[i:]), ") Z") {
134 return cmd
135 }
136 }
137 t.Fatalf("%s did not exit", path)
138 return nil
139}
140
141func TestVerifyPlacementKernel(t *testing.T) {
142 m := shimKernelManager(t)
143 if err := m.VerifyPlacement(context.Background()); err != nil {
144 t.Fatalf("VerifyPlacement: %v", err)
145 }
146 assertNoPlacementCheckDir(t, m.root)
147 if m.placementsUnder(m.root) != 0 {
148 t.Fatal("a placement is still registered")
149 }
150}
151
152// A leftover from an earlier run (empty: the unit's restart killed its
153// members) is replaced, and the check still passes.
154func TestVerifyPlacementKernelReplacesALeftover(t *testing.T) {
155 m := shimKernelManager(t)
156 if err := os.Mkdir(filepath.Join(m.root, placementCheckName), 0o755); err != nil {
157 t.Fatal(err)
158 }
159 if err := m.VerifyPlacement(context.Background()); err != nil {
160 t.Fatalf("VerifyPlacement over a leftover: %v", err)
161 }
162 assertNoPlacementCheckDir(t, m.root)
163}
164
165// A shim that cannot place (here, one that is not the carlos binary at all)
166// fails the check, and the scratch cgroup is removed.
167func TestVerifyPlacementKernelBrokenShim(t *testing.T) {
168 m := shimKernelManager(t)
169 prev := shimExe
170 shimExe = "/bin/true"
171 t.Cleanup(func() { shimExe = prev })
172 err := m.VerifyPlacement(context.Background())
173 if err == nil {
174 t.Fatal("a shim that cannot place passed the check")
175 }
176 t.Logf("broken shim: %v", err)
177 assertNoPlacementCheckDir(t, m.root)
178}
179
180// A probe whose output is not its cgroup, or that exits non-zero, fails
181// the check: placement is proven only by what the placed process saw.
182func TestVerifyPlacementKernelChecksTheProbe(t *testing.T) {
183 m := shimKernelManager(t)
184 for name, args := range map[string][]string{
185 // The test binary with no tests: exits 0, prints "PASS" and a warning.
186 "wrong output": {"-test.run=^$"},
187 // An unknown flag: exits 2.
188 "non-zero exit": {"-test.run=^$", "-test.no-such-flag"},
189 } {
190 t.Run(name, func(t *testing.T) {
191 prev := placementProbeArgs
192 placementProbeArgs = args
193 t.Cleanup(func() { placementProbeArgs = prev })
194 err := m.VerifyPlacement(context.Background())
195 if err == nil {
196 t.Fatal("passed with a probe that did not report its cgroup")
197 }
198 t.Logf("%s: %v", name, err)
199 assertNoPlacementCheckDir(t, m.root)
200 })
201 }
202}
203
204// A probe that does not exit is killed when ctx ends, and reaped before the
205// scratch cgroup is removed: the removal succeeds.
206func TestVerifyPlacementKernelKillsAProbeThatHangs(t *testing.T) {
207 m := shimKernelManager(t)
208 prev := placementProbeArgs
209 placementProbeArgs = []string{"-test.run=^TestVerifyPlacementKernelChild$", "-test.count=1"}
210 t.Cleanup(func() { placementProbeArgs = prev })
211 ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
212 defer cancel()
213 start := time.Now()
214 err := m.VerifyPlacement(ctx)
215 if !errors.Is(err, context.DeadlineExceeded) {
216 t.Fatalf("hung probe: %v, want the context's deadline", err)
217 }
218 if d := time.Since(start); d > 10*time.Second {
219 t.Fatalf("took %v to give up on a hung probe", d)
220 }
221 t.Logf("hung probe: %v", err)
222 assertNoPlacementCheckDir(t, m.root)
223}
224
225// A shim that stalls, before placement or after the release, cannot hold
226// the arm-time check past its budget: the decline, the reap and the removal
227// of the scratch cgroup all land within placementCheckBudget plus a few
228// milliseconds, under 5 s, with the caller's ctx carrying no deadline at
229// all, as placementCheck's does not. A sooner ctx deadline caps the shim's
230// waits the same way.
231func TestVerifyPlacementKernelStalledShimDeclinesWithinBudget(t *testing.T) {
232 const hard = 5 * time.Second
233 for _, c := range []struct {
234 name string
235 hold int
236 ctx time.Duration // 0: context.Background()
237 want time.Duration // the wait that must end it
238 }{
239 {"before placement", 1, 0, placementCheckBudget},
240 {"after release", 2, 0, placementCheckBudget},
241 {"before placement, sooner ctx", 1, 300 * time.Millisecond, 300 * time.Millisecond},
242 } {
243 t.Run(c.name, func(t *testing.T) {
244 m := shimKernelManager(t)
245 setShimVar(t, &shimTestHold, c.hold)
246 ctx := context.Background()
247 if c.ctx > 0 {
248 var cancel context.CancelFunc
249 ctx, cancel = context.WithTimeout(ctx, c.ctx)
250 defer cancel()
251 }
252 start := time.Now()
253 err := m.VerifyPlacement(ctx)
254 took := time.Since(start)
255 t.Logf("hold %d: took %v: %v", c.hold, took, err)
256 if err == nil || !isPlacementError(err) {
257 t.Fatalf("a stalled shim: %v, want a placement error", err)
258 }
259 if took < c.want-50*time.Millisecond {
260 t.Fatalf("declined after %v, before the %v wait ended: not the stall the test is about", took, c.want)
261 }
262 if took > c.want+500*time.Millisecond || took > hard {
263 t.Fatalf("declined after %v, want within %v plus 500ms and under %v", took, c.want, hard)
264 }
265 assertNoPlacementCheckDir(t, m.root)
266 assertNothingInFlight(t, m)
267 })
268 }
269}
270