//! unixtest -- stage 18: AF_UNIX sockets and /proc/self. Run with one //! group name; exit code = number of failed checks: //! stream a path socket (ramfs, ext2), echo across fork, abstract //! names, autobind, EOF/ECONNRESET/EPIPE, SO_PEERCRED //! pair socketpair: stream, seqpacket (message borders), dgram //! dgram sendto/recvfrom between named sockets, MSG_TRUNC, connect //! rights SCM_RIGHTS (a file and a pipe end), MSG_CTRUNC, //! MSG_CMSG_CLOEXEC, in-flight fds closed, SCM_CREDENTIALS //! nonblock EAGAIN, buffer limits (backpressure), poll and epoll //! (EPOLLRDHUP, EPOLLHUP), nonblocking accept //! proc /proc/self/fd and /proc/self/exe #![no_std] #![no_main] #[macro_use] #[path = "ulib.rs"] mod ulib; extern crate alloc; use alloc::format; use alloc::string::String; use alloc::vec::Vec; use ulib::*; const SYS_SENDMSG: u64 = 46; const SYS_RECVMSG: u64 = 47; const SYS_SHUTDOWN: u64 = 48; const SYS_GETSOCKNAME: u64 = 51; const SYS_GETPEERNAME: u64 = 52; const SYS_SOCKETPAIR: u64 = 53; const SYS_GETSOCKOPT: u64 = 55; const SYS_ACCEPT4: u64 = 288; const AF_UNIX: u64 = 1; const SOCK_STREAM: u64 = 1; const SOCK_DGRAM: u64 = 2; const SOCK_SEQPACKET: u64 = 5; const SOCK_NONBLOCK: u64 = 0o4000; const SOCK_CLOEXEC: u64 = 0o2000000; const MSG_PEEK: u64 = 2; const MSG_CTRUNC: i32 = 8; const MSG_TRUNC: u64 = 0x20; const MSG_DONTWAIT: u64 = 0x40; const MSG_WAITALL: u64 = 0x100; const MSG_NOSIGNAL: u64 = 0x4000; const MSG_CMSG_CLOEXEC: u64 = 0x4000_0000; const SOL_SOCKET: u64 = 1; const SO_TYPE: u64 = 3; const SO_ERROR: u64 = 4; const SO_SNDBUF: u64 = 7; const SO_RCVBUF: u64 = 8; const SO_PASSCRED: u64 = 16; const SO_PEERCRED: u64 = 17; const SO_ACCEPTCONN: u64 = 30; const SO_DOMAIN: u64 = 39; const POLLIN: u16 = 1; const POLLOUT: u16 = 4; const POLLHUP: u16 = 0x10; const EPOLLRDHUP: u32 = 0x2000; const EPOLLHUP: u32 = 0x10; const ENXIO: i64 = 6; const EPIPE: i64 = 32; const EMSGSIZE: i64 = 90; const EPROTOTYPE: i64 = 91; const ENOTSOCK: i64 = 88; const EADDRINUSE: i64 = 98; const ECONNRESET: i64 = 104; const EISCONN: i64 = 106; const ENOTCONN: i64 = 107; const ECONNREFUSED: i64 = 111; const EOPNOTSUPP: i64 = 95; // --- helpers --------------------------------------------------------------------- /// struct sockaddr_un: (bytes, length to pass). A name starting with /// '@' is abstract (a 0 byte in front, no trailing 0), as `ss` shows them. fn sun(name: &str) -> ([u8; 110], u64) { let mut a = [0u8; 110]; a[0] = AF_UNIX as u8; let b = name.as_bytes(); if let Some(rest) = name.strip_prefix('@') { a[3..3 + rest.len()].copy_from_slice(rest.as_bytes()); (a, 3 + rest.len() as u64) } else { a[2..2 + b.len()].copy_from_slice(b); (a, 2 + b.len() as u64 + 1) } } fn usock(ty: u64) -> i64 { syscall3(SYS_SOCKET, AF_UNIX, ty, 0) } fn ubind(fd: i64, name: &str) -> i64 { let (a, len) = sun(name); syscall3(SYS_BIND, fd as u64, a.as_ptr() as u64, len) } fn uconnect(fd: i64, name: &str) -> i64 { let (a, len) = sun(name); syscall3(SYS_CONNECT, fd as u64, a.as_ptr() as u64, len) } fn uaccept(fd: i64) -> i64 { syscall3(SYS_ACCEPT, fd as u64, 0, 0) } /// getsockname/getpeername: the name as text ('@' for abstract, "" for /// unnamed), or the error. fn name_of(nr: u64, fd: i64) -> Result { let mut a = [0u8; 110]; let mut len = 110u32; let r = syscall3(nr, fd as u64, a.as_mut_ptr() as u64, &mut len as *mut u32 as u64); if r < 0 { return Err(r); } let path = &a[2..len as usize]; Ok(match path.first() { None => String::new(), Some(0) => format!("@{}", core::str::from_utf8(&path[1..]).unwrap_or("?")), Some(_) => String::from(core::str::from_utf8(&path[..path.len() - 1]).unwrap_or("?")), }) } fn pair(ty: u64) -> (i64, i64) { let mut sv = [-1i32; 2]; let r = syscall5(SYS_SOCKETPAIR, AF_UNIX, ty, 0, sv.as_mut_ptr() as u64, 0); if r < 0 { return (r, r); } (sv[0] as i64, sv[1] as i64) } fn getopt(fd: i64, opt: u64) -> (i64, u32) { let mut v = 0u32; let mut len = 4u32; let r = syscall5(SYS_GETSOCKOPT, fd as u64, SOL_SOCKET, opt, &mut v as *mut u32 as u64, &mut len as *mut u32 as u64); (r, v) } fn setopt(fd: i64, opt: u64, v: u32) -> i64 { syscall5(SYS_SETSOCKOPT, fd as u64, SOL_SOCKET, opt, &v as *const u32 as u64, 4) } /// SO_PEERCRED: (pid, uid, gid). fn peercred(fd: i64) -> (i64, [u32; 3]) { let mut v = [0u32; 3]; let mut len = 12u32; let r = syscall5(SYS_GETSOCKOPT, fd as u64, SOL_SOCKET, SO_PEERCRED, v.as_mut_ptr() as u64, &mut len as *mut u32 as u64); (r, v) } fn send(fd: i64, data: &[u8], flags: u64) -> i64 { syscall6(SYS_SENDTO, fd as u64, data.as_ptr() as u64, data.len() as u64, flags, 0, 0) } fn recv(fd: i64, buf: &mut [u8], flags: u64) -> i64 { syscall6(SYS_RECVFROM, fd as u64, buf.as_mut_ptr() as u64, buf.len() as u64, flags, 0, 0) } fn send_to(fd: i64, data: &[u8], name: &str) -> i64 { let (a, len) = sun(name); syscall6(SYS_SENDTO, fd as u64, data.as_ptr() as u64, data.len() as u64, 0, a.as_ptr() as u64, len) } /// recvfrom with the sender's name. fn recv_from(fd: i64, buf: &mut [u8], flags: u64) -> (i64, String) { let mut a = [0u8; 110]; let mut len = 110u32; let n = syscall6(SYS_RECVFROM, fd as u64, buf.as_mut_ptr() as u64, buf.len() as u64, flags, a.as_mut_ptr() as u64, &mut len as *mut u32 as u64); let name = if len <= 2 { String::new() } else if a[2] == 0 { format!("@{}", core::str::from_utf8(&a[3..len as usize]).unwrap_or("?")) } else { String::from(core::str::from_utf8(&a[2..len as usize - 1]).unwrap_or("?")) }; (n, name) } #[repr(C)] struct MsgHdr { name: u64, namelen: u32, iov: u64, iovlen: u64, control: u64, controllen: u64, flags: i32, } #[repr(C)] struct IoVec { base: u64, len: u64, } /// sendmsg: `parts` gathered, plus fds (SCM_RIGHTS) if any. fn sendmsg(fd: i64, parts: &[&[u8]], fds: &[i32], flags: u64) -> i64 { let iov: Vec = parts.iter().map(|p| IoVec { base: p.as_ptr() as u64, len: p.len() as u64 }).collect(); let mut ctl = Vec::new(); if !fds.is_empty() { ctl.extend_from_slice(&(16 + 4 * fds.len() as u64).to_ne_bytes()); ctl.extend_from_slice(&1i32.to_ne_bytes()); ctl.extend_from_slice(&1i32.to_ne_bytes()); for f in fds { ctl.extend_from_slice(&f.to_ne_bytes()); } while ctl.len() % 8 != 0 { ctl.push(0); } } let h = MsgHdr { name: 0, namelen: 0, iov: iov.as_ptr() as u64, iovlen: iov.len() as u64, control: ctl.as_ptr() as u64, controllen: ctl.len() as u64, flags: 0 }; syscall3(SYS_SENDMSG, fd as u64, &h as *const MsgHdr as u64, flags) } struct Got { n: i64, data: Vec, fds: Vec, cred: Option<[u32; 3]>, flags: i32, } /// recvmsg into two buffers of `len/2`, with `ctl_room` bytes of control. fn recvmsg(fd: i64, len: usize, ctl_room: usize, flags: u64) -> Got { let mut a = alloc::vec![0u8; len / 2]; let mut b = alloc::vec![0u8; len - len / 2]; let iov = [IoVec { base: a.as_mut_ptr() as u64, len: a.len() as u64 }, IoVec { base: b.as_mut_ptr() as u64, len: b.len() as u64 }]; let mut ctl = alloc::vec![0u8; ctl_room]; let mut h = MsgHdr { name: 0, namelen: 0, iov: iov.as_ptr() as u64, iovlen: 2, control: ctl.as_mut_ptr() as u64, controllen: ctl_room as u64, flags: 0 }; let n = syscall3(SYS_RECVMSG, fd as u64, &mut h as *mut MsgHdr as u64, flags); let mut got = Got { n, data: Vec::new(), fds: Vec::new(), cred: None, flags: h.flags }; if n < 0 { return got; } a.extend_from_slice(&b); got.data = a[..(n as usize).min(len)].to_vec(); let ctl = &ctl[..h.controllen as usize]; let mut off = 0; while off + 16 <= ctl.len() { let clen = u64::from_ne_bytes(ctl[off..off + 8].try_into().unwrap()) as usize; let ty = i32::from_ne_bytes(ctl[off + 12..off + 16].try_into().unwrap()); let body = &ctl[off + 16..(off + clen).min(ctl.len())]; match ty { 1 => got.fds.extend(body.chunks(4).map(|c| i32::from_ne_bytes(c.try_into().unwrap()))), 2 => { let w: Vec = body.chunks(4).map(|c| u32::from_ne_bytes(c.try_into().unwrap())).collect(); got.cred = Some([w[0], w[1], w[2]]); } _ => {} } off += (clen + 7) & !7; } got } fn read_all(fd: i64) -> Vec { let mut out = Vec::new(); let mut buf = [0u8; 256]; loop { let n = read(fd, &mut buf); if n <= 0 { break; } out.extend_from_slice(&buf[..n as usize]); } out } fn link(path: &str) -> String { let mut buf = [0u8; 256]; let n = readlink(path, &mut buf); if n < 0 { return format!("error {}", n); } String::from(core::str::from_utf8(&buf[..n as usize]).unwrap_or("?")) } fn is_socket(st: &Stat) -> bool { st.file_type() == S_IFSOCK } fn main() -> i64 { let group = arg(1).unwrap_or(""); let mut c = Checks::new("unixtest"); sigaction_raw(13, 1, 0, 0); // SIGPIPE: SIG_IGN (EPIPE checks) match group { "stream" => stream(&mut c), "pair" => pairs(&mut c), "dgram" => dgram(&mut c), "rights" => rights(&mut c), "nonblock" => nonblock(&mut c), "proc" => proc(&mut c), _ => c.check(false, "usage: unixtest stream|pair|dgram|rights|nonblock|proc"), } c.done() } // --- stream -------------------------------------------------------------------------- /// The classic: a server binds a path, listens, a forked child connects /// and talks to it. fn echo_on(c: &mut Checks, path: &str) { unlink(path); let srv = usock(SOCK_STREAM); c.check(srv >= 0, &format!("{}: socket(AF_UNIX, SOCK_STREAM)", path)); c.check(ubind(srv, path) == 0, &format!("{}: bind", path)); let mut s = Stat::default(); c.check(lstat(path, &mut s) == 0 && is_socket(&s), &format!("{}: bind made a socket file (S_IFSOCK)", path)); c.check(s.perm() == 0o755, &format!("{}: ...mode 0777 minus the umask (0o{:o})", path, s.perm())); let other = usock(SOCK_STREAM); c.check(ubind(other, path) == -EADDRINUSE, &format!("{}: binding the name again: EADDRINUSE", path)); c.check(ubind(srv, "/tmp/second.sock") == -EINVAL, &format!("{}: binding a bound socket again: EINVAL", path)); c.check(uconnect(other, path) == -ECONNREFUSED, &format!("{}: connect before listen: ECONNREFUSED", path)); close(other); c.check(listen(srv, 4) == 0, &format!("{}: listen", path)); c.check(getopt(srv, SO_ACCEPTCONN) == (0, 1), &format!("{}: SO_ACCEPTCONN 1", path)); c.check(name_of(SYS_GETSOCKNAME, srv).as_deref() == Ok(path), &format!("{}: getsockname gives the path", path)); c.check(open(path, O_RDONLY) == -ENXIO, &format!("{}: open() on a socket file: ENXIO", path)); let child = fork(); if child == 0 { // The client. let cl = usock(SOCK_STREAM); let mut bad = 0; if uconnect(cl, path) != 0 { bad += 1; } if write(cl, b"hello, ") != 7 || write(cl, b"server") != 6 { bad += 1; } let mut buf = [0u8; 64]; let n = read(cl, &mut buf); if n != 13 || &buf[..13] != b"HELLO, SERVER" { bad += 1; } if read(cl, &mut buf) != 0 { bad += 1; // the server closed: end-of-file } exit(bad); } let conn = uaccept(srv); c.check(conn >= 0, &format!("{}: accept", path)); let (r, cred) = peercred(conn); c.check(r == 0 && cred[0] as i64 == child, &format!("{}: SO_PEERCRED on the server side: the client's pid", path)); c.check(name_of(SYS_GETPEERNAME, conn).as_deref() == Ok(""), &format!("{}: getpeername: the client is unnamed", path)); c.check(name_of(SYS_GETSOCKNAME, conn).as_deref() == Ok(path), &format!("{}: the accepted socket's name is the server's", path)); let mut buf = [0u8; 13]; let n = recv(conn, &mut buf, MSG_WAITALL); c.check(n == 13 && &buf == b"hello, server", &format!("{}: two writes read back as one stream (MSG_WAITALL)", path)); let upper: Vec = buf.iter().map(|b| b.to_ascii_uppercase()).collect(); c.check(write(conn, &upper) == 13, &format!("{}: server answers", path)); close(conn); c.check(wait(child) == 0, &format!("{}: the client saw the echo and EOF", path)); close(srv); let late = usock(SOCK_STREAM); c.check(uconnect(late, path) == -ECONNREFUSED, &format!("{}: connect after the server closed: ECONNREFUSED", path)); close(late); c.check(unlink(path) == 0 && lstat(path, &mut s) == -ENOENT, &format!("{}: unlink removes the socket file", path)); let late = usock(SOCK_STREAM); c.check(uconnect(late, path) == -ENOENT, &format!("{}: connect to a missing path: ENOENT", path)); close(late); } fn stream(c: &mut Checks) { echo_on(c, "/tmp/echo.sock"); mkdir("/mnt/disk1/us"); echo_on(c, "/mnt/disk1/us/echo.sock"); rmdir("/mnt/disk1/us"); // FAT can't store a socket file. let s = usock(SOCK_STREAM); c.check(ubind(s, "/mnt/disk0/x.sock") == -EPERM, "bind on FAT: EPERM"); close(s); // A relative path, from the cwd. chdir("/tmp"); unlink("/tmp/rel.sock"); let srv = usock(SOCK_STREAM); c.check(ubind(srv, "rel.sock") == 0 && listen(srv, 1) == 0, "bind a relative path"); let cl = usock(SOCK_STREAM); c.check(uconnect(cl, "/tmp/rel.sock") == 0, "...and connect to it by its full path"); c.check(name_of(SYS_GETSOCKNAME, srv).as_deref() == Ok("rel.sock"), "getsockname: the path as bound"); // Connect to a regular file: ECONNREFUSED. let fd = open("/tmp/plain.txt", O_WRONLY | O_CREAT); close(fd); let x = usock(SOCK_STREAM); c.check(uconnect(x, "/tmp/plain.txt") == -ECONNREFUSED, "connect to a plain file: ECONNREFUSED"); close(x); unlink("/tmp/plain.txt"); // Close the server with the connection never accepted: the client // gets ECONNRESET, then EOF. close(srv); let mut buf = [0u8; 8]; c.check(read(cl, &mut buf) == -ECONNRESET, "server closed before accept: ECONNRESET"); c.check(read(cl, &mut buf) == 0, "...then end-of-file"); c.check(send(cl, b"x", MSG_NOSIGNAL) == -EPIPE, "...and writing: EPIPE"); close(cl); unlink("/tmp/rel.sock"); // Abstract names: no file anywhere. let a = usock(SOCK_STREAM); c.check(ubind(a, "@rustbelt-test") == 0 && listen(a, 2) == 0, "bind an abstract name"); c.check(name_of(SYS_GETSOCKNAME, a).as_deref() == Ok("@rustbelt-test"), "getsockname: @rustbelt-test"); let b = usock(SOCK_STREAM); c.check(ubind(b, "@rustbelt-test") == -EADDRINUSE, "the same abstract name again: EADDRINUSE"); c.check(uconnect(b, "@rustbelt-test") == 0, "connect to the abstract name"); let conn = uaccept(a); c.check(name_of(SYS_GETPEERNAME, b).as_deref() == Ok("@rustbelt-test"), "getpeername: @rustbelt-test"); // ECONNRESET: close with data the peer sent still unread. write(b, b"unread"); sched_yield(); close(conn); c.check(read(b, &mut buf) == -ECONNRESET, "peer closed with our data unread: ECONNRESET"); c.check(getopt(b, SO_ERROR) == (0, 0), "SO_ERROR cleared after that"); close(b); close(a); let again = usock(SOCK_STREAM); c.check(ubind(again, "@rustbelt-test") == 0, "the abstract name is free once its socket closed"); close(again); // Autobind: bind with just the family. let au = usock(SOCK_STREAM); let fam = [AF_UNIX as u8, 0]; c.check(syscall3(SYS_BIND, au as u64, fam.as_ptr() as u64, 2) == 0, "bind with only the family (autobind)"); let n = name_of(SYS_GETSOCKNAME, au).unwrap_or_default(); c.check(n.len() == 6 && n.starts_with('@'), &format!("...gets an abstract name of 5 hex digits ({})", n)); close(au); // Odd calls. let s = usock(SOCK_STREAM); c.check(read(s, &mut buf) == -EINVAL, "read on an unconnected stream socket: EINVAL"); c.check(send(s, b"x", MSG_NOSIGNAL) == -ENOTCONN, "send on it: ENOTCONN"); c.check(name_of(SYS_GETPEERNAME, s) == Err(-ENOTCONN), "getpeername: ENOTCONN"); c.check(name_of(SYS_GETSOCKNAME, s).as_deref() == Ok(""), "getsockname of an unbound socket: just the family"); c.check(getopt(s, SO_DOMAIN) == (0, 1) && getopt(s, SO_TYPE) == (0, 1), "SO_DOMAIN AF_UNIX, SO_TYPE SOCK_STREAM"); let (r, cred) = peercred(s); c.check(r == 0 && cred == [0, u32::MAX, u32::MAX], "SO_PEERCRED with no peer: pid 0, ids -1"); c.check(listen(s, 1) == -EINVAL, "listen while unbound: EINVAL"); close(s); c.check(syscall3(SYS_SOCKET, AF_UNIX, 9, 0) == -94, "socket type 9: ESOCKTNOSUPPORT"); c.check(syscall3(SYS_SOCKET, AF_UNIX, SOCK_STREAM, 7) == -93, "protocol 7: EPROTONOSUPPORT"); let d = usock(SOCK_DGRAM); c.check(listen(d, 1) == -EOPNOTSUPP, "listen on a datagram socket: EOPNOTSUPP"); close(d); let fd = open("/dev/null", O_RDONLY); c.check(syscall3(SYS_SHUTDOWN, fd as u64, 1, 0) == -ENOTSOCK, "shutdown on a non-socket: ENOTSOCK"); close(fd); } // --- socketpair ------------------------------------------------------------------------ fn pairs(c: &mut Checks) { let (a, b) = pair(SOCK_STREAM); c.check(a >= 0 && b >= 0, "socketpair(AF_UNIX, SOCK_STREAM)"); c.check(write(a, b"ab") == 2 && write(b, b"cd") == 2, "both ends write"); let mut buf = [0u8; 8]; c.check(read(b, &mut buf) == 2 && &buf[..2] == b"ab", "b reads a's bytes"); c.check(read(a, &mut buf) == 2 && &buf[..2] == b"cd", "a reads b's bytes"); let (r, cred) = peercred(a); c.check(r == 0 && cred[0] as i64 == getpid(), "SO_PEERCRED: our own pid"); c.check(name_of(SYS_GETPEERNAME, a).as_deref() == Ok(""), "getpeername: unnamed (but no error)"); // Across fork: the child talks through its copy. let child = fork(); if child == 0 { close(a); let mut buf = [0u8; 16]; let n = read(b, &mut buf); let ok = n == 4 && &buf[..4] == b"ping" && write(b, b"pong") == 4; exit(if ok { 0 } else { 1 }); } write(a, b"ping"); c.check(read(a, &mut buf) == 4 && &buf[..4] == b"pong", "a forked child answers through the pair"); c.check(wait(child) == 0, "child happy"); // Shutdown one way. c.check(syscall3(SYS_SHUTDOWN, a as u64, 1, 0) == 0, "shutdown(SHUT_WR)"); c.check(read(b, &mut buf) == 0, "the other end reads EOF"); c.check(send(a, b"x", MSG_NOSIGNAL) == -EPIPE, "writing after SHUT_WR: EPIPE"); c.check(write(b, b"still") == 5 && read(a, &mut buf) == 5, "the other direction still works"); c.check(syscall3(SYS_SHUTDOWN, a as u64, 7, 0) == -EINVAL, "shutdown(7): EINVAL"); close(b); c.check(read(a, &mut buf) == 0, "peer closed: EOF"); c.check(send(a, b"x", MSG_NOSIGNAL) == -EPIPE, "peer closed: EPIPE"); close(a); // MSG_PEEK let (a, b) = pair(SOCK_STREAM); write(a, b"peekaboo"); c.check(recv(b, &mut buf[..4], MSG_PEEK) == 4 && &buf[..4] == b"peek", "MSG_PEEK looks"); c.check(read(b, &mut buf) == 8 && &buf == b"peekaboo", "...without taking"); close(a); close(b); // SEQPACKET: connected, but message borders stay. let (a, b) = pair(SOCK_SEQPACKET); c.check(a >= 0, "socketpair(SOCK_SEQPACKET)"); write(a, b"one"); write(a, b"three"); c.check(read(b, &mut buf) == 3 && &buf[..3] == b"one", "seqpacket: first message alone"); c.check(read(b, &mut buf[..2]) == 2 && &buf[..2] == b"th", "seqpacket: a short read..."); c.check(recv(b, &mut buf, MSG_DONTWAIT) == -EAGAIN, "...throws the rest away"); c.check(getopt(a, SO_TYPE) == (0, 5), "SO_TYPE: SOCK_SEQPACKET"); close(a); c.check(read(b, &mut buf) == 0, "seqpacket: peer closed, EOF"); close(b); // DGRAM pair. let (a, b) = pair(SOCK_DGRAM); send(a, b"x", 0); send(a, b"yz", 0); c.check(read(b, &mut buf) == 1 && read(b, &mut buf) == 2, "dgram pair: two messages, two reads"); c.check(send(a, b"", 0) == 0 && read(b, &mut buf) == 0, "an empty datagram is a message too"); close(a); close(b); // AF_INET can't do pairs; other families don't exist. let mut sv = [0i32; 2]; c.check(syscall5(SYS_SOCKETPAIR, 2, 1, 0, sv.as_mut_ptr() as u64, 0) == -EOPNOTSUPP, "socketpair(AF_INET): EOPNOTSUPP"); c.check(syscall5(SYS_SOCKETPAIR, 1, 1, 0, 0, 0) == -EFAULT, "socketpair to a bad pointer: EFAULT"); // CLOEXEC / NONBLOCK flags. let (a, b) = pair(SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK); c.check(fcntl(a, F_GETFD, 0) == FD_CLOEXEC as i64 && fcntl(b, F_GETFD, 0) == FD_CLOEXEC as i64, "SOCK_CLOEXEC sets FD_CLOEXEC"); c.check(read(a, &mut buf) == -EAGAIN, "SOCK_NONBLOCK: EAGAIN"); close(a); close(b); } // --- datagrams -------------------------------------------------------------------------- fn dgram(c: &mut Checks) { unlink("/tmp/d1.sock"); let a = usock(SOCK_DGRAM); let b = usock(SOCK_DGRAM); c.check(ubind(a, "/tmp/d1.sock") == 0 && ubind(b, "@dgram-b") == 0, "bind two datagram sockets"); c.check(send_to(b, b"to a", "/tmp/d1.sock") == 4, "sendto a path"); c.check(send_to(a, b"to b!", "@dgram-b") == 5, "sendto an abstract name"); let mut buf = [0u8; 16]; let (n, from) = recv_from(a, &mut buf, 0); c.check(n == 4 && from == "@dgram-b", &format!("recvfrom says who sent it ({})", from)); let (n, from) = recv_from(b, &mut buf, 0); c.check(n == 5 && from == "/tmp/d1.sock", &format!("...a path, too ({})", from)); // Truncation. send_to(b, b"0123456789", "/tmp/d1.sock"); c.check(recv(a, &mut buf[..4], MSG_TRUNC) == 10, "MSG_TRUNC: the real length"); send_to(b, b"0123456789", "/tmp/d1.sock"); let g = recvmsg(a, 4, 0, 0); c.check(g.n == 4 && g.flags & 0x20 != 0, "recvmsg: MSG_TRUNC in msg_flags"); // From an unnamed socket. let u = usock(SOCK_DGRAM); send_to(u, b"anon", "@dgram-b"); let (n, from) = recv_from(b, &mut buf, 0); c.check(n == 4 && from.is_empty(), "from an unbound socket: no name"); c.check(send(u, b"x", 0) == -ENOTCONN, "send with no address, not connected: ENOTCONN"); c.check(send_to(u, b"x", "@nobody-here") == -ECONNREFUSED, "sendto a name nobody has: ECONNREFUSED"); // connect sets the default destination; then only that peer may send to us. c.check(uconnect(u, "@dgram-b") == 0 && send(u, b"conn", 0) == 4, "connect + send"); c.check(recv(b, &mut buf, 0) == 4, "...arrives"); c.check(uconnect(b, "/tmp/d1.sock") == 0, "b connects to a"); c.check(send_to(u, b"x", "@dgram-b") == -EPERM, "b is connected to a: others get EPERM"); c.check(name_of(SYS_GETPEERNAME, b).as_deref() == Ok("/tmp/d1.sock"), "getpeername on a connected datagram socket"); // Disconnect with AF_UNSPEC. let unspec = [0u8; 2]; c.check(syscall3(SYS_CONNECT, b as u64, unspec.as_ptr() as u64, 2) == 0 && send_to(u, b"ok", "@dgram-b") == 2, "connect(AF_UNSPEC) undoes it"); recv(b, &mut buf, 0); // Wrong kinds. let s = usock(SOCK_STREAM); c.check(uconnect(s, "@dgram-b") == -EPROTOTYPE, "a stream socket connecting to a datagram one: EPROTOTYPE"); close(s); c.check(send_to(b, &alloc::vec![0u8; 300_000], "/tmp/d1.sock") == -EMSGSIZE, "a datagram bigger than the buffer: EMSGSIZE"); // The peer goes away. close(a); c.check(send_to(b, b"x", "/tmp/d1.sock") == -ECONNREFUSED, "sendto a closed socket's name: ECONNREFUSED"); c.check(send(u, b"x", 0) >= 0, "(u is still connected to b)"); close(b); c.check(send(u, b"x", 0) == -ECONNREFUSED, "connected peer closed: ECONNREFUSED"); close(u); unlink("/tmp/d1.sock"); } // --- SCM_RIGHTS --------------------------------------------------------------------------- fn rights(c: &mut Checks) { let fd = open("/tmp/rights.txt", O_RDWR | O_CREAT | O_TRUNC); write(fd, b"file data"); lseek(fd, 0, SEEK_SET); let (pr, pw) = pipe().unwrap(); let (a, b) = pair(SOCK_STREAM); c.check(sendmsg(a, &[b"fd", b"s!"], &[fd as i32, pr as i32], 0) == 4, "sendmsg with two fds (a file and a pipe end)"); close(fd); close(pr); let g = recvmsg(b, 16, 64, 0); c.check(g.n == 4 && g.data == b"fds!", "recvmsg: the data, gathered and scattered"); c.check(g.fds.len() == 2 && g.flags & MSG_CTRUNC == 0, &format!("recvmsg: two new fds ({:?})", g.fds)); if g.fds.len() == 2 { let (f, p) = (g.fds[0] as i64, g.fds[1] as i64); c.check(read_all(f) == b"file data", "read the file through the received fd"); write(pw, b"pipe data"); close(pw); c.check(read_all(p) == b"pipe data", "read the pipe through the received end"); c.check(fcntl(f, F_GETFD, 0) == 0, "no FD_CLOEXEC without MSG_CMSG_CLOEXEC"); close(f); close(p); } else { close(pw); } // More fds than room: MSG_CTRUNC, the rest closed. let (pr, pw) = pipe().unwrap(); let three = [pr as i32, pr as i32, pr as i32]; c.check(sendmsg(a, &[b"3"], &three, 0) == 1, "send the same fd three times"); close(pr); let g = recvmsg(b, 8, 16 + 4, MSG_CMSG_CLOEXEC); // CMSG_LEN(4) c.check(g.fds.len() == 1 && g.flags & MSG_CTRUNC != 0, &format!("room for one: one fd and MSG_CTRUNC ({:?})", g.fds)); if let Some(&f) = g.fds.first() { c.check(fcntl(f as i64, F_GETFD, 0) == FD_CLOEXEC as i64, "MSG_CMSG_CLOEXEC sets FD_CLOEXEC"); close(f as i64); } c.check(write(pw, b"x") == -EPIPE, "the copies that didn't fit were closed (the pipe has no reader)"); close(pw); // In-flight fds die with the socket: send a pipe's read end, never // receive it, close the pair. The pipe's writer sees no reader left. let (pr, pw) = pipe().unwrap(); sendmsg(a, &[b"x"], &[pr as i32], 0); close(pr); c.check(write(pw, b"y") == 1, "the read end is alive while in flight"); close(a); close(b); c.check(write(pw, b"y") == -EPIPE, "closing the sockets closed the fd in flight"); close(pw); // Data before an fd message doesn't merge with it. let (a, b) = pair(SOCK_STREAM); let (pr, pw) = pipe().unwrap(); write(a, b"aa"); sendmsg(a, &[b"bb"], &[pr as i32], 0); let g = recvmsg(b, 16, 64, 0); c.check(g.n == 2 && g.data == b"aa" && g.fds.is_empty(), "stream: a read stops before a message carrying fds"); let g = recvmsg(b, 16, 64, 0); c.check(g.n == 2 && g.fds.len() == 1, "...which comes with the next read"); for f in g.fds { close(f as i64); } close(pr); close(pw); // read() can't take fds: they're closed. let (pr, pw) = pipe().unwrap(); sendmsg(a, &[b"z"], &[pr as i32], 0); close(pr); let mut buf = [0u8; 4]; c.check(read(b, &mut buf) == 1, "plain read of a message with an fd"); c.check(write(pw, b"x") == -EPIPE, "...closed the fd"); close(pw); // Bad fds, bad control messages. c.check(sendmsg(a, &[b"z"], &[999], 0) == -EBADF, "sending fd 999: EBADF"); // SCM_CREDENTIALS with SO_PASSCRED. c.check(setopt(b, SO_PASSCRED, 1) == 0 && getopt(b, SO_PASSCRED) == (0, 1), "SO_PASSCRED on"); write(a, b"who"); let g = recvmsg(b, 8, 64, 0); c.check(g.cred.is_some_and(|cr| cr[0] as i64 == getpid() && cr[1] == 0), &format!("SCM_CREDENTIALS: our pid, uid 0 ({:?})", g.cred)); close(a); close(b); // A datagram socket can carry fds too. let (a, b) = pair(SOCK_DGRAM); let (pr, pw) = pipe().unwrap(); sendmsg(a, &[b"d"], &[pw as i32], 0); close(pw); let g = recvmsg(b, 8, 64, 0); c.check(g.fds.len() == 1 && write(g.fds[0] as i64, b"via dgram") == 9, "a pipe's write end through a datagram"); for f in g.fds { close(f as i64); } c.check(read_all(pr) == b"via dgram", "...and the data arrives"); close(pr); close(a); close(b); unlink("/tmp/rights.txt"); } // --- nonblocking, buffers, poll --------------------------------------------------------- fn nonblock(c: &mut Checks) { let (a, b) = pair(SOCK_STREAM | SOCK_NONBLOCK); let mut buf = [0u8; 4096]; c.check(read(a, &mut buf) == -EAGAIN, "nothing to read: EAGAIN"); let mut fds = [PollFd { fd: a as i32, events: POLLIN | POLLOUT, revents: 0 }]; c.check(poll(&mut fds, 0) == 1 && fds[0].revents == POLLOUT, "poll: writable, not readable"); // Small buffers, so they fill fast. setsockopt doubles (like Linux). c.check(setopt(a, SO_SNDBUF, 4096) == 0 && getopt(a, SO_SNDBUF) == (0, 8192), "SO_SNDBUF 4096 reads back 8192"); c.check(getopt(b, SO_RCVBUF).1 == 212_992, "SO_RCVBUF default 212992"); let mut total = 0; loop { let n = write(a, &buf); if n < 0 { c.check(n == -EAGAIN, "full: EAGAIN"); break; } total += n; if total > 1 << 20 { break; } } c.check(total == 8192, &format!("the buffer limit: {} bytes went in", total)); fds[0].events = POLLOUT; c.check(poll(&mut fds, 0) == 0, "poll: not writable when full"); c.check(read(b, &mut buf[..1000]) == 1000, "read some"); c.check(poll(&mut fds, 0) == 1, "writable again"); while read(b, &mut buf) > 0 {} // Blocking backpressure: a child writes 100 KB through a small // buffer; it has to wait for us to read. let (x, y) = pair(SOCK_STREAM); setopt(x, SO_SNDBUF, 2048); let child = fork(); if child == 0 { let data = alloc::vec![7u8; 100_000]; let n = write(x, &data); exit(if n == 100_000 { 0 } else { 1 }); } close(x); sleep_ms(100); let mut got = 0; loop { let n = read(y, &mut buf); if n <= 0 { break; } got += n; } c.check(got == 100_000, &format!("a blocked writer got everything through ({} bytes)", got)); c.check(wait(child) == 0, "...in one write() call"); close(y); // epoll: EPOLLRDHUP when the peer shuts down writing, EPOLLHUP when it's gone. let ep = epoll_create1(0); epoll_ctl(ep, EPOLL_CTL_ADD, a, EPOLLIN | EPOLLRDHUP, 1); let mut ev = [EpollEvent::default(); 2]; c.check(epoll_wait(ep, &mut ev, 0) == 0, "epoll: nothing yet"); syscall3(SYS_SHUTDOWN, b as u64, 1, 0); let n = epoll_wait(ep, &mut ev, 100); let e = ev[0].events; c.check(n == 1 && e & EPOLLRDHUP != 0 && e & EPOLLIN != 0 && e & EPOLLHUP == 0, &format!("peer SHUT_WR: EPOLLIN|EPOLLRDHUP (0x{:x})", e)); close(b); let n = epoll_wait(ep, &mut ev, 100); let e = ev[0].events; c.check(n == 1 && e & EPOLLHUP != 0, &format!("peer closed: EPOLLHUP (0x{:x})", e)); close(ep); close(a); // A nonblocking listener; poll says when a connection waits. let l = usock(SOCK_STREAM | SOCK_NONBLOCK); ubind(l, "@nb-listen"); listen(l, 1); c.check(uaccept(l) == -EAGAIN, "accept with nobody waiting: EAGAIN"); let mut pf = [PollFd { fd: l as i32, events: POLLIN, revents: 0 }]; c.check(poll(&mut pf, 0) == 0, "poll: listener not readable"); let cl = usock(SOCK_STREAM); c.check(uconnect(cl, "@nb-listen") == 0, "connect (goes into the backlog)"); c.check(poll(&mut pf, 0) == 1 && pf[0].revents == POLLIN, "poll: listener readable"); let conn = syscall5(SYS_ACCEPT4, l as u64, 0, 0, SOCK_NONBLOCK | SOCK_CLOEXEC, 0); c.check(conn >= 0 && fcntl(conn, F_GETFD, 0) == FD_CLOEXEC as i64, "accept4(SOCK_CLOEXEC|SOCK_NONBLOCK)"); c.check(read(conn, &mut buf) == -EAGAIN, "...the new socket is nonblocking"); c.check(syscall5(SYS_ACCEPT4, l as u64, 0, 0, 1, 0) == -EINVAL, "accept4 with a bad flag: EINVAL"); // A full backlog: a nonblocking connect says EAGAIN. let mut extra = Vec::new(); let mut r = 0; for _ in 0..5 { let s = usock(SOCK_STREAM | SOCK_NONBLOCK); r = uconnect(s, "@nb-listen"); extra.push(s); if r < 0 { break; } } c.check(r == -EAGAIN, "backlog full: nonblocking connect gets EAGAIN"); for s in extra { close(s); } close(conn); close(cl); // Unconnected stream socket: POLLOUT|POLLHUP (like Linux). let s = usock(SOCK_STREAM); let mut pf = [PollFd { fd: s as i32, events: POLLIN | POLLOUT, revents: 0 }]; poll(&mut pf, 0); c.check(pf[0].revents == POLLOUT | POLLHUP, "an unconnected stream socket polls OUT|HUP"); close(s); close(l); // recv(MSG_DONTWAIT) on a blocking socket. let (a, b) = pair(SOCK_STREAM); c.check(recv(a, &mut buf, MSG_DONTWAIT) == -EAGAIN, "MSG_DONTWAIT: EAGAIN on a blocking socket"); // SO_RCVTIMEO set_recv_timeout(a, 100); let t = now_us(); c.check(read(a, &mut buf) == -EAGAIN && now_us() - t >= 90_000, "SO_RCVTIMEO: EAGAIN after the timeout"); c.check(send_to(a, b"x", "@somewhere") == -EISCONN, "sendto with an address on a connected stream: EISCONN"); let mut v = 0u32; c.check(ioctl(b, 0x541B, &mut v as *mut u32 as u64) == 0 && v == 0, "FIONREAD 0"); write(a, b"12345"); c.check(ioctl(b, 0x541B, &mut v as *mut u32 as u64) == 0 && v == 5, "FIONREAD 5"); close(a); close(b); } // --- /proc/self ----------------------------------------------------------------------- fn proc(c: &mut Checks) { let f = open("/tmp/proc.txt", O_RDWR | O_CREAT | O_TRUNC); write(f, b"proc data"); let (pr, pw) = pipe().unwrap(); let (a, b) = pair(SOCK_STREAM); // The listing. let dir = open("/proc/self/fd", O_RDONLY | O_DIRECTORY); c.check(dir >= 0, "open /proc/self/fd"); let mut names = Vec::new(); let mut buf = [0u8; 2048]; loop { let n = getdents64(dir, &mut buf); if n <= 0 { break; } for_each_dirent(&buf, n as usize, |name, _| names.push(String::from(name))); } close(dir); let want = [0, 1, 2, f, pr, pw, a, b, dir]; c.check(want.iter().all(|fd| names.iter().any(|n| *n == format!("{}", fd))), &format!("the listing has every fd ({:?})", names)); c.check(!names.iter().any(|n| *n == "99"), "...and no others"); // readlink c.check(link(&format!("/proc/self/fd/{}", f)) == "/tmp/proc.txt", "readlink: a file's path"); let pl = link(&format!("/proc/self/fd/{}", pr)); let mut st = Stat::default(); fstat(pr, &mut st); c.check(pl == format!("pipe:[{}]", st.ino) && link(&format!("/proc/self/fd/{}", pw)) == pl, &format!("readlink: {} for both pipe ends (= st_ino)", pl)); let sl = link(&format!("/proc/self/fd/{}", a)); fstat(a, &mut st); c.check(sl == format!("socket:[{}]", st.ino) && is_socket(&st), &format!("readlink: {} (= st_ino, S_IFSOCK)", sl)); c.check(link(&format!("/proc/self/fd/{}", b)) != sl, "the other end is another socket"); let mut ls = Stat::default(); c.check(lstat(&format!("/proc/self/fd/{}", f), &mut ls) == 0 && ls.is_symlink(), "lstat: a symlink"); c.check(stat(&format!("/proc/self/fd/{}", a), &mut ls) == 0 && is_socket(&ls) && ls.ino == st.ino, "stat follows it, even to a socket"); c.check(link("/proc/self/fd/99") == "error -2", "readlink of a closed fd: ENOENT"); let exe = link("/proc/self/exe"); c.check(exe.ends_with("/unixtest"), &format!("/proc/self/exe: {}", exe)); // Opening an entry. let again = open(&format!("/proc/self/fd/{}", f), O_RDONLY); c.check(read_all(again) == b"proc data", "open /proc/self/fd/N of a file: a fresh offset"); close(again); let rd = open(&format!("/proc/self/fd/{}", pr), O_RDONLY); write(pw, b"via proc"); let mut b8 = [0u8; 8]; c.check(rd >= 0 && read(rd, &mut b8) == 8 && &b8 == b"via proc", "open /proc/self/fd/N of a pipe: the same pipe"); close(rd); c.check(open(&format!("/proc/self/fd/{}", a), O_RDWR) == -ENXIO, "open /proc/self/fd/N of a socket: ENXIO"); c.check(open("/proc/self/fd/99", O_RDONLY) == -ENOENT, "open of a closed fd: ENOENT"); let null = open("/dev/null", O_RDONLY); c.check(link(&format!("/proc/self/fd/{}", null)) == "/dev/null", "readlink: /dev/null"); close(null); let ep = epoll_create1(0); c.check(link(&format!("/proc/self/fd/{}", ep)) == "anon_inode:[eventpoll]", "readlink: anon_inode:[eventpoll]"); close(ep); let mut root = Vec::new(); let d = open("/proc", O_RDONLY | O_DIRECTORY); let n = getdents64(d, &mut buf); if n > 0 { for_each_dirent(&buf, n as usize, |name, _| root.push(String::from(name))); } close(d); c.check(root.iter().any(|n| n == "self"), "/proc lists self"); c.check(mkdir("/proc/self/x") < 0, "nothing can be made in /proc"); // A child sees its own fds. let child = fork(); if child == 0 { close(f); exit(if link(&format!("/proc/self/fd/{}", f)) == "error -2" { 0 } else { 1 }); } c.check(wait(child) == 0 && link(&format!("/proc/self/fd/{}", f)) == "/tmp/proc.txt", "self is whoever asks"); for fd in [f, pr, pw, a, b] { close(fd); } unlink("/tmp/proc.txt"); }