[PATCH RFC v3 64/90] cells: add the "caps" slot

From: Josef Bacik

Date: Thu Nov 05 2026 - 10:39:17 EST


The first task in a new user namespace has every capability, over
everything that the namespace owns. That puts a large amount of code that
was written for root within reach of every user. A container runtime then
spends its time taking most of those capabilities away again.

A cell only has what it is given, so start from the other end. Being uid 0
in a cell comes with no privilege. It is one id out of 65536. It cannot
switch to another id, give a file away, or signal a task that runs under
another id.

Let the builder name the capabilities that the tasks of the cell have. The
"caps" slot takes a __u64 with one bit per capability. An empty slot means
none.

The value is used as the bounding set as well as the initial permitted and
effective sets, so nothing in the cell can ever have more. Every task of a
cell has no_new_privs, so it gains nothing from a file, or from being uid
0 when it runs one. And no task of a cell can create a user namespace to
get a full set in.

Filling the slot requires no privilege. The capabilities apply to the user
namespace of the cell, so they cover its ids, its tasks and whatever else
that namespace owns. That is no more than anybody gets from
unshare(CLONE_NEWUSER).

CAP_SETFCAP is refused, because nothing that a cell writes may confer
privilege.

A task of a cell with a single id is not uid 0 in the cell, so it would
lose its capabilities as soon as it runs a program. The seal refuses
capabilities for such a cell instead of letting them disappear silently.

For reference: systemd 255 boots in a cell without any capability and runs
units that stay root. To run units as other users, give files and cgroups
to those users, and stop the units again, it needs CAP_SETUID, CAP_SETGID,
CAP_CHOWN, CAP_FOWNER, CAP_DAC_OVERRIDE and CAP_KILL.

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

diff --git a/kernel/cell/slots.c b/kernel/cell/slots.c
index 0a406422ce34..a96d2f262aa1 100644
--- a/kernel/cell/slots.c
+++ b/kernel/cell/slots.c
@@ -105,6 +105,28 @@ static int cell_set_ids_binary(struct cell *cell, const void __user *uvalue,
return cell_set_ids(cell, count);
}

+/*
+ * The capabilities that the tasks of the cell start with, and can never have more
+ * than. They are over the user namespace of the cell, so they are good for what
+ * that owns and for the ids of the cell, which is why naming them takes nothing.
+ */
+static int cell_set_caps(struct cell *cell, const void __user *uvalue, size_t len)
+{
+ u64 caps;
+
+ if (len != sizeof(caps))
+ return -EINVAL;
+ if (copy_from_user(&caps, uvalue, sizeof(caps)))
+ return -EFAULT;
+ if (caps & ~CAP_VALID_MASK)
+ return -EINVAL;
+ /* Nothing a cell writes imparts privilege, so it writes no file capability. */
+ if (caps & BIT_ULL(CAP_SETFCAP))
+ return -EINVAL;
+ cell->caps.val = caps;
+ return 0;
+}
+
static int cell_set_string(struct cell *cell, const char *key, const char *val)
{
if (!strcmp(key, "hostname"))
@@ -117,6 +139,8 @@ static int cell_set_binary(struct cell *cell, const char *key,
{
if (!strcmp(key, "ids"))
return cell_set_ids_binary(cell, uvalue, len);
+ if (!strcmp(key, "caps"))
+ return cell_set_caps(cell, uvalue, len);
return -EINVAL;
}

@@ -163,6 +187,9 @@ static int cell_seal(struct cell *cell)
if (ret)
return ret;
}
+ /* Who is not uid 0 loses them as soon as it runs a program. */
+ if (cell->ids_count == 1 && !cap_isclear(cell->caps))
+ return -EINVAL;
ret = cell_make_pid_ns(cell);
if (ret)
return ret;

--
2.55.0