package cgroup import ( "bytes" "errors" "fmt" "os" "strconv" "strings" "sync/atomic" "time" ) // ShimVerb is the carlos binary's hidden placement verb (Paul's ruling, // #272, 2026-10-13). carlos-edge.service's RestrictNamespaces=yes makes // systemd's seccomp filter answer clone3 with ENOSYS, so no child can be // created inside its cgroup. Instead the child starts as this binary, // writes its own PID into the target cgroup through a directory fd the // parent validated, checks where it landed, and only then execs the // tenant: the tenant's code never runs outside its limits. const ShimVerb = "__cgexec" // ProbeVerb prints the process's own cgroup and exits: the tenant the // arm-time placement check (VerifyPlacement) execs through the shim. const ProbeVerb = "__cgprobe" var shimHooked atomic.Bool // shimForcedForTest makes shimAvailable answer true while // FakePlacementForTest has swapped the placement for one that runs no shim. // startViaShim itself still asks shimHooked, so the override can never make // a test binary without the hook start itself as a shim. var shimForcedForTest atomic.Bool // RunShimIfRequested must be the first statement of main(), and of TestMain // in a test binary that places processes. It marks this process as able to // serve the shim (shimAvailable), and when this process IS a shim or a // probe it runs it and exits; it returns only when it is neither. func RunShimIfRequested() { shimHooked.Store(true) if len(os.Args) < 2 { return } switch os.Args[1] { case ShimVerb: os.Exit(runShim(os.Args[2:])) case ProbeVerb: os.Exit(runProbe()) } } // placementError is a start whose process could not be placed in its // cgroup before exec. Nothing of the tenant ran. type placementError struct{ err error } func (e *placementError) Error() string { return "placement before exec: " + e.err.Error() } func (e *placementError) Unwrap() error { return e.err } func isPlacementError(err error) bool { var p *placementError return errors.As(err, &p) } // ErrPlacementInFlight is a cleanup asked for members while a process is // being placed in its cgroup (Task 2). var ErrPlacementInFlight = errors.New("cgroup: a process is being placed in this cgroup") // shimDeadline bounds each of the parent's two waits on the shim: start to // placement, and release to exec (Ruling 9). var shimDeadline = 10 * time.Second // shimExe is what the parent starts as the shim: the running build, even // after a roll has replaced the file on disk. var shimExe = "/proc/self/exe" // shimTestHold is copied into each spec's TestHold. Only test files set it, // and only a test binary's shim honours it. var shimTestHold int // shimAfterRelease, when set, runs between the release byte and the read of // the exec status. Only test files set it. var shimAfterRelease func() // shimSpec is what the parent sends the shim over the spec pipe: never in // the shim's argv or environment, so no secret shows in /proc//cmdline // and no tenant variable shapes the shim before placement. type shimSpec struct { Path string Dir string // the tenant's cmd.Dir; the shim changes to it after placement Argv []string Env []string Expected string // "/..." as /proc/self/cgroup shows it after "0::" KeepFDs int // fds 0..KeepFDs-1 are the tenant's TestHold int // test binaries only: 0 none, 1 hold before step 4, 2 hold after step 10 (before R) // SigIgn is the set of signals the agent ignored when it started the // shim, as /proc/self/status's SigIgn shows it: bit n-1 is signal n. // A direct start's child keeps exactly these ignored through its exec // (the runtime resets only the signals it handles, clearSignalHandlers), // but the shim's own Go runtime replaces most inherited ignores with its // handlers, which the tenant's execve then resets to default. So the // shim ignores these again immediately before it execs (whole-branch // review, finding 2). SigIgn uint64 } const ( shimSpecMagic = "carlos-cgexec-2" // shimSpecMax bounds the encoded spec the shim will read. shimSpecMax = 16 << 20 // shimSpecMaxCount bounds each decoded count (argv, env, KeepFDs). shimSpecMaxCount = 1 << 20 ) // encodeShimSpec writes each field followed by a NUL, in order: the magic, // Path, Dir, Expected, KeepFDs, TestHold, SigIgn, len(Argv), each argv element, // len(Env), each env entry. Bytes pass through untouched (invalid UTF-8 // included); a string holding a NUL is refused, as execve could not carry // it either. func encodeShimSpec(s shimSpec) ([]byte, error) { var b []byte put := func(field, v string) error { if strings.IndexByte(v, 0) >= 0 { return fmt.Errorf("cgroup: shim spec: %s contains a NUL byte", field) } b = append(b, v...) b = append(b, 0) return nil } if err := put("magic", shimSpecMagic); err != nil { return nil, err } for _, f := range []struct{ name, v string }{ {"path", s.Path}, {"dir", s.Dir}, {"expected", s.Expected}, {"keep", strconv.Itoa(s.KeepFDs)}, {"hold", strconv.Itoa(s.TestHold)}, {"sigign", strconv.FormatUint(s.SigIgn, 10)}, {"argc", strconv.Itoa(len(s.Argv))}, } { if err := put(f.name, f.v); err != nil { return nil, err } } for _, a := range s.Argv { if err := put("argv", a); err != nil { return nil, err } } if err := put("envc", strconv.Itoa(len(s.Env))); err != nil { return nil, err } for _, e := range s.Env { if err := put("env", e); err != nil { return nil, err } } return b, nil } // decodeShimSpec is encodeShimSpec's inverse. It refuses a wrong magic, a // buffer that ends early or runs past the last field, and a count above // shimSpecMaxCount. func decodeShimSpec(b []byte) (shimSpec, error) { var s shimSpec next := func() (string, error) { i := bytes.IndexByte(b, 0) if i < 0 { return "", errors.New("cgroup: shim spec: truncated") } v := string(b[:i]) b = b[i+1:] return v, nil } count := func() (int, error) { v, err := next() if err != nil { return 0, err } n, err := strconv.Atoi(v) if err != nil || n < 0 || n > shimSpecMaxCount { return 0, fmt.Errorf("cgroup: shim spec: bad count %q", v) } return n, nil } magic, err := next() if err != nil { return s, err } if magic != shimSpecMagic { return s, fmt.Errorf("cgroup: shim spec: magic %q, want %q", magic, shimSpecMagic) } for _, p := range []*string{&s.Path, &s.Dir, &s.Expected} { if *p, err = next(); err != nil { return s, err } } if s.KeepFDs, err = count(); err != nil { return s, err } if s.TestHold, err = count(); err != nil { return s, err } sigign, err := next() if err != nil { return s, err } if s.SigIgn, err = strconv.ParseUint(sigign, 10, 64); err != nil { return s, fmt.Errorf("cgroup: shim spec: bad ignored-signal set %q", sigign) } argc, err := count() if err != nil { return s, err } s.Argv = []string{} for range argc { a, err := next() if err != nil { return s, err } s.Argv = append(s.Argv, a) } envc, err := count() if err != nil { return s, err } s.Env = []string{} for range envc { e, err := next() if err != nil { return s, err } s.Env = append(s.Env, e) } if len(b) != 0 { return s, fmt.Errorf("cgroup: shim spec: %d trailing bytes", len(b)) } return s, nil }