[PATCH RFC v3 68/90] af_unix: tell a sender that control data will not cross

From: Josef Bacik

Date: Thu Nov 05 2026 - 10:35:51 EST


Control data that is sent to another cell is dropped on the receiving
side. It is better for the sender to find out.

Record in each socket the cell that holds it:

- For a new socket, the cell of its creator.
- For an accepted socket, the cell of the listener.
- For a socket that pidfd_configure() gives to a new task, the cell of
that task, once its exec has succeeded. If the task never starts, the
socket stays with its current holder.

Sending control data to a socket that is held by another cell fails with
EPERM.

This is only a convenience for the sender. What is delivered is still
decided on the receiving side.

Signed-off-by: Josef Bacik <josef@xxxxxxxxxxxxxx>
---
include/net/af_unix.h | 4 ++++
kernel/embryo.c | 17 ++++++++++++++++
net/unix/af_unix.c | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 77 insertions(+)

diff --git a/include/net/af_unix.h b/include/net/af_unix.h
index bb1b3dee02e8..10c1613b4d42 100644
--- a/include/net/af_unix.h
+++ b/include/net/af_unix.h
@@ -14,6 +14,7 @@

#if IS_ENABLED(CONFIG_UNIX)
struct unix_sock *unix_get_socket(struct file *filp);
+void unix_file_cross_cell(struct file *file, u64 cell_id);
#else
static inline struct unix_sock *unix_get_socket(struct file *filp)
{
@@ -50,6 +51,9 @@ struct unix_sock {
int inq_len;
bool recvmsg_inq;
bool scm_rights_notrunc;
+#ifdef CONFIG_CELLS
+ u64 cell_id; /* of the cell that holds it */
+#endif
#if IS_ENABLED(CONFIG_AF_UNIX_OOB)
struct sk_buff *oob_skb;
#endif
diff --git a/kernel/embryo.c b/kernel/embryo.c
index 657ce07441e0..1f8d0b14e6a4 100644
--- a/kernel/embryo.c
+++ b/kernel/embryo.c
@@ -78,6 +78,7 @@ struct embryo {

struct pid *pid; /* the reference alloc_pid_struct() returned */
struct cell *cell; /* where the task is going to be */
+ u64 cell_id; /* of that cell */
struct user_namespace *user_ns; /* likewise */

int result;
@@ -126,6 +127,19 @@ static int embryo_install_fds(struct embryo *e)
return 0;
}

+/*
+ * The exec went through, so these ends are the cell's from here on. Not any
+ * earlier: a socket that was to go to a task which never started stays with
+ * whoever has it.
+ */
+static void embryo_fds_crossed(struct embryo *e)
+{
+ unsigned int i;
+
+ for (i = 0; i < e->nr_fds; i++)
+ unix_file_cross_cell(e->fds[i].file, e->cell_id);
+}
+
/*
* What was configured has been used, or never will be. The pidfd may be around
* for long, and must not keep the other end of a pipe from seeing the last
@@ -241,6 +255,8 @@ static int embryo_main(void *arg)

/* @started is set, so nobody changes anything any more. */
ret = embryo_exec(e);
+ if (!ret)
+ embryo_fds_crossed(e);
embryo_drop_config(e);
/*
* A signal to the embryo is no signal to whoever started it. Whatever
@@ -356,6 +372,7 @@ int embryo_pidfd_open(int cfd, unsigned int flags)
}
e->pid = alloc_pid_struct(cell_nsproxy(cell)->pid_ns_for_children);
e->cell = cell_get(cell);
+ e->cell_id = cell_id(cell);
e->user_ns = get_user_ns(cell_user_ns(cell));
cell_fork_finish(cell);

diff --git a/net/unix/af_unix.c b/net/unix/af_unix.c
index 58fef56c0f7e..198898b86c1c 100644
--- a/net/unix/af_unix.c
+++ b/net/unix/af_unix.c
@@ -829,6 +829,49 @@ static inline u64 unix_skb_cell(const struct sk_buff *skb) { return 0; }
static inline bool unix_skb_foreign(const struct sk_buff *skb) { return false; }
#endif

+/*
+ * A sender is told right away that control data will not arrive, where that can
+ * be known: each socket records the cell that holds it. That is a courtesy. The
+ * rule above is what counts.
+ */
+#ifdef CONFIG_CELLS
+static inline bool unix_sk_foreign(const struct sock *other)
+{
+ return READ_ONCE(unix_sk(other)->cell_id) != current_cell_id();
+}
+
+static inline void unix_sk_set_cell(struct sock *sk, u64 cell_id)
+{
+ WRITE_ONCE(unix_sk(sk)->cell_id, cell_id);
+}
+
+static inline u64 unix_sk_cell(const struct sock *sk)
+{
+ return READ_ONCE(unix_sk(sk)->cell_id);
+}
+
+/**
+ * unix_file_cross_cell - a file is being given to a task of another cell
+ * @file: any file
+ * @cell_id: the id of that cell
+ *
+ * If @file is a unix socket, it is held by that cell from here on. This only
+ * serves to tell a sender early that control data will not arrive. Whether it
+ * does is decided where it is received.
+ */
+void unix_file_cross_cell(struct file *file, u64 cell_id)
+{
+ struct unix_sock *u = unix_get_socket(file);
+
+ if (u)
+ unix_sk_set_cell(&u->sk, cell_id);
+}
+#else
+static inline bool unix_sk_foreign(const struct sock *other) { return false; }
+static inline void unix_sk_set_cell(struct sock *sk, u64 cell_id) { }
+static inline u64 unix_sk_cell(const struct sock *sk) { return 0; }
+#endif
+
static bool unix_may_passcred(const struct sock *sk)
{
return sk->sk_scm_credentials || sk->sk_scm_pidfd ||
@@ -1146,6 +1189,7 @@ static struct sock *unix_create1(struct net *net, struct socket *sock, int kern,
sock_init_data(sock, sk);

sk->sk_scm_rights = 1;
+ unix_sk_set_cell(sk, current_cell_id());
sk->sk_hash = unix_unbound_hash(sk);
sk->sk_allocation = GFP_KERNEL_ACCOUNT;
sk->sk_write_space = unix_write_space;
@@ -1777,6 +1821,8 @@ static int unix_stream_connect(struct socket *sock, struct sockaddr_unsized *uad
newsk->sk_state = TCP_ESTABLISHED;
newsk->sk_type = sk->sk_type;
newsk->sk_scm_recv_flags = other->sk_scm_recv_flags;
+ /* It is the listener's end, whoever made it. */
+ unix_sk_set_cell(newsk, unix_sk_cell(other));
init_peercred(newsk, &peercred);

newu = unix_sk(newsk);
@@ -2284,6 +2330,11 @@ static int unix_dgram_sendmsg(struct socket *sock, struct msghdr *msg,
goto out_unlock;
}

+ if (msg->msg_controllen && unix_sk_foreign(other)) {
+ err = -EPERM;
+ goto out_unlock;
+ }
+
if (sk->sk_type != SOCK_SEQPACKET) {
err = security_unix_may_send(sk->sk_socket, other->sk_socket);
if (err)
@@ -2456,6 +2507,11 @@ static int unix_stream_sendmsg(struct socket *sock, struct msghdr *msg,

otheru = unix_sk(other);

+ if (msg->msg_controllen && unix_sk_foreign(other)) {
+ err = -EPERM;
+ goto out_err;
+ }
+
if (READ_ONCE(sk->sk_shutdown) & SEND_SHUTDOWN)
goto out_pipe;


--
2.55.0