//go:build linux package cgroup import ( "context" "errors" "fmt" "os" "os/exec" "path/filepath" "slices" "strconv" "strings" "syscall" "testing" ) // Every Manager start places through the shim, and the shim keeps the // SysProcAttr the caller built (Setpgid, Pdeathsig, ...). No start may set // the clone3 cgroup fields: the shim would carry them to its own fork, and // the shipped unit answers clone3 with ENOSYS (amadan #272). Only the // strict claims path sets them (strict_linux.go). func TestManagerStartsNeverSetTheCgroupFD(t *testing.T) { const host = "h.example.com" m, _ := genManager(t, host) var seen []*syscall.SysProcAttr stubPlacement(t, func(_ *Manager, _ string, cmd *exec.Cmd, _ func() error) (bool, error) { seen = append(seen, cmd.SysProcAttr) cmd.Process, _ = os.FindProcess(os.Getpid()) return false, nil }) build := func() *exec.Cmd { cmd := exec.Command("/bin/true") cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} return cmd } if _, err := m.Start("flat.example.com", Limit{}, build); err != nil { t.Fatal(err) } if _, err := m.StartChild(host, GenChild(1), TierDefault, TierDefault, build); err != nil { t.Fatal(err) } if len(seen) != 2 { t.Fatalf("placed %d starts, want the flat and the generation start", len(seen)) } for i, attr := range seen { if attr == nil || !attr.Setpgid { t.Errorf("start %d: SysProcAttr = %+v, want build()'s Setpgid kept", i, attr) } else if attr.UseCgroupFD || attr.CgroupFD != 0 { t.Errorf("start %d: SysProcAttr = %+v, want no clone3 cgroup fd", i, attr) } } } // Ruling 2: a flat start whose placement the shim proved failed before exec // falls back, for that spawn, to starting the command and moving its PID. // The fallback builds a fresh command (a Cmd cannot be reused) and asks // before again, since ownership can change between the two attempts. func TestFlatPlacementFailureFallsBackToPIDMove(t *testing.T) { root := newDelegatedRoot(t) m, err := SetupAt(root, t.Logf) if err != nil { t.Fatal(err) } quietOOM(t) stubPlacement(t, func(_ *Manager, _ string, _ *exec.Cmd, before func() error) (bool, error) { // The shim was placed and before agreed; then it died before exec. if err := before(); err != nil { return true, err } return false, &placementError{errors.New("the shim exited before exec")} }) calls := fakeStart(t, func(*exec.Cmd) error { return nil }) builds, asked := 0, 0 cmd, err := m.StartGated("h.example.com", Limit{}, func() *exec.Cmd { builds++; return buildTrue() }, func() error { asked++; return nil }) if err != nil || cmd == nil { t.Fatalf("StartGated = %v, %v: want the PID-move start", cmd, err) } if builds != 2 || asked != 2 || len(*calls) != 1 { t.Fatalf("built %d, asked %d, started %d: want two builds, two asks, one start-and-move", builds, asked, len(*calls)) } if got := readFixture(t, filepath.Join(m.hostDir("h.example.com"), "cgroup.procs")); got != strconv.Itoa(os.Getpid()) { t.Fatalf("cgroup.procs = %q, want the child's PID moved in", got) } } // The fallback asks before again, and that second answer stands on its // own (whole-branch review triage): before agrees at the shim, the shim then // fails before exec, and before refuses at the PID-move fallback. Nothing // starts, the host dir this spawn made is removed, and the start answers // before's own error, unjoined with the placement failure. func TestFlatFallbackRefusedByBeforeStartsNothing(t *testing.T) { root := newDelegatedRoot(t) m, err := SetupAt(root, t.Logf) if err != nil { t.Fatal(err) } quietOOM(t) placementFailed := &placementError{errors.New("the shim exited before exec")} stubPlacement(t, func(_ *Manager, _ string, _ *exec.Cmd, before func() error) (bool, error) { if err := before(); err != nil { return true, err } return false, placementFailed }) calls := fakeStart(t, func(*exec.Cmd) error { return nil }) refusal := errors.New("ownership changed between the two attempts") builds, asked := 0, 0 cmd, err := m.StartGated("h.example.com", Limit{}, func() *exec.Cmd { builds++; return buildTrue() }, func() error { asked++ if asked == 1 { return nil } return refusal }) if cmd != nil || err != refusal { t.Fatalf("StartGated = %v, %v: want no command and before's own error, unjoined", cmd, err) } if errors.Is(err, placementFailed) || isPlacementError(err) { t.Fatalf("the refusal %v carries the placement failure", err) } if builds != 2 || asked != 2 || len(*calls) != 0 { t.Fatalf("built %d, asked %d, started %d: want two builds, two asks and nothing started", builds, asked, len(*calls)) } if n := m.placementsUnder(root); n != 0 { t.Fatalf("%d placements left registered", n) } if _, err := os.Stat(m.hostDir("h.example.com")); !os.IsNotExist(err) { t.Fatalf("host dir after a refused fallback: %v, want it removed", err) } } // A failure that is the command's own (a missing binary) is returned as // is, with no PID-move retry: a retry would fail the same way, and an // uncertain start must never run a tenant twice. func TestFlatCommandFailureIsNotRetried(t *testing.T) { root := newDelegatedRoot(t) m, err := SetupAt(root, t.Logf) if err != nil { t.Fatal(err) } missing := &os.PathError{Op: "fork/exec", Path: "/x", Err: syscall.ENOENT} stubPlacement(t, func(*Manager, string, *exec.Cmd, func() error) (bool, error) { return false, missing }) calls := fakeStart(t, func(*exec.Cmd) error { return nil }) builds := 0 cmd, err := m.Start("h.example.com", Limit{}, func() *exec.Cmd { builds++; return buildTrue() }) if err != missing || cmd != nil { t.Fatalf("Start = %v, %v: want the command's own error as is", cmd, err) } if builds != 1 || len(*calls) != 0 { t.Fatalf("built %d, started %d: want one build and no retry", builds, len(*calls)) } if _, err := os.Stat(m.hostDir("h.example.com")); !os.IsNotExist(err) { t.Fatalf("host dir after a failed start: %v, want it removed", err) } } // A flat start whose context is already cancelled (Task 1's review carry): // the shim cannot start, which is a placement error by the protocol, so the // fallback runs once, and its own Start answers the context's error at // once. That error comes back as is, with no box condition beyond the one // fallback line, and nothing ran. This uses the real placement: a temp dir // is refused by the shim's checks before any fork, as a cancelled start is // refused at its fork. func TestFlatCancelledContextIsTheCommandsError(t *testing.T) { root := newDelegatedRoot(t) m, err := SetupAt(root, t.Logf) if err != nil { t.Fatal(err) } var lines []string m.logf = func(format string, args ...any) { lines = append(lines, fmt.Sprintf(format, args...)) } ctx, cancel := context.WithCancel(t.Context()) cancel() cmd, err := m.Start("h.example.com", Limit{}, func() *exec.Cmd { return exec.CommandContext(ctx, "/bin/true") }) if !errors.Is(err, context.Canceled) || cmd != nil { t.Fatalf("Start = %v, %v: want context.Canceled and no command", cmd, err) } if isPlacementError(err) || errors.Is(err, ErrLimitNotApplied) { t.Fatalf("a cancelled start read as a placement failure: %v", err) } if len(lines) != 1 || !strings.Contains(lines[0], "using the PID-move fallback for this spawn") { t.Fatalf("log lines = %q, want only the one fallback line", lines) } if _, err := os.Stat(m.hostDir("h.example.com")); !os.IsNotExist(err) { t.Fatalf("host dir after a cancelled start: %v, want it removed", err) } } // startAndMoveIn's child runs in the agent's cgroup from its start until its // PID is written, so its target is registered in flight for exactly that // window: not while before is asked, through the start, and no longer once // the PID write has returned. func TestStartAndMoveRegistersItsTarget(t *testing.T) { root := newDelegatedRoot(t) m, err := SetupAt(root, t.Logf) if err != nil { t.Fatal(err) } quietOOM(t) dir := m.hostDir("h.example.com") if err := os.MkdirAll(dir, 0o755); err != nil { t.Fatal(err) } var atBefore, atStart int fakeStart(t, func(*exec.Cmd) error { atStart = m.placementsAt(dir); return nil }) cmd, refused, err := m.startAndMoveIn("h.example.com", dir, buildTrue, func() error { atBefore = m.placementsAt(dir); return nil }) if err != nil || refused || cmd == nil { t.Fatalf("startAndMoveIn = %v, %v, %v", cmd, refused, err) } if atBefore != 0 || atStart != 1 || m.placementsAt(dir) != 0 { t.Fatalf("in flight: %d at before, %d at the start, %d after the move; want 0, 1, 0", atBefore, atStart, m.placementsAt(dir)) } // A failed start ends its registration too. fakeStart(t, func(*exec.Cmd) error { return errors.New("no such binary") }) if _, _, err := m.startAndMoveIn("h.example.com", dir, buildTrue, nil); err == nil { t.Fatal("a failing start succeeded") } if n := m.placementsUnder(root); n != 0 { t.Fatalf("%d placements left in flight after a failed start", n) } } // A refusal before any start: nothing starts, no retry. The real placement // cannot place in a temp dir (its checks refuse before any fork, so before // is not asked there); the fallback then asks before, which refuses. func TestStartGatedRefusedBeforeTheFirstStart(t *testing.T) { root := newDelegatedRoot(t) m, err := SetupAt(root, t.Logf) if err != nil { t.Fatal(err) } calls := fakeStart(t, func(*exec.Cmd) error { return nil }) refused := errors.New("lease lost") if cmd, err := m.StartGated("h.example.com", Limit{}, buildTrue, func() error { return refused }); err != refused || cmd != nil { t.Fatalf("StartGated = %v, %v: want the hook's own error and no command", cmd, err) } if len(*calls) != 0 { t.Fatalf("started %d times after the hook refused, want none", len(*calls)) } if _, err := os.Stat(filepath.Join(root, ExecName("h.example.com"))); !os.IsNotExist(err) { t.Fatalf("host dir after a refused start: %v, want it removed", err) } } // A refusal through the shim (before said no after placement): nothing of // the command ran, the refusal comes back as the hook's own error, and // there is no PID-move retry, for a flat start and a generation alike. func TestARefusalAtPlacementIsNotRetried(t *testing.T) { const host = "h.example.com" m, parent := genManager(t, host) refused := errors.New("the attempt was superseded") stubPlacement(t, func(_ *Manager, _ string, _ *exec.Cmd, before func() error) (bool, error) { return true, before() }) calls := fakeStart(t, func(*exec.Cmd) error { return nil }) if cmd, err := m.StartGated("flat.example.com", Limit{}, buildTrue, func() error { return refused }); err != refused || cmd != nil { t.Fatalf("StartGated = %v, %v: want the hook's own error", cmd, err) } if cmd, err := m.StartChildGated(host, GenChild(1), TierDefault, Limit{}, buildTrue, func() error { return refused }); err != refused || cmd != nil { t.Fatalf("StartChildGated = %v, %v: want the hook's own error", cmd, err) } if len(*calls) != 0 { t.Fatalf("started %d times after a refusal at placement, want none", len(*calls)) } for _, dir := range []string{m.hostDir("flat.example.com"), filepath.Join(parent, "g1")} { if _, err := os.Stat(dir); !os.IsNotExist(err) { t.Errorf("%s after a refused start: %v, want it removed", dir, err) } } } // FakePlacementForTest lets another package's fixture (a temp directory, // where the shim cannot place) start a generation: the command starts // directly behind its before hook, as if placed, and the shim counts as // available even where the binary has no hook. Both are restored at the // test's cleanup. func TestFakePlacementForTestStartsWithoutTheShimAndRestores(t *testing.T) { const host = "h.example.com" m, _ := genFixture(t, host) t.Run("faked", func(t *testing.T) { noShim(t) // as in a test binary whose TestMain has no hook FakePlacementForTest(t) if !shimAvailable() { t.Fatal("the fake did not make the shim available") } var asked int calls := fakeStart(t, func(*exec.Cmd) error { return nil }) if _, err := m.StartChildGated(host, GenChild(1), TierDefault, TierDefault, buildTrue, func() error { asked++; return nil }); err != nil { t.Fatalf("StartChildGated under the fake = %v", err) } if len(*calls) != 1 || asked != 1 { t.Fatalf("started %d times, asked %d: want one direct start behind one ask", len(*calls), asked) } if slices.ContainsFunc(*calls, func(c *exec.Cmd) bool { return slices.Contains(c.Args, ShimVerb) }) { t.Fatal("the fake started a shim") } }) if shimForcedForTest.Load() { t.Fatal("the fake's availability override outlived its test") } calls := fakeStart(t, func(*exec.Cmd) error { return nil }) if _, err := m.StartChild(host, GenChild(2), TierDefault, TierDefault, buildTrue); !errors.Is(err, ErrLimitNotApplied) || !isPlacementError(err) { t.Fatalf("StartChild after the fake = %v: want the real shim's placement refusal on a temp dir", err) } if len(*calls) != 0 { t.Fatal("the fake placement outlived its test") } } // The real shim cannot place a process in a temp directory (no cgroup2 // filesystem). A generation start there is refused as the box's condition, // having built the command once and started nothing. func TestAGenerationTheShimCannotPlaceIsRefused(t *testing.T) { const host = "h.example.com" m, parent := genFixture(t, host) calls := fakeStart(t, func(*exec.Cmd) error { return nil }) builds := 0 cmd, err := m.StartChild(host, RoleChild("seed"), TierDefault, Limit{}, func() *exec.Cmd { builds++; return buildTrue() }) if !errors.Is(err, ErrLimitNotApplied) || !isPlacementError(err) || cmd != nil { t.Fatalf("StartChild = %v, %v: want ErrLimitNotApplied from the shim's placement error", cmd, err) } if builds != 1 || len(*calls) != 0 { t.Fatalf("built %d, started %d: want one build and nothing started", builds, len(*calls)) } noGenerationChild(t, parent) }