From: Jori Koolstra <jkoolstra@xs4all.nl>
To: Alexander Viro <viro@zeniv.linux.org.uk>,
Christian Brauner <brauner@kernel.org>, Jan Kara <jack@suse.cz>,
Eric Dumazet <edumazet@google.com>,
Kuniyuki Iwashima <kuniyu@google.com>,
Paolo Abeni <pabeni@redhat.com>,
Willem de Bruijn <willemb@google.com>,
"David S . Miller" <davem@davemloft.net>,
Jakub Kicinski <kuba@kernel.org>, Jens Axboe <axboe@kernel.dk>,
Kees Cook <kees@kernel.org>
Cc: Simon Horman <horms@kernel.org>,
Andy Lutomirski <luto@amacapital.net>,
Will Drewry <wad@chromium.org>, Jeff Layton <jlayton@kernel.org>,
Jori Koolstra <jkoolstra@xs4all.nl>,
Oleg Nesterov <oleg@redhat.com>, Andrei Vagin <avagin@gmail.com>,
Pavel Tikhomirov <ptikhomirov@virtuozzo.com>,
Mateusz Guzik <mjguzik@gmail.com>,
Joel Granados <joel.granados@kernel.org>,
Charlie Mirabile <cmirabil@redhat.com>,
Aleksa Sarai <cyphar@cyphar.com>,
linux-fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org,
netdev@vger.kernel.org, io-uring@vger.kernel.org
Subject: [RFC PATCH 1/2] net: af_unix: Useful handling of LSM denials on SCM_RIGHTS
Date: Tue, 28 Apr 2026 19:51:24 +0200 [thread overview]
Message-ID: <20260428175125.2705296-2-jkoolstra@xs4all.nl> (raw)
In-Reply-To: <20260428175125.2705296-1-jkoolstra@xs4all.nl>
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@xs4all.nl>
---
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
next prev parent reply other threads:[~2026-04-28 17:51 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-04-28 17:51 [RFC PATCH 0/2] net: af_unix: Useful handling of LSM denials on SCM_RIGHTS Jori Koolstra
2026-04-28 17:51 ` Jori Koolstra [this message]
2026-04-30 2:04 ` [RFC PATCH 1/2] " Kuniyuki Iwashima
2026-05-01 15:34 ` Jori Koolstra
2026-05-02 1:24 ` Kuniyuki Iwashima
2026-05-04 17:43 ` Jori Koolstra
2026-04-28 17:51 ` [RFC PATCH 2/2] selftest: Add tests for useful " Jori Koolstra
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260428175125.2705296-2-jkoolstra@xs4all.nl \
--to=jkoolstra@xs4all.nl \
--cc=avagin@gmail.com \
--cc=axboe@kernel.dk \
--cc=brauner@kernel.org \
--cc=cmirabil@redhat.com \
--cc=cyphar@cyphar.com \
--cc=davem@davemloft.net \
--cc=edumazet@google.com \
--cc=horms@kernel.org \
--cc=io-uring@vger.kernel.org \
--cc=jack@suse.cz \
--cc=jlayton@kernel.org \
--cc=joel.granados@kernel.org \
--cc=kees@kernel.org \
--cc=kuba@kernel.org \
--cc=kuniyu@google.com \
--cc=linux-fsdevel@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=luto@amacapital.net \
--cc=mjguzik@gmail.com \
--cc=netdev@vger.kernel.org \
--cc=oleg@redhat.com \
--cc=pabeni@redhat.com \
--cc=ptikhomirov@virtuozzo.com \
--cc=viro@zeniv.linux.org.uk \
--cc=wad@chromium.org \
--cc=willemb@google.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox