fix(nix,podmanx): containers as user@lab scopes — linger + systemd cgroup manager; cross-unit payload attach is kernel-blocked (EACCES), holder still carries the restart trap #239
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab#239
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?
| This was generated by AI during a live diagnosis session with the maintainer.
Incident
2026-07-26, dev-new: after the issue #237 holder-unit fix (deployed ~01:32 as "ADR-0059") every container spawn fails at
podman create— provider login panes return 500 (start login session: session "lab-login-claude-code" exited immediately), and agent-run containers are equally dead. Journal signature, one per attempt:~1460 leftover empty
libpod-*dirs have accumulated underpayload/from the failed creates (harmless, prune during cleanup).Diagnosis (verified live on dev-new)
The deployed layout is exactly as #237 specified and is not misconfigured:
lab-payload.servicehasDelegate=yes,User=lab, its cgroup root +cgroup.procs+cgroup.subtree_controlare lab-owned,payload/is lab-owned withcpuset cpu io memory pidsenabled, andlab.servicehasDelegate=no. The destination files crun writes are writable by lab.The failure is the cgroup-v2 delegation containment rule: to migrate a PID between cgroups, a non-root writer needs write access to
cgroup.procsof the common ancestor of source and destination — not just the destination. crun runs insidelab.service; the target islab-payload.service; the common ancestor issystem.slice, whosecgroup.procsis root-owned → EACCES on every attach.Probes (all as described, reproduced deterministically):
payload/→Permission denied.lab-payload.servicesubtree → succeeds.Probe 3 is the smoking gun: destination permissions are fine; only the cross-delegation ancestor check fails.
Why the holder cannot be patched (do not re-attempt these)
lab-payload.servicerunsKillMode=process,Restart=no,X-RestartIfChanged=false,DelegateSubgroup=main, with controllers enabled on its cgroup root. Any restart while containers survive (manual restart, thesleepdying, any future unit edit) hits the same executor-spawn-into-root EBUSY, becauseDelegateSubgroup=does not apply to the main-process spawn (verified in systemd v260exec_params_needs_control_subcgroup(), see #237). "Never restart it" is a pin, not a fix.podman system service(REST API) does not help. The invariant is: an unprivileged process can only attach PIDs into a delegated subtree it already lives in. So the API service would have to live inside the payload unit — making it just another spawner carrying the same restart trap, and a worse one: it idle-exits, is socket-activated, and its binary changes every deploy.General form: whoever spawns containers must share the delegated subtree, and whoever shares the subtree cannot safely restart with survivors. Only root/systemd escapes this. Both architectures tried so far (ADR-0058 in-unit payload → restart-EBUSY; #237/ADR-0059 cross-unit payload → spawn-EACCES) are two sides of the same kernel wall.
Decided fix (maintainer-approved 2026-07-26 — don't re-grill the direction)
Linger +
user@labmanager: stop performing cgroup placement as an unprivileged process; let systemd do it. This is ADR-0058's documented fallback, now promoted to the primary design:users.users.lab.linger = true→ permanentuser@lab.serviceuser manager for the lab user.lab.serviceitself stays a system service running as lab.--cgroup-parent=…/payload --cgroup-manager=cgroupfsto--cgroup-manager=systemdin all three shapes (run pane, login pane, non-interactive CLI poke — one renderer,podmanx.RunArgv). Each container becomes its own transient scope (libpod-<id>.scope) underuser@lab.service, created via the lab user manager; cross-delegation attaches are forwarded by the user manager to PID 1 (AttachProcessesToUnit), so the ancestor rule never applies.--memory/--pids-limitland as per-scope caps.XDG_RUNTIME_DIR=/run/user/<lab-uid>andDBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/<lab-uid>/bus. This re-opens ADR-0057'sXDG_RUNTIME_DIR=/run/lab+RuntimeDirectoryPreservedecisions: decide whether podman's runroot/tmpdir move to/run/user/<uid>(linger keeps it alive, soRuntimeDirectoryPreservemachinery may become unnecessary) and document in the ADR.lab-payload.serviceholder,lab-cgroup-hygieneoneshot,podmanx.setupCgroups, the per-spawn restart-safety guard, andDelegate/DelegateSubgrouponlab.service.lab.servicerestarts can then never EBUSY, and containers survive lab restarts by construction (independent scope units).--memoryset, assert it starts and the cap landed in its scope cgroup. That catches this whole class regardless of layout (preserves #205's no-false-green property for real).Structural wins: each failure mode disappears rather than being pinned — no unit we manage carries delegation,
user@labis systemd's own machinery (never restarted by nixos-rebuild, and stopping it kills its whole subtree, so it has no survivor-EBUSY mode either).Migration / repo state notes
docs/adr/ends at 0058). Locate that branch or supersede in place; the new ADR should take the next free number and note the phantom deployed "ADR-0059".libpod-*leftovers under the holder'spayload/, then the holder unit can be removed (its cgroup dies with its last survivor).chown lab /sys/fs/cgroup/system.slice/cgroup.procssatisfies the ancestor check; holds until systemd re-realizes the slice.Acceptance
/auth/login/start200).--memory/--pids-limitverifiably applied to their scope cgroup.systemctl restart labwith live containers: lab comes back clean, containers survive, and a new container can be spawned afterwards.lab-payload.service,lab-cgroup-hygiene,setupCgroups, and the spawn guard are gone from module + code.References: #237 (root-cause of the restart side, closed), ADR-0052, ADR-0057, ADR-0058 (fallback promoted by this issue), deployed-only "ADR-0059".