git: 9b16399225f8 - main - unix: implement basic SO_PASSRIGHTS functionality
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Fri, 19 Jun 2026 04:05:08 UTC
The branch main has been updated by kevans:
URL: https://cgit.FreeBSD.org/src/commit/?id=9b16399225f8acfba675cf0a9312dd9eac563ce3
commit 9b16399225f8acfba675cf0a9312dd9eac563ce3
Author: Kyle Evans <kevans@FreeBSD.org>
AuthorDate: 2026-06-19 04:03:30 +0000
Commit: Kyle Evans <kevans@FreeBSD.org>
CommitDate: 2026-06-19 04:03:30 +0000
unix: implement basic SO_PASSRIGHTS functionality
With exception to sockopt functionality, implement the so_options flag
in unix(4) itself. The general argument for the flag is that SCM_RIGHTS
can be used maliciously for, e.g., a DoS that the receiving side can't
avoid if it is expecting other control messages.
This option gives the receiver a way to disable SCM_RIGHTS on the
sender-side, surfacing an EPERM to them instead. This seems to match
the semantics that Linux offers.
If an SCM_RIGHTS was already sent before we disabled SO_PASSRIGHTS, then
a subsequent recvmsg(2) will silently discard any in-flight files. This
has the downside of punting a file with the potential to hang over to
the deferred-close task, but perhaps usage of the option would
discourage folks from attempting to take advantage of that possibility
anyways.
Various manpages updated to describe the new behavior.
The ru_msgsnd accounting here might need to be re-evaluated: some error
paths will increment it while others will not, and it isn't really
clearly labelled. At some point in the operation enough work has been
done that one could consider it enough resources to count, but it's not
obvious that it should work this way. This change doesn't attempt to
address that.
Reviewed by: glebius
Differential Revision: https://reviews.freebsd.org/D57423
---
lib/libsys/getsockopt.2 | 3 ++-
lib/libsys/send.2 | 14 +++++++++--
share/man/man4/unix.4 | 13 ++++++++++-
sys/kern/uipc_usrreq.c | 62 +++++++++++++++++++++++++++++++++++++------------
sys/sys/socket.h | 1 +
5 files changed, 74 insertions(+), 19 deletions(-)
diff --git a/lib/libsys/getsockopt.2 b/lib/libsys/getsockopt.2
index 85d94e014631..1cfbe6ee913a 100644
--- a/lib/libsys/getsockopt.2
+++ b/lib/libsys/getsockopt.2
@@ -25,7 +25,7 @@
.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
.\" SUCH DAMAGE.
.\"
-.Dd April 21, 2026
+.Dd June 3, 2026
.Dt GETSOCKOPT 2
.Os
.Sh NAME
@@ -192,6 +192,7 @@ The following options are recognized in
.It Dv SO_NO_OFFLOAD Ta "disables protocol offloads"
.It Dv SO_NO_DDP Ta "disables direct data placement offload"
.It Dv SO_SPLICE Ta "splice two sockets together"
+.It Dv SO_PASSRIGHTS Ta "enables passing of SCM_RIGHTS over unix(4) sockets"
.El
.Pp
.Dv SO_DEBUG
diff --git a/lib/libsys/send.2 b/lib/libsys/send.2
index 678eef139f4a..3a66e0f30471 100644
--- a/lib/libsys/send.2
+++ b/lib/libsys/send.2
@@ -25,7 +25,7 @@
.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
.\" SUCH DAMAGE.
.\"
-.Dd April 27, 2020
+.Dd June 3, 2026
.Dt SEND 2
.Os
.Sh NAME
@@ -250,6 +250,15 @@ The process using a
socket was jailed and the source
address specified in the IP header did not match the IP
address bound to the prison.
+.It Bq Er EPERM
+The
+.Fa msg
+contained an
+.Dv SCM_RIGHTS
+control message, and the receiving
+.Xr unix 4
+socket is configured to reject new
+.Dv SCM_RIGHTS .
.It Bq Er EPIPE
The socket is unable to send anymore data
.Dv ( SBS_CANTSENDMORE
@@ -265,7 +274,8 @@ is not connected.
.Xr select 2 ,
.Xr socket 2 ,
.Xr write 2 ,
-.Xr CMSG_DATA 3
+.Xr CMSG_DATA 3 ,
+.Xr unix 4
.Sh HISTORY
The
.Fn send
diff --git a/share/man/man4/unix.4 b/share/man/man4/unix.4
index 2fdfde225b14..f83f9ddffea3 100644
--- a/share/man/man4/unix.4
+++ b/share/man/man4/unix.4
@@ -25,7 +25,7 @@
.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
.\" SUCH DAMAGE.
.\"
-.Dd October 31, 2024
+.Dd June 3, 2026
.Dt UNIX 4
.Os
.Sh NAME
@@ -163,9 +163,20 @@ depending on whether
is passed in the
.Xr recvmsg 2
call.
+.Pp
Descriptors that are awaiting delivery, or that are
purposely not received, are automatically closed by the system
when the destination socket is closed.
+The receiving socket can reject
+.Dv SCM_RIGHTS
+at the
+.Xr sendmsg 2
+call with the socket option
+.Dv SO_PASSRIGHTS ,
+which can be set with
+.Xr setsockopt 2
+and tested with
+.Xr getsockopt 2 .
.Pp
Credentials of the sending process can be transmitted explicitly using a
control message of type
diff --git a/sys/kern/uipc_usrreq.c b/sys/kern/uipc_usrreq.c
index b28aed291895..ad6d546afa25 100644
--- a/sys/kern/uipc_usrreq.c
+++ b/sys/kern/uipc_usrreq.c
@@ -303,9 +303,10 @@ static void unp_scan(struct mbuf *, void (*)(struct filedescent **, int));
static void unp_discard(struct file *);
static void unp_freerights(struct filedescent **, int);
static int unp_internalize(struct mbuf *, struct mchain *,
- struct thread *);
+ struct thread *, int *);
static void unp_internalize_fp(struct file *);
-static int unp_externalize(struct mbuf *, struct mbuf **, int);
+static int unp_externalize(const struct socket *, struct mbuf *,
+ struct mbuf **, int);
static int unp_externalize_fp(struct file *);
static void unp_addsockcred(struct thread *, struct mchain *, int);
static void unp_process_defers(void * __unused, int);
@@ -527,6 +528,7 @@ common:
UNP_PCB_LOCK_INIT(unp);
unp->unp_socket = so;
so->so_pcb = unp;
+ so->so_options |= SO_PASSRIGHTS;
refcount_init(&unp->unp_refcount, 1);
unp->unp_mode = ACCESSPERMS;
@@ -1112,7 +1114,7 @@ uipc_sosend_stream_or_seqpacket(struct socket *so, struct sockaddr *addr,
struct mchain mc, cmc;
size_t resid, sent;
bool nonblock, eor, aio;
- int error;
+ int error, needsopts;
MPASS((uio0 != NULL && m == NULL) || (m != NULL && uio0 == NULL));
MPASS(m == NULL || c == NULL);
@@ -1128,9 +1130,11 @@ uipc_sosend_stream_or_seqpacket(struct socket *so, struct sockaddr *addr,
cmc = MCHAIN_INITIALIZER(&cmc);
sent = 0;
aio = false;
+ needsopts = 0;
if (m == NULL) {
- if (c != NULL && (error = unp_internalize(c, &cmc, td)))
+ if (c != NULL &&
+ (error = unp_internalize(c, &cmc, td, &needsopts)))
goto out;
/*
* This function may read more data from the uio than it would
@@ -1176,6 +1180,14 @@ uipc_sosend_stream_or_seqpacket(struct socket *so, struct sockaddr *addr,
if (__predict_false((error = uipc_lock_peer(so, &unp2)) != 0))
goto out3;
+ /* Check for SO_PASS* flags */
+ so2 = unp2->unp_socket;
+ if ((atomic_load_int(&so2->so_options) & needsopts) != needsopts) {
+ error = EPERM;
+ UNP_PCB_UNLOCK(unp2);
+ goto out3;
+ }
+
if (unp2->unp_flags & UNP_WANTCRED_MASK) {
/*
* Credentials are passed only once on SOCK_STREAM and
@@ -1193,7 +1205,6 @@ uipc_sosend_stream_or_seqpacket(struct socket *so, struct sockaddr *addr,
* observe the SBS_CANTRCVMORE and our sorele() will finalize peer's
* socket destruction.
*/
- so2 = unp2->unp_socket;
soref(so2);
UNP_PCB_UNLOCK(unp2);
sb = &so2->so_rcv;
@@ -1560,7 +1571,7 @@ restart:
* is fine that we need to perform pretty complex
* operation here to reconstruct the buffer.
*/
- error = unp_externalize(control, controlp, flags);
+ error = unp_externalize(so, control, controlp, flags);
control = m_free(control);
if (__predict_false(error && control != NULL)) {
struct mchain cmc;
@@ -1959,11 +1970,11 @@ uipc_sosend_dgram(struct socket *so, struct sockaddr *addr, struct uio *uio,
struct mbuf *f;
u_int cc, ctl, mbcnt;
u_int dcc __diagused, dctl __diagused, dmbcnt __diagused;
- int error;
+ int error, needsopts;
MPASS((uio != NULL && m == NULL) || (m != NULL && uio == NULL));
- error = 0;
+ error = needsopts = 0;
f = NULL;
if (__predict_false(flags & MSG_OOB)) {
@@ -1983,7 +1994,8 @@ uipc_sosend_dgram(struct socket *so, struct sockaddr *addr, struct uio *uio,
f = m_gethdr(M_WAITOK, MT_SONAME);
cc = m->m_pkthdr.len;
mbcnt = MSIZE + m->m_pkthdr.memlen;
- if (c != NULL && (error = unp_internalize(c, &cmc, td)))
+ if (c != NULL &&
+ (error = unp_internalize(c, &cmc, td, &needsopts)))
goto out;
} else {
struct mchain mc;
@@ -2049,6 +2061,13 @@ uipc_sosend_dgram(struct socket *so, struct sockaddr *addr, struct uio *uio,
}
}
+ /* Check for SO_PASS* flags */
+ so2 = unp2->unp_socket;
+ if ((atomic_load_int(&so2->so_options) & needsopts) != needsopts) {
+ error = EPERM;
+ goto out4;
+ }
+
if (unp2->unp_flags & UNP_WANTCRED_MASK)
unp_addsockcred(td, &cmc, unp2->unp_flags);
if (unp->unp_addr != NULL)
@@ -2117,7 +2136,6 @@ uipc_sosend_dgram(struct socket *so, struct sockaddr *addr, struct uio *uio,
* would accumulate counters from all connected buffers potentially
* having sb_ccc > sb_hiwat or sb_mbcnt > sb_mbmax.
*/
- so2 = unp2->unp_socket;
sb = (addr == NULL) ? &so->so_snd : &so2->so_rcv;
SOCK_RECVBUF_LOCK(so2);
if (uipc_dgram_sbspace(sb, cc + ctl, mbcnt)) {
@@ -2143,6 +2161,7 @@ uipc_sosend_dgram(struct socket *so, struct sockaddr *addr, struct uio *uio,
}
}
+out4:
if (addr != NULL)
unp_disconnect(unp, unp2);
else
@@ -2345,7 +2364,7 @@ uipc_soreceive_dgram(struct socket *so, struct sockaddr **psa, struct uio *uio,
* without MT_DATA mbufs.
*/
while (m != NULL && m->m_type == MT_CONTROL) {
- error = unp_externalize(m, controlp, flags);
+ error = unp_externalize(so, m, controlp, flags);
m = m_free(m);
if (error != 0) {
SOCK_IO_RECV_UNLOCK(so);
@@ -3495,7 +3514,8 @@ restrict_rights(struct file *fp, struct thread *td)
}
static int
-unp_externalize(struct mbuf *control, struct mbuf **controlp, int flags)
+unp_externalize(const struct socket *so, struct mbuf *control,
+ struct mbuf **controlp, int flags)
{
struct thread *td = curthread; /* XXX */
struct cmsghdr *cm = mtod(control, struct cmsghdr *);
@@ -3527,8 +3547,18 @@ unp_externalize(struct mbuf *control, struct mbuf **controlp, int flags)
goto next;
fdep = data;
- /* If we're not outputting the descriptors free them. */
- if (error || controlp == NULL) {
+ /*
+ * If we're not outputting the descriptors, free them.
+ *
+ * In the case of having revoked SCM_PASSRIGHTS, the
+ * receiver must have toggled it before trying to
+ * receive control messages- we'll take that as a signal
+ * that they didn't want these, but they raced against
+ * the sender trying to pass files anyways.
+ */
+ if (error || controlp == NULL ||
+ (atomic_load_int(&so->so_options) &
+ SO_PASSRIGHTS) == 0) {
unp_freerights(fdep, newfds);
goto next;
}
@@ -3673,7 +3703,8 @@ unp_internalize_cleanup_rights(struct mbuf *control)
}
static int
-unp_internalize(struct mbuf *control, struct mchain *mc, struct thread *td)
+unp_internalize(struct mbuf *control, struct mchain *mc, struct thread *td,
+ int *needsopts)
{
struct proc *p;
struct filedesc *fdesc;
@@ -3729,6 +3760,7 @@ unp_internalize(struct mbuf *control, struct mchain *mc, struct thread *td)
break;
case SCM_RIGHTS:
+ *needsopts |= SO_PASSRIGHTS;
oldfds = datalen / sizeof (int);
if (oldfds == 0)
continue;
diff --git a/sys/sys/socket.h b/sys/sys/socket.h
index 8477e122520a..d9c31d572b4f 100644
--- a/sys/sys/socket.h
+++ b/sys/sys/socket.h
@@ -147,6 +147,7 @@ typedef __uintptr_t uintptr_t;
#define SO_NO_DDP 0x00008000 /* int; disable direct data placement */
#define SO_REUSEPORT_LB 0x00010000 /* int; reuse with load balancing */
#define SO_RERROR 0x00020000 /* int; keep track of receive errors */
+#define SO_PASSRIGHTS 0x00040000 /* int; unix(4) accepts SCM_RIGHTS */
/*
* Additional options, not kept in so_options.