foundry
Foundry OS: the operating system built on the Rustbelt kernel
| author | Edward <edward@localhost> | 2026-10-31 17:45:51 -0700 |
|---|---|---|
| commit | 4b17d4fa030840eee1bab6fff28d3b5e14e22997 | |
| parent | 85f5cd4654028d87fb6ef32f915a0073f0064d2d | |
| tree | browse files at this commit | |
Stage 18: Unix sockets and /proc/self (0.18.0) Local IPC good enough for Wayland, in src/net/unix.rs: - AF_UNIX SOCK_STREAM, SOCK_DGRAM, SOCK_SEQPACKET; path (ramfs, ext2 socket inodes, S_IFSOCK; EPERM on FAT/exFAT; open() -> ENXIO), abstract and unnamed addresses, autobind - socket/socketpair/bind/listen/connect/accept/accept4/send(to)/ recv(from)/sendmsg/recvmsg/shutdown/getsockname/getpeername/ get/setsockopt, FIONREAD; poll/select/epoll incl. POLLRDHUP/POLLHUP - SCM_RIGHTS with in-flight refcounts (closed if the receiver dies), MSG_CTRUNC, MSG_CMSG_CLOEXEC, SCM_MAX_FD; SCM_CREDENTIALS (checked), SO_PASSCRED, SO_PEERCRED - byte backpressure: min(SO_SNDBUF, peer SO_RCVBUF), setsockopt doubles like Linux, getsockopt honours *optlen; dgram adds 256 B/message - sendmsg/recvmsg (46/47) for INET sockets too (name + iovecs) - /proc (src/fs/procfs.rs): /proc/self/fd listing, readlink (path, pipe:[N], socket:[N], anon_inode:[eventpoll]), open-as-reopen/ dup, stat following; /proc/self/exe moved here Tests: user/unixtest.rs + tests/unix_sockets.rs (7 new, 173 total). README stage 18 chapter and table updates. Deferred: GC for socket cycles, dgram qlen limit, SO_PASSCRED autobind, /proc/<pid>, control messages on INET sockets.
| M | coretest.rs | +4 -1 |
| M | nbtest.rs | +1 -1 |
| A | unixtest.rs | +867 -0 |
3 files changed, 872 insertions, 2 deletions
@@ -540,7 +540,10 @@ fn sockets(c: &mut Checks) { } close(nb); let mut sv = [0i32; 2];- c.check(syscall5(SYS_SOCKETPAIR, 1, 1, 0, sv.as_mut_ptr() as u64, 0) == -EAFNOSUPPORT, "socketpair(AF_UNIX): EAFNOSUPPORT (no AF_UNIX yet)");+ c.check(syscall5(SYS_SOCKETPAIR, 1, 1, 0, sv.as_mut_ptr() as u64, 0) == 0, "socketpair(AF_UNIX) works (stage 18)");+ close(sv[0] as i64);+ close(sv[1] as i64);+ c.check(syscall5(SYS_SOCKETPAIR, 3, 1, 0, sv.as_mut_ptr() as u64, 0) == -EAFNOSUPPORT, "socketpair(family 3): EAFNOSUPPORT"); c.check(syscall3(SYS_SHUTDOWN, 0, 1, 0) == -88, "shutdown on a non-socket: ENOTSOCK"); }
@@ -137,7 +137,7 @@ fn main() -> i64 { c.check(n > 0 && cwd[0] == b'/', "getcwd gives an absolute path"); let mut ub = [0u8; 6 * 65]; let u = uname(&mut ub);- c.check(u[0] == "Rustbelt" && u[3].starts_with("stage 17"), "uname: Rustbelt, stage 17");+ c.check(u[0] == "Rustbelt" && u[3].starts_with("stage 18"), "uname: Rustbelt, stage 18"); let (secs, _) = clock_gettime(0); c.check(secs > 1_600_000_000, "CLOCK_REALTIME is a real date (after 2020)");
@@ -0,0 +1,867 @@+//! 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<String, i64> {+ 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<IoVec> = 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<u8>,+ fds: Vec<i32>,+ 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<u32> = 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<u8> {+ 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<u8> = 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");+}