[PATCH RFC v3 54/90] cgroup: leave the limits of a budget to who is above

From: Josef Bacik

Date: Thu Nov 05 2026 - 10:37:41 EST


The budget limits what a cell can use. The limits are set by the builder.
The seal gives the root user of the cell ownership of the directory,
cgroup.procs, cgroup.threads and cgroup.subtree_control, and of nothing
else.

That relies on modes and owners, which is not enough:

- The files of a cgroup belong to whoever created it. An unprivileged
builder creates the budget itself, so it owns the limits. A cell with
a single id runs with the id of that builder, so it can write whatever
it likes to its own pids.max and memory.max.
- A cell with a block of ids can do the same through a file that
somebody outside of the cell has opened and passed to it.
- The cell owns the directory, so it can make it accessible to everybody
on the host and set extended attributes on it.

cgroup already has the right rule, for cgroup namespaces with the
nsdelegate mount option. From inside of the namespace, the only files in
its root that can be written are those marked CFTYPE_NS_DELEGATABLE. The
cgroup namespace of a cell is rooted at the budget. But that rule depends
on how cgroupfs was mounted, it looks at the task that opened the file,
and it does not cover attributes.

So apply the rule to every task of a cell, in cgroup_may_change(). In the
budget itself such a task can create and remove cgroups and write to the
delegatable files. It cannot write to any other file there, and it cannot
set attributes on the directory or on the files in it. Below the budget
there are no restrictions.

Signed-off-by: Josef Bacik <josef@xxxxxxxxxxxxxx>
---
kernel/cgroup/cgroup.c | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)

diff --git a/kernel/cgroup/cgroup.c b/kernel/cgroup/cgroup.c
index accae30a901d..a0836b115c79 100644
--- a/kernel/cgroup/cgroup.c
+++ b/kernel/cgroup/cgroup.c
@@ -6488,9 +6488,25 @@ int cgroup_rmdir(struct kernfs_node *kn)
return ret;
}

+/*
+ * Is this what a cgroup is handed to somebody for? Making cgroups in it, and what
+ * is written to across a delegation boundary: see CGRP_ROOT_NS_DELEGATE, which
+ * goes by who opened the file and leaves the attributes to their owner.
+ */
+static bool cgroup_change_is_delegated(struct kernfs_node *kn,
+ enum kernfs_change what)
+{
+ const struct cftype *cft = kn->priv;
+
+ if (what != KERNFS_CHANGE_CONTENT)
+ return false;
+ return kernfs_type(kn) == KERNFS_DIR || (cft->flags & CFTYPE_NS_DELEGATABLE);
+}
+
/* Whatever the modes say, and whoever owns it. */
int cgroup_may_change(struct kernfs_node *kn, enum kernfs_change what)
{
+ struct kernfs_node *asked = kn;
struct cgroup *cgrp;

if (likely(!current_cell()))
@@ -6503,6 +6519,10 @@ int cgroup_may_change(struct kernfs_node *kn, enum kernfs_change what)
cgrp = rcu_dereference(*(void __rcu __force **)&kn->priv);
if (!cgrp || !cell_may_change_cgroup(cgrp))
return -EPERM;
+ /* The budget is what bounds the cell. Its limits are not the cell's. */
+ if (test_bit(CGRP_CELL_BUDGET, &cgrp->flags) &&
+ !cgroup_change_is_delegated(asked, what))
+ return -EPERM;
return 0;
}


--
2.55.0