The branch main has been updated by kevans: URL: https://cgit.FreeBSD.org/src/commit/?id=9b16399225f8acfba675cf0a9312dd9eac563ce3
commit 9b16399225f8acfba675cf0a9312dd9eac563ce3 Author: Kyle Evans <[email protected]> AuthorDate: 2026-06-19 04:03:30 +0000 Commit: Kyle Evans <[email protected]> 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.
