[RFC PATCH 1/2] net: af_unix: Useful handling of LSM denials on SCM_RIGHTS

From: Jori Koolstra

Date: Tue Apr 28 2026 - 13:52:56 EST


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 two MSG_* flags:
- MSG_RIGHTS_DENIAL is set whenever any file is rejected by the LSM
during recvmsg() of SCM_RIGHTS fds.
- If MSG_RIGHTS_FILTER is passed as a flag to recvmsg(), the SCM_RIGHTS
fd array is always passed in its full original size. However, any
files rejected by the LSM are replaced in this array with -EPERM
instead of an assigned fd, while keeping the original order. If the
flag is not set, the original truncate behavior is used.

[1]: https://github.com/uapi-group/kernel-features#useful-handling-of-lsm-denials-on-scm_rights

Signed-off-by: Jori Koolstra <jkoolstra@xxxxxxxxx>
---
fs/file.c | 21 ++++++++++++++++++---
include/linux/file.h | 4 +++-
include/linux/socket.h | 3 +++
include/net/scm.h | 8 ++++----
io_uring/openclose.c | 2 +-
kernel/pid.c | 2 +-
kernel/seccomp.c | 2 +-
net/compat.c | 7 ++++---
net/core/scm.c | 11 ++++++-----
9 files changed, 41 insertions(+), 19 deletions(-)

diff --git a/fs/file.c b/fs/file.c
index 2c81c0b162d0..cc33a1e77049 100644
--- a/fs/file.c
+++ b/fs/file.c
@@ -1370,10 +1370,11 @@ int replace_fd(unsigned fd, struct file *file, unsigned flags)
}

/**
- * receive_fd() - Install received file into file descriptor table
+ * receive_fd_msg() - Install received file into file descriptor table
* @file: struct file that was received from another process
* @ufd: __user pointer to write new fd number to
* @o_flags: the O_* flags to apply to the new fd entry
+ * @msg_flags: the MSG_* flags to set for recvmsg(2)
*
* Installs a received file into the file descriptor table, with appropriate
* checks and count updates. Optionally writes the fd number to userspace, if
@@ -1384,13 +1385,21 @@ int replace_fd(unsigned fd, struct file *file, unsigned flags)
*
* Returns newly install fd or -ve on error.
*/
-int receive_fd(struct file *file, int __user *ufd, unsigned int o_flags)
+int receive_fd_msg(struct file *file, int __user *ufd, unsigned int o_flags,
+ unsigned int *msg_flags)
{
int error;

error = security_file_receive(file);
- if (error)
+ if (error) {
+ if (msg_flags)
+ *msg_flags |= MSG_RIGHTS_DENIAL;
+
+ if (ufd)
+ put_user(-EPERM, ufd);
+
return error;
+ }

FD_PREPARE(fdf, o_flags, file);
if (fdf.err)
@@ -1406,6 +1415,12 @@ int receive_fd(struct file *file, int __user *ufd, unsigned int o_flags)
__receive_sock(fd_prepare_file(fdf));
return fd_publish(fdf);
}
+EXPORT_SYMBOL_GPL(receive_fd_msg);
+
+int receive_fd(struct file *file, unsigned int o_flags)
+{
+ return receive_fd_msg(file, NULL, o_flags, NULL);
+}
EXPORT_SYMBOL_GPL(receive_fd);

int receive_fd_replace(int new_fd, struct file *file, unsigned int o_flags)
diff --git a/include/linux/file.h b/include/linux/file.h
index 27484b444d31..38f022d997a6 100644
--- a/include/linux/file.h
+++ b/include/linux/file.h
@@ -118,7 +118,9 @@ DEFINE_FREE(fput, struct file *, if (!IS_ERR_OR_NULL(_T)) fput(_T))

extern void fd_install(unsigned int fd, struct file *file);

-int receive_fd(struct file *file, int __user *ufd, unsigned int o_flags);
+int receive_fd_msg(struct file *file, int __user *ufd, unsigned int o_flags,
+ unsigned int *msg_flags);
+int receive_fd(struct file *file, unsigned int o_flags);

int receive_fd_replace(int new_fd, struct file *file, unsigned int o_flags);

diff --git a/include/linux/socket.h b/include/linux/socket.h
index ec4a0a025793..3809a8add2fc 100644
--- a/include/linux/socket.h
+++ b/include/linux/socket.h
@@ -342,6 +342,9 @@ struct ucred {
* plain text and require encryption
*/

+#define MSG_RIGHTS_DENIAL 0x200000
+#define MSG_RIGHTS_FILTER 0x400000
+
#define MSG_SOCK_DEVMEM 0x2000000 /* Receive devmem skbs as cmsg */
#define MSG_ZEROCOPY 0x4000000 /* Use user data in kernel path */
#define MSG_SPLICE_PAGES 0x8000000 /* Splice the pages from the iterator in sendmsg() */
diff --git a/include/net/scm.h b/include/net/scm.h
index c52519669349..983efa952c8e 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, int recv_flags);
+void scm_detach_fds_compat(struct msghdr *msg, struct scm_cookie *scm, int recv_flags);
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);
@@ -108,11 +108,11 @@ 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)
+ unsigned int o_flags, unsigned int *msg_flags)
{
if (!ufd)
return -EFAULT;
- return receive_fd(f, ufd, flags);
+ return receive_fd_msg(f, ufd, o_flags, msg_flags);
}

#endif /* __LINUX_NET_SCM_H */
diff --git a/io_uring/openclose.c b/io_uring/openclose.c
index c71242915dad..1b6cb05b0e3d 100644
--- a/io_uring/openclose.c
+++ b/io_uring/openclose.c
@@ -308,7 +308,7 @@ int io_install_fixed_fd(struct io_kiocb *req, unsigned int issue_flags)
int ret;

ifi = io_kiocb_to_cmd(req, struct io_fixed_install);
- ret = receive_fd(req->file, NULL, ifi->o_flags);
+ ret = receive_fd(req->file, ifi->o_flags);
if (ret < 0)
req_set_fail(req);
io_req_set_res(req, ret, 0);
diff --git a/kernel/pid.c b/kernel/pid.c
index fd5c2d4aa349..62af6874192d 100644
--- a/kernel/pid.c
+++ b/kernel/pid.c
@@ -929,7 +929,7 @@ static int pidfd_getfd(struct pid *pid, int fd)
if (IS_ERR(file))
return PTR_ERR(file);

- ret = receive_fd(file, NULL, O_CLOEXEC);
+ ret = receive_fd(file, O_CLOEXEC);
fput(file);

return ret;
diff --git a/kernel/seccomp.c b/kernel/seccomp.c
index 066909393c38..ad5ab16fe2b1 100644
--- a/kernel/seccomp.c
+++ b/kernel/seccomp.c
@@ -1130,7 +1130,7 @@ static void seccomp_handle_addfd(struct seccomp_kaddfd *addfd, struct seccomp_kn
*/
list_del_init(&addfd->list);
if (!addfd->setfd)
- fd = receive_fd(addfd->file, NULL, addfd->flags);
+ fd = receive_fd(addfd->file, addfd->flags);
else
fd = receive_fd_replace(addfd->fd, addfd->file, addfd->flags);
addfd->ret = fd;
diff --git a/net/compat.c b/net/compat.c
index 2c9bd0edac99..056bce0927c4 100644
--- a/net/compat.c
+++ b/net/compat.c
@@ -287,18 +287,19 @@ 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, int recv_flags)
{
struct compat_cmsghdr __user *cm =
(struct compat_cmsghdr __user *)msg->msg_control_user;
unsigned int o_flags = (msg->msg_flags & MSG_CMSG_CLOEXEC) ? O_CLOEXEC : 0;
+ bool filter_rights = recv_flags & MSG_RIGHTS_FILTER;
int fdmax = min_t(int, scm_max_fds_compat(msg), scm->fp->count);
int __user *cmsg_data = CMSG_COMPAT_DATA(cm);
int err = 0, i;

for (i = 0; i < fdmax; i++) {
- err = scm_recv_one_fd(scm->fp->fp[i], cmsg_data + i, o_flags);
- if (err < 0)
+ err = scm_recv_one_fd(scm->fp->fp[i], cmsg_data + i, o_flags, &msg->msg_flags);
+ if (err < 0 && !filter_rights)
break;
}

diff --git a/net/core/scm.c b/net/core/scm.c
index eec13f50ecaf..035329645d8f 100644
--- a/net/core/scm.c
+++ b/net/core/scm.c
@@ -351,10 +351,11 @@ 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)
+void scm_detach_fds(struct msghdr *msg, struct scm_cookie *scm, int recv_flags)
{
struct cmsghdr __user *cm =
(__force struct cmsghdr __user *)msg->msg_control_user;
+ bool filter_rights = recv_flags & MSG_RIGHTS_FILTER;
unsigned int o_flags = (msg->msg_flags & MSG_CMSG_CLOEXEC) ? O_CLOEXEC : 0;
int fdmax = min_t(int, scm_max_fds(msg), scm->fp->count);
int __user *cmsg_data = CMSG_USER_DATA(cm);
@@ -365,13 +366,13 @@ 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, recv_flags);
return;
}

for (i = 0; i < fdmax; i++) {
- err = scm_recv_one_fd(scm->fp->fp[i], cmsg_data + i, o_flags);
- if (err < 0)
+ err = scm_recv_one_fd(scm->fp->fp[i], cmsg_data + i, o_flags, &msg->msg_flags);
+ if (err < 0 && !filter_rights)
break;
}

@@ -524,7 +525,7 @@ static bool __scm_recv_common(struct sock *sk, struct msghdr *msg,
scm_passec(sk, msg, scm);

if (scm->fp)
- scm_detach_fds(msg, scm);
+ scm_detach_fds(msg, scm, flags);

return true;
}
--
2.54.0