//! Test 2 — fd passing via sendmsg/recvmsg with SCM_RIGHTS. //! //! This is THE critical primitive for Wayland: clients pass shm fds //! (and dmabuf fds) to the compositor through this mechanism. //! //! relibc Redox (platform/redox/socket.rs:272,447,835,924) declares //! support — this test validates the actual behaviour. //! //! Flow: //! 1. socketpair(AF_UNIX, SOCK_STREAM) //! 2. open a tmp file, write a known marker into it //! 3. sendmsg the fd via SCM_RIGHTS through socket A //! 4. recvmsg on socket B, extract the fd //! 5. read from the received fd, check marker matches //! //! Single-process test (no fork needed): we send the fd to ourselves //! through the socketpair to validate the mechanism works end-to-end. use std::ffi::CString; use std::io; use std::mem::{self, MaybeUninit}; use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}; use std::process::ExitCode; use std::ptr; const MARKER: &[u8] = b"REDOX-FD-PASSING-OK-2026"; fn errno_str() -> String { let e = io::Error::last_os_error(); format!("{e} (errno={})", e.raw_os_error().unwrap_or(0)) } unsafe fn make_socketpair() -> Result<(OwnedFd, OwnedFd), String> { let mut fds: [RawFd; 2] = [-1, -1]; let r = libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, fds.as_mut_ptr()); if r != 0 { return Err(format!("socketpair: {}", errno_str())); } Ok((OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1]))) } unsafe fn open_tmp_with_marker() -> Result { let path = CString::new("/tmp/test-02-fd-passing.bin").unwrap(); let fd = libc::open( path.as_ptr(), libc::O_RDWR | libc::O_CREAT | libc::O_TRUNC, 0o600, ); if fd < 0 { return Err(format!("open tmp: {}", errno_str())); } let n = libc::write(fd, MARKER.as_ptr() as *const _, MARKER.len()); if n != MARKER.len() as isize { return Err(format!("write tmp: {}", errno_str())); } libc::lseek(fd, 0, libc::SEEK_SET); Ok(OwnedFd::from_raw_fd(fd)) } unsafe fn send_fd_over(socket: RawFd, fd_to_send: RawFd) -> Result<(), String> { let mut iov_buf = [b'X'; 1]; let mut iov = libc::iovec { iov_base: iov_buf.as_mut_ptr() as *mut _, iov_len: 1, }; let cmsg_space = libc::CMSG_SPACE(mem::size_of::() as u32) as usize; let mut cmsg_buf = vec![0u8; cmsg_space]; let mut msg: libc::msghdr = mem::zeroed(); msg.msg_iov = &mut iov as *mut _; msg.msg_iovlen = 1; msg.msg_control = cmsg_buf.as_mut_ptr() as *mut _; msg.msg_controllen = cmsg_space as _; let cmsg = libc::CMSG_FIRSTHDR(&msg); if cmsg.is_null() { return Err("CMSG_FIRSTHDR null".into()); } (*cmsg).cmsg_level = libc::SOL_SOCKET; (*cmsg).cmsg_type = libc::SCM_RIGHTS; (*cmsg).cmsg_len = libc::CMSG_LEN(mem::size_of::() as u32) as _; let data_ptr = libc::CMSG_DATA(cmsg) as *mut RawFd; ptr::write_unaligned(data_ptr, fd_to_send); let n = libc::sendmsg(socket, &msg, 0); if n < 0 { return Err(format!("sendmsg: {}", errno_str())); } println!("[test-02] sendmsg sent {n} bytes + 1 fd via SCM_RIGHTS"); Ok(()) } unsafe fn recv_fd_from(socket: RawFd) -> Result { let mut iov_buf = [0u8; 1]; let mut iov = libc::iovec { iov_base: iov_buf.as_mut_ptr() as *mut _, iov_len: 1, }; let cmsg_space = libc::CMSG_SPACE(mem::size_of::() as u32) as usize; let mut cmsg_buf = vec![MaybeUninit::::uninit(); cmsg_space]; let mut msg: libc::msghdr = mem::zeroed(); msg.msg_iov = &mut iov as *mut _; msg.msg_iovlen = 1; msg.msg_control = cmsg_buf.as_mut_ptr() as *mut _; msg.msg_controllen = cmsg_space as _; let n = libc::recvmsg(socket, &mut msg, 0); if n < 0 { return Err(format!("recvmsg: {}", errno_str())); } println!("[test-02] recvmsg got {n} bytes + control of {} bytes", msg.msg_controllen); let cmsg = libc::CMSG_FIRSTHDR(&msg); if cmsg.is_null() { return Err("no cmsg in received message".into()); } if (*cmsg).cmsg_level != libc::SOL_SOCKET || (*cmsg).cmsg_type != libc::SCM_RIGHTS { return Err(format!( "unexpected cmsg level={} type={}", (*cmsg).cmsg_level, (*cmsg).cmsg_type )); } let data_ptr = libc::CMSG_DATA(cmsg) as *const RawFd; let received_fd = ptr::read_unaligned(data_ptr); if received_fd < 0 { return Err(format!("received negative fd: {received_fd}")); } Ok(OwnedFd::from_raw_fd(received_fd)) } fn run() -> Result<(), String> { println!("[test-02] fd passing via SCM_RIGHTS"); unsafe { let (sock_a, sock_b) = make_socketpair()?; let tmp_fd = open_tmp_with_marker()?; println!("[test-02] tmp fd = {}", tmp_fd.as_raw_fd()); // Strategy: send a dup'd fd, then close BOTH the original and the dup. // If SCM_RIGHTS works per POSIX, the received fd is an independent // reference to the open file and must remain readable. let dup_fd = libc::dup(tmp_fd.as_raw_fd()); if dup_fd < 0 { return Err(format!("dup: {}", errno_str())); } println!("[test-02] dup'd tmp_fd ({}) -> {dup_fd}", tmp_fd.as_raw_fd()); send_fd_over(sock_a.as_raw_fd(), dup_fd)?; let received = recv_fd_from(sock_b.as_raw_fd())?; let recv_raw = received.as_raw_fd(); println!("[test-02] received fd = {recv_raw}"); // Close BOTH the original and the dup. If SCM_RIGHTS gave us a real // duplicate, the received fd must survive. let orig_raw = tmp_fd.as_raw_fd(); drop(tmp_fd); libc::close(dup_fd); println!("[test-02] closed original fd ({orig_raw}) and dup ({dup_fd}); reading via received only"); libc::lseek(recv_raw, 0, libc::SEEK_SET); let mut readback = vec![0u8; MARKER.len()]; let n = libc::read(recv_raw, readback.as_mut_ptr() as *mut _, readback.len()); if n != MARKER.len() as isize { return Err(format!("read from received fd: only {n} bytes (errno {})", io::Error::last_os_error().raw_os_error().unwrap_or(0))); } if readback != MARKER { return Err(format!("marker mismatch: got {:?}", String::from_utf8_lossy(&readback))); } println!("[test-02] read {} bytes via received fd: marker matches", n); // Cleanup tmp file let path = std::ffi::CString::new("/tmp/test-02-fd-passing.bin").unwrap(); libc::unlink(path.as_ptr()); } Ok(()) } fn main() -> ExitCode { match run() { Ok(()) => { println!("[test-02] PASS: fd passing works, received fd reads correct marker"); ExitCode::SUCCESS } Err(e) => { eprintln!("[test-02] FAIL: {e}"); ExitCode::FAILURE } } }