Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 87 additions & 1 deletion varlink/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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<usize> {
let raw: Vec<RawFd> = 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<MRequest, MReply, MError>
where
MRequest: Serialize,
Expand All @@ -1012,6 +1046,11 @@ where
reader: Option<BufReader<Box<dyn Read + Send + Sync>>>,
writer: Option<Box<dyn Write + Send + Sync>>,
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<OwnedFd>,
phantom_reply: PhantomData<MReply>,
phantom_error: PhantomData<MError>,
}
Expand Down Expand Up @@ -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<usize> {
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();
Expand Down Expand Up @@ -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);
Expand Down
99 changes: 99 additions & 0 deletions varlink/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<serde_json::Value, serde_json::Value, Error>::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::<serde_json::Value, serde_json::Value, Error>::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::<i32>() 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(())
}