Skip to main content

unix_ancillary/
lib.rs

1//! Safe, ergonomic Unix socket ancillary data (SCM_RIGHTS fd passing).
2//!
3//! This crate provides a safe Rust API for sending and receiving file
4//! descriptors over Unix domain sockets via `SCM_RIGHTS`.
5//!
6//! # Design
7//!
8//! - **No `RawFd` in the public API** — `OwnedFd` and `BorrowedFd` only.
9//! - **Automatic cleanup** — received fds are `OwnedFd`, closed on drop.
10//! - **No fd leaks on truncation** — the high-level API sizes the receive
11//!   cmsg buffer past every Unix kernel's per-message fd cap, so the kernel
12//!   cannot deliver more fds than we can wrap. Surplus fds beyond the
13//!   caller's `N` are wrapped in `OwnedFd` and closed automatically.
14//! - **CLOEXEC errors are surfaced** — on platforms without
15//!   `MSG_CMSG_CLOEXEC` (notably macOS) we set `FD_CLOEXEC` post-recv; if
16//!   that fails, every received fd is closed and the error is returned.
17//!
18//! # Truncation safety
19//!
20//! The high-level [`UnixStreamExt::recv_fds`] / [`UnixDatagramExt::recv_fds`]
21//! size the receive cmsg buffer to a platform-specific upper bound the kernel
22//! cannot exceed for a single `SCM_RIGHTS` message:
23//!
24//! - **Linux / *BSD**: fixed `SCM_MAX_FD = 253`. The peer's kernel rejects
25//!   oversized sends with `EINVAL`.
26//! - **macOS**: the receiver's current `RLIMIT_NOFILE`, queried per recv
27//!   call. The kernel must allocate an fd table entry per delivered fd and
28//!   cannot exceed that limit.
29//!
30//! Result: truncation is kernel-impossible. Every fd the kernel delivers
31//! becomes an `OwnedFd` we control. Surplus fds beyond the caller's `N`
32//! are closed automatically. If the kernel still reports `MSG_CTRUNC`
33//! (defensive path; unreachable in practice), every fd we extracted is
34//! closed and an error is returned.
35//!
36//! Low-level callers using [`SocketAncillary`] manage their own buffer and
37//! must size it appropriately — the [`is_truncated`](SocketAncillary::is_truncated)
38//! flag is exposed for that path.
39//!
40//! # CLOEXEC race on macOS
41//!
42//! macOS lacks `MSG_CMSG_CLOEXEC` on `recvmsg`. This crate sets `FD_CLOEXEC`
43//! via `fcntl` immediately after the syscall returns, but a concurrent
44//! `fork`+`exec` between the two can leak the fd into the child. If your
45//! workload forks concurrently with fd-receiving threads, hold a fork lock
46//! around the receive.
47//!
48//! # Quick start
49//!
50//! ```no_run
51//! use std::os::unix::net::UnixStream;
52//! use unix_ancillary::UnixStreamExt;
53//!
54//! let (tx, rx) = UnixStream::pair().unwrap();
55//!
56//! let file = std::fs::File::open("/dev/null").unwrap();
57//! tx.send_fds(b"hello", &[&file]).unwrap();
58//!
59//! let recv = rx.recv_fds::<1>().unwrap();
60//! assert_eq!(&recv.data[..], b"hello");
61//! assert_eq!(recv.fds.len(), 1);
62//! ```
63
64#![deny(unsafe_op_in_unsafe_fn)]
65
66#[cfg(not(unix))]
67compile_error!("unix-ancillary only supports Unix platforms");
68
69mod ancillary;
70mod cmsg;
71mod ext;
72mod platform;
73
74pub use ancillary::{AncillaryData, AncillaryError, Messages, ScmRights, SocketAncillary};
75pub use ext::{ReceivedFds, UnixDatagramExt, UnixStreamExt};
76
77#[doc(hidden)]
78pub use ancillary::__fuzz_parse;
79
80use std::io;
81use std::os::unix::io::BorrowedFd;
82
83/// Result returned by [`cmsg_recvmsg`].
84#[derive(Debug, Clone, Copy)]
85pub struct RecvResult {
86    /// Bytes written into the iov buffers.
87    pub bytes_read: usize,
88    /// `true` if `MSG_CTRUNC` was set on the underlying `recvmsg`.
89    pub truncated: bool,
90}
91
92/// Send data with ancillary control messages over a Unix socket.
93///
94/// Low-level API. Prefer [`UnixStreamExt::send_fds`] for convenience.
95pub fn cmsg_sendmsg(
96    fd: BorrowedFd<'_>,
97    iov: &[io::IoSlice<'_>],
98    ancillary: &SocketAncillary<'_>,
99) -> io::Result<usize> {
100    cmsg::sendmsg_vectored(fd, iov, ancillary.buffer, ancillary.length)
101}
102
103/// Receive data with ancillary control messages from a Unix socket.
104///
105/// Low-level API. Prefer [`UnixStreamExt::recv_fds`] for convenience.
106///
107/// On non-`MSG_CMSG_CLOEXEC` platforms, all received fds have `FD_CLOEXEC`
108/// set before this function returns. If that fails for any fd, every
109/// received fd is closed and the error is propagated.
110pub fn cmsg_recvmsg(
111    fd: BorrowedFd<'_>,
112    iov: &mut [io::IoSliceMut<'_>],
113    ancillary: &mut SocketAncillary<'_>,
114) -> io::Result<RecvResult> {
115    let result = cmsg::recvmsg_vectored(fd, iov, ancillary.buffer)?;
116    ancillary.length = result.ancillary_len;
117    ancillary.truncated = result.truncated;
118    Ok(RecvResult {
119        bytes_read: result.bytes_read,
120        truncated: result.truncated,
121    })
122}