#![allow(unsafe_code)]
use std::io;
use std::os::fd::{AsRawFd, RawFd};
use std::os::unix::net::UnixStream;
use core::ffi::c_void;
const SOL_SOCKET: i32 = 1;
const SCM_RIGHTS: i32 = 1;
const MSG_NOSIGNAL: i32 = 0x4000;
#[repr(C)]
struct IoVec {
base: *mut c_void,
len: usize,
}
#[repr(C)]
struct MsgHdr {
name: *mut c_void,
namelen: u32,
iov: *mut IoVec,
iovlen: usize,
control: *mut c_void,
controllen: usize,
flags: i32,
}
unsafe extern "C" {
fn sendmsg(fd: i32, msg: *const MsgHdr, flags: i32) -> isize;
fn recvmsg(fd: i32, msg: *mut MsgHdr, flags: i32) -> isize;
}
const CMSG_HDR: usize = 16;
fn align8(n: usize) -> usize {
n.div_ceil(8) * 8
}
fn cmsg_space(n: usize) -> usize {
CMSG_HDR + align8(n * 4)
}
pub fn send_with_fds(stream: &UnixStream, data: &[u8], fds: &[RawFd]) -> io::Result<usize> {
let mut iov = IoVec {
base: data.as_ptr() as *mut c_void,
len: data.len(),
};
let mut cbuf = vec![0u8; cmsg_space(fds.len()).max(1)];
let mut msg: MsgHdr = unsafe { core::mem::zeroed() };
msg.iov = &mut iov;
msg.iovlen = 1;
if !fds.is_empty() {
let cmsg_len = CMSG_HDR + fds.len() * 4;
cbuf[0..8].copy_from_slice(&cmsg_len.to_ne_bytes());
cbuf[8..12].copy_from_slice(&SOL_SOCKET.to_ne_bytes());
cbuf[12..16].copy_from_slice(&SCM_RIGHTS.to_ne_bytes());
for (i, fd) in fds.iter().enumerate() {
cbuf[CMSG_HDR + i * 4..CMSG_HDR + i * 4 + 4].copy_from_slice(&fd.to_ne_bytes());
}
msg.control = cbuf.as_mut_ptr() as *mut c_void;
msg.controllen = cmsg_space(fds.len());
}
let n = unsafe { sendmsg(stream.as_raw_fd(), &msg, MSG_NOSIGNAL) };
if n < 0 {
return Err(io::Error::last_os_error());
}
Ok(n as usize)
}
pub fn recv_with_fds(
stream: &UnixStream,
buf: &mut [u8],
fds_out: &mut Vec<RawFd>,
max_fds: usize,
) -> io::Result<usize> {
let mut iov = IoVec {
base: buf.as_mut_ptr() as *mut c_void,
len: buf.len(),
};
let mut cbuf = vec![0u8; cmsg_space(max_fds).max(1)];
let mut msg: MsgHdr = unsafe { core::mem::zeroed() };
msg.iov = &mut iov;
msg.iovlen = 1;
msg.control = cbuf.as_mut_ptr() as *mut c_void;
msg.controllen = cmsg_space(max_fds);
let n = unsafe { recvmsg(stream.as_raw_fd(), &mut msg, 0) };
if n < 0 {
return Err(io::Error::last_os_error());
}
if msg.controllen >= CMSG_HDR {
let cmsg_len = usize::from_ne_bytes(cbuf[0..8].try_into().unwrap());
let level = i32::from_ne_bytes(cbuf[8..12].try_into().unwrap());
let typ = i32::from_ne_bytes(cbuf[12..16].try_into().unwrap());
if level == SOL_SOCKET && typ == SCM_RIGHTS && cmsg_len >= CMSG_HDR {
let count = ((cmsg_len - CMSG_HDR) / 4).min(max_fds);
for i in 0..count {
let off = CMSG_HDR + i * 4;
fds_out.push(i32::from_ne_bytes(cbuf[off..off + 4].try_into().unwrap()));
}
}
}
Ok(n as usize)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::{Read, Seek, SeekFrom, Write};
use std::os::fd::FromRawFd;
#[test]
fn passes_a_real_fd_to_the_peer() {
let mut tmp = tempfile();
tmp.write_all(b"stipple-scm").unwrap();
tmp.flush().unwrap();
let (a, b) = UnixStream::pair().unwrap();
let sent = send_with_fds(&a, b"x", &[tmp.as_raw_fd()]).unwrap();
assert_eq!(sent, 1);
let mut buf = [0u8; 8];
let mut fds = Vec::new();
let n = recv_with_fds(&b, &mut buf, &mut fds, 4).unwrap();
assert_eq!(n, 1);
assert_eq!(buf[0], b'x');
assert_eq!(fds.len(), 1, "expected exactly one passed fd");
let mut got = unsafe { std::fs::File::from_raw_fd(fds[0]) };
got.seek(SeekFrom::Start(0)).unwrap();
let mut s = String::new();
got.read_to_string(&mut s).unwrap();
assert_eq!(s, "stipple-scm");
}
#[test]
fn no_fds_when_none_were_sent() {
let (a, b) = UnixStream::pair().unwrap();
send_with_fds(&a, b"hi", &[]).unwrap();
let mut buf = [0u8; 8];
let mut fds = Vec::new();
let n = recv_with_fds(&b, &mut buf, &mut fds, 4).unwrap();
assert_eq!(&buf[..n], b"hi");
assert!(fds.is_empty());
}
fn tempfile() -> std::fs::File {
let mut path = std::env::temp_dir();
path.push(format!("stipple-scm-test-{}", std::process::id()));
let f = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(&path)
.unwrap();
let _ = std::fs::remove_file(&path); f
}
}