diff --git a/varlink/src/lib.rs b/varlink/src/lib.rs index 342ab3c..a30cab0 100644 --- a/varlink/src/lib.rs +++ b/varlink/src/lib.rs @@ -247,6 +247,8 @@ use std::convert::From; use std::io::{BufRead, BufReader, Read, Write}; use std::marker::PhantomData; use std::ops::{Deref, DerefMut}; +#[cfg(unix)] +use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}; use std::process::Child; use std::sync::{Arc, RwLock}; @@ -1000,6 +1002,38 @@ impl Drop for Connection { } } +/// Send `buf` on `sock` with `fds` attached as a single SCM_RIGHTS control +/// message, returning the number of data bytes accepted (a short send is +/// possible for large buffers; the caller writes any remainder normally). +/// msghdr field widths differ across libcs (glibc/musl), hence the `as _` casts. +#[cfg(unix)] +fn sendmsg_with_fds(sock: RawFd, buf: &[u8], fds: &[OwnedFd]) -> std::io::Result { + let raw: Vec = fds.iter().map(|f| f.as_raw_fd()).collect(); + let fd_bytes = std::mem::size_of_val(raw.as_slice()); + let mut cmsg = vec![0u8; unsafe { libc::CMSG_SPACE(fd_bytes as u32) } as usize]; + let iov = libc::iovec { + iov_base: buf.as_ptr() as *mut libc::c_void, + iov_len: buf.len(), + }; + let mut msg: libc::msghdr = unsafe { std::mem::zeroed() }; + msg.msg_iov = &iov as *const _ as *mut _; + msg.msg_iovlen = 1 as _; + msg.msg_control = cmsg.as_mut_ptr() as *mut libc::c_void; + msg.msg_controllen = cmsg.len() as _; + unsafe { + let c = libc::CMSG_FIRSTHDR(&msg); + (*c).cmsg_level = libc::SOL_SOCKET; + (*c).cmsg_type = libc::SCM_RIGHTS; + (*c).cmsg_len = libc::CMSG_LEN(fd_bytes as u32) as _; + std::ptr::copy_nonoverlapping(raw.as_ptr() as *const u8, libc::CMSG_DATA(c), fd_bytes); + } + let n = unsafe { libc::sendmsg(sock, &msg, libc::MSG_NOSIGNAL) }; + if n < 0 { + return Err(std::io::Error::last_os_error()); + } + Ok(n as usize) +} + pub struct MethodCall where MRequest: Serialize, @@ -1012,6 +1046,11 @@ where reader: Option>>, writer: Option>, continues: bool, + /// File descriptors queued via [push_fd](#method.push_fd), attached to the + /// next send() as SCM_RIGHTS ancillary data. Kept as OwnedFd (dup'd copies) + /// so they stay valid until the message is on the wire, then dropped. + #[cfg(unix)] + fds: Vec, phantom_reply: PhantomData, phantom_error: PhantomData, } @@ -1050,11 +1089,37 @@ where continues: false, reader: None, writer: None, + #[cfg(unix)] + fds: Vec::new(), phantom_reply: PhantomData, phantom_error: PhantomData, } } + /// Queue a file descriptor to be passed with the next send of this call, via + /// SCM_RIGHTS ancillary data on the underlying socket. Returns the fd's index + /// in push order — the value a method's parameters use to reference a passed fd. + /// + /// The fd is dup'd (F_DUPFD_CLOEXEC), so the caller keeps ownership of the + /// original. Only works on socket-backed connections (created via + /// [Connection::with_address]); a reader/writer-pair connection has no socket + /// to carry ancillary data and returns an error. + #[cfg(unix)] + pub fn push_fd(&mut self, fd: RawFd) -> std::io::Result { + if self.connection.read().unwrap().stream.is_none() { + return Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "fd passing requires a socket-backed varlink connection", + )); + } + let dup = unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 3) }; + if dup < 0 { + return Err(std::io::Error::last_os_error()); + } + self.fds.push(unsafe { OwnedFd::from_raw_fd(dup) }); + Ok(self.fds.len() - 1) + } + fn send(&mut self, oneway: bool, more: bool, upgrade: bool) -> std::result::Result<(), MError> { { let mut conn = self.connection.write().unwrap(); @@ -1091,7 +1156,28 @@ where // Use sans-io protocol serialization let b = crate::sansio::protocol::serialize_request(&req)?; - w.write_all(&b).map_err(map_context!())?; + // If fds were queued (push_fd), send the request via sendmsg with + // SCM_RIGHTS on the underlying socket instead of the boxed writer. + // Both share the same socket, so any tail past the single sendmsg is + // written through the writer in order. + let mut sent = false; + #[cfg(unix)] + if !self.fds.is_empty() { + let sock = conn + .stream + .as_ref() + .map(|s| s.as_raw_fd()) + .ok_or_else(|| MError::from(context!(ErrorKind::ConnectionBusy)))?; + let n = sendmsg_with_fds(sock, &b, &self.fds).map_err(map_context!())?; + self.fds.clear(); + if n < b.len() { + w.write_all(&b[n..]).map_err(map_context!())?; + } + sent = true; + } + if !sent { + w.write_all(&b).map_err(map_context!())?; + } w.flush().map_err(map_context!())?; if oneway { conn.writer = Some(w); diff --git a/varlink/src/test.rs b/varlink/src/test.rs index f5b0b37..45dba3c 100644 --- a/varlink/src/test.rs +++ b/varlink/src/test.rs @@ -209,3 +209,102 @@ fn test_handle() -> Result<()> { ); Ok(()) } + +// push_fd is only meaningful on a socket-backed connection: a reader/writer-pair +// connection (Connection::default here) has no socket for ancillary data. +#[cfg(unix)] +#[test] +fn test_push_fd_requires_socket() { + let conn = Arc::new(RwLock::new(Connection::default())); + let mut call = MethodCall::::new( + conn, + "org.example.Test.Method", + serde_json::json!({}), + ); + let err = call.push_fd(0).unwrap_err(); + assert_eq!(err.kind(), std::io::ErrorKind::Unsupported); +} + +// push_fd attaches descriptors to the outgoing message via SCM_RIGHTS. Send over +// one end of a socketpair and recvmsg() the other end to confirm the fd arrives, +// referring to the same underlying object (a pipe we can then read through). +#[cfg(unix)] +#[test] +fn test_push_fd_passes_descriptor() -> Result<()> { + use std::io::Read; + use std::os::fd::{AsRawFd, FromRawFd, OwnedFd}; + use std::os::unix::net::UnixStream; + + let (client, server) = UnixStream::pair().unwrap(); + + // Pipe whose write end we pass; reading its read end proves the fd traveled. + let mut pipe = [0i32; 2]; + assert_eq!(unsafe { libc::pipe(pipe.as_mut_ptr()) }, 0); + let (pipe_r, pipe_w) = + unsafe { (OwnedFd::from_raw_fd(pipe[0]), OwnedFd::from_raw_fd(pipe[1])) }; + + let reader = Box::new(client.try_clone().unwrap()); + let writer = Box::new(client.try_clone().unwrap()); + // Connection implements Drop, so the fields must be listed explicitly (no + // ..Default::default() functional update). + let conn = Arc::new(RwLock::new(Connection { + reader: Some(BufReader::new(reader)), + writer: Some(writer), + address: String::new(), + stream: Some(Box::new(client)), + child: None, + tempdir: None, + })); + + let mut call = MethodCall::::new( + conn, + "org.example.Test.Method", + serde_json::json!({}), + ); + assert_eq!(call.push_fd(pipe_w.as_raw_fd()).unwrap(), 0); + call.oneway()?; // sends the request with the fd attached + drop(pipe_w); // leave the received dup as the pipe's only writer + + // recvmsg() the request bytes plus the SCM_RIGHTS descriptor. + let mut buf = [0u8; 256]; + let mut iov = libc::iovec { + iov_base: buf.as_mut_ptr() as *mut libc::c_void, + iov_len: buf.len(), + }; + let mut cbuf = + vec![0u8; unsafe { libc::CMSG_SPACE(std::mem::size_of::() as u32) } as usize]; + let mut msg: libc::msghdr = unsafe { std::mem::zeroed() }; + msg.msg_iov = &mut iov; + msg.msg_iovlen = 1 as _; + msg.msg_control = cbuf.as_mut_ptr() as *mut libc::c_void; + msg.msg_controllen = cbuf.len() as _; + + let n = unsafe { libc::recvmsg(server.as_raw_fd(), &mut msg, 0) }; + assert!(n > 0, "recvmsg returned {}", n); + // A varlink message is NUL-terminated JSON. + assert!(buf[..n as usize].contains(&0)); + + let cmsg = unsafe { libc::CMSG_FIRSTHDR(&msg) }; + assert!(!cmsg.is_null(), "no control message received"); + assert_eq!(unsafe { (*cmsg).cmsg_level }, libc::SOL_SOCKET); + assert_eq!(unsafe { (*cmsg).cmsg_type }, libc::SCM_RIGHTS); + let received_fd = unsafe { std::ptr::read_unaligned(libc::CMSG_DATA(cmsg) as *const i32) }; + let received = unsafe { OwnedFd::from_raw_fd(received_fd) }; + + // Writing through the received fd must surface on our pipe's read end. + let payload = b"hi"; + let w = unsafe { + libc::write( + received.as_raw_fd(), + payload.as_ptr() as *const libc::c_void, + payload.len(), + ) + }; + assert_eq!(w, payload.len() as isize); + drop(received); + + let mut got = [0u8; 2]; + std::fs::File::from(pipe_r).read_exact(&mut got).unwrap(); + assert_eq!(&got, payload); + Ok(()) +}