RustForge

foundry

Foundry OS: the operating system built on the Rustbelt kernel

authorEdward <edward@localhost>2026-10-31 17:45:51 -0700
commit4b17d4fa030840eee1bab6fff28d3b5e14e22997
parent85f5cd4654028d87fb6ef32f915a0073f0064d2d
treebrowse 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.
Mcoretest.rs+4 -1
Mnbtest.rs+1 -1
Aunixtest.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");+}