* [PATCH net-next v4 0/3] net: af_unix: useful handling of LSM denials on SCM_RIGHTS
@ 2026-07-05 12:38 Jori Koolstra
2026-07-05 12:38 ` [PATCH net-next v4 1/3] net: scm: move scm_detach_fds() from common path to scm_recv_unix() Jori Koolstra
` (2 more replies)
0 siblings, 3 replies; 8+ messages in thread
From: Jori Koolstra @ 2026-07-05 12:38 UTC (permalink / raw)
To: Christian Brauner, Aleksa Sarai, Kuniyuki Iwashima,
David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman
Cc: netdev, linux-fsdevel, linux-kernel, Jori Koolstra
Right now if some LSM denies an AF_UNIX socket peer to receive a
SCM_RIGHTS fd, the SCM_RIGHTS fd array will be cut short at
that point, and MSG_CTRUNC is set on return of recvmsg(2). This is
highly problematic behaviour, because it leaves the receiver
wondering what happened. As per man page MSG_CTRUNC is supposed to
indicate that the control buffer was sized too short, but suddenly
a permission error might result in the exact same flag being set.
Moreover, the receiver has no chance to determine how many fds got
originally sent and how many were suppressed.[1]
Add a SO_RIGHTS_NOTRUNC option to UNIX sockets to enable more useful
handling of LSM denials when receiving SCM_RIGHTS messages: instead of
truncating the message at the first blocked fd, keep every fd slot
and store the LSM errno in the blocked slot.
[1]: https://github.com/uapi-group/kernel-features#useful-handling-of-lsm-denials-on-scm_rights
Changes:
v4:
- Removed the __receive_fd() helper and moved logic into
scm_recv_one_fd() directly (suggested by Brauner).
- Moved selftest from Smack to BPF (LLM assisted).
- Add arch specific socket option values for SO_RIGHTS_NOTRUNC.
- Undo patch that replaced copy_from_sockptr() with
copy_safe_from_sockptr().
v3:
- Separated net and vfs changes.
- Use kselftest_harness.h and system() to call the test script.
v2: https://lore.kernel.org/netdev/20260616143020.3458085-2-jkoolstra@xs4all.nl/
- Reimplemented as a UNIX socket option instead of a per recvmsg(2) flag.
v1: https://lore.kernel.org/netdev/20260428175125.2705296-1-jkoolstra@xs4all.nl/
Jori Koolstra (3):
net: scm: move scm_detach_fds() from common path to scm_recv_unix()
net: af_unix: useful handling of LSM denials on SCM_RIGHTS
selftest: Add tests for useful handling of LSM denials on SCM_RIGHTS
arch/alpha/include/uapi/asm/socket.h | 2 +
arch/mips/include/uapi/asm/socket.h | 2 +
arch/parisc/include/uapi/asm/socket.h | 2 +
arch/sparc/include/uapi/asm/socket.h | 2 +
include/net/af_unix.h | 1 +
include/net/scm.h | 13 +-
include/uapi/asm-generic/socket.h | 2 +
net/compat.c | 4 +-
net/core/scm.c | 42 ++-
net/unix/af_unix.c | 9 +
.../testing/selftests/net/af_unix/.gitignore | 2 +
tools/testing/selftests/net/af_unix/Makefile | 8 +
.../net/af_unix/scm_rights_denial_lsm.bpf.c | 36 +++
.../net/af_unix/scm_rights_denial_lsm.c | 263 ++++++++++++++++++
14 files changed, 371 insertions(+), 17 deletions(-)
create mode 100644 tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.bpf.c
create mode 100644 tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.c
base-commit: b73bc9ca3686b78b642fb35dcc1fdf874ecb74a1
--
2.55.0
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH net-next v4 1/3] net: scm: move scm_detach_fds() from common path to scm_recv_unix()
2026-07-05 12:38 [PATCH net-next v4 0/3] net: af_unix: useful handling of LSM denials on SCM_RIGHTS Jori Koolstra
@ 2026-07-05 12:38 ` Jori Koolstra
2026-07-06 18:20 ` Kuniyuki Iwashima
2026-07-05 12:38 ` [PATCH net-next v4 2/3] net: af_unix: useful handling of LSM denials on SCM_RIGHTS Jori Koolstra
2026-07-05 12:38 ` [PATCH net-next v4 3/3] selftest: Add tests for " Jori Koolstra
2 siblings, 1 reply; 8+ messages in thread
From: Jori Koolstra @ 2026-07-05 12:38 UTC (permalink / raw)
To: Christian Brauner, Aleksa Sarai, Kuniyuki Iwashima,
David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman
Cc: netdev, linux-fsdevel, linux-kernel, Jori Koolstra
scm->fp can only be set when using UNIX sockets, therefore we should
move it out of the common path __scm_recv_common() into
scm_recv_unix().
Signed-off-by: Jori Koolstra <jkoolstra@xs4all.nl>
---
net/core/scm.c | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/net/core/scm.c b/net/core/scm.c
index eec13f50ecaf..a73b1eb30fd2 100644
--- a/net/core/scm.c
+++ b/net/core/scm.c
@@ -523,9 +523,6 @@ static bool __scm_recv_common(struct sock *sk, struct msghdr *msg,
scm_passec(sk, msg, scm);
- if (scm->fp)
- scm_detach_fds(msg, scm);
-
return true;
}
@@ -545,6 +542,9 @@ void scm_recv_unix(struct socket *sock, struct msghdr *msg,
if (!__scm_recv_common(sock->sk, msg, scm, flags))
return;
+ if (scm->fp)
+ scm_detach_fds(msg, scm);
+
if (sock->sk->sk_scm_pidfd)
scm_pidfd_recv(msg, scm);
--
2.55.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH net-next v4 2/3] net: af_unix: useful handling of LSM denials on SCM_RIGHTS
2026-07-05 12:38 [PATCH net-next v4 0/3] net: af_unix: useful handling of LSM denials on SCM_RIGHTS Jori Koolstra
2026-07-05 12:38 ` [PATCH net-next v4 1/3] net: scm: move scm_detach_fds() from common path to scm_recv_unix() Jori Koolstra
@ 2026-07-05 12:38 ` Jori Koolstra
2026-07-07 11:02 ` Christian Brauner
2026-07-05 12:38 ` [PATCH net-next v4 3/3] selftest: Add tests for " Jori Koolstra
2 siblings, 1 reply; 8+ messages in thread
From: Jori Koolstra @ 2026-07-05 12:38 UTC (permalink / raw)
To: Christian Brauner, Aleksa Sarai, Kuniyuki Iwashima,
David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman
Cc: netdev, linux-fsdevel, linux-kernel, Jori Koolstra
Right now if some LSM such as Smack denies an AF_UNIX socket peer to
receive an SCM_RIGHTS fd, the SCM_RIGHTS fd array will be cut short at
that point, and MSG_CTRUNC is set on return of recvmsg(). This is
highly problematic behaviour, because it leaves the receiver
wondering what happened. As per man page MSG_CTRUNC is supposed to
indicate that the control buffer was sized too short, but suddenly
a permission error might result in the exact same flag being set.
Moreover, the receiver has no chance to determine how many fds got
originally sent and how many were suppressed.[1]
Add a SO_RIGHTS_NOTRUNC option to UNIX sockets to enable more useful
handling of LSM denials when receiving SCM_RIGHTS messages: instead of
truncating the message at the first blocked fd, keep every fd slot
and store the LSM errno in the blocked slot.
[1]: https://github.com/uapi-group/kernel-features#useful-handling-of-lsm-denials-on-scm_rights
Signed-off-by: Jori Koolstra <jkoolstra@xs4all.nl>
---
arch/alpha/include/uapi/asm/socket.h | 2 ++
arch/mips/include/uapi/asm/socket.h | 2 ++
arch/parisc/include/uapi/asm/socket.h | 2 ++
arch/sparc/include/uapi/asm/socket.h | 2 ++
include/net/af_unix.h | 1 +
include/net/scm.h | 13 +++------
include/uapi/asm-generic/socket.h | 2 ++
net/compat.c | 4 +--
net/core/scm.c | 40 +++++++++++++++++++++++----
net/unix/af_unix.c | 9 ++++++
10 files changed, 61 insertions(+), 16 deletions(-)
diff --git a/arch/alpha/include/uapi/asm/socket.h b/arch/alpha/include/uapi/asm/socket.h
index 5ef57f88df6b..946a5fad2691 100644
--- a/arch/alpha/include/uapi/asm/socket.h
+++ b/arch/alpha/include/uapi/asm/socket.h
@@ -155,6 +155,8 @@
#define SO_INQ 84
#define SCM_INQ SO_INQ
+#define SO_RIGHTS_NOTRUNC 85
+
#if !defined(__KERNEL__)
#if __BITS_PER_LONG == 64
diff --git a/arch/mips/include/uapi/asm/socket.h b/arch/mips/include/uapi/asm/socket.h
index 72fb1b006da9..f1641dde135f 100644
--- a/arch/mips/include/uapi/asm/socket.h
+++ b/arch/mips/include/uapi/asm/socket.h
@@ -166,6 +166,8 @@
#define SO_INQ 84
#define SCM_INQ SO_INQ
+#define SO_RIGHTS_NOTRUNC 85
+
#if !defined(__KERNEL__)
#if __BITS_PER_LONG == 64
diff --git a/arch/parisc/include/uapi/asm/socket.h b/arch/parisc/include/uapi/asm/socket.h
index c16ec36dfee6..f3a3815c7dc2 100644
--- a/arch/parisc/include/uapi/asm/socket.h
+++ b/arch/parisc/include/uapi/asm/socket.h
@@ -147,6 +147,8 @@
#define SO_INQ 0x4052
#define SCM_INQ SO_INQ
+#define SO_RIGHTS_NOTRUNC 0x4053
+
#if !defined(__KERNEL__)
#if __BITS_PER_LONG == 64
diff --git a/arch/sparc/include/uapi/asm/socket.h b/arch/sparc/include/uapi/asm/socket.h
index 71befa109e1c..7907f3b1f0ee 100644
--- a/arch/sparc/include/uapi/asm/socket.h
+++ b/arch/sparc/include/uapi/asm/socket.h
@@ -148,6 +148,8 @@
#define SO_INQ 0x005d
#define SCM_INQ SO_INQ
+#define SO_RIGHTS_NOTRUNC 0x005e
+
#if !defined(__KERNEL__)
diff --git a/include/net/af_unix.h b/include/net/af_unix.h
index 34f53dde65ce..bb1b3dee02e8 100644
--- a/include/net/af_unix.h
+++ b/include/net/af_unix.h
@@ -49,6 +49,7 @@ struct unix_sock {
struct scm_stat scm_stat;
int inq_len;
bool recvmsg_inq;
+ bool scm_rights_notrunc;
#if IS_ENABLED(CONFIG_AF_UNIX_OOB)
struct sk_buff *oob_skb;
#endif
diff --git a/include/net/scm.h b/include/net/scm.h
index c52519669349..86ae6bc109ec 100644
--- a/include/net/scm.h
+++ b/include/net/scm.h
@@ -50,8 +50,8 @@ struct scm_cookie {
#endif
};
-void scm_detach_fds(struct msghdr *msg, struct scm_cookie *scm);
-void scm_detach_fds_compat(struct msghdr *msg, struct scm_cookie *scm);
+void scm_detach_fds(struct msghdr *msg, struct scm_cookie *scm, bool notrunc);
+void scm_detach_fds_compat(struct msghdr *msg, struct scm_cookie *scm, bool notrunc);
int __scm_send(struct socket *sock, struct msghdr *msg, struct scm_cookie *scm);
void __scm_destroy(struct scm_cookie *scm);
struct scm_fp_list *scm_fp_dup(struct scm_fp_list *fpl);
@@ -107,13 +107,8 @@ void scm_recv(struct socket *sock, struct msghdr *msg,
void scm_recv_unix(struct socket *sock, struct msghdr *msg,
struct scm_cookie *scm, int flags);
-static inline int scm_recv_one_fd(struct file *f, int __user *ufd,
- unsigned int flags)
-{
- if (!ufd)
- return -EFAULT;
- return receive_fd(f, ufd, flags);
-}
+int scm_recv_one_fd(struct file *f, int __user *ufd, unsigned int flags,
+ bool notrunc);
#endif /* __LINUX_NET_SCM_H */
diff --git a/include/uapi/asm-generic/socket.h b/include/uapi/asm-generic/socket.h
index 53b5a8c002b1..84ea7b92936e 100644
--- a/include/uapi/asm-generic/socket.h
+++ b/include/uapi/asm-generic/socket.h
@@ -150,6 +150,8 @@
#define SO_INQ 84
#define SCM_INQ SO_INQ
+#define SO_RIGHTS_NOTRUNC 85
+
#if !defined(__KERNEL__)
#if __BITS_PER_LONG == 64 || (defined(__x86_64__) && defined(__ILP32__))
diff --git a/net/compat.c b/net/compat.c
index d68cf9c3aad5..6bdf4a2c9077 100644
--- a/net/compat.c
+++ b/net/compat.c
@@ -286,7 +286,7 @@ static int scm_max_fds_compat(struct msghdr *msg)
return (msg->msg_controllen - sizeof(struct compat_cmsghdr)) / sizeof(int);
}
-void scm_detach_fds_compat(struct msghdr *msg, struct scm_cookie *scm)
+void scm_detach_fds_compat(struct msghdr *msg, struct scm_cookie *scm, bool notrunc)
{
struct compat_cmsghdr __user *cm =
(struct compat_cmsghdr __user *)msg->msg_control_user;
@@ -296,7 +296,7 @@ void scm_detach_fds_compat(struct msghdr *msg, struct scm_cookie *scm)
int err = 0, i;
for (i = 0; i < fdmax; i++) {
- err = scm_recv_one_fd(scm->fp->fp[i], cmsg_data + i, o_flags);
+ err = scm_recv_one_fd(scm->fp->fp[i], cmsg_data + i, o_flags, notrunc);
if (err < 0)
break;
}
diff --git a/net/core/scm.c b/net/core/scm.c
index a73b1eb30fd2..9454d06ce5ed 100644
--- a/net/core/scm.c
+++ b/net/core/scm.c
@@ -351,7 +351,31 @@ static int scm_max_fds(struct msghdr *msg)
return (msg->msg_controllen - sizeof(struct cmsghdr)) / sizeof(int);
}
-void scm_detach_fds(struct msghdr *msg, struct scm_cookie *scm)
+int scm_recv_one_fd(struct file *f, int __user *ufd, unsigned int flags,
+ bool notrunc)
+{
+ int error;
+
+ if (!ufd)
+ return -EFAULT;
+
+ error = security_file_receive(f);
+ if (error)
+ return notrunc ? put_user(error, ufd) : error;
+
+ FD_PREPARE(fdf, flags, get_file(f));
+ if (fdf.err)
+ return fdf.err;
+
+ error = put_user(fd_prepare_fd(fdf), ufd);
+ if (error)
+ return error;
+
+ __receive_sock(fd_prepare_file(fdf));
+ return fd_publish(fdf);
+}
+
+void scm_detach_fds(struct msghdr *msg, struct scm_cookie *scm, bool notrunc)
{
struct cmsghdr __user *cm =
(__force struct cmsghdr __user *)msg->msg_control_user;
@@ -365,12 +389,12 @@ void scm_detach_fds(struct msghdr *msg, struct scm_cookie *scm)
return;
if (msg->msg_flags & MSG_CMSG_COMPAT) {
- scm_detach_fds_compat(msg, scm);
+ scm_detach_fds_compat(msg, scm, notrunc);
return;
}
for (i = 0; i < fdmax; i++) {
- err = scm_recv_one_fd(scm->fp->fp[i], cmsg_data + i, o_flags);
+ err = scm_recv_one_fd(scm->fp->fp[i], cmsg_data + i, o_flags, notrunc);
if (err < 0)
break;
}
@@ -542,8 +566,14 @@ void scm_recv_unix(struct socket *sock, struct msghdr *msg,
if (!__scm_recv_common(sock->sk, msg, scm, flags))
return;
- if (scm->fp)
- scm_detach_fds(msg, scm);
+ if (scm->fp) {
+ struct unix_sock *u;
+ bool notrunc;
+
+ u = unix_sk(sock->sk);
+ notrunc = READ_ONCE(u->scm_rights_notrunc);
+ scm_detach_fds(msg, scm, notrunc);
+ }
if (sock->sk->sk_scm_pidfd)
scm_pidfd_recv(msg, scm);
diff --git a/net/unix/af_unix.c b/net/unix/af_unix.c
index f7a9d55eee8a..83274ce18e06 100644
--- a/net/unix/af_unix.c
+++ b/net/unix/af_unix.c
@@ -921,6 +921,7 @@ static bool unix_custom_sockopt(int optname)
{
switch (optname) {
case SO_INQ:
+ case SO_RIGHTS_NOTRUNC:
return true;
default:
return false;
@@ -956,6 +957,14 @@ static int unix_setsockopt(struct socket *sock, int level, int optname,
WRITE_ONCE(u->recvmsg_inq, val);
break;
+
+ case SO_RIGHTS_NOTRUNC:
+ if (val > 1 || val < 0)
+ return -EINVAL;
+
+ WRITE_ONCE(u->scm_rights_notrunc, val);
+ break;
+
default:
return -ENOPROTOOPT;
}
--
2.55.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH net-next v4 3/3] selftest: Add tests for useful handling of LSM denials on SCM_RIGHTS
2026-07-05 12:38 [PATCH net-next v4 0/3] net: af_unix: useful handling of LSM denials on SCM_RIGHTS Jori Koolstra
2026-07-05 12:38 ` [PATCH net-next v4 1/3] net: scm: move scm_detach_fds() from common path to scm_recv_unix() Jori Koolstra
2026-07-05 12:38 ` [PATCH net-next v4 2/3] net: af_unix: useful handling of LSM denials on SCM_RIGHTS Jori Koolstra
@ 2026-07-05 12:38 ` Jori Koolstra
2026-07-06 23:17 ` Kuniyuki Iwashima
2026-07-07 11:02 ` Christian Brauner
2 siblings, 2 replies; 8+ messages in thread
From: Jori Koolstra @ 2026-07-05 12:38 UTC (permalink / raw)
To: Christian Brauner, Aleksa Sarai, Kuniyuki Iwashima,
David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman
Cc: netdev, linux-fsdevel, linux-kernel, Jori Koolstra
Tests SCM_RIGHTS fd passing on a socket with the new socket option
SO_RIGHTS_NOTRUNC turned on. To hook into the security_file_receive()
call, BPF is used. The BPF program shares a hashmap with userspace that
lists the inos to be blocked (of the receiver tgid).
Signed-off-by: Jori Koolstra <jkoolstra@xs4all.nl>
Assisted-by: LLM # used LLM to get skeleton BPF and libbpf code
---
.../testing/selftests/net/af_unix/.gitignore | 2 +
tools/testing/selftests/net/af_unix/Makefile | 8 +
.../net/af_unix/scm_rights_denial_lsm.bpf.c | 36 +++
.../net/af_unix/scm_rights_denial_lsm.c | 263 ++++++++++++++++++
4 files changed, 309 insertions(+)
create mode 100644 tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.bpf.c
create mode 100644 tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.c
diff --git a/tools/testing/selftests/net/af_unix/.gitignore b/tools/testing/selftests/net/af_unix/.gitignore
index 240b26740c9e..5034482f8864 100644
--- a/tools/testing/selftests/net/af_unix/.gitignore
+++ b/tools/testing/selftests/net/af_unix/.gitignore
@@ -3,6 +3,8 @@ msg_oob
scm_inq
scm_pidfd
scm_rights
+scm_rights_denial_lsm
+scm_rights_denial_lsm.bpf.o
so_peek_off
unix_connect
unix_connreset
diff --git a/tools/testing/selftests/net/af_unix/Makefile b/tools/testing/selftests/net/af_unix/Makefile
index 4c0375e28bbe..594cd26ec398 100644
--- a/tools/testing/selftests/net/af_unix/Makefile
+++ b/tools/testing/selftests/net/af_unix/Makefile
@@ -11,9 +11,17 @@ TEST_GEN_PROGS := \
scm_inq \
scm_pidfd \
scm_rights \
+ scm_rights_denial_lsm \
so_peek_off \
unix_connect \
unix_connreset \
# end of TEST_GEN_PROGS
+TEST_GEN_FILES := scm_rights_denial_lsm.bpf.o
+
include ../../lib.mk
+include ../bpf.mk
+
+$(OUTPUT)/scm_rights_denial_lsm: $(BPFOBJ)
+$(OUTPUT)/scm_rights_denial_lsm: CFLAGS += -I$(SCRATCH_DIR)/include
+$(OUTPUT)/scm_rights_denial_lsm: LDLIBS += -lelf -lz
diff --git a/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.bpf.c b/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.bpf.c
new file mode 100644
index 000000000000..4f2414465bfd
--- /dev/null
+++ b/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.bpf.c
@@ -0,0 +1,36 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <linux/bpf.h>
+#include <linux/errno.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+
+char _license[] SEC("license") = "GPL";
+
+struct inode {
+ unsigned long i_ino;
+} __attribute__((preserve_access_index));
+
+struct file {
+ struct inode *f_inode;
+} __attribute__((preserve_access_index));
+
+struct {
+ __uint(type, BPF_MAP_TYPE_HASH);
+ __uint(max_entries, 16);
+ __type(key, __u64); /* inode number */
+ __type(value, __u32); /* tgid of the receiver being tested */
+} denied_inodes SEC(".maps");
+
+SEC("lsm/file_receive")
+int BPF_PROG(scm_rights_deny, struct file *file)
+{
+ __u32 tgid = bpf_get_current_pid_tgid() >> 32;
+ __u64 ino = file->f_inode->i_ino;
+ __u32 *owner;
+
+ owner = bpf_map_lookup_elem(&denied_inodes, &ino);
+ if (owner && *owner == tgid)
+ return -EPERM;
+
+ return 0;
+}
diff --git a/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.c b/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.c
new file mode 100644
index 000000000000..b8656de86efe
--- /dev/null
+++ b/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.c
@@ -0,0 +1,263 @@
+// SPDX-License-Identifier: GPL-2.0
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <bpf/bpf.h>
+#include <bpf/libbpf.h>
+
+#include "kselftest_harness.h"
+
+#ifndef SO_RIGHTS_NOTRUNC
+#define SO_RIGHTS_NOTRUNC 85
+#endif
+
+#define NR_FILES 2
+
+/* Per-file content, so a received fd can be matched to the file sent */
+#define SECRET(n) "secret %d", (n)
+
+/* Indices into the socketpair */
+#define SK_SENDER 0
+#define SK_RECEIVER 1
+
+FIXTURE(scm_rights_denial_bpf)
+{
+ struct bpf_object *obj;
+ struct bpf_link *link;
+ int map_fd;
+ int sk[2];
+ int files[NR_FILES];
+ __u64 inos[NR_FILES];
+ char paths[NR_FILES][64];
+};
+
+FIXTURE_SETUP(scm_rights_denial_bpf)
+{
+ struct bpf_program *prog;
+ char lsms[256] = {};
+ int i, fd;
+
+ if (geteuid() != 0)
+ SKIP(return, "requires root");
+
+ fd = open("/sys/kernel/security/lsm", O_RDONLY);
+ ASSERT_LE(0, fd);
+ ASSERT_LT(0, read(fd, lsms, sizeof(lsms) - 1));
+ close(fd);
+
+ if (!strstr(lsms, "bpf"))
+ SKIP(return, "BPF LSM not active (boot with lsm=...,bpf)");
+
+ self->obj = bpf_object__open_file("scm_rights_denial_lsm.bpf.o", NULL);
+ ASSERT_NE(NULL, self->obj);
+ ASSERT_EQ(0, bpf_object__load(self->obj));
+
+ prog = bpf_object__find_program_by_name(self->obj, "scm_rights_deny");
+ ASSERT_NE(NULL, prog);
+
+ self->link = bpf_program__attach_lsm(prog);
+ ASSERT_NE(NULL, self->link);
+
+ self->map_fd = bpf_object__find_map_fd_by_name(self->obj,
+ "denied_inodes");
+ ASSERT_LE(0, self->map_fd);
+
+ ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, self->sk));
+
+ for (i = 0; i < NR_FILES; i++) {
+ struct stat st;
+
+ snprintf(self->paths[i], sizeof(self->paths[i]),
+ "/tmp/scm_rights_denial_bpf.%d.XXXXXX", i);
+ self->files[i] = mkstemp(self->paths[i]);
+ ASSERT_LE(0, self->files[i]);
+
+ ASSERT_LT(0, dprintf(self->files[i], SECRET(i)));
+
+ ASSERT_EQ(0, fstat(self->files[i], &st));
+ self->inos[i] = st.st_ino;
+ }
+}
+
+FIXTURE_TEARDOWN(scm_rights_denial_bpf)
+{
+ bpf_link__destroy(self->link);
+ bpf_object__close(self->obj);
+
+ for (int i = 0; i < NR_FILES; i++) {
+ if (self->files[i] >= 0) {
+ close(self->files[i]);
+ unlink(self->paths[i]);
+ }
+ }
+
+ close(self->sk[SK_SENDER]);
+ close(self->sk[SK_RECEIVER]);
+}
+
+static int deny_inode(int map_fd, __u64 ino)
+{
+ __u32 tgid = getpid();
+ return bpf_map_update_elem(map_fd, &ino, &tgid, BPF_ANY);
+}
+
+static int set_notrunc(int sk)
+{
+ int one = 1;
+ return setsockopt(sk, SOL_SOCKET, SO_RIGHTS_NOTRUNC,
+ &one, sizeof(one));
+}
+
+static int send_fds(int sk, int *fds, int n)
+{
+ char ctrl[CMSG_SPACE(NR_FILES * sizeof(int))] = {};
+ char data = 'x';
+ struct iovec iov = {
+ .iov_base = &data,
+ .iov_len = sizeof(data),
+ };
+ struct msghdr msg = {
+ .msg_iov = &iov,
+ .msg_iovlen = 1,
+ .msg_control = ctrl,
+ .msg_controllen = CMSG_SPACE(n * sizeof(int)),
+ };
+ struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
+
+ cmsg->cmsg_level = SOL_SOCKET;
+ cmsg->cmsg_type = SCM_RIGHTS;
+ cmsg->cmsg_len = CMSG_LEN(n * sizeof(int));
+ memcpy(CMSG_DATA(cmsg), fds, n * sizeof(int));
+
+ return sendmsg(sk, &msg, 0);
+}
+
+static int recv_fd_slots(int sk, int *slots, int *msg_flags)
+{
+ int nr_slots;
+ char ctrl[CMSG_SPACE(NR_FILES * sizeof(int))];
+ char data;
+ struct iovec iov = {
+ .iov_base = &data,
+ .iov_len = sizeof(data),
+ };
+ struct msghdr msg = {
+ .msg_iov = &iov,
+ .msg_iovlen = 1,
+ .msg_control = ctrl,
+ .msg_controllen = sizeof(ctrl),
+ };
+ struct cmsghdr *cmsg;
+
+ if (recvmsg(sk, &msg, 0) < 0)
+ return -1;
+
+ *msg_flags = msg.msg_flags;
+
+ cmsg = CMSG_FIRSTHDR(&msg);
+ if (!cmsg)
+ return 0;
+
+ nr_slots = (cmsg->cmsg_len - CMSG_LEN(0)) / sizeof(int);
+ memcpy(slots, CMSG_DATA(cmsg), nr_slots * sizeof(int));
+
+ return nr_slots;
+}
+
+/* Prove a received fd works by reading back the file's content. */
+static int check_secret(int fd, int idx)
+{
+ char want[32], got[32] = {};
+
+ snprintf(want, sizeof(want), SECRET(idx));
+ if (pread(fd, got, sizeof(got) - 1, 0) < 0)
+ return -1;
+
+ return strcmp(want, got);
+}
+
+TEST_F(scm_rights_denial_bpf, all_allowed)
+{
+ int slots[NR_FILES], nr_slots, flags, i;
+
+ ASSERT_EQ(0, set_notrunc(self->sk[SK_RECEIVER]));
+ ASSERT_NE(-1, send_fds(self->sk[SK_SENDER], self->files, NR_FILES));
+ nr_slots = recv_fd_slots(self->sk[SK_RECEIVER], slots, &flags);
+
+ ASSERT_EQ(NR_FILES, nr_slots);
+ EXPECT_EQ(0, flags & MSG_CTRUNC);
+
+ for (i = 0; i < NR_FILES; i++) {
+ ASSERT_LE(0, slots[i]);
+ EXPECT_EQ(0, check_secret(slots[i], i));
+ close(slots[i]);
+ }
+}
+
+TEST_F(scm_rights_denial_bpf, first_denied)
+{
+ int slots[NR_FILES], nr_slots, flags;
+
+ ASSERT_EQ(0, deny_inode(self->map_fd, self->inos[0]));
+
+ ASSERT_EQ(0, set_notrunc(self->sk[SK_RECEIVER]));
+ ASSERT_NE(-1, send_fds(self->sk[SK_SENDER], self->files, NR_FILES));
+ nr_slots = recv_fd_slots(self->sk[SK_RECEIVER], slots, &flags);
+
+ ASSERT_EQ(NR_FILES, nr_slots);
+ EXPECT_EQ(0, flags & MSG_CTRUNC);
+ EXPECT_EQ(-EPERM, slots[0]);
+
+ ASSERT_LE(0, slots[1]);
+ EXPECT_EQ(0, check_secret(slots[1], 1));
+ close(slots[1]);
+}
+
+TEST_F(scm_rights_denial_bpf, all_denied)
+{
+ int slots[NR_FILES], nr_slots, flags, i;
+
+ for (i = 0; i < NR_FILES; i++)
+ ASSERT_EQ(0, deny_inode(self->map_fd, self->inos[i]));
+
+ ASSERT_EQ(0, set_notrunc(self->sk[SK_RECEIVER]));
+ ASSERT_NE(-1, send_fds(self->sk[SK_SENDER], self->files, NR_FILES));
+ nr_slots = recv_fd_slots(self->sk[SK_RECEIVER], slots, &flags);
+
+ ASSERT_EQ(NR_FILES, nr_slots);
+ EXPECT_EQ(0, flags & MSG_CTRUNC);
+
+ for (i = 0; i < NR_FILES; i++)
+ EXPECT_EQ(-EPERM, slots[i]);
+}
+
+TEST_F(scm_rights_denial_bpf, denied_without_notrunc)
+{
+ int slots[NR_FILES], nr_slots, flags;
+
+ /*
+ * Baseline behaviour without SO_RIGHTS_NOTRUNC: the fd array is
+ * truncated at the first denied fd and MSG_CTRUNC is set.
+ */
+ ASSERT_EQ(0, deny_inode(self->map_fd, self->inos[1]));
+
+ ASSERT_NE(-1, send_fds(self->sk[SK_SENDER], self->files, NR_FILES));
+ nr_slots = recv_fd_slots(self->sk[SK_RECEIVER], slots, &flags);
+
+ ASSERT_EQ(1, nr_slots);
+ EXPECT_NE(0, flags & MSG_CTRUNC);
+
+ ASSERT_LE(0, slots[0]);
+ EXPECT_EQ(0, check_secret(slots[0], 0));
+ close(slots[0]);
+}
+
+TEST_HARNESS_MAIN
--
2.55.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* Re: [PATCH net-next v4 1/3] net: scm: move scm_detach_fds() from common path to scm_recv_unix()
2026-07-05 12:38 ` [PATCH net-next v4 1/3] net: scm: move scm_detach_fds() from common path to scm_recv_unix() Jori Koolstra
@ 2026-07-06 18:20 ` Kuniyuki Iwashima
0 siblings, 0 replies; 8+ messages in thread
From: Kuniyuki Iwashima @ 2026-07-06 18:20 UTC (permalink / raw)
To: Jori Koolstra
Cc: Christian Brauner, Aleksa Sarai, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, netdev, linux-fsdevel,
linux-kernel
On Sun, Jul 5, 2026 at 5:37 AM Jori Koolstra <jkoolstra@xs4all.nl> wrote:
>
> scm->fp can only be set when using UNIX sockets, therefore we should
> move it out of the common path __scm_recv_common() into
> scm_recv_unix().
>
> Signed-off-by: Jori Koolstra <jkoolstra@xs4all.nl>
Reviewed-by: Kuniyuki Iwashima <kuniyu@google.com>
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH net-next v4 3/3] selftest: Add tests for useful handling of LSM denials on SCM_RIGHTS
2026-07-05 12:38 ` [PATCH net-next v4 3/3] selftest: Add tests for " Jori Koolstra
@ 2026-07-06 23:17 ` Kuniyuki Iwashima
2026-07-07 11:02 ` Christian Brauner
1 sibling, 0 replies; 8+ messages in thread
From: Kuniyuki Iwashima @ 2026-07-06 23:17 UTC (permalink / raw)
To: Jori Koolstra
Cc: Christian Brauner, Aleksa Sarai, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, netdev, linux-fsdevel,
linux-kernel
On Sun, Jul 5, 2026 at 5:37 AM Jori Koolstra <jkoolstra@xs4all.nl> wrote:
>
> Tests SCM_RIGHTS fd passing on a socket with the new socket option
> SO_RIGHTS_NOTRUNC turned on. To hook into the security_file_receive()
> call, BPF is used. The BPF program shares a hashmap with userspace that
> lists the inos to be blocked (of the receiver tgid).
>
> Signed-off-by: Jori Koolstra <jkoolstra@xs4all.nl>
> Assisted-by: LLM # used LLM to get skeleton BPF and libbpf code
> ---
> .../testing/selftests/net/af_unix/.gitignore | 2 +
> tools/testing/selftests/net/af_unix/Makefile | 8 +
> .../net/af_unix/scm_rights_denial_lsm.bpf.c | 36 +++
> .../net/af_unix/scm_rights_denial_lsm.c | 263 ++++++++++++++++++
> 4 files changed, 309 insertions(+)
> create mode 100644 tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.bpf.c
> create mode 100644 tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.c
>
> diff --git a/tools/testing/selftests/net/af_unix/.gitignore b/tools/testing/selftests/net/af_unix/.gitignore
> index 240b26740c9e..5034482f8864 100644
> --- a/tools/testing/selftests/net/af_unix/.gitignore
> +++ b/tools/testing/selftests/net/af_unix/.gitignore
> @@ -3,6 +3,8 @@ msg_oob
> scm_inq
> scm_pidfd
> scm_rights
> +scm_rights_denial_lsm
> +scm_rights_denial_lsm.bpf.o
> so_peek_off
> unix_connect
> unix_connreset
> diff --git a/tools/testing/selftests/net/af_unix/Makefile b/tools/testing/selftests/net/af_unix/Makefile
> index 4c0375e28bbe..594cd26ec398 100644
> --- a/tools/testing/selftests/net/af_unix/Makefile
> +++ b/tools/testing/selftests/net/af_unix/Makefile
> @@ -11,9 +11,17 @@ TEST_GEN_PROGS := \
> scm_inq \
> scm_pidfd \
> scm_rights \
> + scm_rights_denial_lsm \
> so_peek_off \
> unix_connect \
> unix_connreset \
> # end of TEST_GEN_PROGS
>
> +TEST_GEN_FILES := scm_rights_denial_lsm.bpf.o
> +
> include ../../lib.mk
> +include ../bpf.mk
> +
> +$(OUTPUT)/scm_rights_denial_lsm: $(BPFOBJ)
> +$(OUTPUT)/scm_rights_denial_lsm: CFLAGS += -I$(SCRATCH_DIR)/include
> +$(OUTPUT)/scm_rights_denial_lsm: LDLIBS += -lelf -lz
> diff --git a/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.bpf.c b/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.bpf.c
> new file mode 100644
> index 000000000000..4f2414465bfd
> --- /dev/null
> +++ b/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.bpf.c
> @@ -0,0 +1,36 @@
> +// SPDX-License-Identifier: GPL-2.0
> +#include <linux/bpf.h>
> +#include <linux/errno.h>
> +#include <bpf/bpf_helpers.h>
> +#include <bpf/bpf_tracing.h>
> +
> +char _license[] SEC("license") = "GPL";
> +
> +struct inode {
> + unsigned long i_ino;
> +} __attribute__((preserve_access_index));
> +
> +struct file {
> + struct inode *f_inode;
> +} __attribute__((preserve_access_index));
> +
> +struct {
> + __uint(type, BPF_MAP_TYPE_HASH);
> + __uint(max_entries, 16);
> + __type(key, __u64); /* inode number */
> + __type(value, __u32); /* tgid of the receiver being tested */
> +} denied_inodes SEC(".maps");
> +
> +SEC("lsm/file_receive")
> +int BPF_PROG(scm_rights_deny, struct file *file)
> +{
> + __u32 tgid = bpf_get_current_pid_tgid() >> 32;
> + __u64 ino = file->f_inode->i_ino;
> + __u32 *owner;
> +
> + owner = bpf_map_lookup_elem(&denied_inodes, &ino);
> + if (owner && *owner == tgid)
> + return -EPERM;
> +
> + return 0;
> +}
> diff --git a/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.c b/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.c
> new file mode 100644
> index 000000000000..b8656de86efe
> --- /dev/null
> +++ b/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.c
> @@ -0,0 +1,263 @@
> +// SPDX-License-Identifier: GPL-2.0
> +#define _GNU_SOURCE
> +#include <errno.h>
> +#include <fcntl.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <unistd.h>
> +#include <sys/socket.h>
> +#include <sys/stat.h>
> +#include <sys/types.h>
> +
> +#include <bpf/bpf.h>
> +#include <bpf/libbpf.h>
> +
> +#include "kselftest_harness.h"
> +
> +#ifndef SO_RIGHTS_NOTRUNC
> +#define SO_RIGHTS_NOTRUNC 85
> +#endif
> +
> +#define NR_FILES 2
> +
> +/* Per-file content, so a received fd can be matched to the file sent */
> +#define SECRET(n) "secret %d", (n)
> +
> +/* Indices into the socketpair */
> +#define SK_SENDER 0
> +#define SK_RECEIVER 1
> +
> +FIXTURE(scm_rights_denial_bpf)
> +{
> + struct bpf_object *obj;
> + struct bpf_link *link;
> + int map_fd;
> + int sk[2];
> + int files[NR_FILES];
> + __u64 inos[NR_FILES];
> + char paths[NR_FILES][64];
> +};
Could you add FIXTURE_VARIANT() and cover all types ?
SOCK_STREAM, SOCK_DGRAM, SOCK_SEQPACKET
> +
> +FIXTURE_SETUP(scm_rights_denial_bpf)
> +{
> + struct bpf_program *prog;
> + char lsms[256] = {};
> + int i, fd;
> +
> + if (geteuid() != 0)
> + SKIP(return, "requires root");
> +
> + fd = open("/sys/kernel/security/lsm", O_RDONLY);
> + ASSERT_LE(0, fd);
> + ASSERT_LT(0, read(fd, lsms, sizeof(lsms) - 1));
This style is easier to follow the logic for me.
err = func();
ASSERT_LT(val, err);
> + close(fd);
> +
> + if (!strstr(lsms, "bpf"))
> + SKIP(return, "BPF LSM not active (boot with lsm=...,bpf)");
Could you add minimal relevant configs (CONFIG_BPF_LSM etc)
to tools/testing/selftests/net/af_unix/config ?
NIPA (netdev CI) uses it as the base config.
> +
> + self->obj = bpf_object__open_file("scm_rights_denial_lsm.bpf.o", NULL);
> + ASSERT_NE(NULL, self->obj);
> + ASSERT_EQ(0, bpf_object__load(self->obj));
> +
> + prog = bpf_object__find_program_by_name(self->obj, "scm_rights_deny");
> + ASSERT_NE(NULL, prog);
> +
> + self->link = bpf_program__attach_lsm(prog);
> + ASSERT_NE(NULL, self->link);
> +
> + self->map_fd = bpf_object__find_map_fd_by_name(self->obj,
> + "denied_inodes");
> + ASSERT_LE(0, self->map_fd);
> +
> + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, self->sk));
> +
> + for (i = 0; i < NR_FILES; i++) {
> + struct stat st;
> +
> + snprintf(self->paths[i], sizeof(self->paths[i]),
> + "/tmp/scm_rights_denial_bpf.%d.XXXXXX", i);
> + self->files[i] = mkstemp(self->paths[i]);
> + ASSERT_LE(0, self->files[i]);
> +
> + ASSERT_LT(0, dprintf(self->files[i], SECRET(i)));
> +
> + ASSERT_EQ(0, fstat(self->files[i], &st));
> + self->inos[i] = st.st_ino;
> + }
> +}
> +
> +FIXTURE_TEARDOWN(scm_rights_denial_bpf)
> +{
> + bpf_link__destroy(self->link);
> + bpf_object__close(self->obj);
> +
> + for (int i = 0; i < NR_FILES; i++) {
> + if (self->files[i] >= 0) {
> + close(self->files[i]);
> + unlink(self->paths[i]);
> + }
> + }
> +
> + close(self->sk[SK_SENDER]);
> + close(self->sk[SK_RECEIVER]);
> +}
> +
> +static int deny_inode(int map_fd, __u64 ino)
> +{
> + __u32 tgid = getpid();
nit: newline here
> + return bpf_map_update_elem(map_fd, &ino, &tgid, BPF_ANY);
> +}
> +
> +static int set_notrunc(int sk)
> +{
> + int one = 1;
nit: ditto.
> + return setsockopt(sk, SOL_SOCKET, SO_RIGHTS_NOTRUNC,
> + &one, sizeof(one));
> +}
> +
> +static int send_fds(int sk, int *fds, int n)
> +{
> + char ctrl[CMSG_SPACE(NR_FILES * sizeof(int))] = {};
> + char data = 'x';
> + struct iovec iov = {
> + .iov_base = &data,
> + .iov_len = sizeof(data),
> + };
> + struct msghdr msg = {
> + .msg_iov = &iov,
> + .msg_iovlen = 1,
> + .msg_control = ctrl,
> + .msg_controllen = CMSG_SPACE(n * sizeof(int)),
> + };
> + struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
> +
> + cmsg->cmsg_level = SOL_SOCKET;
> + cmsg->cmsg_type = SCM_RIGHTS;
> + cmsg->cmsg_len = CMSG_LEN(n * sizeof(int));
> + memcpy(CMSG_DATA(cmsg), fds, n * sizeof(int));
> +
> + return sendmsg(sk, &msg, 0);
> +}
> +
> +static int recv_fd_slots(int sk, int *slots, int *msg_flags)
> +{
> + int nr_slots;
> + char ctrl[CMSG_SPACE(NR_FILES * sizeof(int))];
> + char data;
> + struct iovec iov = {
> + .iov_base = &data,
> + .iov_len = sizeof(data),
> + };
> + struct msghdr msg = {
> + .msg_iov = &iov,
> + .msg_iovlen = 1,
> + .msg_control = ctrl,
> + .msg_controllen = sizeof(ctrl),
> + };
> + struct cmsghdr *cmsg;
> +
> + if (recvmsg(sk, &msg, 0) < 0)
> + return -1;
> +
> + *msg_flags = msg.msg_flags;
> +
> + cmsg = CMSG_FIRSTHDR(&msg);
> + if (!cmsg)
> + return 0;
> +
> + nr_slots = (cmsg->cmsg_len - CMSG_LEN(0)) / sizeof(int);
> + memcpy(slots, CMSG_DATA(cmsg), nr_slots * sizeof(int));
> +
> + return nr_slots;
> +}
> +
> +/* Prove a received fd works by reading back the file's content. */
> +static int check_secret(int fd, int idx)
> +{
> + char want[32], got[32] = {};
> +
> + snprintf(want, sizeof(want), SECRET(idx));
> + if (pread(fd, got, sizeof(got) - 1, 0) < 0)
> + return -1;
> +
> + return strcmp(want, got);
> +}
> +
> +TEST_F(scm_rights_denial_bpf, all_allowed)
> +{
> + int slots[NR_FILES], nr_slots, flags, i;
> +
> + ASSERT_EQ(0, set_notrunc(self->sk[SK_RECEIVER]));
> + ASSERT_NE(-1, send_fds(self->sk[SK_SENDER], self->files, NR_FILES));
> + nr_slots = recv_fd_slots(self->sk[SK_RECEIVER], slots, &flags);
> +
> + ASSERT_EQ(NR_FILES, nr_slots);
> + EXPECT_EQ(0, flags & MSG_CTRUNC);
> +
> + for (i = 0; i < NR_FILES; i++) {
> + ASSERT_LE(0, slots[i]);
> + EXPECT_EQ(0, check_secret(slots[i], i));
> + close(slots[i]);
> + }
> +}
> +
> +TEST_F(scm_rights_denial_bpf, first_denied)
> +{
> + int slots[NR_FILES], nr_slots, flags;
> +
> + ASSERT_EQ(0, deny_inode(self->map_fd, self->inos[0]));
> +
> + ASSERT_EQ(0, set_notrunc(self->sk[SK_RECEIVER]));
> + ASSERT_NE(-1, send_fds(self->sk[SK_SENDER], self->files, NR_FILES));
> + nr_slots = recv_fd_slots(self->sk[SK_RECEIVER], slots, &flags);
> +
> + ASSERT_EQ(NR_FILES, nr_slots);
> + EXPECT_EQ(0, flags & MSG_CTRUNC);
> + EXPECT_EQ(-EPERM, slots[0]);
> +
> + ASSERT_LE(0, slots[1]);
> + EXPECT_EQ(0, check_secret(slots[1], 1));
> + close(slots[1]);
> +}
> +
> +TEST_F(scm_rights_denial_bpf, all_denied)
> +{
> + int slots[NR_FILES], nr_slots, flags, i;
> +
> + for (i = 0; i < NR_FILES; i++)
> + ASSERT_EQ(0, deny_inode(self->map_fd, self->inos[i]));
> +
> + ASSERT_EQ(0, set_notrunc(self->sk[SK_RECEIVER]));
> + ASSERT_NE(-1, send_fds(self->sk[SK_SENDER], self->files, NR_FILES));
> + nr_slots = recv_fd_slots(self->sk[SK_RECEIVER], slots, &flags);
> +
> + ASSERT_EQ(NR_FILES, nr_slots);
> + EXPECT_EQ(0, flags & MSG_CTRUNC);
> +
> + for (i = 0; i < NR_FILES; i++)
> + EXPECT_EQ(-EPERM, slots[i]);
> +}
> +
> +TEST_F(scm_rights_denial_bpf, denied_without_notrunc)
> +{
> + int slots[NR_FILES], nr_slots, flags;
> +
> + /*
> + * Baseline behaviour without SO_RIGHTS_NOTRUNC: the fd array is
> + * truncated at the first denied fd and MSG_CTRUNC is set.
> + */
> + ASSERT_EQ(0, deny_inode(self->map_fd, self->inos[1]));
> +
> + ASSERT_NE(-1, send_fds(self->sk[SK_SENDER], self->files, NR_FILES));
> + nr_slots = recv_fd_slots(self->sk[SK_RECEIVER], slots, &flags);
> +
> + ASSERT_EQ(1, nr_slots);
> + EXPECT_NE(0, flags & MSG_CTRUNC);
> +
> + ASSERT_LE(0, slots[0]);
> + EXPECT_EQ(0, check_secret(slots[0], 0));
> + close(slots[0]);
> +}
> +
> +TEST_HARNESS_MAIN
> --
> 2.55.0
>
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH net-next v4 2/3] net: af_unix: useful handling of LSM denials on SCM_RIGHTS
2026-07-05 12:38 ` [PATCH net-next v4 2/3] net: af_unix: useful handling of LSM denials on SCM_RIGHTS Jori Koolstra
@ 2026-07-07 11:02 ` Christian Brauner
0 siblings, 0 replies; 8+ messages in thread
From: Christian Brauner @ 2026-07-07 11:02 UTC (permalink / raw)
To: Jori Koolstra
Cc: Christian Brauner, Aleksa Sarai, Kuniyuki Iwashima,
David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, netdev, linux-fsdevel, linux-kernel
> Right now if some LSM such as Smack denies an AF_UNIX socket peer to
> receive an SCM_RIGHTS fd, the SCM_RIGHTS fd array will be cut short at
> that point, and MSG_CTRUNC is set on return of recvmsg(). This is
> highly problematic behaviour, because it leaves the receiver
> wondering what happened. As per man page MSG_CTRUNC is supposed to
> indicate that the control buffer was sized too short, but suddenly
> a permission error might result in the exact same flag being set.
> Moreover, the receiver has no chance to determine how many fds got
> originally sent and how many were suppressed.[1]
>
> Add a SO_RIGHTS_NOTRUNC option to UNIX sockets to enable more useful
> handling of LSM denials when receiving SCM_RIGHTS messages: instead of
> truncating the message at the first blocked fd, keep every fd slot
> and store the LSM errno in the blocked slot.
>
> [1]: https://github.com/uapi-group/kernel-features#useful-handling-of-lsm-denials-on-scm_rights
>
> Signed-off-by: Jori Koolstra <jkoolstra@xs4all.nl>
Reviewed-by: Christian Brauner (Amutable) <brauner@kernel.org>
> -void scm_detach_fds(struct msghdr *msg, struct scm_cookie *scm)
> +int scm_recv_one_fd(struct file *f, int __user *ufd, unsigned int flags,
> + bool notrunc)
> +{
> + int error;
> +
> + if (!ufd)
> + return -EFAULT;
> +
> + error = security_file_receive(f);
> + if (error)
> + return notrunc ? put_user(error, ufd) : error;
> +
> + FD_PREPARE(fdf, flags, get_file(f));
> + if (fdf.err)
> + return fdf.err;
> +
> + error = put_user(fd_prepare_fd(fdf), ufd);
> + if (error)
> + return error;
> +
> + __receive_sock(fd_prepare_file(fdf));
> + return fd_publish(fdf);
> +}
Seems good.
> +
> +void scm_detach_fds(struct msghdr *msg, struct scm_cookie *scm, bool notrunc)
> {
> struct cmsghdr __user *cm =
> (__force struct cmsghdr __user *)msg->msg_control_user;
> @@ -365,12 +389,12 @@ void scm_detach_fds(struct msghdr *msg, struct scm_cookie *scm)
> return;
>
> if (msg->msg_flags & MSG_CMSG_COMPAT) {
> - scm_detach_fds_compat(msg, scm);
> + scm_detach_fds_compat(msg, scm, notrunc);
> return;
> }
>
> for (i = 0; i < fdmax; i++) {
> - err = scm_recv_one_fd(scm->fp->fp[i], cmsg_data + i, o_flags);
> + err = scm_recv_one_fd(scm->fp->fp[i], cmsg_data + i, o_flags, notrunc);
> if (err < 0)
> break;
> }
> @@ -542,8 +566,14 @@ void scm_recv_unix(struct socket *sock, struct msghdr *msg,
> if (!__scm_recv_common(sock->sk, msg, scm, flags))
> return;
>
> - if (scm->fp)
> - scm_detach_fds(msg, scm);
> + if (scm->fp) {
> + struct unix_sock *u;
> + bool notrunc;
> +
> + u = unix_sk(sock->sk);
> + notrunc = READ_ONCE(u->scm_rights_notrunc);
> + scm_detach_fds(msg, scm, notrunc);
Minor nit: Really no need for the boolean. Would be enough to do:
scm_detach_fds(msg, scm, READ_ONCE(u->scm_rights_notrunc));
--
Christian Brauner <brauner@kernel.org>
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH net-next v4 3/3] selftest: Add tests for useful handling of LSM denials on SCM_RIGHTS
2026-07-05 12:38 ` [PATCH net-next v4 3/3] selftest: Add tests for " Jori Koolstra
2026-07-06 23:17 ` Kuniyuki Iwashima
@ 2026-07-07 11:02 ` Christian Brauner
1 sibling, 0 replies; 8+ messages in thread
From: Christian Brauner @ 2026-07-07 11:02 UTC (permalink / raw)
To: Jori Koolstra
Cc: Christian Brauner, Aleksa Sarai, Kuniyuki Iwashima,
David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, netdev, linux-fsdevel, linux-kernel
> Tests SCM_RIGHTS fd passing on a socket with the new socket option
> SO_RIGHTS_NOTRUNC turned on. To hook into the security_file_receive()
> call, BPF is used. The BPF program shares a hashmap with userspace that
> lists the inos to be blocked (of the receiver tgid).
>
> Signed-off-by: Jori Koolstra <jkoolstra@xs4all.nl>
>
> diff --git a/tools/testing/selftests/net/af_unix/.gitignore b/tools/testing/selftests/net/af_unix/.gitignore
> index 240b26740c9e..5034482f8864 100644
> --- a/tools/testing/selftests/net/af_unix/.gitignore
> +++ b/tools/testing/selftests/net/af_unix/.gitignore
> @@ -3,6 +3,8 @@ msg_oob
> scm_inq
> scm_pidfd
> scm_rights
> +scm_rights_denial_lsm
> +scm_rights_denial_lsm.bpf.o
> so_peek_off
> unix_connect
> unix_connreset
> diff --git a/tools/testing/selftests/net/af_unix/Makefile b/tools/testing/selftests/net/af_unix/Makefile
> index 4c0375e28bbe..594cd26ec398 100644
> --- a/tools/testing/selftests/net/af_unix/Makefile
> +++ b/tools/testing/selftests/net/af_unix/Makefile
> @@ -11,9 +11,17 @@ TEST_GEN_PROGS := \
> scm_inq \
> scm_pidfd \
> scm_rights \
> + scm_rights_denial_lsm \
> so_peek_off \
> unix_connect \
> unix_connreset \
> # end of TEST_GEN_PROGS
>
> +TEST_GEN_FILES := scm_rights_denial_lsm.bpf.o
> +
> include ../../lib.mk
> +include ../bpf.mk
> +
> +$(OUTPUT)/scm_rights_denial_lsm: $(BPFOBJ)
> +$(OUTPUT)/scm_rights_denial_lsm: CFLAGS += -I$(SCRATCH_DIR)/include
> +$(OUTPUT)/scm_rights_denial_lsm: LDLIBS += -lelf -lz
> diff --git a/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.bpf.c b/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.bpf.c
> new file mode 100644
> index 000000000000..4f2414465bfd
> --- /dev/null
> +++ b/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.bpf.c
> @@ -0,0 +1,36 @@
> +// SPDX-License-Identifier: GPL-2.0
> +#include <linux/bpf.h>
> +#include <linux/errno.h>
> +#include <bpf/bpf_helpers.h>
> +#include <bpf/bpf_tracing.h>
> +
> +char _license[] SEC("license") = "GPL";
> +
> +struct inode {
> + unsigned long i_ino;
> +} __attribute__((preserve_access_index));
> +
> +struct file {
> + struct inode *f_inode;
> +} __attribute__((preserve_access_index));
> +
> +struct {
> + __uint(type, BPF_MAP_TYPE_HASH);
> + __uint(max_entries, 16);
> + __type(key, __u64); /* inode number */
> + __type(value, __u32); /* tgid of the receiver being tested */
> +} denied_inodes SEC(".maps");
> +
> +SEC("lsm/file_receive")
> +int BPF_PROG(scm_rights_deny, struct file *file)
> +{
> + __u32 tgid = bpf_get_current_pid_tgid() >> 32;
> + __u64 ino = file->f_inode->i_ino;
> + __u32 *owner;
> +
> + owner = bpf_map_lookup_elem(&denied_inodes, &ino);
> + if (owner && *owner == tgid)
> + return -EPERM;
> +
> + return 0;
> +}
> diff --git a/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.c b/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.c
> new file mode 100644
> index 000000000000..b8656de86efe
> --- /dev/null
> +++ b/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.c
> @@ -0,0 +1,263 @@
> +// SPDX-License-Identifier: GPL-2.0
> +#define _GNU_SOURCE
> +#include <errno.h>
> +#include <fcntl.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <unistd.h>
> +#include <sys/socket.h>
> +#include <sys/stat.h>
> +#include <sys/types.h>
> +
> +#include <bpf/bpf.h>
> +#include <bpf/libbpf.h>
> +
> +#include "kselftest_harness.h"
> +
> +#ifndef SO_RIGHTS_NOTRUNC
> +#define SO_RIGHTS_NOTRUNC 85
> +#endif
> +
> +#define NR_FILES 2
> +
> +/* Per-file content, so a received fd can be matched to the file sent */
> +#define SECRET(n) "secret %d", (n)
> +
> +/* Indices into the socketpair */
> +#define SK_SENDER 0
> +#define SK_RECEIVER 1
> +
> +FIXTURE(scm_rights_denial_bpf)
> +{
> + struct bpf_object *obj;
> + struct bpf_link *link;
> + int map_fd;
> + int sk[2];
> + int files[NR_FILES];
> + __u64 inos[NR_FILES];
> + char paths[NR_FILES][64];
> +};
> +
> +FIXTURE_SETUP(scm_rights_denial_bpf)
> +{
> + struct bpf_program *prog;
> + char lsms[256] = {};
> + int i, fd;
> +
> + if (geteuid() != 0)
> + SKIP(return, "requires root");
> +
> + fd = open("/sys/kernel/security/lsm", O_RDONLY);
> + ASSERT_LE(0, fd);
> + ASSERT_LT(0, read(fd, lsms, sizeof(lsms) - 1));
> + close(fd);
> +
> + if (!strstr(lsms, "bpf"))
> + SKIP(return, "BPF LSM not active (boot with lsm=...,bpf)");
> +
> + self->obj = bpf_object__open_file("scm_rights_denial_lsm.bpf.o", NULL);
> + ASSERT_NE(NULL, self->obj);
> + ASSERT_EQ(0, bpf_object__load(self->obj));
> +
> + prog = bpf_object__find_program_by_name(self->obj, "scm_rights_deny");
> + ASSERT_NE(NULL, prog);
> +
> + self->link = bpf_program__attach_lsm(prog);
> + ASSERT_NE(NULL, self->link);
> +
> + self->map_fd = bpf_object__find_map_fd_by_name(self->obj,
> + "denied_inodes");
> + ASSERT_LE(0, self->map_fd);
> +
> + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, self->sk));
> +
> + for (i = 0; i < NR_FILES; i++) {
> + struct stat st;
> +
> + snprintf(self->paths[i], sizeof(self->paths[i]),
> + "/tmp/scm_rights_denial_bpf.%d.XXXXXX", i);
> + self->files[i] = mkstemp(self->paths[i]);
> + ASSERT_LE(0, self->files[i]);
> +
> + ASSERT_LT(0, dprintf(self->files[i], SECRET(i)));
> +
> + ASSERT_EQ(0, fstat(self->files[i], &st));
> + self->inos[i] = st.st_ino;
> + }
> +}
> +
> +FIXTURE_TEARDOWN(scm_rights_denial_bpf)
> +{
> + bpf_link__destroy(self->link);
> + bpf_object__close(self->obj);
> +
> + for (int i = 0; i < NR_FILES; i++) {
> + if (self->files[i] >= 0) {
> + close(self->files[i]);
> + unlink(self->paths[i]);
> + }
> + }
> +
> + close(self->sk[SK_SENDER]);
> + close(self->sk[SK_RECEIVER]);
> +}
> +
> +static int deny_inode(int map_fd, __u64 ino)
> +{
> + __u32 tgid = getpid();
> + return bpf_map_update_elem(map_fd, &ino, &tgid, BPF_ANY);
> +}
> +
> +static int set_notrunc(int sk)
> +{
> + int one = 1;
> + return setsockopt(sk, SOL_SOCKET, SO_RIGHTS_NOTRUNC,
> + &one, sizeof(one));
> +}
> +
> +static int send_fds(int sk, int *fds, int n)
> +{
> + char ctrl[CMSG_SPACE(NR_FILES * sizeof(int))] = {};
> + char data = 'x';
> + struct iovec iov = {
> + .iov_base = &data,
> + .iov_len = sizeof(data),
> + };
> + struct msghdr msg = {
> + .msg_iov = &iov,
> + .msg_iovlen = 1,
> + .msg_control = ctrl,
> + .msg_controllen = CMSG_SPACE(n * sizeof(int)),
> + };
> + struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
> +
> + cmsg->cmsg_level = SOL_SOCKET;
> + cmsg->cmsg_type = SCM_RIGHTS;
> + cmsg->cmsg_len = CMSG_LEN(n * sizeof(int));
> + memcpy(CMSG_DATA(cmsg), fds, n * sizeof(int));
> +
> + return sendmsg(sk, &msg, 0);
> +}
> +
> +static int recv_fd_slots(int sk, int *slots, int *msg_flags)
> +{
> + int nr_slots;
> + char ctrl[CMSG_SPACE(NR_FILES * sizeof(int))];
> + char data;
> + struct iovec iov = {
> + .iov_base = &data,
> + .iov_len = sizeof(data),
> + };
> + struct msghdr msg = {
> + .msg_iov = &iov,
> + .msg_iovlen = 1,
> + .msg_control = ctrl,
> + .msg_controllen = sizeof(ctrl),
> + };
> + struct cmsghdr *cmsg;
> +
> + if (recvmsg(sk, &msg, 0) < 0)
> + return -1;
> +
> + *msg_flags = msg.msg_flags;
> +
> + cmsg = CMSG_FIRSTHDR(&msg);
> + if (!cmsg)
> + return 0;
> +
> + nr_slots = (cmsg->cmsg_len - CMSG_LEN(0)) / sizeof(int);
> + memcpy(slots, CMSG_DATA(cmsg), nr_slots * sizeof(int));
> +
> + return nr_slots;
> +}
> +
> +/* Prove a received fd works by reading back the file's content. */
> +static int check_secret(int fd, int idx)
> +{
> + char want[32], got[32] = {};
> +
> + snprintf(want, sizeof(want), SECRET(idx));
> + if (pread(fd, got, sizeof(got) - 1, 0) < 0)
> + return -1;
> +
> + return strcmp(want, got);
> +}
> +
> +TEST_F(scm_rights_denial_bpf, all_allowed)
> +{
> + int slots[NR_FILES], nr_slots, flags, i;
> +
> + ASSERT_EQ(0, set_notrunc(self->sk[SK_RECEIVER]));
> + ASSERT_NE(-1, send_fds(self->sk[SK_SENDER], self->files, NR_FILES));
> + nr_slots = recv_fd_slots(self->sk[SK_RECEIVER], slots, &flags);
> +
> + ASSERT_EQ(NR_FILES, nr_slots);
> + EXPECT_EQ(0, flags & MSG_CTRUNC);
> +
> + for (i = 0; i < NR_FILES; i++) {
> + ASSERT_LE(0, slots[i]);
Why do you assert less-or-equal? You want a valid fd so you should
expect >= 0?
> + EXPECT_EQ(0, check_secret(slots[i], i));
> + close(slots[i]);
> + }
> +}
> +
> +TEST_F(scm_rights_denial_bpf, first_denied)
> +{
> + int slots[NR_FILES], nr_slots, flags;
> +
> + ASSERT_EQ(0, deny_inode(self->map_fd, self->inos[0]));
> +
> + ASSERT_EQ(0, set_notrunc(self->sk[SK_RECEIVER]));
> + ASSERT_NE(-1, send_fds(self->sk[SK_SENDER], self->files, NR_FILES));
> + nr_slots = recv_fd_slots(self->sk[SK_RECEIVER], slots, &flags);
> +
> + ASSERT_EQ(NR_FILES, nr_slots);
> + EXPECT_EQ(0, flags & MSG_CTRUNC);
> + EXPECT_EQ(-EPERM, slots[0]);
> +
> + ASSERT_LE(0, slots[1]);
> + EXPECT_EQ(0, check_secret(slots[1], 1));
> + close(slots[1]);
> +}
> +
> +TEST_F(scm_rights_denial_bpf, all_denied)
> +{
> + int slots[NR_FILES], nr_slots, flags, i;
> +
> + for (i = 0; i < NR_FILES; i++)
> + ASSERT_EQ(0, deny_inode(self->map_fd, self->inos[i]));
> +
> + ASSERT_EQ(0, set_notrunc(self->sk[SK_RECEIVER]));
> + ASSERT_NE(-1, send_fds(self->sk[SK_SENDER], self->files, NR_FILES));
> + nr_slots = recv_fd_slots(self->sk[SK_RECEIVER], slots, &flags);
> +
> + ASSERT_EQ(NR_FILES, nr_slots);
> + EXPECT_EQ(0, flags & MSG_CTRUNC);
> +
> + for (i = 0; i < NR_FILES; i++)
> + EXPECT_EQ(-EPERM, slots[i]);
> +}
> +
> +TEST_F(scm_rights_denial_bpf, denied_without_notrunc)
> +{
> + int slots[NR_FILES], nr_slots, flags;
> +
> + /*
> + * Baseline behaviour without SO_RIGHTS_NOTRUNC: the fd array is
> + * truncated at the first denied fd and MSG_CTRUNC is set.
> + */
> + ASSERT_EQ(0, deny_inode(self->map_fd, self->inos[1]));
> +
> + ASSERT_NE(-1, send_fds(self->sk[SK_SENDER], self->files, NR_FILES));
> + nr_slots = recv_fd_slots(self->sk[SK_RECEIVER], slots, &flags);
> +
> + ASSERT_EQ(1, nr_slots);
> + EXPECT_NE(0, flags & MSG_CTRUNC);
> +
> + ASSERT_LE(0, slots[0]);
Why do you expect less-equal than zero? Don't you need ASSERT_GE()
because you want slots[0] to be a valid file desscriptor?
And even the other way around... Zero is a valid file descriptor so on
failure you must expect ASSERT_LT()?
--
Christian Brauner <brauner@kernel.org>
^ permalink raw reply [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-07-07 11:02 UTC | newest]
Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-05 12:38 [PATCH net-next v4 0/3] net: af_unix: useful handling of LSM denials on SCM_RIGHTS Jori Koolstra
2026-07-05 12:38 ` [PATCH net-next v4 1/3] net: scm: move scm_detach_fds() from common path to scm_recv_unix() Jori Koolstra
2026-07-06 18:20 ` Kuniyuki Iwashima
2026-07-05 12:38 ` [PATCH net-next v4 2/3] net: af_unix: useful handling of LSM denials on SCM_RIGHTS Jori Koolstra
2026-07-07 11:02 ` Christian Brauner
2026-07-05 12:38 ` [PATCH net-next v4 3/3] selftest: Add tests for " Jori Koolstra
2026-07-06 23:17 ` Kuniyuki Iwashima
2026-07-07 11:02 ` Christian Brauner
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox