* [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; 9+ 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] 9+ 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; 9+ 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] 9+ 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; 9+ 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] 9+ 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; 9+ 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] 9+ 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; 9+ 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] 9+ 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; 9+ 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] 9+ 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; 9+ 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] 9+ 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 2026-07-12 18:23 ` Jori Koolstra 1 sibling, 1 reply; 9+ 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] 9+ messages in thread
* Re: [PATCH net-next v4 3/3] selftest: Add tests for useful handling of LSM denials on SCM_RIGHTS 2026-07-07 11:02 ` Christian Brauner @ 2026-07-12 18:23 ` Jori Koolstra 0 siblings, 0 replies; 9+ messages in thread From: Jori Koolstra @ 2026-07-12 18:23 UTC (permalink / raw) To: Christian Brauner Cc: Aleksa Sarai, Kuniyuki Iwashima, David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman, netdev, linux-fsdevel, linux-kernel > Op 07-07-2026 13:02 CEST schreef Christian Brauner <brauner@kernel.org>: > > > + > > +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? > This says 0 <= slots[i], or equivalently slots[i] >= 0. > > + > > +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? > Idem. But I do think ASSERT_GE() read a bit better here, so I have changed it. > 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> Thanks, Jori. ^ permalink raw reply [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-07-12 18:23 UTC | newest] Thread overview: 9+ 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 2026-07-12 18:23 ` Jori Koolstra
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox