| 1 | //go:build linux |
| 2 | |
| 3 | package cgroup |
| 4 | |
| 5 | import ( |
| 6 | "bytes" |
| 7 | "errors" |
| 8 | "fmt" |
| 9 | "io" |
| 10 | "os" |
| 11 | "os/exec" |
| 12 | "os/signal" |
| 13 | "path/filepath" |
| 14 | "slices" |
| 15 | "strconv" |
| 16 | "strings" |
| 17 | "syscall" |
| 18 | "testing" |
| 19 | "time" |
| 20 | "unsafe" |
| 21 | |
| 22 | "golang.org/x/sys/unix" |
| 23 | ) |
| 24 | |
| 25 | // The placement shim against a real kernel (zdd readiness 3, Task 1). |
| 26 | // Opt-in: CARLOS_SHIM_KERNEL=1 inside a disposable unit with Delegate=yes: |
| 27 | // |
| 28 | // systemd-run --user --wait --pipe --collect -p Delegate=yes \ |
| 29 | // -E TMPDIR=/tmp/k/zddr3 -E CARLOS_SHIM_KERNEL=1 -E CARLOS_GEN_KERNEL=1 \ |
| 30 | // cgroup.test -test.run 'ShimKernel|ShimExecFailure' -test.v -test.count=1 |
| 31 | // |
| 32 | // Without the variable every test here skips. With it, a missing delegation |
| 33 | // fails the test: a run that was asked for cannot pass by skipping. |
| 34 | |
| 35 | // shimKernelManager is genKernelManager's setup, except that once |
| 36 | // CARLOS_SHIM_KERNEL=1 asks for the run, no delegation is a failure. |
| 37 | func shimKernelManager(t *testing.T) *Manager { |
| 38 | t.Helper() |
| 39 | if os.Getenv("CARLOS_SHIM_KERNEL") != "1" { |
| 40 | t.Skip("set CARLOS_SHIM_KERNEL=1 inside a unit with Delegate=yes (zdd readiness 3, Task 1)") |
| 41 | } |
| 42 | raw, err := os.ReadFile("/proc/self/cgroup") |
| 43 | if err != nil { |
| 44 | t.Fatalf("CARLOS_SHIM_KERNEL=1 but no cgroup: %v", err) |
| 45 | } |
| 46 | rel, ok := ParseSelfCgroup(string(raw)) |
| 47 | if !ok { |
| 48 | t.Fatal("CARLOS_SHIM_KERNEL=1 but no unified cgroup path") |
| 49 | } |
| 50 | root := filepath.Join("/sys/fs/cgroup", rel) |
| 51 | if filepath.Base(root) == "supervise" { |
| 52 | root = filepath.Dir(root) // an earlier test in this process moved us |
| 53 | } |
| 54 | sup := filepath.Join(root, "supervise") |
| 55 | if err := os.MkdirAll(sup, 0o755); err != nil { |
| 56 | t.Fatalf("CARLOS_SHIM_KERNEL=1 but no delegation: %v", err) |
| 57 | } |
| 58 | if err := os.WriteFile(filepath.Join(sup, "cgroup.procs"), []byte(strconv.Itoa(os.Getpid())), 0o644); err != nil { |
| 59 | t.Fatalf("CARLOS_SHIM_KERNEL=1 but no delegation: %v", err) |
| 60 | } |
| 61 | if err := enableChildren(root); err != nil { |
| 62 | t.Fatalf("CARLOS_SHIM_KERNEL=1 but no delegation: %v", err) |
| 63 | } |
| 64 | return &Manager{root: root, logf: t.Logf} |
| 65 | } |
| 66 | |
| 67 | // shimKernelTarget makes m.root/shim-<name> with the default tier's limits, |
| 68 | // and removes it when the test ends. |
| 69 | func shimKernelTarget(t *testing.T, m *Manager, name string) string { |
| 70 | t.Helper() |
| 71 | dir := filepath.Join(m.root, "shim-"+name) |
| 72 | if err := os.Mkdir(dir, 0o755); err != nil { |
| 73 | t.Fatal(err) |
| 74 | } |
| 75 | t.Cleanup(func() { |
| 76 | if err := os.Remove(dir); err != nil { |
| 77 | t.Errorf("remove %s: %v", dir, err) |
| 78 | } |
| 79 | }) |
| 80 | if err := m.applyLimit(name, dir, TierDefault, true); err != nil { |
| 81 | t.Fatal(err) |
| 82 | } |
| 83 | return dir |
| 84 | } |
| 85 | |
| 86 | // markerCmd is a tenant that touches marker first thing, then runs script. |
| 87 | func markerCmd(marker, script string) *exec.Cmd { |
| 88 | return exec.Command("/bin/sh", "-c", `touch "$0"; `+script, marker) |
| 89 | } |
| 90 | |
| 91 | func assertNoMarker(t *testing.T, marker string) { |
| 92 | t.Helper() |
| 93 | if _, err := os.Stat(marker); err == nil { |
| 94 | t.Fatal("tenant code ran") |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | func assertReaped(t *testing.T, cmd *exec.Cmd) { |
| 99 | t.Helper() |
| 100 | if cmd.Process == nil { |
| 101 | t.Fatal("no shim was started") |
| 102 | } |
| 103 | if _, err := os.Stat(fmt.Sprintf("/proc/%d", cmd.Process.Pid)); err == nil { |
| 104 | t.Fatalf("pid %d was not reaped", cmd.Process.Pid) |
| 105 | } |
| 106 | } |
| 107 | |
| 108 | func assertNothingInFlight(t *testing.T, m *Manager) { |
| 109 | t.Helper() |
| 110 | if n := m.placementsUnder(m.root); n != 0 { |
| 111 | t.Fatalf("%d placements still registered", n) |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | func shimCgroupPath(dir string) string { return strings.TrimPrefix(dir, "/sys/fs/cgroup") } |
| 116 | |
| 117 | // setShimVar sets a package var for one test. |
| 118 | func setShimVar[T any](t *testing.T, p *T, v T) { |
| 119 | orig := *p |
| 120 | *p = v |
| 121 | t.Cleanup(func() { *p = orig }) |
| 122 | } |
| 123 | |
| 124 | // shimDumpSep separates the dump's argv, env and cwd. It cannot be "\x00\x00": |
| 125 | // an empty argv element joins to exactly that. |
| 126 | const shimDumpSep = "\x00--shim-dump--\x00" |
| 127 | |
| 128 | // TestShimKernelChild is the synthetic tenant. Outside a child run it does |
| 129 | // nothing, so a kernel run shows no skip for it. |
| 130 | func TestShimKernelChild(t *testing.T) { |
| 131 | switch os.Getenv("CARLOS_SHIM_CHILD") { |
| 132 | case "dump": |
| 133 | wd, err := os.Getwd() |
| 134 | if err != nil { |
| 135 | os.Exit(3) |
| 136 | } |
| 137 | var b bytes.Buffer |
| 138 | b.WriteString(strings.Join(os.Args, "\x00")) |
| 139 | b.WriteString(shimDumpSep) |
| 140 | b.WriteString(strings.Join(os.Environ(), "\x00")) |
| 141 | b.WriteString(shimDumpSep) |
| 142 | b.WriteString(wd) |
| 143 | os.Stdout.Write(b.Bytes()) |
| 144 | os.Exit(0) |
| 145 | case "state": |
| 146 | var sig int |
| 147 | if err := unix.Prctl(unix.PR_GET_PDEATHSIG, uintptr(unsafe.Pointer(&sig)), 0, 0, 0); err != nil { |
| 148 | os.Exit(3) |
| 149 | } |
| 150 | pgid, err := unix.Getpgid(0) |
| 151 | if err != nil { |
| 152 | os.Exit(3) |
| 153 | } |
| 154 | fmt.Printf("pdeathsig %d\npgid-leader %v\n", sig, pgid == os.Getpid()) |
| 155 | os.Exit(0) |
| 156 | } |
| 157 | } |
| 158 | |
| 159 | func TestShimKernelPlacesBeforeExec(t *testing.T) { |
| 160 | m := shimKernelManager(t) |
| 161 | dir := shimKernelTarget(t, m, "places") |
| 162 | cmd := exec.Command("/bin/sh", "-c", `cat /proc/self/cgroup; echo $$; read x || :`) |
| 163 | var out bytes.Buffer |
| 164 | cmd.Stdout = &out |
| 165 | stdin, err := cmd.StdinPipe() |
| 166 | if err != nil { |
| 167 | t.Fatal(err) |
| 168 | } |
| 169 | refused, err := m.startViaShim(dir, cmd, nil) |
| 170 | if refused || err != nil { |
| 171 | t.Fatalf("refused=%v err=%v", refused, err) |
| 172 | } |
| 173 | assertNothingInFlight(t, m) |
| 174 | want := "0::" + shimCgroupPath(dir) |
| 175 | raw, err := os.ReadFile(fmt.Sprintf("/proc/%d/cgroup", cmd.Process.Pid)) |
| 176 | if err != nil { |
| 177 | t.Fatal(err) |
| 178 | } |
| 179 | if strings.TrimSpace(string(raw)) != want { |
| 180 | t.Fatalf("/proc/%d/cgroup = %q, want %q", cmd.Process.Pid, raw, want) |
| 181 | } |
| 182 | stdin.Close() |
| 183 | if err := cmd.Wait(); err != nil { |
| 184 | t.Fatal(err) |
| 185 | } |
| 186 | lines := strings.Split(strings.TrimSpace(out.String()), "\n") |
| 187 | if len(lines) != 2 || lines[0] != want || lines[1] != strconv.Itoa(cmd.Process.Pid) { |
| 188 | t.Fatalf("tenant printed %q, want %q and pid %d", out.String(), want, cmd.Process.Pid) |
| 189 | } |
| 190 | } |
| 191 | |
| 192 | func TestShimKernelExitCodePassesThrough(t *testing.T) { |
| 193 | m := shimKernelManager(t) |
| 194 | dir := shimKernelTarget(t, m, "exitcode") |
| 195 | cmd := exec.Command("/bin/sh", "-c", "exit 7") |
| 196 | if _, err := m.startViaShim(dir, cmd, nil); err != nil { |
| 197 | t.Fatal(err) |
| 198 | } |
| 199 | err := cmd.Wait() |
| 200 | var ee *exec.ExitError |
| 201 | if !errors.As(err, &ee) || ee.ExitCode() != 7 { |
| 202 | t.Fatalf("Wait = %v, want exit status 7", err) |
| 203 | } |
| 204 | } |
| 205 | |
| 206 | func TestShimKernelPassesArgvEnvExactly(t *testing.T) { |
| 207 | m := shimKernelManager(t) |
| 208 | dir := shimKernelTarget(t, m, "argvenv") |
| 209 | wd := t.TempDir() |
| 210 | mk := func(large string) *exec.Cmd { |
| 211 | return &exec.Cmd{ |
| 212 | Path: os.Args[0], |
| 213 | Args: []string{os.Args[0], "-test.run=^TestShimKernelChild$", "", "two words", "line\nbreak"}, |
| 214 | Env: []string{"CARLOS_SHIM_CHILD=dump", "EMPTY=", "EQ=a=b", "BAD=\xff\xfe", "LARGE=" + large}, |
| 215 | Dir: wd, |
| 216 | } |
| 217 | } |
| 218 | run := func(cmd *exec.Cmd, viaShim bool) []byte { |
| 219 | t.Helper() |
| 220 | var out bytes.Buffer |
| 221 | cmd.Stdout = &out |
| 222 | cmd.Stderr = os.Stderr |
| 223 | var err error |
| 224 | if viaShim { |
| 225 | _, err = m.startViaShim(dir, cmd, nil) |
| 226 | } else { |
| 227 | err = cmd.Start() |
| 228 | } |
| 229 | if err != nil { |
| 230 | t.Fatalf("start (shim %v): %v", viaShim, err) |
| 231 | } |
| 232 | if err := cmd.Wait(); err != nil { |
| 233 | t.Fatalf("wait (shim %v): %v", viaShim, err) |
| 234 | } |
| 235 | return out.Bytes() |
| 236 | } |
| 237 | large := strings.Repeat("v", 100<<10) |
| 238 | want := mk(large) |
| 239 | viaShim := run(mk(large), true) |
| 240 | direct := run(mk(large), false) |
| 241 | if !bytes.Equal(viaShim, direct) { |
| 242 | t.Fatalf("the shim's tenant saw different argv, env or cwd from a direct start's") |
| 243 | } |
| 244 | parts := bytes.Split(viaShim, []byte(shimDumpSep)) |
| 245 | if len(parts) != 3 { |
| 246 | t.Fatalf("dump has %d parts", len(parts)) |
| 247 | } |
| 248 | if got := strings.Split(string(parts[0]), "\x00"); !slices.Equal(got, want.Args) { |
| 249 | t.Fatalf("argv = %q, want %q", got, want.Args) |
| 250 | } |
| 251 | if got := strings.Split(string(parts[1]), "\x00"); !slices.Equal(got, want.Env) { |
| 252 | t.Fatalf("env differs from cmd.Env (%d entries, want %d)", len(got), len(want.Env)) |
| 253 | } |
| 254 | if string(parts[2]) != wd { |
| 255 | t.Fatalf("cwd = %q, want %q", parts[2], wd) |
| 256 | } |
| 257 | |
| 258 | // Above Linux's per-string limit (32 pages): both fail with E2BIG, and |
| 259 | // the shim's is the command's own error, not a placement failure. |
| 260 | huge := strings.Repeat("v", 200<<10) |
| 261 | if err := mk(huge).Start(); !errors.Is(err, syscall.E2BIG) { |
| 262 | t.Fatalf("direct start = %v, want E2BIG", err) |
| 263 | } |
| 264 | cmd := mk(huge) |
| 265 | refused, err := m.startViaShim(dir, cmd, nil) |
| 266 | var pe *os.PathError |
| 267 | if refused || isPlacementError(err) || !errors.As(err, &pe) || pe.Op != "fork/exec" || !errors.Is(err, syscall.E2BIG) { |
| 268 | t.Fatalf("shim start = %v, want *os.PathError fork/exec E2BIG", err) |
| 269 | } |
| 270 | assertReaped(t, cmd) |
| 271 | assertNothingInFlight(t, m) |
| 272 | } |
| 273 | |
| 274 | // stateScript prints the tenant's inherited state from a shell, which, |
| 275 | // unlike a Go program, leaves its signal mask and RLIMIT_NOFILE as it found |
| 276 | // them, then execs the Go child for what only prctl can read. ls lists the |
| 277 | // shell's descriptors, not its own. |
| 278 | const stateScript = `echo fds-begin; ls /proc/$$/fd; echo fds-end |
| 279 | grep -E '^(SigBlk|SigIgn|CapInh|CapPrm|CapEff|CapBnd|CapAmb|NoNewPrivs):' /proc/$$/status |
| 280 | echo nofile $(ulimit -Sn) $(ulimit -Hn) |
| 281 | exec "$0" "$@"` |
| 282 | |
| 283 | func TestShimKernelInheritedStateMatchesADirectStart(t *testing.T) { |
| 284 | m := shimKernelManager(t) |
| 285 | dir := shimKernelTarget(t, m, "state") |
| 286 | // SIGHUP is the case Go's runtime keeps ignored by itself; SIGTERM and |
| 287 | // SIGUSR1 are ones the shim's runtime would replace with its handlers, |
| 288 | // so the tenant's exec would reset them to default unless the shim |
| 289 | // ignores them again (whole-branch review, finding 2). |
| 290 | ignored := []syscall.Signal{syscall.SIGHUP, syscall.SIGTERM, syscall.SIGUSR1} |
| 291 | for _, sig := range ignored { |
| 292 | signal.Ignore(sig) |
| 293 | } |
| 294 | t.Cleanup(func() { |
| 295 | for _, sig := range ignored { |
| 296 | signal.Reset(sig) |
| 297 | } |
| 298 | }) |
| 299 | opened, err := syscall.Open("/dev/null", syscall.O_RDONLY, 0) // no O_CLOEXEC |
| 300 | if err != nil { |
| 301 | t.Fatal(err) |
| 302 | } |
| 303 | // Moved to 100 or above (F_DUPFD leaves close-on-exec clear), so the |
| 304 | // tenant's ExtraFiles at 3 and 4 cannot cover it in a direct start. |
| 305 | leaked, err := unix.FcntlInt(uintptr(opened), unix.F_DUPFD, 100) |
| 306 | syscall.Close(opened) |
| 307 | if err != nil { |
| 308 | t.Fatal(err) |
| 309 | } |
| 310 | t.Cleanup(func() { syscall.Close(leaked) }) |
| 311 | |
| 312 | run := func(viaShim bool) (fds []string, rest []string) { |
| 313 | t.Helper() |
| 314 | r1, w1, err := os.Pipe() |
| 315 | if err != nil { |
| 316 | t.Fatal(err) |
| 317 | } |
| 318 | defer r1.Close() |
| 319 | defer w1.Close() |
| 320 | r2, w2, err := os.Pipe() |
| 321 | if err != nil { |
| 322 | t.Fatal(err) |
| 323 | } |
| 324 | defer r2.Close() |
| 325 | defer w2.Close() |
| 326 | cmd := exec.Command("/bin/sh", "-c", stateScript, os.Args[0], "-test.run=^TestShimKernelChild$") |
| 327 | cmd.Env = []string{"CARLOS_SHIM_CHILD=state"} |
| 328 | cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true, Pdeathsig: syscall.SIGKILL} |
| 329 | cmd.ExtraFiles = []*os.File{r1, w2} |
| 330 | var out bytes.Buffer |
| 331 | cmd.Stdout = &out |
| 332 | cmd.Stderr = os.Stderr |
| 333 | if viaShim { |
| 334 | _, err = m.startViaShim(dir, cmd, nil) |
| 335 | } else { |
| 336 | err = cmd.Start() |
| 337 | } |
| 338 | if err != nil { |
| 339 | t.Fatalf("start (shim %v): %v", viaShim, err) |
| 340 | } |
| 341 | if err := cmd.Wait(); err != nil { |
| 342 | t.Fatalf("wait (shim %v): %v", viaShim, err) |
| 343 | } |
| 344 | inFDs := false |
| 345 | for _, l := range strings.Split(strings.TrimSpace(out.String()), "\n") { |
| 346 | switch { |
| 347 | case l == "fds-begin": |
| 348 | inFDs = true |
| 349 | case l == "fds-end": |
| 350 | inFDs = false |
| 351 | case inFDs: |
| 352 | fds = append(fds, l) |
| 353 | default: |
| 354 | rest = append(rest, l) |
| 355 | } |
| 356 | } |
| 357 | return fds, rest |
| 358 | } |
| 359 | shimFDs, shimState := run(true) |
| 360 | directFDs, directState := run(false) |
| 361 | t.Logf("shim tenant: fds %v %q", shimFDs, shimState) |
| 362 | t.Logf("direct tenant: fds %v %q", directFDs, directState) |
| 363 | |
| 364 | if !slices.Equal(shimFDs, []string{"0", "1", "2", "3", "4"}) { |
| 365 | t.Fatalf("the shim's tenant has fds %v, want exactly 0 1 2 3 4", shimFDs) |
| 366 | } |
| 367 | // The control: a direct start does inherit the leaked descriptor, so the |
| 368 | // shim's clean listing is the shim's doing and not an absent leak. |
| 369 | if !slices.Contains(directFDs, strconv.Itoa(leaked)) { |
| 370 | t.Fatalf("the direct start's fds %v lack the leaked fd %d: the control is broken", directFDs, leaked) |
| 371 | } |
| 372 | if !slices.Equal(shimState, directState) { |
| 373 | t.Fatalf("inherited state differs:\nshim %q\ndirect %q", shimState, directState) |
| 374 | } |
| 375 | // And the compared state is not vacuous. |
| 376 | for _, want := range []string{"pdeathsig 9", "pgid-leader true"} { |
| 377 | if !slices.Contains(shimState, want) { |
| 378 | t.Fatalf("shim state %q lacks %q", shimState, want) |
| 379 | } |
| 380 | } |
| 381 | sawSigIgn := false |
| 382 | for _, l := range shimState { |
| 383 | if v, ok := strings.CutPrefix(l, "SigIgn:\t"); ok { |
| 384 | sawSigIgn = true |
| 385 | mask, err := strconv.ParseUint(v, 16, 64) |
| 386 | if err != nil { |
| 387 | t.Fatalf("SigIgn %q: %v", v, err) |
| 388 | } |
| 389 | for _, sig := range ignored { |
| 390 | if mask&(1<<(uint(sig)-1)) == 0 { |
| 391 | t.Fatalf("SigIgn %q does not carry the ignored %v", v, sig) |
| 392 | } |
| 393 | } |
| 394 | } |
| 395 | } |
| 396 | if !sawSigIgn { |
| 397 | t.Fatal("the shim tenant's state has no SigIgn line") |
| 398 | } |
| 399 | } |
| 400 | |
| 401 | func TestShimKernelChdirFailureIsTheCommands(t *testing.T) { |
| 402 | m := shimKernelManager(t) |
| 403 | dir := shimKernelTarget(t, m, "chdir") |
| 404 | marker := filepath.Join(t.TempDir(), "ran") |
| 405 | cmd := markerCmd(marker, "") |
| 406 | missing := filepath.Join(t.TempDir(), "missing") |
| 407 | cmd.Dir = missing |
| 408 | refused, err := m.startViaShim(dir, cmd, nil) |
| 409 | var pe *os.PathError |
| 410 | if refused || isPlacementError(err) || !errors.As(err, &pe) || pe.Op != "chdir" || pe.Path != missing || !errors.Is(err, syscall.ENOENT) { |
| 411 | t.Fatalf("refused=%v err=%v, want *os.PathError chdir ENOENT", refused, err) |
| 412 | } |
| 413 | assertReaped(t, cmd) |
| 414 | assertNoMarker(t, marker) |
| 415 | assertNothingInFlight(t, m) |
| 416 | } |
| 417 | |
| 418 | func TestShimExecFailureIsTheCommands(t *testing.T) { |
| 419 | m := shimKernelManager(t) |
| 420 | dir := shimKernelTarget(t, m, "execfail") |
| 421 | cmd := &exec.Cmd{Path: "/nonexistent/app", Args: []string{"app"}} |
| 422 | refused, err := m.startViaShim(dir, cmd, nil) |
| 423 | var pe *os.PathError |
| 424 | if refused || isPlacementError(err) || !errors.As(err, &pe) || pe.Op != "fork/exec" || pe.Path != "/nonexistent/app" || !errors.Is(err, syscall.ENOENT) { |
| 425 | t.Fatalf("refused=%v err=%v, want *os.PathError fork/exec ENOENT", refused, err) |
| 426 | } |
| 427 | t.Logf("the command's own error: %v", err) |
| 428 | assertReaped(t, cmd) |
| 429 | assertNothingInFlight(t, m) |
| 430 | } |
| 431 | |
| 432 | func TestShimKernelPlacementFailureNeverRunsTenant(t *testing.T) { |
| 433 | m := shimKernelManager(t) |
| 434 | dir := shimKernelTarget(t, m, "noprocs") |
| 435 | procs := filepath.Join(dir, "cgroup.procs") |
| 436 | if err := os.Chmod(procs, 0o444); err != nil { |
| 437 | t.Fatal(err) |
| 438 | } |
| 439 | t.Cleanup(func() { _ = os.Chmod(procs, 0o644) }) |
| 440 | marker := filepath.Join(t.TempDir(), "ran") |
| 441 | cmd := markerCmd(marker, "") |
| 442 | refused, err := m.startViaShim(dir, cmd, nil) |
| 443 | if refused || !isPlacementError(err) || !strings.Contains(err.Error(), "procs") { |
| 444 | t.Fatalf("refused=%v err=%v, want the shim's procs refusal as a placement error", refused, err) |
| 445 | } |
| 446 | t.Logf("placement error: %v", err) |
| 447 | assertReaped(t, cmd) |
| 448 | assertNoMarker(t, marker) |
| 449 | assertNothingInFlight(t, m) |
| 450 | } |
| 451 | |
| 452 | func TestShimKernelBeforeRunsAfterPlacement(t *testing.T) { |
| 453 | m := shimKernelManager(t) |
| 454 | dir := shimKernelTarget(t, m, "before") |
| 455 | var events string |
| 456 | inFlight := -1 |
| 457 | before := func() error { |
| 458 | b, err := os.ReadFile(filepath.Join(dir, "cgroup.events")) |
| 459 | events = string(b) |
| 460 | inFlight = m.placementsUnder(dir) |
| 461 | return err |
| 462 | } |
| 463 | cmd := exec.Command("/bin/true") |
| 464 | if refused, err := m.startViaShim(dir, cmd, before); refused || err != nil { |
| 465 | t.Fatalf("refused=%v err=%v", refused, err) |
| 466 | } |
| 467 | if err := cmd.Wait(); err != nil { |
| 468 | t.Fatal(err) |
| 469 | } |
| 470 | if !slices.Contains(strings.Split(events, "\n"), "populated 1") { |
| 471 | t.Fatalf("before saw cgroup.events %q: the shim was not in place when it was asked", events) |
| 472 | } |
| 473 | if inFlight != 0 { |
| 474 | t.Fatalf("before ran with %d placements registered; the registry covers only fork to placement", inFlight) |
| 475 | } |
| 476 | |
| 477 | refusal := errors.New("not yours any more") |
| 478 | marker := filepath.Join(t.TempDir(), "ran") |
| 479 | cmd = markerCmd(marker, "") |
| 480 | refused, err := m.startViaShim(dir, cmd, func() error { return refusal }) |
| 481 | if !refused || err != refusal { |
| 482 | t.Fatalf("refused=%v err=%v, want before's own error", refused, err) |
| 483 | } |
| 484 | assertReaped(t, cmd) |
| 485 | assertNoMarker(t, marker) |
| 486 | assertNothingInFlight(t, m) |
| 487 | } |
| 488 | |
| 489 | // waitShimHeld waits until the shim has marked its spec fd close-on-exec |
| 490 | // (its step 3), which it does just before the step-1 test hold. |
| 491 | func waitShimHeld(pid, specFD int) bool { |
| 492 | path := fmt.Sprintf("/proc/%d/fdinfo/%d", pid, specFD) |
| 493 | for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(time.Millisecond) { |
| 494 | b, err := os.ReadFile(path) |
| 495 | if err != nil { |
| 496 | continue |
| 497 | } |
| 498 | for _, l := range strings.Split(string(b), "\n") { |
| 499 | if v, ok := strings.CutPrefix(l, "flags:"); ok { |
| 500 | flags, err := strconv.ParseUint(strings.TrimSpace(v), 8, 64) |
| 501 | if err == nil && flags&unix.O_CLOEXEC != 0 { |
| 502 | return true |
| 503 | } |
| 504 | } |
| 505 | } |
| 506 | } |
| 507 | return false |
| 508 | } |
| 509 | |
| 510 | func TestShimKernelSignalBeforePlacement(t *testing.T) { |
| 511 | m := shimKernelManager(t) |
| 512 | dir := shimKernelTarget(t, m, "sigplace") |
| 513 | setShimVar(t, &shimTestHold, 1) |
| 514 | for _, sig := range []syscall.Signal{syscall.SIGTERM, syscall.SIGKILL} { |
| 515 | orig := startCmd |
| 516 | held := make(chan bool, 1) |
| 517 | startCmd = func(cmd *exec.Cmd) error { |
| 518 | if err := orig(cmd); err != nil { |
| 519 | return err |
| 520 | } |
| 521 | // The spec fd is the first of the shim's three, after the |
| 522 | // tenant's ExtraFiles: 3 + (len(cmd.ExtraFiles) - 3). |
| 523 | specFD := len(cmd.ExtraFiles) |
| 524 | go func() { |
| 525 | held <- waitShimHeld(cmd.Process.Pid, specFD) |
| 526 | _ = cmd.Process.Signal(sig) |
| 527 | }() |
| 528 | return nil |
| 529 | } |
| 530 | marker := filepath.Join(t.TempDir(), "ran") |
| 531 | cmd := markerCmd(marker, "") |
| 532 | _, err := m.startViaShim(dir, cmd, nil) |
| 533 | startCmd = orig |
| 534 | if !<-held { |
| 535 | t.Fatalf("%v: the shim never reached its hold", sig) |
| 536 | } |
| 537 | if !isPlacementError(err) || !strings.Contains(err.Error(), "exited before placement") { |
| 538 | t.Fatalf("%v: err=%v, want a placement error, exited before placement", sig, err) |
| 539 | } |
| 540 | t.Logf("%v: %v", sig, err) |
| 541 | assertReaped(t, cmd) |
| 542 | assertNoMarker(t, marker) |
| 543 | assertNothingInFlight(t, m) |
| 544 | } |
| 545 | } |
| 546 | |
| 547 | func TestShimKernelSignalBeforeExec(t *testing.T) { |
| 548 | m := shimKernelManager(t) |
| 549 | dir := shimKernelTarget(t, m, "sigexec") |
| 550 | setShimVar(t, &shimTestHold, 2) |
| 551 | marker := filepath.Join(t.TempDir(), "ran") |
| 552 | cmd := markerCmd(marker, "") |
| 553 | setShimVar(t, &shimAfterRelease, func() { _ = cmd.Process.Kill() }) |
| 554 | _, err := m.startViaShim(dir, cmd, nil) |
| 555 | if !isPlacementError(err) || !strings.Contains(err.Error(), "exited before exec") { |
| 556 | t.Fatalf("err=%v, want a placement error, exited before exec", err) |
| 557 | } |
| 558 | assertReaped(t, cmd) |
| 559 | assertNoMarker(t, marker) |
| 560 | assertNothingInFlight(t, m) |
| 561 | } |
| 562 | |
| 563 | // TestShimKernelExecBeforeAcknowledgement pins the rule that an exec that |
| 564 | // happened is never reported as a failure before exec: the tenant runs, the |
| 565 | // parent's deadline passes before it reads R, and the start is still |
| 566 | // reported started. |
| 567 | func TestShimKernelExecBeforeAcknowledgement(t *testing.T) { |
| 568 | m := shimKernelManager(t) |
| 569 | dir := shimKernelTarget(t, m, "lateack") |
| 570 | setShimVar(t, &shimDeadline, shimDeadline) |
| 571 | // The short deadline goes in once the shim is started, so it governs the |
| 572 | // release-to-exec read the test is about, and a loaded box's slow shim |
| 573 | // start cannot fail the placement read instead. |
| 574 | orig := startCmd |
| 575 | t.Cleanup(func() { startCmd = orig }) |
| 576 | startCmd = func(cmd *exec.Cmd) error { |
| 577 | shimDeadline = 200 * time.Millisecond |
| 578 | return orig(cmd) |
| 579 | } |
| 580 | marker := filepath.Join(t.TempDir(), "ran") |
| 581 | cmd := markerCmd(marker, "") |
| 582 | setShimVar(t, &shimAfterRelease, func() { |
| 583 | for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(time.Millisecond) { |
| 584 | if _, err := os.Stat(marker); err == nil { |
| 585 | break |
| 586 | } |
| 587 | } |
| 588 | time.Sleep(400 * time.Millisecond) |
| 589 | }) |
| 590 | refused, err := m.startViaShim(dir, cmd, nil) |
| 591 | if refused || err != nil { |
| 592 | t.Fatalf("refused=%v err=%v, want started", refused, err) |
| 593 | } |
| 594 | if err := cmd.Wait(); err != nil { |
| 595 | t.Fatalf("Wait = %v", err) |
| 596 | } |
| 597 | if _, err := os.Stat(marker); err != nil { |
| 598 | t.Fatal("the tenant did not run") |
| 599 | } |
| 600 | } |
| 601 | |
| 602 | func TestShimKernelDeadlineBeforePlacement(t *testing.T) { |
| 603 | m := shimKernelManager(t) |
| 604 | dir := shimKernelTarget(t, m, "deadline") |
| 605 | setShimVar(t, &shimTestHold, 1) |
| 606 | setShimVar(t, &shimDeadline, 300*time.Millisecond) |
| 607 | marker := filepath.Join(t.TempDir(), "ran") |
| 608 | cmd := markerCmd(marker, "") |
| 609 | _, err := m.startViaShim(dir, cmd, nil) |
| 610 | if !isPlacementError(err) || !strings.Contains(err.Error(), "no answer from the shim within 300ms") { |
| 611 | t.Fatalf("err=%v, want a placement error naming the deadline", err) |
| 612 | } |
| 613 | assertReaped(t, cmd) |
| 614 | assertNoMarker(t, marker) |
| 615 | assertNothingInFlight(t, m) |
| 616 | } |
| 617 | |
| 618 | // TestShimKernelSurvivesDeletedExecutable: a roll replaces the carlos binary |
| 619 | // on disk under a running agent; /proc/self/exe still execs the build the |
| 620 | // agent is running. |
| 621 | func TestShimKernelSurvivesDeletedExecutable(t *testing.T) { |
| 622 | shimKernelManager(t) |
| 623 | self, err := os.Executable() |
| 624 | if err != nil { |
| 625 | t.Fatal(err) |
| 626 | } |
| 627 | copyPath := filepath.Join(t.TempDir(), "copy.test") |
| 628 | src, err := os.Open(self) |
| 629 | if err != nil { |
| 630 | t.Fatal(err) |
| 631 | } |
| 632 | dst, err := os.OpenFile(copyPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o755) |
| 633 | if err != nil { |
| 634 | t.Fatal(err) |
| 635 | } |
| 636 | if _, err := io.Copy(dst, src); err != nil { |
| 637 | t.Fatal(err) |
| 638 | } |
| 639 | src.Close() |
| 640 | if err := dst.Close(); err != nil { |
| 641 | t.Fatal(err) |
| 642 | } |
| 643 | child := exec.Command(copyPath, "-test.run=^TestShimKernelDeletedChild$", "-test.v", "-test.count=1") |
| 644 | child.Env = []string{ |
| 645 | "CARLOS_SHIM_KERNEL=1", "CARLOS_SHIM_DELETED_CHILD=1", "CARLOS_GEN_KERNEL=1", |
| 646 | "TMPDIR=" + os.TempDir(), "PATH=" + os.Getenv("PATH"), |
| 647 | } |
| 648 | out, err := child.CombinedOutput() |
| 649 | t.Logf("child:\n%s", out) |
| 650 | if err != nil || !strings.Contains(string(out), "--- PASS: TestShimKernelDeletedChild") { |
| 651 | t.Fatalf("the child with a deleted executable failed: %v", err) |
| 652 | } |
| 653 | } |
| 654 | |
| 655 | // TestShimKernelDeletedChild is TestShimKernelSurvivesDeletedExecutable's |
| 656 | // child. Outside that run it does nothing. |
| 657 | func TestShimKernelDeletedChild(t *testing.T) { |
| 658 | if os.Getenv("CARLOS_SHIM_DELETED_CHILD") != "1" { |
| 659 | return |
| 660 | } |
| 661 | if err := os.Remove(os.Args[0]); err != nil { |
| 662 | t.Fatal(err) |
| 663 | } |
| 664 | exe, err := os.Readlink("/proc/self/exe") |
| 665 | if err != nil || !strings.HasSuffix(exe, " (deleted)") { |
| 666 | t.Fatalf("/proc/self/exe = %q (%v), want a deleted file", exe, err) |
| 667 | } |
| 668 | t.Logf("running from %s", exe) |
| 669 | m := shimKernelManager(t) |
| 670 | dir := shimKernelTarget(t, m, "deleted") |
| 671 | cmd := exec.Command("/bin/sh", "-c", "cat /proc/self/cgroup") |
| 672 | var out bytes.Buffer |
| 673 | cmd.Stdout = &out |
| 674 | if refused, err := m.startViaShim(dir, cmd, nil); refused || err != nil { |
| 675 | t.Fatalf("refused=%v err=%v", refused, err) |
| 676 | } |
| 677 | if err := cmd.Wait(); err != nil { |
| 678 | t.Fatal(err) |
| 679 | } |
| 680 | if got, want := strings.TrimSpace(out.String()), "0::"+shimCgroupPath(dir); got != want { |
| 681 | t.Fatalf("tenant ran in %q, want %q", got, want) |
| 682 | } |
| 683 | } |
| 684 | |
| 685 | // TestShimKernelOverhead is informational: it never fails on time. |
| 686 | func TestShimKernelOverhead(t *testing.T) { |
| 687 | m := shimKernelManager(t) |
| 688 | dir := shimKernelTarget(t, m, "overhead") |
| 689 | const n = 20 |
| 690 | median := func(viaShim bool) time.Duration { |
| 691 | var d []time.Duration |
| 692 | for range n { |
| 693 | cmd := exec.Command("/bin/true") |
| 694 | start := time.Now() |
| 695 | var err error |
| 696 | if viaShim { |
| 697 | _, err = m.startViaShim(dir, cmd, nil) |
| 698 | } else { |
| 699 | err = cmd.Start() |
| 700 | } |
| 701 | if err != nil { |
| 702 | t.Fatal(err) |
| 703 | } |
| 704 | if err := cmd.Wait(); err != nil { |
| 705 | t.Fatal(err) |
| 706 | } |
| 707 | d = append(d, time.Since(start)) |
| 708 | } |
| 709 | slices.Sort(d) |
| 710 | return d[n/2] |
| 711 | } |
| 712 | shim := median(true) |
| 713 | direct := median(false) |
| 714 | t.Logf("start-to-exit of /bin/true, median of %d: shim %v, direct %v, overhead %v", n, shim, direct, shim-direct) |
| 715 | } |
| 716 | |