Skip to main content

saluki_io/net/unix/
linux.rs

1use std::{
2    io, mem,
3    os::fd::{AsRawFd, FromRawFd as _, OwnedFd},
4};
5
6use bytes::BufMut;
7use socket2::{Domain, MaybeUninitSlice, MsgHdrMut, Protocol, SockAddr, SockAddrStorage, SockRef, Socket, Type};
8use tokio::net::UnixDatagram;
9
10use super::ancillary::{ControlMessage, SocketCredentialsAncillaryData};
11use crate::net::addr::{ConnectionAddress, ProcessCredentials, ProcessCredentialsError, ProcessIdentity};
12
13/// Enables the `SO_PASSCRED` option on the given socket.
14///
15/// ## Errors
16///
17/// If the underlying system call fails, an error is returned.
18pub fn enable_uds_socket_credentials<'sock, S>(socket: &'sock S) -> io::Result<()>
19where
20    SockRef<'sock>: From<&'sock S>,
21{
22    let sock_ref = SockRef::from(socket);
23    sock_ref.set_passcred(true)
24}
25
26pub(super) fn uds_recvmsg<'sock, S, B: BufMut>(socket: &'sock S, buf: &mut B) -> io::Result<(usize, ConnectionAddress)>
27where
28    SockRef<'sock>: From<&'sock S>,
29{
30    let sock_ref = SockRef::from(socket);
31
32    // Create the message header struct that will be populated by the call to `recvmsg`, which includes the peer
33    // address, message data, and any ancillary (out-of-band) data.
34    //
35    // SAFETY: We're allocating `sockaddr_storage`, which is always large enough to hold any address family's socket
36    // address structure.
37    let sock_storage = SockAddrStorage::zeroed();
38    let sock_storage_len = sock_storage.size_of();
39    let mut sock_addr = unsafe { SockAddr::new(sock_storage, sock_storage_len) };
40
41    let mut ancillary_data = SocketCredentialsAncillaryData::new();
42
43    let data_buf = unsafe { MaybeUninitSlice::new(buf.chunk_mut().as_uninit_slice_mut()) };
44    let mut data_bufs = [data_buf];
45
46    let mut msg_hdr = MsgHdrMut::new()
47        .with_addr(&mut sock_addr)
48        .with_buffers(&mut data_bufs)
49        .with_control(ancillary_data.as_mut_uninit());
50
51    let n = sock_ref.recvmsg(&mut msg_hdr, libc::MSG_CMSG_CLOEXEC)?;
52
53    // If we got any socket credentials back, parse them.
54    //
55    // We intentionally don't treat truncated ancillary data (`MSG_CTRUNC`) as a failure: if the kernel had to truncate
56    // the credentials themselves, the resulting control message is too short and gets rejected during parsing, so any
57    // credentials we do find are complete, and truncation means that some _other_ control message didn't fit, such as
58    // file descriptors sent by the peer.
59    let control_len = msg_hdr.control_len();
60
61    let process_identity = if control_len > 0 {
62        // SAFETY: `recvmsg` initialized the first `control_len` bytes of the ancillary data buffer.
63        unsafe {
64            ancillary_data.set_len(control_len);
65        }
66
67        // We have to look at every control message, even after finding the credentials, so that we can close any file
68        // descriptors the peer sent.
69        let mut credentials = None;
70
71        // SAFETY: We've just set the length of the ancillary data buffer to the number of bytes `recvmsg` initialized.
72        for message in unsafe { ancillary_data.messages() } {
73            match message {
74                ControlMessage::Credentials(creds) => {
75                    credentials.get_or_insert(creds);
76                }
77                ControlMessage::FileDescriptors(fds) => {
78                    for fd in fds {
79                        // SAFETY: The kernel installed this file descriptor in our process during the `recvmsg` call
80                        // above, and nothing else knows about it. This is the only pass we make over the control
81                        // messages, so we take ownership of it, and close it by dropping it, exactly once.
82                        drop(unsafe { OwnedFd::from_raw_fd(fd) });
83                    }
84                }
85            }
86        }
87
88        match credentials {
89            Some(creds) => ProcessIdentity::new(creds.pid, creds.uid, creds.gid),
90            None => ProcessIdentity::Error(ProcessCredentialsError::InvalidCredentials),
91        }
92    } else {
93        ProcessIdentity::Unavailable
94    };
95
96    let conn_addr = ConnectionAddress::ProcessLike(process_identity);
97
98    // Finally, update our buffer to reflect the bytes we've read.
99    unsafe {
100        buf.advance_mut(n);
101    }
102
103    Ok((n, conn_addr))
104}
105
106/// Returns `true` if `SO_REUSEPORT` is supported for UDP sockets on the current platform.
107pub fn socket_reuseport_supported() -> bool {
108    let socket = match Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP)) {
109        Ok(socket) => socket,
110        Err(_) => return false,
111    };
112
113    match socket.set_reuse_port(true) {
114        Ok(()) => true,
115        Err(_) => false,
116    }
117}
118
119/// Sends data to the Unix domain socket.
120///
121/// This function is specifically for connected Unix domain sockets in datagram mode (that's, SOCK_DGRAM), which are
122/// represented via `UnixDatagram` in `tokio`.
123///
124/// The payload is sent with an `SCM_CREDENTIALS` ancillary block using the provided `ProcessCredentials`.
125///
126/// Linux permits a sender to use its own PID, UID, and GID normally. Sending a forged PID requires `CAP_SYS_ADMIN`,
127/// sending a forged UID requires `CAP_SETUID`, and sending a forged GID requires `CAP_SETGID`.
128///
129/// ## Errors
130///
131/// If socket readiness or the underlying system call fails, an error is returned.
132pub async fn uds_sendmsg_with_creds(
133    socket: &UnixDatagram, payload: &[u8], credentials: &ProcessCredentials,
134) -> io::Result<usize> {
135    let creds = libc::ucred {
136        pid: credentials.pid as libc::pid_t,
137        uid: credentials.uid,
138        gid: credentials.gid,
139    };
140
141    socket
142        .async_io(tokio::io::Interest::WRITABLE, || {
143            sendmsg_with_ucred(socket.as_raw_fd(), payload, &creds)
144        })
145        .await
146}
147
148/// Synchronously writes one payload with the given credentials to the raw file descriptor.
149///
150/// Constructs a `cmsghdr` header followed by the `ucred` body in a single control buffer, then invokes `sendmsg`.
151fn sendmsg_with_ucred(fd: libc::c_int, payload: &[u8], creds: &libc::ucred) -> io::Result<usize> {
152    // SAFETY: `CMSG_SPACE` is a const expression on `size_of::<ucred>()`; the call is safe and returns the byte count
153    // needed to hold one aligned cmsghdr plus a ucred payload.
154    let control_len = unsafe { libc::CMSG_SPACE(mem::size_of::<libc::ucred>() as u32) as usize };
155
156    // `CMSG_SPACE` gives us the padded byte length, but the backing storage also has to be aligned for `cmsghdr` and
157    // `ucred` because the CMSG_* macros return typed pointers into it.
158    let control_words = control_len.div_ceil(mem::size_of::<usize>());
159    let mut control_buf = vec![0usize; control_words];
160
161    // SAFETY: we construct a `msghdr` pointing at the payload and the control buffer, then walk the control buffer
162    // with the libc CMSG_FIRSTHDR / CMSG_DATA macros to write the cmsghdr header and ucred body. Pointers all
163    // reference live local memory; lifetimes don't escape the call.
164    let n = unsafe {
165        // We use `IoSlice`-style iovec entries pointing at the payload.
166        let mut iov = libc::iovec {
167            iov_base: payload.as_ptr() as *mut libc::c_void,
168            iov_len: payload.len(),
169        };
170
171        let mut msg: libc::msghdr = mem::zeroed();
172        msg.msg_iov = &mut iov;
173        msg.msg_iovlen = 1;
174        msg.msg_control = control_buf.as_mut_ptr().cast::<libc::c_void>();
175        msg.msg_controllen = control_len as _;
176
177        // Populate the cmsghdr at the start of the control buffer.
178        let cmsg = libc::CMSG_FIRSTHDR(&msg);
179        if cmsg.is_null() {
180            return Err(io::Error::other("failed to obtain cmsghdr from control buffer"));
181        }
182        (*cmsg).cmsg_level = libc::SOL_SOCKET;
183        (*cmsg).cmsg_type = libc::SCM_CREDENTIALS;
184        (*cmsg).cmsg_len = libc::CMSG_LEN(mem::size_of::<libc::ucred>() as u32) as _;
185
186        // Copy the ucred body into the cmsg data region.
187        let data_ptr = libc::CMSG_DATA(cmsg) as *mut libc::ucred;
188        std::ptr::write(data_ptr, *creds);
189
190        // Send.
191        libc::sendmsg(fd, &msg, libc::MSG_NOSIGNAL)
192    };
193
194    if n < 0 {
195        Err(io::Error::last_os_error())
196    } else {
197        Ok(n as usize)
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use std::{
204        io::Read as _,
205        os::{
206            fd::{AsRawFd, FromRawFd, OwnedFd},
207            unix::net::UnixStream,
208        },
209    };
210
211    use bytes::BytesMut;
212
213    use super::*;
214
215    // Creates a connected AF_UNIX/SOCK_DGRAM socket pair, returning `(sender, receiver)`.
216    fn unix_dgram_socketpair() -> (Socket, Socket) {
217        let mut fds: [libc::c_int; 2] = [-1, -1];
218        let rc = unsafe { libc::socketpair(libc::AF_UNIX, libc::SOCK_DGRAM, 0, fds.as_mut_ptr()) };
219        assert_eq!(rc, 0, "socketpair failed: {}", io::Error::last_os_error());
220
221        // SAFETY: `socketpair` succeeded, so both descriptors are valid and owned by us.
222        unsafe { (Socket::from_raw_fd(fds[0]), Socket::from_raw_fd(fds[1])) }
223    }
224
225    #[test]
226    fn uds_recvmsg_reads_peer_credentials() {
227        // With SO_PASSCRED enabled on the receiver, the kernel attaches the sender's real PID/UID/GID as an
228        // SCM_CREDENTIALS control message, which `uds_recvmsg` must parse into `ProcessIdentity::Credentials`.
229        let (sender, receiver) = unix_dgram_socketpair();
230        enable_uds_socket_credentials(&receiver).expect("enabling SO_PASSCRED should succeed");
231
232        let payload = b"origin-detection-payload";
233        let sent = sender.send(payload).expect("send should succeed");
234        assert_eq!(sent, payload.len());
235
236        let mut buf = BytesMut::with_capacity(128);
237        let (n, addr) = uds_recvmsg(&receiver, &mut buf).expect("recvmsg should succeed");
238        assert_eq!(n, payload.len());
239        assert_eq!(&buf[..], payload);
240
241        let creds = addr
242            .process_credentials()
243            .expect("peer credentials should be present after SO_PASSCRED");
244        assert_eq!(creds.pid, std::process::id() as libc::pid_t);
245        assert_eq!(creds.uid, unsafe { libc::getuid() });
246        assert_eq!(creds.gid, unsafe { libc::getgid() });
247    }
248
249    #[test]
250    fn uds_recvmsg_without_passcred_reports_unavailable() {
251        // Without SO_PASSCRED, no ancillary credentials are delivered: control length is zero, so the identity is
252        // `Unavailable` (a "no origin info" state), not an error and not fabricated credentials.
253        let (sender, receiver) = unix_dgram_socketpair();
254
255        let payload = b"no-creds-payload";
256        sender.send(payload).expect("send should succeed");
257
258        let mut buf = BytesMut::with_capacity(128);
259        let (n, addr) = uds_recvmsg(&receiver, &mut buf).expect("recvmsg should succeed");
260        assert_eq!(n, payload.len());
261        assert_eq!(&buf[..], payload);
262
263        assert!(matches!(
264            addr,
265            ConnectionAddress::ProcessLike(ProcessIdentity::Unavailable)
266        ));
267        assert!(addr.process_credentials().is_none());
268    }
269
270    // Synchronously writes one payload to the raw file descriptor, passing the given file descriptors to the receiver
271    // via an `SCM_RIGHTS` ancillary block.
272    fn sendmsg_with_fds(fd: libc::c_int, payload: &[u8], fds: &[libc::c_int]) -> io::Result<usize> {
273        let fds_len = mem::size_of_val(fds);
274        let control_len = unsafe { libc::CMSG_SPACE(fds_len as u32) as usize };
275        let control_words = control_len.div_ceil(mem::size_of::<usize>());
276        let mut control_buf = vec![0usize; control_words];
277
278        // SAFETY: Mirrors `sendmsg_with_ucred`, with the cmsghdr body holding file descriptors instead of a ucred.
279        let n = unsafe {
280            let mut iov = libc::iovec {
281                iov_base: payload.as_ptr() as *mut libc::c_void,
282                iov_len: payload.len(),
283            };
284
285            let mut msg: libc::msghdr = mem::zeroed();
286            msg.msg_iov = &mut iov;
287            msg.msg_iovlen = 1;
288            msg.msg_control = control_buf.as_mut_ptr().cast::<libc::c_void>();
289            msg.msg_controllen = control_len as _;
290
291            let cmsg = libc::CMSG_FIRSTHDR(&msg);
292            (*cmsg).cmsg_level = libc::SOL_SOCKET;
293            (*cmsg).cmsg_type = libc::SCM_RIGHTS;
294            (*cmsg).cmsg_len = libc::CMSG_LEN(fds_len as u32) as _;
295            std::ptr::copy_nonoverlapping(fds.as_ptr().cast::<u8>(), libc::CMSG_DATA(cmsg), fds_len);
296
297            libc::sendmsg(fd, &msg, libc::MSG_NOSIGNAL)
298        };
299
300        if n < 0 {
301            Err(io::Error::last_os_error())
302        } else {
303            Ok(n as usize)
304        }
305    }
306
307    // Sends a payload along with several copies of a file descriptor, receives it with `uds_recvmsg`, and asserts that
308    // no copy of the file descriptor was left open in our process, returning the address the payload was received from.
309    fn receive_payload_with_fds(enable_credentials: bool) -> ConnectionAddress {
310        let (sender, receiver) = unix_dgram_socketpair();
311        if enable_credentials {
312            enable_uds_socket_credentials(&receiver).expect("enabling SO_PASSCRED should succeed");
313        }
314
315        // We send copies of one end of a stream socket pair, and drop our own copy once sent: reading from the other
316        // end then only reaches EOF if every copy that was installed in our process when receiving was closed again.
317        //
318        // We send more copies than can fit in the ancillary data buffer, even without credentials, which the kernel
319        // discards rather than installs.
320        let (sent_end, probe_end) = UnixStream::pair().expect("stream socketpair should succeed");
321        probe_end.set_nonblocking(true).unwrap();
322
323        let payload = b"origin-detection-payload";
324        let sent =
325            sendmsg_with_fds(sender.as_raw_fd(), payload, &[sent_end.as_raw_fd(); 8]).expect("send should succeed");
326        assert_eq!(sent, payload.len());
327        drop(sent_end);
328
329        let mut buf = BytesMut::with_capacity(128);
330        let (n, addr) = uds_recvmsg(&receiver, &mut buf).expect("recvmsg should succeed");
331        assert_eq!(n, payload.len());
332        assert_eq!(&buf[..], payload);
333
334        let mut probe_buf = [0u8; 1];
335        match (&probe_end).read(&mut probe_buf) {
336            Ok(0) => {}
337            Ok(_) => panic!("unexpected data read from probe socket"),
338            Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
339                panic!("file descriptors sent by the peer were left open in our process")
340            }
341            Err(e) => panic!("failed to read from probe socket: {e}"),
342        }
343
344        addr
345    }
346
347    #[test]
348    fn uds_recvmsg_reads_peer_credentials_when_peer_also_sends_file_descriptors() {
349        // When the peer sends file descriptors, any that don't fit in the ancillary data buffer are discarded with
350        // `MSG_CTRUNC` set. That truncation must not cause us to throw away complete credentials, and none of the
351        // file descriptors may be left open in our process.
352        let addr = receive_payload_with_fds(true);
353
354        let creds = addr
355            .process_credentials()
356            .expect("peer credentials should be present despite truncated ancillary data");
357        assert_eq!(creds.pid, std::process::id() as libc::pid_t);
358    }
359
360    #[test]
361    fn uds_recvmsg_closes_file_descriptors_received_without_credentials() {
362        // Without credentials taking up the ancillary data buffer, the kernel installs as many of the peer's file
363        // descriptors as fit in our process, which we must close rather than leak.
364        let addr = receive_payload_with_fds(false);
365
366        assert!(addr.process_credentials().is_none());
367    }
368
369    #[test]
370    fn sendmsg_with_current_credentials_round_trips_payload() {
371        // Construct a socketpair, send a payload with our own creds, read it back from the receiver, assert payload
372        // bytes match. This exercises the sendmsg construction path without requiring elevated capabilities.
373        let (sender, receiver) = unsafe {
374            let mut fds: [libc::c_int; 2] = [-1, -1];
375            let rc = libc::socketpair(libc::AF_UNIX, libc::SOCK_DGRAM, 0, fds.as_mut_ptr());
376            assert_eq!(rc, 0, "socketpair failed: {}", io::Error::last_os_error());
377            (OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1]))
378        };
379
380        let creds = libc::ucred {
381            pid: std::process::id() as libc::pid_t,
382            uid: unsafe { libc::getuid() },
383            gid: unsafe { libc::getgid() },
384        };
385        let payload = b"uds-sendmsg-test-payload";
386        let written = sendmsg_with_ucred(sender.as_raw_fd(), payload, &creds).expect("send should succeed");
387        assert_eq!(written, payload.len());
388
389        // Read back to confirm the receiver got the bytes.
390        let mut buf = [0u8; 64];
391        let read = unsafe {
392            libc::recv(
393                receiver.as_raw_fd(),
394                buf.as_mut_ptr() as *mut libc::c_void,
395                buf.len(),
396                0,
397            )
398        };
399        assert!(read > 0, "recv failed: {}", io::Error::last_os_error());
400        assert_eq!(&buf[..read as usize], payload);
401    }
402}