git: 9b16399225f8 - main - unix: implement basic SO_PASSRIGHTS functionality

From: Kyle Evans <kevans_at_FreeBSD.org>
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.