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 "bufio"
7 "bytes"
8 "encoding/binary"
9 "errors"
10 "fmt"
11 "io"
12 "math"
13 "os"
14 "os/exec"
15 "os/signal"
16 "runtime"
17 "slices"
18 "strconv"
19 "strings"
20 "syscall"
21 "testing"
22 "time"
23
24 "golang.org/x/sys/unix"
25)
26
27// shimAvailable reports that RunShimIfRequested ran in this process, so
28// /proc/self/exe will serve ShimVerb, or that FakePlacementForTest stands
29// in for the shim.
30func shimAvailable() bool { return shimHooked.Load() || shimForcedForTest.Load() }
31
32// startViaShim starts cmd so that its process is in dir before any tenant
33// code runs: the child starts as this binary (shimExe ShimVerb), writes its
34// own PID into dir's cgroup.procs, checks where it landed, answers placed,
35// and execs the tenant only once released.
36//
37// On a nil error the tenant is running in dir and cmd.Process is it.
38// refused means before said no: the shim was placed, never released, and
39// is reaped. Any other error means no tenant code ran and any shim process
40// is reaped: a *placementError is the placement's failure, anything else is
41// the command's own error, as exec.Cmd.Start would have returned it. A
42// failed start's cmd is spent and must not be reused.
43//
44// The classification rests on one rule: the shim writes R immediately
45// before execve, and never earlier. A start is reported failed only when
46// the status stream proves no R; a start that may have exec'd is reported
47// started, so its caller owns the process and nothing runs a tenant twice.
48func (m *Manager) startViaShim(dir string, cmd *exec.Cmd, before func() error) (refused bool, err error) {
49 return m.startViaShimBy(dir, cmd, before, time.Time{})
50}
51
52// startViaShimBy is startViaShim with neither of its two waits running past
53// by. The zero time caps nothing: each wait is shimDeadline, as for every
54// tenant start (Ruling 9). Only VerifyPlacement sets by, so the arm-time
55// check fits its budget whatever the shim does in user space; past by, a
56// start fails and is reaped exactly as one past shimDeadline is. The reap
57// of a SIGKILLed shim is outside both waits and assumes the kernel ends a
58// killed process promptly (reap).
59func (m *Manager) startViaShimBy(dir string, cmd *exec.Cmd, before func() error, by time.Time) (refused bool, err error) {
60 // The hook itself, not shimAvailable: FakePlacementForTest's override
61 // must never send a binary without the hook to start itself as a shim.
62 if !shimHooked.Load() {
63 return false, &placementError{errors.New("the placement shim is not available in this process")}
64 }
65 if cmd.Err != nil {
66 return false, cmd.Err
67 }
68 fd, err := unix.Open(dir, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
69 if err != nil {
70 return false, &placementError{fmt.Errorf("open %s: %w", dir, err)}
71 }
72 dirFile := os.NewFile(uintptr(fd), dir)
73 var st unix.Statfs_t
74 if err := unix.Fstatfs(fd, &st); err != nil {
75 dirFile.Close()
76 return false, &placementError{fmt.Errorf("statfs %s: %w", dir, err)}
77 }
78 if st.Type != unix.CGROUP2_SUPER_MAGIC {
79 dirFile.Close()
80 return false, &placementError{fmt.Errorf("%s is not a cgroup2 directory", dir)}
81 }
82 // The shim checks /proc/self/cgroup against this, and the path below
83 // /sys/fs/cgroup against the fd, so dir must be named there.
84 rel, ok := strings.CutPrefix(dir, "/sys/fs/cgroup/")
85 if !ok || rel == "" {
86 dirFile.Close()
87 return false, &placementError{fmt.Errorf("%s is not a cgroup below /sys/fs/cgroup", dir)}
88 }
89 return m.startShimCore(dir, dirFile, "/"+rel, cmd, before, by)
90}
91
92// shimWait is one of the parent's waits on the shim: shimDeadline from now,
93// or by when that is sooner (and set). within is the wait it allows, for the
94// error that names it.
95func shimWait(by time.Time) (deadline time.Time, within time.Duration) {
96 now := time.Now()
97 deadline = now.Add(shimDeadline)
98 if by.IsZero() || !by.Before(deadline) {
99 return deadline, shimDeadline
100 }
101 return by, max(by.Sub(now), 0).Round(time.Millisecond)
102}
103
104// envHasNUL reports whether Start would refuse cmd's environment for a NUL.
105// With Env nil, Start uses os.Environ (which cannot hold a NUL) plus
106// PWD=Dir made absolute, so a NUL in Dir is the only way in.
107func envHasNUL(cmd *exec.Cmd) bool {
108 if cmd.Env == nil {
109 return cmd.Dir != "" && strings.IndexByte(cmd.Dir, 0) >= 0
110 }
111 for _, kv := range cmd.Env {
112 if strings.IndexByte(kv, 0) >= 0 {
113 return true
114 }
115 }
116 return false
117}
118
119// startShimCore is startViaShim after its checks: dirFile is dir opened,
120// and expected is dir as /proc/self/cgroup will show it. It closes dirFile.
121// by caps both waits, as startViaShimBy describes.
122func (m *Manager) startShimCore(dir string, dirFile *os.File, expected string, cmd *exec.Cmd, before func() error, by time.Time) (refused bool, err error) {
123 // The tenant, captured before cmd is rewritten into the shim's start:
124 // exactly what exec.Cmd.Start would have passed.
125 path := cmd.Path
126 argv := cmd.Args
127 if len(argv) == 0 {
128 argv = []string{cmd.Path}
129 }
130 wd := cmd.Dir
131 keep := 3 + len(cmd.ExtraFiles)
132 // cmd.Environ() silently drops an entry that contains a NUL, where
133 // Start refuses the command (Go issue #56284). Refuse it the same way,
134 // with Start's own error, before anything starts or is registered.
135 if envHasNUL(cmd) {
136 dirFile.Close()
137 return false, errors.New("exec: environment variable contains NUL")
138 }
139 // The signals this process ignores now, which a direct start's child
140 // would keep ignored (the shim restores them before its exec). Unknown
141 // is a placement failure: nothing has started, and the flat start's
142 // fallback is a direct start, which keeps them by itself.
143 sigIgn, err := ignoredSignals()
144 if err != nil {
145 dirFile.Close()
146 return false, &placementError{fmt.Errorf("read the ignored signals: %w", err)}
147 }
148 spec, err := encodeShimSpec(shimSpec{
149 Path: path, Dir: wd, Argv: argv, Env: cmd.Environ(),
150 Expected: expected, KeepFDs: keep, TestHold: shimTestHold,
151 SigIgn: sigIgn,
152 })
153 if err != nil {
154 // A NUL execve cannot carry: what execve answers for it.
155 dirFile.Close()
156 return false, &os.PathError{Op: "fork/exec", Path: path, Err: syscall.EINVAL}
157 }
158 if len(spec) > shimSpecMax {
159 dirFile.Close()
160 return false, &os.PathError{Op: "fork/exec", Path: path, Err: syscall.E2BIG}
161 }
162
163 specR, specW, err := os.Pipe()
164 if err != nil {
165 dirFile.Close()
166 return false, &placementError{fmt.Errorf("spec pipe: %w", err)}
167 }
168 statusR, statusW, err := os.Pipe()
169 if err != nil {
170 dirFile.Close()
171 specR.Close()
172 specW.Close()
173 return false, &placementError{fmt.Errorf("status pipe: %w", err)}
174 }
175 // Every return closes specW and statusR exactly once; specW may go
176 // early, as the release or the refusal.
177 specWOpen := true
178 closeSpecW := func() {
179 if specWOpen {
180 specWOpen = false
181 specW.Close()
182 }
183 }
184 defer closeSpecW()
185 defer statusR.Close()
186
187 // The shim's own start: no tenant environment and no working directory
188 // (the shim changes to wd itself, after placement, so a bad wd is the
189 // command's error, never the placement's). Stdin, Stdout, Stderr,
190 // SysProcAttr, Cancel and WaitDelay stay the caller's.
191 cmd.Path = shimExe
192 cmd.Args = []string{"carlos-cgexec", ShimVerb, strconv.Itoa(keep), strconv.Itoa(keep + 1), strconv.Itoa(keep + 2)}
193 cmd.Env = []string{}
194 cmd.Dir = ""
195 cmd.ExtraFiles = append(slices.Clip(cmd.ExtraFiles), specR, dirFile, statusW)
196
197 // One deadline covers the spec and the wait for placement.
198 deadline, within := shimWait(by)
199 done := m.beginPlacement(dir)
200 startErr := startCmd(cmd)
201 specR.Close()
202 dirFile.Close()
203 statusW.Close()
204 if startErr != nil {
205 // The carlos binary itself did not start; the tenant was not part
206 // of this start.
207 done()
208 return false, &placementError{fmt.Errorf("start the shim: %w", startErr)}
209 }
210
211 msg := make([]byte, 8+len(spec))
212 binary.BigEndian.PutUint64(msg, uint64(len(spec)))
213 copy(msg[8:], spec)
214 _ = specW.SetWriteDeadline(deadline)
215 if _, err := specW.Write(msg); err != nil {
216 reap(cmd)
217 done()
218 if errors.Is(err, os.ErrDeadlineExceeded) {
219 return false, &placementError{fmt.Errorf("no answer from the shim within %v", within)}
220 }
221 return false, &placementError{fmt.Errorf("the shim exited before placement (spec: %v)", err)}
222 }
223
224 _ = statusR.SetReadDeadline(deadline)
225 br := bufio.NewReader(statusR)
226 line, rerr := br.ReadBytes('\n')
227 placed, perr := classifyPlacement(line, rerr != nil && !errors.Is(rerr, io.EOF), within)
228 if !placed {
229 // P, EOF or the deadline: no release byte was sent, so the shim
230 // cannot have exec'd. reap's SIGKILL ends one still running.
231 reap(cmd)
232 done()
233 return false, perr
234 }
235 // The process is in dir now, visible to every reader of the cgroup: the
236 // registry covers only fork to placement, and no caller code runs
237 // inside it.
238 done()
239
240 // The caller's last check, after placement and immediately before exec
241 // (internal/activator/genbackend.go). A refusal closes specW without the
242 // release byte: the shim reads EOF and exits without exec.
243 if before != nil {
244 if err := before(); err != nil {
245 closeSpecW()
246 reap(cmd)
247 return true, err
248 }
249 }
250
251 // Release. A failed write leaves the shim to read EOF; the status read
252 // below then proves no R.
253 _ = specW.SetWriteDeadline(time.Time{})
254 _, _ = specW.Write([]byte{'X'})
255 closeSpecW()
256 deadline, within = shimWait(by)
257 _ = statusR.SetReadDeadline(deadline)
258 if shimAfterRelease != nil {
259 shimAfterRelease()
260 }
261 rest, rerr := io.ReadAll(br)
262 killed := false
263 if rerr != nil {
264 // The deadline passed before EOF. With R read, an execve is still
265 // loading: started, and nothing is killed. An E line is the shim's
266 // answer. Otherwise kill the shim, which was not released into an
267 // exec it has announced, and read what it wrote before it died.
268 started, err := classifyExec(path, wd, rest, false, within)
269 if started {
270 return false, nil
271 }
272 if !isPlacementError(err) {
273 reap(cmd)
274 return false, err
275 }
276 // EOF comes when the killed shim's status fd closes, which is when
277 // the kernel has ended it: unbounded by the deadline, under reap's
278 // assumption.
279 _ = cmd.Process.Kill()
280 _ = statusR.SetReadDeadline(time.Time{})
281 more, _ := io.ReadAll(br)
282 rest = append(rest, more...)
283 killed = true
284 }
285 started, err := classifyExec(path, wd, rest, killed, within)
286 if started {
287 return false, nil
288 }
289 reap(cmd)
290 return false, err
291}
292
293// reap ends a failed start's shim: kill, then wait, which reaps the PID.
294// exec.Cmd's copying goroutines and pipes are released in the background,
295// so the caller's I/O never holds this call, and no tenant ran to hold the
296// child's pipes open.
297//
298// The wait is bounded only by the kernel finishing the SIGKILL. SIGKILL is
299// not proof of termination: a process in uninterruptible sleep (a stuck
300// cgroupfs write, a hung disk or network filesystem read of /proc/self/exe)
301// dies only when that sleep ends, and this call, and so the start's
302// answer, waits that long. Normally that is microseconds. Neither
303// shimDeadline nor VerifyPlacement's budget covers it: they bound the
304// protocol's waits, on the assumption that a killed process ends promptly
305// (whole-branch review, finding 4). Bounding the reap too would need the
306// start to return while still owning an unreaped shim and its in-flight
307// registration, and to report the target unproven until the reap is done.
308func reap(cmd *exec.Cmd) {
309 _ = cmd.Process.Kill()
310 _, _ = cmd.Process.Wait()
311 go func() { _ = cmd.Wait() }()
312}
313
314// classifyPlacement reads the stream up to placement (parent step 7).
315// placed is true only for "A". timedOut is the read deadline, which allowed
316// within, having passed.
317func classifyPlacement(b []byte, timedOut bool, within time.Duration) (placed bool, err error) {
318 if i := bytes.IndexByte(b, '\n'); i >= 0 {
319 line := string(b[:i])
320 switch {
321 case line == "A":
322 return true, nil
323 case strings.HasPrefix(line, "P "):
324 return false, &placementError{fmt.Errorf("the shim could not place the process: %s", line[2:])}
325 default:
326 return false, &placementError{fmt.Errorf("the shim answered %q before placement", line)}
327 }
328 }
329 if timedOut {
330 return false, &placementError{fmt.Errorf("no answer from the shim within %v", within)}
331 }
332 return false, &placementError{errors.New("the shim exited before placement")}
333}
334
335// classifyExec reads the stream after the release byte (parent step 9).
336// killed means the deadline (which allowed within) passed with no R, the
337// shim was SIGKILLed, and b is everything read until EOF after that.
338//
339// R, and no "E exec" after it, is started: the tenant may have run. An
340// "E chdir" before R, or an "E exec" after it, is the command's own error,
341// as exec.Cmd.Start reports it. No R is a placement error.
342func classifyExec(path, wd string, b []byte, killed bool, within time.Duration) (started bool, err error) {
343 sawR := false
344 for _, line := range strings.Split(string(b), "\n") {
345 if line == "R" {
346 sawR = true
347 continue
348 }
349 op, n, ok := parseShimErr(line)
350 switch {
351 case !ok:
352 case !sawR && op == "chdir":
353 return false, &os.PathError{Op: "chdir", Path: wd, Err: syscall.Errno(n)}
354 case sawR && op == "exec":
355 return false, &os.PathError{Op: "fork/exec", Path: path, Err: syscall.Errno(n)}
356 }
357 }
358 if sawR {
359 return true, nil
360 }
361 if killed {
362 return false, &placementError{fmt.Errorf("the shim did not exec within %v", within)}
363 }
364 return false, &placementError{errors.New("the shim exited before exec")}
365}
366
367// parseShimErr parses "E <op> <errno>".
368func parseShimErr(line string) (op string, errno int, ok bool) {
369 f := strings.Fields(line)
370 if len(f) != 3 || f[0] != "E" {
371 return "", 0, false
372 }
373 n, err := strconv.Atoi(f[2])
374 if err != nil || n <= 0 {
375 return "", 0, false
376 }
377 return f[1], n, true
378}
379
380// runShim is the shim process: args are the spec, directory and status
381// fds, which are keep, keep+1 and keep+2. It writes nothing to fds 0 to 2.
382// A failure before placement is confirmed writes "P <step>: <error>" and
383// returns 125; tenant code has not run.
384//
385// Plan 14 root-read census, census ruling zddr3: the shim, its probe and
386// the parent-side start read only
387//
388// the agent's own delegated cgroup subtree (including its scratch probe child), /proc/self, and pipes to and from its own parent, running as carlos
389//
390// On that basis their reads are accepted-legacy rows
391// (internal/provision/testdata/plan14-root-reads-zddr3.tsv). If the shim
392// ever runs as root, or reads anything outside that boundary, it stops
393// being legacy and needs the Plan 14 owner's ruling.
394func runShim(args []string) int {
395 if len(args) != 3 {
396 return 125
397 }
398 var fds [3]int
399 for i, a := range args {
400 n, err := strconv.Atoi(a)
401 if err != nil {
402 return 125
403 }
404 fds[i] = n
405 }
406 keep, specFD, dirFD, statusFD := fds[0], fds[0], fds[1], fds[2]
407 if keep < 3 || dirFD != keep+1 || statusFD != keep+2 {
408 return 125
409 }
410 // First, so every later path, the exec included, closes it.
411 if _, err := unix.FcntlInt(uintptr(statusFD), unix.F_SETFD, unix.FD_CLOEXEC); err != nil {
412 return 125
413 }
414 fail := func(step string, err error) int {
415 shimWrite(statusFD, "P "+step+": "+err.Error()+"\n")
416 return 125
417 }
418 // The pipes may carry the parent's O_NONBLOCK; the shim's reads and
419 // writes block.
420 for _, fd := range []int{specFD, statusFD} {
421 if err := unix.SetNonblock(fd, false); err != nil {
422 return fail("fds", err)
423 }
424 }
425 spec, err := readShimSpec(specFD)
426 if err != nil {
427 return fail("spec", err)
428 }
429 if spec.KeepFDs != keep {
430 return fail("spec", fmt.Errorf("the spec keeps %d fds, the shim was given %d", spec.KeepFDs, keep))
431 }
432 // Everything from keep up closes at exec: the shim's three fds and any
433 // the agent leaked without close-on-exec.
434 if err := cloexecFrom(keep); err != nil {
435 return fail("cloexec", err)
436 }
437 shimHold(spec.TestHold, 1)
438 var st unix.Statfs_t
439 if err := unix.Fstatfs(dirFD, &st); err != nil {
440 return fail("statfs", err)
441 }
442 if st.Type != unix.CGROUP2_SUPER_MAGIC {
443 return fail("statfs", errors.New("not a cgroup2 directory"))
444 }
445 // Moves the whole process, every Go runtime thread, while it is still
446 // carlos code.
447 if err := joinCgroup(dirFD); err != nil {
448 return fail("procs", err)
449 }
450 if err := verifyCgroup(dirFD, spec.Expected); err != nil {
451 return fail("verify", err)
452 }
453 unix.Close(dirFD)
454 if !shimWrite(statusFD, "A\n") {
455 return 125
456 }
457 // The release byte. EOF, an error or another byte: the parent refused
458 // or is gone.
459 var rel [1]byte
460 if n, err := unix.Read(specFD, rel[:]); n != 1 || err != nil || rel[0] != 'X' {
461 return 125
462 }
463 shimHold(spec.TestHold, 2)
464 if spec.Dir != "" {
465 if err := os.Chdir(spec.Dir); err != nil {
466 shimWrite(statusFD, fmt.Sprintf("E chdir %d\n", errnoOf(err)))
467 return 126
468 }
469 }
470 // The exec state a direct start under systemd would have: an empty
471 // signal mask on the thread that execs, and the agent's ignored signals
472 // ignored. execve resets every handled signal to default and keeps
473 // ignored ones ignored, and syscall.Exec restores the RLIMIT_NOFILE
474 // this process started with.
475 runtime.LockOSThread()
476 if err := unix.PthreadSigmask(unix.SIG_SETMASK, &unix.Sigset_t{}, nil); err != nil {
477 return 125
478 }
479 // A failure here, before R, is "the shim exited before exec": a
480 // placement failure, with no tenant code run.
481 if err := ignoreSignals(spec.SigIgn); err != nil {
482 return 125
483 }
484 // R immediately before execve, and never earlier: without it the parent
485 // may report the start failed, so no exec may follow a failed write.
486 if !shimWrite(statusFD, "R\n") {
487 return 125
488 }
489 err = syscall.Exec(spec.Path, spec.Argv, spec.Env)
490 shimWrite(statusFD, fmt.Sprintf("E exec %d\n", errnoOf(err)))
491 return 126
492}
493
494// ignoredSignals reads this process's ignored-signal set from
495// /proc/self/status (SigIgn, hexadecimal; bit n-1 is signal n). Signal
496// dispositions are process-wide, so any thread's view is the process's.
497func ignoredSignals() (uint64, error) {
498 raw, err := os.ReadFile("/proc/self/status")
499 if err != nil {
500 return 0, err
501 }
502 for _, line := range strings.Split(string(raw), "\n") {
503 if v, ok := strings.CutPrefix(line, "SigIgn:"); ok {
504 return strconv.ParseUint(strings.TrimSpace(v), 16, 64)
505 }
506 }
507 return 0, errors.New("no SigIgn line in /proc/self/status")
508}
509
510// ignoreSignals sets every signal in set (bit n-1 is signal n) to SIG_IGN
511// through os/signal, which keeps the shim's runtime in step, and then
512// checks the kernel's view: each one must read back ignored.
513//
514// os/signal will not ignore SIGPROF, nor signals 32 and 33, which have no
515// Notify slot. None of them can be in the set while it does nothing:
516// SIGPROF and (without cgo) 33 get the agent runtime's own handler at
517// start, whatever the agent inherited, so the agent never has them ignored
518// (a CPU profile does not leave SIGPROF ignored either: checked on Go 1.26,
519// Linux); 32, and 33 with cgo, the runtime leaves as inherited, in the
520// agent and in the shim alike, so an inherited ignore is still in place.
521// The read-back check turns anything this reasoning misses into a failed
522// start (a placement failure: a flat start falls back to a direct start,
523// a generation start is refused), never a tenant with a handled signal it
524// should have ignored. Signals the shim inherited ignored beyond set are
525// left as they are: they come from the same agent, so a direct start at
526// that moment would have kept them too.
527func ignoreSignals(set uint64) error {
528 for n := 1; n <= 64; n++ {
529 if set&(1<<(n-1)) != 0 {
530 signal.Ignore(syscall.Signal(n))
531 }
532 }
533 got, err := ignoredSignals()
534 if err != nil {
535 return err
536 }
537 if missing := set &^ got; missing != 0 {
538 return fmt.Errorf("signals %#x did not read back ignored", missing)
539 }
540 return nil
541}
542
543// runProbe prints this process's cgroup as /proc/self/cgroup shows it.
544func runProbe() int {
545 raw, err := os.ReadFile("/proc/self/cgroup")
546 if err != nil {
547 return 1
548 }
549 rel, ok := ParseSelfCgroup(string(raw))
550 if !ok {
551 return 1
552 }
553 if _, err := os.Stdout.WriteString("/" + rel + "\n"); err != nil {
554 return 1
555 }
556 return 0
557}
558
559// readShimSpec reads the 8-byte big-endian length and then the spec.
560func readShimSpec(fd int) (shimSpec, error) {
561 var hdr [8]byte
562 if err := readFull(fd, hdr[:]); err != nil {
563 return shimSpec{}, err
564 }
565 n := binary.BigEndian.Uint64(hdr[:])
566 if n > shimSpecMax {
567 return shimSpec{}, fmt.Errorf("spec of %d bytes is over %d", n, shimSpecMax)
568 }
569 b := make([]byte, n)
570 if err := readFull(fd, b); err != nil {
571 return shimSpec{}, err
572 }
573 return decodeShimSpec(b)
574}
575
576func readFull(fd int, b []byte) error {
577 for len(b) > 0 {
578 n, err := unix.Read(fd, b)
579 if err == unix.EINTR {
580 continue
581 }
582 if err != nil {
583 return err
584 }
585 if n == 0 {
586 return io.ErrUnexpectedEOF
587 }
588 b = b[n:]
589 }
590 return nil
591}
592
593func shimWrite(fd int, s string) bool {
594 b := []byte(s)
595 for len(b) > 0 {
596 n, err := unix.Write(fd, b)
597 if err == unix.EINTR {
598 continue
599 }
600 if err != nil {
601 return false
602 }
603 b = b[n:]
604 }
605 return true
606}
607
608// cloexecFrom marks every fd >= keep close-on-exec. Without close_range
609// (ENOSYS on an old kernel, or a filter's EPERM or EINVAL) it walks
610// /proc/self/fd.
611func cloexecFrom(keep int) error {
612 err := unix.CloseRange(uint(keep), math.MaxUint32, unix.CLOSE_RANGE_CLOEXEC)
613 if err == nil {
614 return nil
615 }
616 if err != unix.ENOSYS && err != unix.EPERM && err != unix.EINVAL {
617 return err
618 }
619 entries, err := os.ReadDir("/proc/self/fd")
620 if err != nil {
621 return err
622 }
623 for _, e := range entries {
624 n, err := strconv.Atoi(e.Name())
625 if err != nil || n < keep {
626 continue
627 }
628 // EBADF is the listing's own fd, closed since.
629 if _, err := unix.FcntlInt(uintptr(n), unix.F_SETFD, unix.FD_CLOEXEC); err != nil && err != unix.EBADF {
630 return err
631 }
632 }
633 return nil
634}
635
636// joinCgroup writes this process's PID into the cgroup.procs of the
637// directory dirFD names.
638func joinCgroup(dirFD int) error {
639 fd, err := unix.Openat(dirFD, "cgroup.procs", unix.O_WRONLY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
640 if err != nil {
641 return fmt.Errorf("open cgroup.procs: %w", err)
642 }
643 defer unix.Close(fd)
644 if _, err := unix.Write(fd, []byte(strconv.Itoa(os.Getpid()))); err != nil {
645 return fmt.Errorf("write cgroup.procs: %w", err)
646 }
647 return nil
648}
649
650// verifyCgroup checks that the kernel reports this process in expected, and
651// that expected is the directory dirFD names (same device and inode).
652func verifyCgroup(dirFD int, expected string) error {
653 raw, err := os.ReadFile("/proc/self/cgroup")
654 if err != nil {
655 return err
656 }
657 rel, ok := ParseSelfCgroup(string(raw))
658 if !ok {
659 return errors.New("no cgroup v2 path in /proc/self/cgroup")
660 }
661 if "/"+rel != expected {
662 return fmt.Errorf("landed in /%s, want %s", rel, expected)
663 }
664 var fst, lst unix.Stat_t
665 if err := unix.Fstat(dirFD, &fst); err != nil {
666 return err
667 }
668 if err := unix.Lstat("/sys/fs/cgroup"+expected, &lst); err != nil {
669 return err
670 }
671 if fst.Dev != lst.Dev || fst.Ino != lst.Ino {
672 return fmt.Errorf("/sys/fs/cgroup%s is not the directory the parent opened", expected)
673 }
674 return nil
675}
676
677// shimHold sleeps for good at the point at, when a test binary's spec asks
678// for it, until a signal ends the process. Default dispositions: no
679// signal.Notify, so SIGTERM or SIGKILL kills it there.
680func shimHold(hold, at int) {
681 if hold != at || !testing.Testing() {
682 return
683 }
684 for {
685 time.Sleep(time.Hour)
686 }
687}
688
689func errnoOf(err error) int {
690 var e syscall.Errno
691 if errors.As(err, &e) {
692 return int(e)
693 }
694 return int(syscall.EINVAL)
695}
696