Follow-up from the CodeRabbit review on #78 (thread).
Problem
Per-invocation cgroup containment (#77, src/worker/cgroup-containment.ts) creates conduit-<pid>-<n> under the cgroup the kernel runs in, and requires the kernel's user to be able to write that parent cgroup. Station and harness commands run as that same user. Under cgroup v2, a process may migrate to any cgroup whose cgroup.procs it can write, provided it can also write the cgroup.procs of the common ancestor. Here the parent is that common ancestor, and it is writable. So a command can run echo $$ > <parent>/cgroup.procs, leave its invocation's cgroup, and survive cgroup.kill on timeout or after exit.
Commands that detach the ordinary way (setsid, nohup, a daemonizing fork, which is what Claude Code's Bash tool does) do not change cgroup and are still killed. The gap is only a command that sets out to evade containment. #78 narrowed the documented contract to say so (docs/harness-containment.md, "Process-tree termination") and names the container as the boundary for that case.
Possible fix
Manage cgroups under an identity the contained commands do not share, so the parent's cgroup.procs is not writable by them while the spawn wrapper can still join the child cgroup. Options to evaluate:
- run station and harness commands as a separate, unprivileged user, with the kernel's user owning the delegated subtree;
- have a small privileged helper, or systemd (
systemd-run --user --scope), create and populate the invocation cgroup so the kernel's user never needs write access to the parent;
- use a cgroup namespace per invocation (
unshare --cgroup) so the command cannot see the parent. Check whether that is enough: namespacing hides the path but does not by itself remove write permission.
Whichever approach is chosen, add a conformance scenario to src/worker/harness-containment.conformance.ts in which the fixture writes its own pid into the parent cgroup.procs, and assert it is still killed. Then restore the stronger wording in docs/harness-containment.md and the module header.
Follow-up from the CodeRabbit review on #78 (thread).
Problem
Per-invocation cgroup containment (#77,
src/worker/cgroup-containment.ts) createsconduit-<pid>-<n>under the cgroup the kernel runs in, and requires the kernel's user to be able to write that parent cgroup. Station and harness commands run as that same user. Under cgroup v2, a process may migrate to any cgroup whosecgroup.procsit can write, provided it can also write thecgroup.procsof the common ancestor. Here the parent is that common ancestor, and it is writable. So a command can runecho $$ > <parent>/cgroup.procs, leave its invocation's cgroup, and survivecgroup.killon timeout or after exit.Commands that detach the ordinary way (
setsid,nohup, a daemonizing fork, which is what Claude Code's Bash tool does) do not change cgroup and are still killed. The gap is only a command that sets out to evade containment. #78 narrowed the documented contract to say so (docs/harness-containment.md, "Process-tree termination") and names the container as the boundary for that case.Possible fix
Manage cgroups under an identity the contained commands do not share, so the parent's
cgroup.procsis not writable by them while the spawn wrapper can still join the child cgroup. Options to evaluate:systemd-run --user --scope), create and populate the invocation cgroup so the kernel's user never needs write access to the parent;unshare --cgroup) so the command cannot see the parent. Check whether that is enough: namespacing hides the path but does not by itself remove write permission.Whichever approach is chosen, add a conformance scenario to
src/worker/harness-containment.conformance.tsin which the fixture writes its own pid into the parentcgroup.procs, and assert it is still killed. Then restore the stronger wording indocs/harness-containment.mdand the module header.