| 1 | //go:build linux |
| 2 | |
| 3 | package cgroup |
| 4 | |
| 5 | import ( |
| 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. |
| 30 | func 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. |
| 48 | func (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). |
| 59 | func (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. |
| 95 | func 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. |
| 107 | func 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. |
| 122 | func (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. |
| 308 | func 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. |
| 317 | func 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. |
| 342 | func 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>". |
| 368 | func 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. |
| 394 | func 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. |
| 497 | func 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. |
| 527 | func 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. |
| 544 | func 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. |
| 560 | func 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 | |
| 576 | func 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 | |
| 593 | func 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. |
| 611 | func 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. |
| 638 | func 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). |
| 652 | func 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. |
| 680 | func shimHold(hold, at int) { |
| 681 | if hold != at || !testing.Testing() { |
| 682 | return |
| 683 | } |
| 684 | for { |
| 685 | time.Sleep(time.Hour) |
| 686 | } |
| 687 | } |
| 688 | |
| 689 | func 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 | |