Version: 3.3.1 (the same code is on main as of v3.3.4)
Kernel: 7.0.0-15-generic, Ubuntu 26.04.1, systemd, cgroup v2 only; the process runs in user.slice/user-0.slice/session-N.scope
Cgroup::enable_controllers writes to the cgroup.subtree_control of the process's own cgroup, on the assumption that this fails with EBUSY whenever that cgroup is not the hierarchy root. That holds for memory. It does not hold for the threaded controllers: the kernel accepts +pids and +cpu on a populated non-root cgroup and switches it to domain threaded. From then on, writing a pid into cgroup.procs of any domain cgroup below it fails with EOPNOTSUPP, so every later spawn in that cgroup fails in the child's write_self_pid with Operation not supported (os error 95).
Steps, all in one session scope:
- Build a group with only
max_processes set: ProcessGroup::with_options(ProcessGroupOptions::default().max_processes(64)).
- The scope's
cgroup.type is now domain threaded, its cgroup.subtree_control is pids.
- Build any other group and
start a command in it: Operation not supported (os error 95).
echo -pids > cgroup.subtree_control on the scope: cgroup.type is domain again and spawns succeed.
Measured in that scope (5 member processes, cgroup.controllers = cpu memory pids), one write at a time, reverted after each:
write "+memory +pids +cpu" -> rejected, subtree_control unchanged, type=domain
write "+memory" -> rejected, subtree_control unchanged, type=domain
write "+cpu" -> accepted, subtree_control=[cpu], type=domain threaded
write "+pids" -> accepted, subtree_control=[pids], type=domain threaded
With type=domain threaded in place, cgroup_attach_permissions returns -EOPNOTSUPP from __cgroup_procs_write (ftrace).
How a caller ends up there: with_options with max_memory + max_processes fails as documented (the combined write is rejected). A caller that retries without the memory axis then asks for pids only, the write goes through, and that group's own spawn already fails. Because enablement is deliberately not reverted on Drop, the cgroup stays that way for every other process in it until someone writes -pids.
Expected: the pids-only request fails with LimitReason::Unenforceable like the memory one, and the parent cgroup is left untouched.
A possible fix: before writing, refuse with LimitReason::Unenforceable when the parent is not the hierarchy root (for example, when it has a cgroup.type file), instead of relying on the kernel to reject the write.
Version: 3.3.1 (the same code is on
mainas of v3.3.4)Kernel: 7.0.0-15-generic, Ubuntu 26.04.1, systemd, cgroup v2 only; the process runs in
user.slice/user-0.slice/session-N.scopeCgroup::enable_controllerswrites to thecgroup.subtree_controlof the process's own cgroup, on the assumption that this fails withEBUSYwhenever that cgroup is not the hierarchy root. That holds formemory. It does not hold for the threaded controllers: the kernel accepts+pidsand+cpuon a populated non-root cgroup and switches it todomain threaded. From then on, writing a pid intocgroup.procsof any domain cgroup below it fails withEOPNOTSUPP, so every later spawn in that cgroup fails in the child'swrite_self_pidwithOperation not supported (os error 95).Steps, all in one session scope:
max_processesset:ProcessGroup::with_options(ProcessGroupOptions::default().max_processes(64)).cgroup.typeis nowdomain threaded, itscgroup.subtree_controlispids.starta command in it:Operation not supported (os error 95).echo -pids > cgroup.subtree_controlon the scope:cgroup.typeisdomainagain and spawns succeed.Measured in that scope (5 member processes,
cgroup.controllers=cpu memory pids), one write at a time, reverted after each:With
type=domain threadedin place,cgroup_attach_permissionsreturns-EOPNOTSUPPfrom__cgroup_procs_write(ftrace).How a caller ends up there:
with_optionswithmax_memory+max_processesfails as documented (the combined write is rejected). A caller that retries without the memory axis then asks forpidsonly, the write goes through, and that group's own spawn already fails. Because enablement is deliberately not reverted onDrop, the cgroup stays that way for every other process in it until someone writes-pids.Expected: the
pids-only request fails withLimitReason::Unenforceablelike thememoryone, and the parent cgroup is left untouched.A possible fix: before writing, refuse with
LimitReason::Unenforceablewhen the parent is not the hierarchy root (for example, when it has acgroup.typefile), instead of relying on the kernel to reject the write.