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 "encoding/json"
8 "io"
9 "os"
10 "os/exec"
11 "path/filepath"
12 "strconv"
13 "strings"
14 "testing"
15 "time"
16)
17
18type strictKernelReceipt struct {
19 PID int `json:"pid"`
20 Group string `json:"group"`
21 Child int `json:"child,omitempty"`
22}
23
24func strictKernelGroup(t *testing.T) string {
25 t.Helper()
26 raw, e := os.ReadFile("/proc/self/cgroup")
27 if e != nil {
28 t.Fatal(e)
29 }
30 group, ok := ParseSelfCgroup(string(raw))
31 if !ok {
32 t.Fatal("no unified group")
33 }
34 return "/" + group
35}
36func strictKernelSave(t *testing.T, name string, child int) {
37 t.Helper()
38 raw, e := json.Marshal(strictKernelReceipt{os.Getpid(), strictKernelGroup(t), child})
39 if e != nil {
40 t.Fatal(e)
41 }
42 if e = os.WriteFile(filepath.Join(os.Getenv("CARLOS_STRICT_PROOF_DIR"), name+".json"), raw, 0600); e != nil {
43 t.Fatal(e)
44 }
45}
46func strictKernelCommand(mode string) *exec.Cmd {
47 cmd := exec.Command(os.Args[0], "-test.run=^TestStrictKernelChild$", "-test.count=1")
48 cmd.Env = []string{"CARLOS_STRICT_CHILD=" + mode, "CARLOS_STRICT_PROOF_DIR=" + os.Getenv("CARLOS_STRICT_PROOF_DIR")}
49 return cmd
50}
51func TestStrictKernelChild(t *testing.T) {
52 mode := os.Getenv("CARLOS_STRICT_CHILD")
53 if mode == "" {
54 t.Skip("synthetic child only")
55 }
56 if mode == "grandchild" {
57 strictKernelSave(t, "grandchild", 0)
58 time.Sleep(time.Minute)
59 return
60 }
61 if mode == "marker" {
62 strictKernelSave(t, "marker", 0)
63 return
64 }
65 child := strictKernelCommand("grandchild")
66 child.Stdout = os.Stdout
67 child.Stderr = os.Stderr
68 if e := child.Start(); e != nil {
69 t.Fatal(e)
70 }
71 strictKernelSave(t, "worker", child.Process.Pid)
72 if mode == "exit-with-descendant" {
73 return
74 }
75 time.Sleep(time.Minute)
76}
77func strictKernelAwait(t *testing.T, name string) strictKernelReceipt {
78 t.Helper()
79 path := filepath.Join(os.Getenv("CARLOS_STRICT_PROOF_DIR"), name+".json")
80 deadline := time.Now().Add(3 * time.Second)
81 for time.Now().Before(deadline) {
82 raw, e := os.ReadFile(path)
83 if e == nil {
84 var r strictKernelReceipt
85 if json.Unmarshal(raw, &r) == nil {
86 return r
87 }
88 }
89 time.Sleep(5 * time.Millisecond)
90 }
91 t.Fatal("kernel child receipt missing", name)
92 return strictKernelReceipt{}
93}
94
95// The local user unit delegates memory but not io. This scaffold moves the
96// probe into a supervisor leaf and enables only the controller strict mode uses.
97func strictKernelManager(t *testing.T) *Manager {
98 t.Helper()
99 root := filepath.Join("/sys/fs/cgroup", strictKernelGroup(t))
100 leaf := filepath.Join(root, "supervise")
101 if e := os.Mkdir(leaf, 0755); e != nil {
102 return nil
103 }
104 if e := os.WriteFile(filepath.Join(leaf, "cgroup.procs"), []byte(strconv.Itoa(os.Getpid())), 0); e != nil {
105 return nil
106 }
107 if e := os.WriteFile(filepath.Join(root, "cgroup.subtree_control"), []byte("+memory"), 0); e != nil {
108 return nil
109 }
110 return &Manager{root: root, logf: t.Logf}
111}
112
113func TestStrictKernel(t *testing.T) {
114 mode := os.Getenv("CARLOS_STRICT_KERNEL")
115 if mode == "" {
116 t.Skip("requires disposable delegated user-systemd service")
117 }
118 m := strictKernelManager(t)
119 if mode == "permission-denied" {
120 if m != nil {
121 t.Fatal("read-only cgroup delegation accepted")
122 }
123 if p, e := m.PrepareStrict("denied.example", TierDefault); e == nil || p != nil {
124 t.Fatal("missing delegation became ordinary spawn")
125 }
126 return
127 }
128 if m == nil {
129 t.Fatal("disposable service has no cgroup delegation")
130 }
131 if mode == "unsupported" {
132 p, e := m.PrepareStrict("unsupported.example", TierDefault)
133 if e != nil {
134 t.Fatal(e)
135 }
136 if e = p.Start(strictKernelCommand("marker")); e != ErrStrictUnsupported {
137 t.Fatal("blocked clone3 was not unsupported", e)
138 }
139 if e = p.Start(strictKernelCommand("marker")); e != ErrStrictState {
140 t.Fatal("blocked clone retried", e)
141 }
142 if _, e = os.Stat(filepath.Join(os.Getenv("CARLOS_STRICT_PROOF_DIR"), "marker.json")); e == nil {
143 t.Fatal("fallback executed marker")
144 }
145 if e = p.Close(); e != nil {
146 t.Fatal(e)
147 }
148 return
149 }
150 t.Run("concurrent-reservation", func(t *testing.T) {
151 type result struct {
152 p *Prepared
153 err error
154 }
155 results := make(chan result, 12)
156 begin := make(chan struct{})
157 for range 12 {
158 go func() {
159 <-begin
160 p, err := (&Manager{root: m.root, logf: t.Logf}).PrepareStrict("concurrent.example", TierDefault)
161 results <- result{p, err}
162 }()
163 }
164 close(begin)
165 var winner *Prepared
166 for range 12 {
167 r := <-results
168 if r.err == nil {
169 if winner != nil {
170 t.Fatal("two concurrent preparations won")
171 }
172 winner = r.p
173 } else if r.err != ErrStrictState {
174 t.Fatal(r.err)
175 }
176 }
177 if winner == nil {
178 t.Fatal("no preparation won")
179 }
180 if err := winner.Close(); err != nil {
181 t.Fatal(err)
182 }
183 })
184 t.Run("descendant-pipe-and-reservation", func(t *testing.T) {
185 host := "tree.example"
186 p, e := m.PrepareStrict(host, TierDefault)
187 if e != nil {
188 t.Fatal(e)
189 }
190 other := &Manager{root: m.root, logf: t.Logf}
191 if q, e := other.PrepareStrict(host, TierDefault); e != ErrStrictState || q != nil {
192 t.Fatal("second manager reserved same group", e)
193 }
194 read, write, e := os.Pipe()
195 if e != nil {
196 t.Fatal(e)
197 }
198 defer read.Close()
199 defer write.Close()
200 cmd := strictKernelCommand("exit-with-descendant")
201 cmd.Stdout = write
202 if e = p.Start(cmd); e != nil {
203 t.Fatal(e)
204 }
205 write.Close()
206 worker := strictKernelAwait(t, "worker")
207 child := strictKernelAwait(t, "grandchild")
208 expected := strings.TrimPrefix(m.Root(), "/sys/fs/cgroup") + "/" + ExecName(host)
209 if worker.Group != expected || child.Group != expected || worker.Child != child.PID {
210 t.Fatal("child escaped prepared cgroup")
211 }
212 if e = cmd.Wait(); e != nil {
213 t.Fatal(e)
214 }
215 drained := make(chan struct{})
216 go func() { io.Copy(io.Discard, read); close(drained) }()
217 select {
218 case <-drained:
219 t.Fatal("descendant did not retain output pipe")
220 case <-time.After(20 * time.Millisecond):
221 }
222 if e = p.Close(); e != ErrStrictRefused {
223 t.Fatal("live descendant handle discarded", e)
224 }
225 ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second)
226 defer cancel()
227 if e = p.KillAndEmpty(ctx); e != nil {
228 t.Fatal(e)
229 }
230 select {
231 case <-drained:
232 case <-ctx.Done():
233 t.Fatal("empty proof left descendant output blocked")
234 }
235 if populated, e := strictPopulation(p.events); e != nil || populated {
236 t.Fatal("kernel population remained", e)
237 }
238 if e = p.Close(); e != nil {
239 t.Fatal(e)
240 }
241 q, e := other.PrepareStrict(host, TierDefault)
242 if e != nil {
243 t.Fatal("closed reservation was not released", e)
244 }
245 if e = q.Close(); e != nil {
246 t.Fatal(e)
247 }
248 })
249 t.Run("kernel-limit-readback", func(t *testing.T) {
250 for i, limit := range []Limit{TierDefault, TierChildren} {
251 p, e := m.PrepareStrict("limit"+strconv.Itoa(i)+".example", limit)
252 if e != nil {
253 t.Fatal(e)
254 }
255 if e = p.limitsOK(); e != nil {
256 t.Fatal(e)
257 }
258 if e = strictWrite(p.limits["memory.oom.group"], "0"); e != nil {
259 t.Fatal(e)
260 }
261 if e = p.Start(strictKernelCommand("marker")); e != ErrStrictRefused {
262 t.Fatal("changed actual kernel limit reached spawn", e)
263 }
264 if e = p.Close(); e != nil {
265 t.Fatal(e)
266 }
267 }
268 })
269
270 t.Run("permission-denied-controls", func(t *testing.T) {
271 host := "permissions.example"
272 p, e := m.PrepareStrict(host, TierDefault)
273 if e != nil {
274 t.Fatal(e)
275 }
276 if e = p.Close(); e != nil {
277 t.Fatal(e)
278 }
279 for _, name := range []string{"cgroup.kill", "cgroup.events", "memory.max"} {
280 path := filepath.Join(m.root, ExecName(host), name)
281 info, e := os.Stat(path)
282 if e != nil {
283 t.Fatal(e)
284 }
285 if e = os.Chmod(path, 0); e != nil {
286 t.Fatal(e)
287 }
288 candidate, prepareErr := m.PrepareStrict(host, TierDefault)
289 if e = os.Chmod(path, info.Mode().Perm()); e != nil {
290 t.Fatal(e)
291 }
292 if prepareErr == nil || candidate != nil {
293 t.Fatal("unusable required control accepted", name)
294 }
295 }
296 })
297 t.Run("missing-memory-controller", func(t *testing.T) {
298 path := filepath.Join(m.root, "cgroup.subtree_control")
299 if e := os.WriteFile(path, []byte("-memory"), 0); e != nil {
300 t.Fatal(e)
301 }
302 defer func() {
303 if e := os.WriteFile(path, []byte("+memory"), 0); e != nil {
304 t.Error(e)
305 }
306 }()
307 if p, e := m.PrepareStrict("missing.example", TierDefault); e == nil || p != nil {
308 t.Fatal("missing memory limit interface accepted")
309 }
310 })
311}
312func TestStrictKernelSupervisor(t *testing.T) {
313 if os.Getenv("CARLOS_STRICT_SUPERVISOR") != "1" {
314 t.Skip("disposable unit parent-death proof")
315 }
316 if _, e := os.Stat(filepath.Join(os.Getenv("CARLOS_STRICT_PROOF_DIR"), "supervisor.json")); e == nil {
317 strictKernelSave(t, "restarted", 0)
318 time.Sleep(time.Minute)
319 return
320 }
321 m := strictKernelManager(t)
322 if m == nil {
323 t.Fatal("no delegation")
324 }
325 p, e := m.PrepareStrict("parent.example", TierDefault)
326 if e != nil {
327 t.Fatal(e)
328 }
329 cmd := strictKernelCommand("tree")
330 read, write, e := os.Pipe()
331 if e != nil {
332 t.Fatal(e)
333 }
334 defer read.Close()
335 cmd.Stdout = write
336 if e = p.Start(cmd); e != nil {
337 t.Fatal(e)
338 }
339 write.Close()
340 strictKernelSave(t, "supervisor", cmd.Process.Pid)
341 io.Copy(io.Discard, read)
342 t.Fatal("parent-death probe returned before supervisor was killed")
343}
344