//go:build linux
package children
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"sync/atomic"
"syscall"
)
// childGroup is the open directory of the cgroup children are started
// in, or -1 when there is none.
var childGroup atomic.Int64
func init() { childGroup.Store(-1) }
// ConfineMemory gives every child this process starts a memory ceiling
// of its own, separate from the forge's: a runaway child is killed
// inside its group before it can push the machine into the OOM killer,
// whose pick is then a guess (zit annotate of a 33,677-line file grew
// past 4 GB, zit#325). No page may run the forge out of memory.
//
// It needs a delegated cgroup v2 (systemd Delegate=yes): the unit's own
// group is split into main, where this process and its wrapper move,
// and children, whose memory.max is max bytes. cgroup v2 allows no
// process in a group whose controllers are delegated to its children,
// which is why the process itself moves rather than staying put.
// Anything unavailable is an error to log; children then run as before.
func ConfineMemory(max int64) (string, error) {
raw, err := os.ReadFile("/proc/self/cgroup")
if err != nil {
return "", err
}
rel := ""
for _, line := range strings.Split(strings.TrimSpace(string(raw)), "\n") {
if p, ok := strings.CutPrefix(line, "0::"); ok {
rel = p
}
}
if rel == "" || rel == "/" {
return "", fmt.Errorf("no cgroup v2 group of our own (%q)", rel)
}
base := filepath.Join("/sys/fs/cgroup", rel)
if filepath.Base(base) == "main" {
base = filepath.Dir(base)
}
main := filepath.Join(base, "main")
if err := os.Mkdir(main, 0o755); err != nil && !os.IsExist(err) {
return "", fmt.Errorf("make %s (is the unit Delegate=yes?): %w", main, err)
}
procs, err := os.ReadFile(filepath.Join(base, "cgroup.procs"))
if err != nil {
return "", err
}
for _, pid := range strings.Fields(string(procs)) {
if err := os.WriteFile(filepath.Join(main, "cgroup.procs"), []byte(pid), 0); err != nil {
return "", fmt.Errorf("move %s into %s: %w", pid, main, err)
}
}
if err := os.WriteFile(filepath.Join(base, "cgroup.subtree_control"), []byte("+memory"), 0); err != nil {
return "", fmt.Errorf("enable memory for %s's children: %w", base, err)
}
group := filepath.Join(base, "children")
if err := os.Mkdir(group, 0o755); err != nil && !os.IsExist(err) {
return "", err
}
if err := os.WriteFile(filepath.Join(group, "memory.max"), []byte(strconv.FormatInt(max, 10)), 0); err != nil {
return "", err
}
_ = os.WriteFile(filepath.Join(group, "memory.swap.max"), []byte("0"), 0)
fd, err := syscall.Open(group, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_CLOEXEC, 0)
if err != nil {
return "", err
}
childGroup.Store(int64(fd))
return group, nil
}
// confine starts cmd inside the children group, when there is one.
func confine(cmd *exec.Cmd) {
fd := childGroup.Load()
if fd < 0 {
return
}
if cmd.SysProcAttr == nil {
cmd.SysProcAttr = &syscall.SysProcAttr{}
}
cmd.SysProcAttr.UseCgroupFD = true
cmd.SysProcAttr.CgroupFD = int(fd)
}