fix(nix,podmanx): move payload cgroup delegation out of lab.service — DelegateSubgroup does not protect the main-process spawn, restart-EBUSY is back #237
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab#237
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Incident
2026-07-26, dev-new, systemd 260.2: every
lab.servicerestart after the ADR-0058 deploy failed withFailed to spawn executor: Device or resource busy(resultresources), restart-looping past counter 700 while agent containers, tmux, conmon and pasta survived as designed. The on-disk layout was exactly what ADR-0058 specifies (unit root: no procs, controllers insubtree_control;main/: procs, clean;payload/: delegating, cleanlibpod-*leaves) andlab-cgroup-hygieneran green before every attempt. Recovered manually by clearingpayload/then the unit root'ssubtree_controlbottom-up (surviving containers run uncapped until they exit); the trap re-arms on every restart until this issue lands.Root cause (verified in systemd v260 source; unchanged on master)
DelegateSubgroup=does not apply to the main process's spawn. Insrc/core/execute.c:All three flags are required; the main
ExecStartprocess is not a control process, soexec_spawn()passes the unit cgroup root as the clone3(CLONE_INTO_CGROUP) target — sd-executor only migrates itself into theDelegateSubgroupafterwards.posix_spawn_wrapperretries without the cgroup flag only on ENOTSUP/EPERM, not EBUSY. Consequence: once lab has enabled controllers on the unit root'ssubtree_control(required to carry the payload caps) and anything survives a stop (KillMode=process is the product feature), the kernel's no-internal-process rule rejects every subsequent spawn. Fresh starts work only because the root'ssubtree_controlis empty at spawn time — which is why ADR-0058 validated green and then failed on the next deploy.ADR-0058's core placement ("systemd spawns lab into
main/") is therefore wrong, and any design that keeps controllers enabled on lab.service's own cgroup root across restarts is structurally dead. The payload delegation must move out of lab.service's cgroup.Sources: systemd v260 execute.c, systemd master execute.c, systemd v260 process-util.c.
Decided fix direction (maintainer-approved)
A separate, never-restarted holder unit owns the delegated payload cgroup;
lab.servicestops delegating entirely.Agent Brief
Category: bug
Summary: Containers move to a delegated cgroup owned by a static holder unit (
lab-payload.service);lab.servicedropsDelegate/DelegateSubgroupso its restarts can never hit the no-internal-process rule again.Current behavior:
The NixOS module gives
lab.serviceDelegate=yes+DelegateSubgroup=main; lab's preflight (setupCgroups) derives its own unit cgroup from/proc/self/cgroup, verifies it sits in a delegated subgroup (cgroup-layoutcheck), enables+memory +pidson the unit root'ssubtree_control, and creates thepayload/sibling that--cgroup-parentpins for every container (session, AFK, login). A rootlab-cgroup-hygieneoneshot clears stale controller delegation frommain/before each start. Result: first start works, every restart with survivors fails with EBUSY (see root cause above).Desired behavior:
lab-payload.service):Delegate=yes,DelegateSubgroup=main(its own trivial main process must sit in a subgroup so the root may delegate),KillMode=process(a holder stop must never take the containers down with it), same service user as lab, anExecStartthat just sleeps forever, and it must be excluded from deploy-driven restarts (restartIfChanged = false) — it never restarts, so the executor-spawn trap can never fire for it. systemd'sDelegate=yes+User=chowns its cgroup subtree to the lab user, which is what lets rootless podman create thelibpod-*children there.lab.servicelosesDelegateandDelegateSubgroupentirely and gainsWants=/After=on the holder. Its own cgroup must never carry entries insubtree_control.KillMode=process,RuntimeDirectory/RuntimeDirectoryPreservestay as they are./system.slice/lab-payload.service/payload), and that is whatResult.CgroupParent()/RunSpec.CgroupParentpin via--cgroup-parentfor all container shapes (sessions, AFK runs, provider-login panes).cgroup-layoutcheck (lab-in-a-delegated-subgroup) is obsolete in its current form; the restart-safety guard's job becomes asserting that lab's own unit cgroup root has an emptysubtree_control(nothing may ever re-arm the trap) alongside whatever holder-side invariants the implementation needs.lab-cgroup-hygieneoneshot is retargeted or retired: with lab.service undelegated, its cgroup is root-owned and rootless podman cannot dirty it; if a hygiene guard is kept, it belongs to the holder'smain/subgroup.DelegateSubgroup=cannot protect the main-process spawn (with the systemd source receipt) and recording the holder-unit design and its constraints (never restarted, KillMode=process, DelegateSubgroup on the holder).Key interfaces:
Deps/setupCgroups/Cgroupssurface — payload parent path derivation changes from self-cgroup-relative to holder-unit-cgroup;Verify()per-spawn guard keeps existing hard-fail semantics against the new invariants.Result.CgroupParent()— same contract, new value; all three RunSpec shapes must keep pinning it.cgroup-layout,cgroup-delegation,cgroup-restart-safety) — keep stable names where semantics survive, rename/retire where they do not; failure hints must name the holder unit.container.enable.Acceptance criteria:
systemctl restart labsucceeds repeatedly (no EBUSY, no manual surgery) — the exact dev-new incident scenario.lab.serviceunit file contains noDelegate/DelegateSubgroup; its cgroup'ssubtree_controlstays empty through container spawns and restarts.payload/with--memory/--pids-limitcaps observable in theirlibpod-*cgroups, and surviving containers keep their caps across lab restarts and deploys.KillMode=process).Deps(including the re-armed-trap detection on lab's own root).Out of scope:
--cgroup-parentvalue.Validation note: full end-to-end restart validation needs a real NixOS host (dev-new deploy by the maintainer) — CI covers the Go layout/preflight logic and nix eval only.