//go:build linux
package children
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"sync/atomic"
"syscall"
)
// syncGroup and workGroup are the open directories of the cgroups
// children are started in, or -1 when there are none. Sync children
// (zit serve, the sync front) get a group of their own with a floor it
// keeps (memory.min), so a heavy page child that fills the shared
// ceiling is killed in its own group and a concurrent sync is not
// (jva, ^3920).
var syncGroup, workGroup atomic.Int64
func init() {
syncGroup.Store(-1)
workGroup.Store(-1)
}
// syncFloor is the memory a sync child keeps however full the page
// children's share is.
const syncFloor = 512 << 20
// 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)
for _, name := range []string{"sync", "work"} {
if err := os.Mkdir(filepath.Join(group, name), 0o755); err != nil && !os.IsExist(err) {
return "", err
}
}
// A child the last instance left behind (a sync front not yet
// reaped) would keep children from delegating its memory controller,
// since cgroup v2 allows that only on a group holding no process.
if left, err := os.ReadFile(filepath.Join(group, "cgroup.procs")); err == nil {
for _, pid := range strings.Fields(string(left)) {
_ = os.WriteFile(filepath.Join(group, "work", "cgroup.procs"), []byte(pid), 0)
}
}
if err := os.WriteFile(filepath.Join(group, "cgroup.subtree_control"), []byte("+memory"), 0); err != nil {
return "", fmt.Errorf("enable memory for %s's children: %w", group, err)
}
open := func(name string, min int64) (int, error) {
dir := filepath.Join(group, name)
if min > 0 {
if err := os.WriteFile(filepath.Join(dir, "memory.min"), []byte(strconv.FormatInt(min, 10)), 0); err != nil {
return -1, err
}
}
return syscall.Open(dir, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_CLOEXEC, 0)
}
sfd, err := open("sync", min(syncFloor, max/4))
if err != nil {
return "", err
}
wfd, err := open("work", 0)
if err != nil {
return "", err
}
syncGroup.Store(int64(sfd))
workGroup.Store(int64(wfd))
return group, nil
}
// confine starts cmd inside its children group, when there is one: a
// sync child (verb serve) in sync, every other child in work.
func confine(cmd *exec.Cmd, verb string) {
fd := workGroup.Load()
if verb == "serve" {
fd = syncGroup.Load()
}
if fd < 0 {
return
}
if cmd.SysProcAttr == nil {
cmd.SysProcAttr = &syscall.SysProcAttr{}
}
cmd.SysProcAttr.UseCgroupFD = true
cmd.SysProcAttr.CgroupFD = int(fd)
}