[PATCH RFC v3 83/90] cells: add the "net-rights" slot

From: Josef Bacik

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


A cell never owns its network namespace. The capabilities of its tasks are
over the user namespace of the cell, so they do not apply to the network
namespace, and the tasks cannot change anything about their network.

That is right for most workloads but not for all. Some need to add
netfilter rules, run a VPN client, or create a tap device for a VM. A
container gets CAP_NET_ADMIN for any of these, and with it everything else
that the capability allows.

Let the builder list what the tasks of the cell may do to the network
namespace in the "net" slot. The new "net-rights" slot takes a __u64 with
one bit for each enum net_right. An empty slot grants nothing, which is
the behaviour so far.

The caller can only grant what it could do itself. It must pass net_may()
on that network namespace for every right it names. This is why the "net"
slot has to be filled first. For a builder this means having the
capability over the owner of the namespace. Once cells can nest, the same
check will let a task of a cell pass on the rights of its own cell.

This patch only stores the rights. A later patch makes net_may() use them.

Signed-off-by: Josef Bacik <josef@xxxxxxxxxxxxxx>
---
include/uapi/linux/cell.h | 15 +++++++++++++++
kernel/cell/internal.h | 1 +
kernel/cell/slots.c | 45 +++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 61 insertions(+)

diff --git a/include/uapi/linux/cell.h b/include/uapi/linux/cell.h
index 3c10c9179289..c0d17c890552 100644
--- a/include/uapi/linux/cell.h
+++ b/include/uapi/linux/cell.h
@@ -21,6 +21,21 @@ enum cell_configure_cmd {
CELL_CMD_SEAL = 4, /* Validate everything and seal */
};

+/*
+ * The "net-rights" slot: what the tasks of a cell may do to the network of the cell.
+ * What is not here they may not do.
+ */
+#define CELL_NET_LINK (1ULL << 0) /* change a device: up, down, mtu, name */
+#define CELL_NET_LINK_NEW (1ULL << 1) /* make and remove devices */
+#define CELL_NET_ADDR (1ULL << 2) /* addresses */
+#define CELL_NET_ROUTE (1ULL << 3) /* routes, rules, neighbours */
+#define CELL_NET_FILTER (1ULL << 4) /* netfilter */
+#define CELL_NET_TC (1ULL << 5) /* queueing disciplines, classes, filters */
+#define CELL_NET_TUN (1ULL << 6) /* make tun and tap devices */
+#define CELL_NET_SYSCTL (1ULL << 7) /* /proc/sys/net */
+#define CELL_NET_SOCKOPT (1ULL << 8) /* SO_MARK, IP_TRANSPARENT and the like */
+#define CELL_NET_RAW (1ULL << 9) /* raw and packet sockets */
+
/* Life of a cell. It only ever moves down this list. */
enum cell_state {
CELL_BUILDING = 0, /* Slots may be filled */
diff --git a/kernel/cell/internal.h b/kernel/cell/internal.h
index 4eafa19daa25..469842ebee95 100644
--- a/kernel/cell/internal.h
+++ b/kernel/cell/internal.h
@@ -34,6 +34,7 @@ struct cell {
struct pid_namespace *pid_ns;
struct net *net;
bool no_net; /* the slot was left empty */
+ u64 net_rights; /* what its tasks may do to @net */
struct cgroup *budget;
u64 budget_id;
kuid_t budget_uid; /* who owned it before */
diff --git a/kernel/cell/slots.c b/kernel/cell/slots.c
index 2273d9ce733f..5f5378ddfe15 100644
--- a/kernel/cell/slots.c
+++ b/kernel/cell/slots.c
@@ -157,6 +157,49 @@ static int cell_set_caps(struct cell *cell, const void __user *uvalue, size_t le
return 0;
}

+/*
+ * What the tasks of the cell may do to its network. The network namespace is never
+ * the cell's, so no capability of theirs counts for it, see net_may().
+ *
+ * Nobody hands out what is not theirs to do: whoever fills the slot has to be able
+ * to do all of it to that network namespace itself.
+ */
+static int cell_set_net_rights(struct cell *cell, const void __user *uvalue, size_t len)
+{
+ unsigned long rights;
+ unsigned int right;
+ u64 val;
+
+ BUILD_BUG_ON(BIT_ULL(NET_RIGHT_LINK) != CELL_NET_LINK ||
+ BIT_ULL(NET_RIGHT_LINK_NEW) != CELL_NET_LINK_NEW ||
+ BIT_ULL(NET_RIGHT_ADDR) != CELL_NET_ADDR ||
+ BIT_ULL(NET_RIGHT_ROUTE) != CELL_NET_ROUTE ||
+ BIT_ULL(NET_RIGHT_FILTER) != CELL_NET_FILTER ||
+ BIT_ULL(NET_RIGHT_TC) != CELL_NET_TC ||
+ BIT_ULL(NET_RIGHT_TUN) != CELL_NET_TUN ||
+ BIT_ULL(NET_RIGHT_SYSCTL) != CELL_NET_SYSCTL ||
+ BIT_ULL(NET_RIGHT_SOCKOPT) != CELL_NET_SOCKOPT ||
+ BIT_ULL(NET_RIGHT_RAW) != CELL_NET_RAW ||
+ NET_RIGHT_NR > BITS_PER_LONG);
+
+ if (len != sizeof(val))
+ return -EINVAL;
+ if (copy_from_user(&val, uvalue, sizeof(val)))
+ return -EFAULT;
+ if (val & ~(BIT_ULL(NET_RIGHT_NR) - 1))
+ return -EINVAL;
+ /* Rights over what? */
+ if (!cell->net)
+ return -EINVAL;
+ rights = val;
+ for_each_set_bit(right, &rights, NET_RIGHT_NR) {
+ if (!net_may(cell->net, right))
+ return -EPERM;
+ }
+ cell->net_rights = val;
+ return 0;
+}
+
static int cell_set_string(struct cell *cell, const char *key, const char *val)
{
if (!strcmp(key, "hostname"))
@@ -173,6 +216,8 @@ static int cell_set_binary(struct cell *cell, const char *key,
return cell_set_ids_binary(cell, uvalue, len);
if (!strcmp(key, "caps"))
return cell_set_caps(cell, uvalue, len);
+ if (!strcmp(key, "net-rights"))
+ return cell_set_net_rights(cell, uvalue, len);
return -EINVAL;
}


--
2.55.0