Skip to content
quarry Artifact [6dce98ebb0]
Login

Artifact [6dce98ebb0]

Artifact 6dce98ebb0652eeab88fd301e3ad9cf6dcadf207619237e7b432966e3615e816:


//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)
}