#!/bin/sh # zddc-cgroup-init — prepare cgroup v2 hierarchy and exec zddc-server. # # The per-conversion wrapper (zddc-sandbox-exec) creates a transient # child cgroup for each pandoc / chromium invocation, sets memory.max # and pids.max on it, and moves the conversion process in. That only # works when: # # (a) the cgroup v2 hierarchy is mounted at /sys/fs/cgroup, AND # (b) the controllers we need (memory, pids) are enabled in the # parent cgroup's subtree_control file, AND # (c) the parent cgroup has NO processes in it (cgroup v2's # "no internal processes" constraint: a cgroup can have # children OR processes, not both). # # A bare container with PID 1 in the root cgroup violates (c). This # init script does the one-time setup BEFORE exec'ing zddc-server: # # 1. mkdir /sys/fs/cgroup/zddc/ (a sibling for zddc-server) # 2. move every PID out of root into /sys/fs/cgroup/zddc/ # 3. enable +memory +pids in root's subtree_control (now empty) # 4. enable +memory +pids in zddc/'s subtree_control (so its # children — the per-conversion cgroups created by the wrapper # — can use those controllers) # 5. exec zddc-server (which inherits cgroup membership in zddc/) # # After this, the wrapper script creates /sys/fs/cgroup/conv./ # as a sibling of /sys/fs/cgroup/zddc/, sets limits, and moves the # pandoc/chromium process in. Each conversion gets a fresh transient # cgroup that vanishes when the process exits. # # Best-effort: if any step fails (cgroup v1, undelegated subtree, # read-only cgroupfs in some other container shape), this script # still exec's zddc-server. The convert pipeline degrades to # "bwrap sandbox + wall-clock timeout"; an operator notices via # the warning log line below. set -eu setup_cgroup_v2() { cgroot=/sys/fs/cgroup [ -d "$cgroot" ] || return 1 # Detect cgroup v2 by the presence of cgroup.controllers at root. [ -r "$cgroot/cgroup.controllers" ] || return 1 # Need memory + pids in available controllers. if ! grep -qw memory "$cgroot/cgroup.controllers"; then echo "zddc-cgroup-init: cgroup.controllers lacks 'memory' — per-conversion memory cap will be unenforced" >&2 fi # Create the leaf where zddc-server itself will live. mkdir -p "$cgroot/zddc" || return 1 # Move every PID currently in the root cgroup into zddc/. The # root must be empty before we can enable subtree_control. if [ -r "$cgroot/cgroup.procs" ]; then while read -r pid; do [ -n "$pid" ] || continue # Best-effort; processes can exit between read and write. printf "%s\n" "$pid" > "$cgroot/zddc/cgroup.procs" 2>/dev/null || true done < "$cgroot/cgroup.procs" fi # Enable controllers at root → makes them usable in immediate # children (zddc/ and any sibling per-conversion cgroup). printf "+memory +pids" > "$cgroot/cgroup.subtree_control" 2>/dev/null || { echo "zddc-cgroup-init: could not enable +memory +pids in $cgroot/cgroup.subtree_control — caps will not apply" >&2 return 1 } # Enable inside zddc/ too, so any deeper children of zddc-server # (which there shouldn't be, but defense in depth) inherit. printf "+memory +pids" > "$cgroot/zddc/cgroup.subtree_control" 2>/dev/null || true return 0 } if ! setup_cgroup_v2; then echo "zddc-cgroup-init: cgroup v2 setup unavailable — running without per-conversion caps" >&2 fi # Hand off to zddc-server. The exec'd process lands in # /sys/fs/cgroup/zddc/ (we moved ourselves there above). When it # spawns the wrapper, the wrapper creates a transient sibling cgroup # under /sys/fs/cgroup/, NOT a child of zddc/, so the conversion's # cgroup is a peer of zddc-server's — keeping zddc-server's own # resource accounting separate from conversion accounting. exec "$@"