[PATCH RFC v3 74/90] net: give a cell without a network no sockets
From: Josef Bacik
Date: Thu Nov 05 2026 - 10:30:46 EST
A cell whose "net" slot is left empty should have no network. So far it
has a network namespace that nobody has set up, which is not the same
thing. It is an empty network, and all the code that operates on one is
within reach. The tasks can create sockets of every family and talk to
rtnetlink and every other netlink family, although none of that is of any
use to them. There is no equivalent of failfs for the network.
Refuse every socket except a unix socket for a task of such a cell. The
error is EAFNOSUPPORT, which is what a kernel returns that was built
without the family. The check is in __sock_create(), which socket(),
socketpair() and the socket operation of io_uring all go through.
A socket that is handed to the cell works, and so do the sockets that
accept() returns for it. Holding the socket is the authority to use it,
and it is the only network that such a cell has.
Holding the socket is not the authority to inspect the network that it
belongs to, though. Many ioctls are about the network rather than the
socket, and they work on any socket. Through a single socket of the host,
a cell could list the devices of the host and their addresses. Refuse
those ioctls with ENODEV, by number, before anything else looks at them.
That covers every ioctl from SIOCADDRT upwards except SIOCOUTQNSD. The
protocol private range can contain either kind, so it is refused as well.
The check looks at the cell of the caller, not at a mark on the network
namespace. So it holds no matter which network namespace a socket belongs
to.
The decision is recorded once the seal has created the network namespace.
If the seal fails before that point, the slot can still be filled. If it
fails later and the builder then asks for the namespace with cell_open(),
the decision is reverted.
The network namespace still exists, because unix sockets need one. A
single static namespace for all such cells would avoid creating and
destroying a whole network namespace per cell. That requires moving the
unix socket tables out of struct net and is left for later. It would make
no difference from inside of the cell.
Signed-off-by: Josef Bacik <josef@xxxxxxxxxxxxxx>
---
include/linux/cell.h | 7 +++++++
kernel/cell/boundary.c | 19 +++++++++++++++++++
kernel/cell/internal.h | 1 +
kernel/cell/slots.c | 9 +++++++++
net/socket.c | 28 ++++++++++++++++++++++++++++
5 files changed, 64 insertions(+)
diff --git a/include/linux/cell.h b/include/linux/cell.h
index 0451055f74fc..b12e2f124769 100644
--- a/include/linux/cell.h
+++ b/include/linux/cell.h
@@ -249,6 +249,13 @@ bool cell_may_change_cgroup(struct cgroup *cgrp);
static inline bool cell_may_change_cgroup(struct cgroup *cgrp) { return true; }
#endif
+/* A cell without a network. */
+#ifdef CONFIG_CELLS
+bool cell_no_net(void);
+#else
+static inline bool cell_no_net(void) { return false; }
+#endif
+
/* A mount is idmapped for a cell. */
#ifdef CONFIG_CELLS
struct user_namespace *cell_idmap_userns(const struct file *file);
diff --git a/kernel/cell/boundary.c b/kernel/cell/boundary.c
index 3891dfe899f2..af67ef68d4a2 100644
--- a/kernel/cell/boundary.c
+++ b/kernel/cell/boundary.c
@@ -85,3 +85,22 @@ bool cell_may_change_cgroup(struct cgroup *cgrp)
{
return cell_may_migrate(current, cgrp);
}
+
+/**
+ * cell_no_net - is the caller in a cell that was built without a network?
+ *
+ * A cell whose "net" slot was left empty has no network. Its tasks make no
+ * socket but a unix socket, and learn nothing about a network through any socket.
+ * What they are handed works: a socket is the authority to use it.
+ *
+ * It goes by the cell of the caller and not by the network namespace it is in,
+ * so that it holds whatever that is.
+ *
+ * Return: true if so, false for a task of any other cell and of the host.
+ */
+bool cell_no_net(void)
+{
+ struct cell *cell = current_cell();
+
+ return cell && cell->no_net;
+}
diff --git a/kernel/cell/internal.h b/kernel/cell/internal.h
index c7a1895eadd4..4eafa19daa25 100644
--- a/kernel/cell/internal.h
+++ b/kernel/cell/internal.h
@@ -33,6 +33,7 @@ struct cell {
struct mnt_namespace *mnt_ns;
struct pid_namespace *pid_ns;
struct net *net;
+ bool no_net; /* the slot was left empty */
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 75855f2ee020..2273d9ce733f 100644
--- a/kernel/cell/slots.c
+++ b/kernel/cell/slots.c
@@ -211,6 +211,7 @@ static int cell_seal(struct cell *cell)
struct nsproxy *nsproxy;
kuid_t root_uid;
kgid_t root_gid;
+ bool no_net;
int ret;
if (!cell->budget || !cell->mnt_ns)
@@ -227,9 +228,15 @@ static int cell_seal(struct cell *cell)
ret = cell_make_pid_ns(cell);
if (ret)
return ret;
+ /* No network, see cell_no_net(). Unix sockets need a namespace to live in all the same. */
+ no_net = !cell->net;
ret = cell_make_net(cell);
if (ret)
return ret;
+ /* Only now. Had that failed, the slot would still be there to be filled. */
+ if (no_net)
+ cell->no_net = true;
+
cgroup_ns = cgroup_cell_ns(cell->user_ns, cell->budget);
if (IS_ERR(cgroup_ns))
return PTR_ERR(cgroup_ns);
@@ -410,6 +417,8 @@ SYSCALL_DEFINE5(cell_open, int, cfd, unsigned int, what,
ret = cell_make_net(cell);
if (ret)
return ret;
+ /* A seal that failed may have made it. Whoever asks means to set it up. */
+ cell->no_net = false;
ns = &get_net(cell->net)->ns;
} else if (!strcmp(key, "pids")) {
ret = cell_make_pid_ns(cell);
diff --git a/net/socket.c b/net/socket.c
index 2e3b5737c481..c9659f7370ef 100644
--- a/net/socket.c
+++ b/net/socket.c
@@ -106,6 +106,7 @@
#include <linux/route.h>
#include <linux/termios.h>
#include <linux/sockios.h>
+#include <linux/cell.h>
#include <net/busy_poll.h>
#include <linux/errqueue.h>
#include <linux/ptp_clock_kernel.h>
@@ -1352,6 +1353,22 @@ static long sock_do_ioctl(struct net *net, struct socket *sock,
* what to do with it - that's up to the protocol still.
*/
+/*
+ * Is @cmd about the network that a socket is in, its devices, addresses and
+ * routes, and not about the socket? Those are served for any socket, whatever
+ * its family, some of them by the family itself. From SIOCADDRT up that is all
+ * of them but one. What a protocol keeps to itself, SIOCPROTOPRIVATE, may be
+ * either, the counters of multicast routing are in there, so it counts.
+ */
+static bool sock_ioctl_about_net(unsigned int cmd)
+{
+ if (cmd >= SIOCIWFIRST && cmd <= SIOCIWLAST)
+ return true;
+ if (!is_socket_ioctl_cmd(cmd) || _IOC_NR(cmd) < _IOC_NR(SIOCADDRT))
+ return false;
+ return cmd != SIOCOUTQNSD;
+}
+
static long sock_ioctl(struct file *file, unsigned cmd, unsigned long arg)
{
const struct proto_ops *ops;
@@ -1361,6 +1378,10 @@ static long sock_ioctl(struct file *file, unsigned cmd, unsigned long arg)
int pid, err;
struct net *net;
+ /* There is no network, so there are no devices. */
+ if (sock_ioctl_about_net(cmd) && cell_no_net())
+ return -ENODEV;
+
sock = file->private_data;
ops = READ_ONCE(sock->ops);
sk = sock->sk;
@@ -1620,6 +1641,10 @@ int __sock_create(struct net *net, int family, int type, int protocol,
if (type < 0 || type >= SOCK_MAX)
return -EINVAL;
+ /* As in a kernel that was built without the rest of them. */
+ if (!kern && family != PF_UNIX && cell_no_net())
+ return -EAFNOSUPPORT;
+
/* Compatibility.
This uglymoron is moved from INET layer to here to avoid
@@ -3642,6 +3667,9 @@ static long compat_sock_ioctl(struct file *file, unsigned int cmd,
struct sock *sk;
struct net *net;
+ if (sock_ioctl_about_net(cmd) && cell_no_net())
+ return -ENODEV;
+
sk = sock->sk;
net = sock_net(sk);
--
2.55.0