RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

authorEdward <edward@localhost>2026-10-31 17:45:51 -0700
commitdea72d3d03399a30d1eee0627380adfd6a6c888e
parenta91bf2c1317b4b7dc0c293615e379acf56ae0183
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.
MCargo.lock+1 -1
MCargo.toml+1 -1
MREADME.md+252 -12
Mbuild.rs+1 -0
Mscripts/run-headless.sh+6 -6
Mscripts/screenshot.sh+1 -1
Msrc/dev.rs+9 -0
Msrc/errno.rs+7 -3
Msrc/fs/exfat.rs+1 -1
Msrc/fs/ext2.rs+10 -3
Msrc/fs/fat.rs+2 -2
Msrc/fs/file.rs+25 -0
Msrc/fs/mod.rs+20 -5
Msrc/fs/pipe.rs+10 -0
Asrc/fs/procfs.rs+177 -0
Msrc/fs/ramfs.rs+5 -0
Msrc/net/mod.rs+2 -0
Asrc/net/unix.rs+1513 -0
Msrc/process.rs+6 -0
Msrc/syscall.rs+62 -12
Msrc/syscall_ext.rs+1 -1
Msrc/tty.rs+3 -0
Mtests/shell.rs+1 -1
Atests/unix_sockets.rs+107 -0
Mtests/vfs.rs+2 -2
Muser/coretest.rs+4 -1
Muser/nbtest.rs+1 -1
Auser/unixtest.rs+867 -0

28 files changed, 3097 insertions, 53 deletions

@@ -261,7 +261,7 @@ dependencies = [  [[package]] name = "rustbelt"-version = "0.17.0"+version = "0.18.0" dependencies = [  "bootloader",  "conquer-once",
@@ -1,6 +1,6 @@ [package] name = "rustbelt"-version = "0.17.0"+version = "0.18.0" edition = "2024"  [dependencies]
@@ -93,6 +93,12 @@ Under the shell:   file times; `prctl`, `sched_getaffinity`, `madvise` and robust futex   lists. Orphaned children are adopted by a subreaper or clean themselves   up.+- **Unix sockets and /proc/self (stage 18).** `AF_UNIX` stream, datagram+  and seqpacket sockets with file-system, abstract and unnamed addresses,+  `socketpair`, `sendmsg`/`recvmsg` that can **pass open files** between+  programs (`SCM_RIGHTS`) and say who sent them (`SCM_CREDENTIALS`,+  `SO_PEERCRED`), real buffer limits, and `/proc/self/fd` and+  `/proc/self/exe`. That's what Wayland, D-Bus and X11 talk over.  It has a `cargo test` suite that runs inside QEMU, and a headless self-test that drives the shell with a script.@@ -982,7 +988,7 @@ heap_allocation::simple_allocation...               [ok] | `tests/executor.rs` | `SimpleExecutor` runs an async fn to completion; the waker-based `Executor` interleaves two tasks that `yield_now().await` (a1 b1 a2 b2 a3 b3) | | `tests/shell.rs` | **the shell**: tokenizer (operators without spaces, quotes, comments), parser (pipelines, `<` `>` `>>`, `&`, error cases), drive-letter assignment from a mount table, `B:/x` -> `/mnt/disk0/x` translation and back for the prompt; then the real `/bin/sh` runs a script (cd with a drive letter, ls, a pipe, `>`/`>>`/`<`, `ver`, `exit 3`) with its stdout in a pipe and the output is checked; background jobs (`start`, `jobs`, `wait`); 20 processes started and reaped leave `frames_in_use` unchanged | | `tests/user_mode.rs` | programs loaded from the FAT disk: `hello` exits 0, `demo` passes all its checks (exit 0), `evil` is killed by SIGSEGV (code 139), `spin` is preempted; spawn with arguments + trace + exit code 7, then reaped; a child's stdout into a pipe; spawn errors (`ENOENT`, `ENOEXEC`, `EISDIR`, `ECHILD`); `wait` with `WNOHANG` returns `EAGAIN` while the child runs; ELF loader rejects garbage; user pointers give `EFAULT` (length 0 is fine); `sleep` blocks for the right number of ticks; a `WaitQueue` wakes its waiter |-| `tests/vfs.rs` | the root holds `dev`, `etc`, `hello.txt`, `mnt`, `tmp`; read `/hello.txt` to EOF, path normalisation (`.`, `..`, relative to a cwd), `ENOENT`/`ENOTDIR`/`EINVAL`/`EISDIR`/`EEXIST`, `/tmp` is a separate ramfs, longest-prefix mounts (4 with `/dev`), sparse writes zero-fill, listing, remove (`ENOTEMPTY`, `EBUSY` on mount points), seek/stat/truncate, `getdents64` record layout, pipes pass bytes and report EOF / `EPIPE` |+| `tests/vfs.rs` | the root holds `dev`, `etc`, `hello.txt`, `mnt`, `proc` (stage 18), `tmp`; read `/hello.txt` to EOF, path normalisation (`.`, `..`, relative to a cwd), `ENOENT`/`ENOTDIR`/`EINVAL`/`EISDIR`/`EEXIST`, `/tmp` is a separate ramfs, longest-prefix mounts (5 with `/dev` and `/proc`), sparse writes zero-fill, listing, remove (`ENOTEMPTY`, `EBUSY` on mount points), seek/stat/truncate, `getdents64` record layout, pipes pass bytes and report EOF / `EPIPE` | | `tests/block.rs` | PCI scan finds the virtio disk; sector 1 of the image is the GPT header; multi-sector reads equal single-sector reads; write + read back; out-of-range and partial-sector requests fail; the GPT partition is registered as `vda1` (with a FAT32 boot sector); MBR parsing on a RAM disk | | `tests/fat.rs` | the FAT32 volume is mounted at `/mnt/disk0` with the expected files (`bin`, `notes`, `readme.txt`, `scripts`); read `readme.txt`; every program in `bin/` is a valid ELF; create/write/read/truncate/remove a 10 KB file; directories with long names, `ENOTEMPTY`; the `BlockIo` adapter handles unaligned reads and writes | | `tests/ext2.rs` | the three ext2 images are mounted (`/mnt/disk1`, `disk4`, `disk5`); permissions and case-sensitive names; `big.txt` line by line and in one read (direct, single and double indirect blocks); `sparse.bin`'s data at 0 / 40 MiB / 65 MiB (triple indirect) and its holes; fast and slow symlinks, `readlink`, removing a link keeps its target; 4 KiB blocks and revision 0; on each image: mkdir, create, a 300 KB write, append, overwrite across a block boundary, a write at 70 MiB (hole + triple indirect), truncate, `ENOTEMPTY`/`EISDIR`/`ENOTDIR`, unlink, rmdir, and the superblock's free counts are back where they started; 200 files grow a directory; copy from FAT to ext2. `scripts/ext2-check.sh` then checks the same kernel's writes with the host's `e2fsck` |@@ -997,7 +1003,8 @@ heap_allocation::simple_allocation...               [ok] | `tests/libc.rs` | **libc plumbing** (stage 15), by running four user test programs twice each (a warm-up, then a run that must leave the thread, process, futex-waiter, pty, socket and frame counts exactly as they were): `threads` (4 threads sum 1..=40,000,000 on several CPUs; a futex mutex keeps a shared counter exact while an unlocked one loses updates; each thread sees its own TLS value through `fs:`; `gettid`; `FUTEX_WAIT` with a stale value is `EAGAIN`, times out, `FUTEX_WAKE` with no waiters; `CLONE_CHILD_CLEARTID` + futex wake for join; `exit` ends one thread, `exit_group` all; `tgkill` and `ESRCH`); `echoserver` (an epoll server and a poll server on 127.0.0.1, three forked clients each, plus UDP over loopback); `ptytest` (canonical mode holds a half line back, backspace editing, CR -> NL, echo, ONLCR; raw mode passes bytes through without echo; `TCGETS`/`TCSETS`; ^C and ^\ kill the child that made the pty its controlling terminal with SIGINT/SIGQUIT and echo "^C"; `/dev/tty` with and without a controlling terminal; closing the master gives the slave EOF); `nbtest` (`EAGAIN` from pipes, sockets and ptys; 64 KiB pipe capacity; poll bits; `fcntl(F_SETFL, O_NONBLOCK)` and `FIONBIO`; nonblocking `connect` is `EINPROGRESS` then `POLLOUT`; `select`, `ppoll`, `readv`/`writev`, `pread`/`pwrite`, `getcwd`, `uname`, `CLOCK_REALTIME`, `nanosleep`, `readlink /proc/self/exe`) | | `tests/musl_prereq.rs` | **musl prerequisites** (stage 15.1), each program twice with no leaks: the FPU is set up (XSAVE with x87 + SSE (+ AVX) under `-cpu max`); `fputest` (16 xmm/ymm registers with a per-thread pattern in 4 threads x 3 processes at once, fork copies them, exec clears them, a signal handler gets clean registers and Linux's fpstate in its frame and may edit it, 40 async signals mid-check, SIGFPE when TCG raises it); `pathtest` (`EFAULT` for NULL/kernel/unmapped/straddling path pointers, `ENAMETOOLONG`, `struct stat` fields and types, `lstat`, `AT_EMPTY_PATH`, every `*at()` call, `O_EXCL`/`O_DIRECTORY`/`O_NOFOLLOW`, `fchdir`, `rename`); `rawabi` (a gcc-built no-libc C program, unmodified) | | `tests/libc_batch1.rs` | **libc batch 1** (stage 16): `libctest` runs one group per test, twice, and nothing (threads, processes, frames, file locks) may be left over: `wait` (dash's `wait`: SIGCHLD blocked, fork, `rt_sigsuspend` returns `EINTR` once the handler ran and puts the old mask back; a SIGCHLD already pending ends it at once); `signals` (`rt_sigpending`, `rt_sigtimedwait` with and without a timeout and from another process, real-time signals queued 3 times with their values, a standard signal only once, `EPERM`/`ESRCH` for `rt_sigqueueinfo`, `sigaltstack` + `SA_ONSTACK`, `EPERM` on the stack, unknown `sa_flags` dropped); `locks` (`F_GETFL` access modes, `F_SETFL`, `F_GETLK`/`F_SETLK`/`F_SETLKW` between two processes, `EDEADLK`, locks dropped on close and on exit, `RLIMIT_NOFILE` vs `dup` and `F_DUPFD`); `files` (`truncate`/`ftruncate`/`fsync` on ramfs, ext2 and FAT, `umask` on `open` and `mkdir` and across fork, `chmod`/`fchmod`/`fchmodat`, `utimensat` with `UTIME_NOW`/`UTIME_OMIT`, `EROFS` on btrfs); `misc` (rlimits, `prctl`, `sched_getaffinity`, `madvise(MADV_DONTNEED)` reads zeros, a robust futex gets `FUTEX_OWNER_DIED`); `orphans` (running and zombie orphans are freed, a `PR_SET_CHILD_SUBREAPER` process adopts and reaps a grandchild); `--trace` names unknown calls `syscall_N` |-| `tests/coreutils_batch2.rs` | **coreutils batch 2** (stage 17): `coretest` runs one group per test, twice, and nothing (threads, processes, frames, file locks, timers) may be left over: `links` (`link`/`linkat`/`symlink`/`symlinkat` on ramfs and ext2 with `st_nlink` and `st_ino`, a dangling symlink, renaming a full directory and over a second name, `EPERM` on FAT/exFAT, `EROFS` on btrfs, `EXDEV`); `owners` (the `chown` family on ramfs and ext2, `-1`, `lchown` vs `chown` on a symlink, FAT's `EPERM` unless nothing changes); `statfs` (`statfs`/`fstatfs` magic numbers and sizes per mount, `ST_RDONLY`, pipes, `statx` vs `stat`, `st_dev` per mount); `info` (`sysinfo` 99 and 502, `times`, `getrusage` for self and children, the CPU-time clocks, `clock_getres`); `sockets` (`getsockname`/`getpeername`, `SO_REUSEADDR` & co., `SO_TYPE`, `SO_ERROR` after a refused connect, `SHUT_WR` = end of file for the peer, `EPIPE` after it, `socketpair` refused); `timers` (`alarm`, one-shot and periodic `setitimer`, `pause`, POSIX timers with `SI_TIMER` + `si_value`, `TIMER_ABSTIME`, `SIGEV_NONE`, not inherited by fork); `ids` (`setuid` & co. with the saved id, groups, new files owned by their creator, nice values, `sethostname` -> `uname`); `misc` (`flock` between open files and processes, `preadv`/`pwritev`, `sendfile`, mask sizes for `ppoll`/`pselect6`, a `ppoll` mask letting a signal in, `getdents64` while deleting on ramfs and ext2); `--trace` names (`link`, `statx`, `rb_sysinfo`) |+| `tests/coreutils_batch2.rs` | **coreutils batch 2** (stage 17): `coretest` runs one group per test, twice, and nothing (threads, processes, frames, file locks, timers) may be left over: `links` (`link`/`linkat`/`symlink`/`symlinkat` on ramfs and ext2 with `st_nlink` and `st_ino`, a dangling symlink, renaming a full directory and over a second name, `EPERM` on FAT/exFAT, `EROFS` on btrfs, `EXDEV`); `owners` (the `chown` family on ramfs and ext2, `-1`, `lchown` vs `chown` on a symlink, FAT's `EPERM` unless nothing changes); `statfs` (`statfs`/`fstatfs` magic numbers and sizes per mount, `ST_RDONLY`, pipes, `statx` vs `stat`, `st_dev` per mount); `info` (`sysinfo` 99 and 502, `times`, `getrusage` for self and children, the CPU-time clocks, `clock_getres`); `sockets` (`getsockname`/`getpeername`, `SO_REUSEADDR` & co., `SO_TYPE`, `SO_ERROR` after a refused connect, `SHUT_WR` = end of file for the peer, `EPIPE` after it, `socketpair` refused for `AF_INET` and unknown families; since stage 18 it works for `AF_UNIX`); `timers` (`alarm`, one-shot and periodic `setitimer`, `pause`, POSIX timers with `SI_TIMER` + `si_value`, `TIMER_ABSTIME`, `SIGEV_NONE`, not inherited by fork); `ids` (`setuid` & co. with the saved id, groups, new files owned by their creator, nice values, `sethostname` -> `uname`); `misc` (`flock` between open files and processes, `preadv`/`pwritev`, `sendfile`, mask sizes for `ppoll`/`pselect6`, a `ppoll` mask letting a signal in, `getdents64` while deleting on ramfs and ext2); `--trace` names (`link`, `statx`, `rb_sysinfo`) |+| `tests/unix_sockets.rs` | **Unix sockets and /proc/self** (stage 18): `unixtest` runs one group per test, twice, and threads, processes, frames (so no open file stuck "in flight") and bound socket names must be back where they were: `stream` (a server on a path in `/tmp` and on ext2 echoes for a forked client; `S_IFSOCK` with mode 0777 minus the umask, `EADDRINUSE`, `ENXIO` from `open`, `unlink`, `ENOENT`/`ECONNREFUSED`, `EPERM` on FAT; abstract names and autobind; `ECONNRESET` for unread data and for a connection the server closed before `accept`; `EPIPE`; `SO_PEERCRED`, `getsockname`/`getpeername`); `pair` (`socketpair` both ways and across fork, `SHUT_WR`, `MSG_PEEK`, seqpacket message borders, dgram pairs, `SOCK_CLOEXEC`/`SOCK_NONBLOCK`); `dgram` (sender names, `MSG_TRUNC`, `connect` and `EPERM` for strangers, `AF_UNSPEC`, `EPROTOTYPE`, `EMSGSIZE`, `ECONNREFUSED`); `rights` (a file and a pipe end read through received fds, `MSG_CTRUNC`, `MSG_CMSG_CLOEXEC`, `EBADF` for a bad fd, fds still in flight closed with their socket, a stream read stopping before an fd message, `SCM_CREDENTIALS`); `nonblock` (`EAGAIN`, `SO_SNDBUF` 4096 reading back 8192, the buffer limit, poll bits, a blocked writer finishing in one `write`, `EPOLLRDHUP`/`EPOLLHUP`, nonblocking `accept`/`accept4`, a full backlog, `SO_RCVTIMEO`, `FIONREAD`); `proc` (`/proc/self/fd` listing and links, `pipe:[N]`/`socket:[N]` matching `st_ino`, opening an entry, `/proc/self/exe`); `--trace` names (`sendmsg`, `recvmsg`) |  How it works: `#![feature(custom_test_frameworks)]` collects every `#[test_case]` function and passes them to `rustbelt::test_runner` (in `src/lib.rs`), which@@ -1110,6 +1117,7 @@ src/net/mod.rs            networking: the smoltcp Interface, DHCP + DNS, the `ne src/net/virtio_net.rs     legacy virtio-net driver: RX/TX virtqueues, MAC src/net/device.rs         smoltcp's Device: frames for our own MAC/IP (127.x) loop back, the rest go to the NIC src/net/socket.rs         BSD-style sockets (TCP, UDP, ICMP) on smoltcp, blocking on a wait queue+src/net/unix.rs           stage 18: AF_UNIX sockets (stream, dgram, seqpacket), SCM_RIGHTS, sendmsg/recvmsg user/{ifconfig,ping,host,fetch,httpd}.rs  the network programs user/{random,getty,tile}.rs  stage 13: getrandom, a shell on hvc0, the framebuffer tiling demo user/{forktest,cowtest,mmaptest,sigtest}.rs  stage 14: the process-model checks (each prints ok/FAILED@@ -1123,6 +1131,8 @@ user/libctest.rs          stage 16: libc batch 1 checks, one group per argument  user/coretest.rs          stage 17: coreutils batch 2 checks, one group per argument (tests/coreutils_batch2.rs) src/syscall_ext.rs        stage 17: links, chown, statfs/statx, CPU times, socket names/options, timers, ids, flock... (table at the top) src/timer.rs              stage 17: alarm, setitimer and POSIX timers; the timerd thread that sends their signals+user/unixtest.rs          stage 18: Unix socket and /proc/self checks, one group per argument (tests/unix_sockets.rs)+src/fs/procfs.rs          stage 18: /proc/self/fd and /proc/self/exe, made up from the asking process src/fs/lock.rs            stage 16: fcntl record locks (F_GETLK/F_SETLK/F_SETLKW), deadlock detection src/fpu.rs                stage 15.1: FPU/SSE/AVX on per CPU (CR0/CR4/XCR0), XSAVE or FXSAVE save/restore scripts/hvc0-session.txt  the commands run-headless.sh feeds into hvc0 (for `getty -1 /dev/hvc0`)@@ -1579,19 +1589,21 @@ Numbers are Linux's wherever Linux has the same call, so man pages and | 38 | `setitimer` | which, *new, *old | **stage 17**: `ITIMER_REAL`, one-shot or periodic, in microseconds; fires up to ~110 ms late, never early | | 39 | `getpid` | | | | 40 | `sendfile` | out_fd, in_fd, *offset, count | **stage 17**: in-kernel copy; the input must be seekable (`EINVAL` for a pipe); with `*offset` the input position doesn't move |-| 41 | `socket` | domain, type, protocol | `AF_INET` (2) only; `SOCK_STREAM` (1) = TCP, `SOCK_DGRAM` (2) = UDP, or `SOCK_DGRAM` + `IPPROTO_ICMP` (1) = ping socket (like Linux's unprivileged ping sockets). Returns an fd; `close` closes it |-| 42 | `connect` | fd, *sockaddr_in, len | TCP: blocks for the handshake (10 s, `ETIMEDOUT`; `ECONNREFUSED` on a reset). UDP: sets the default peer |-| 43 | `accept` | fd, *sockaddr_in, *len | blocks for a connection on a listening socket; returns a new fd. Since 15.1 `*len` is read and updated like Linux |+| 41 | `socket` | domain, type, protocol | **Stage 18**: `AF_UNIX` (1) too, with `SOCK_STREAM`, `SOCK_DGRAM` and `SOCK_SEQPACKET` (5), plus `SOCK_NONBLOCK`/`SOCK_CLOEXEC`; see "Unix sockets". `AF_INET` (2): `SOCK_STREAM` (1) = TCP, `SOCK_DGRAM` (2) = UDP, or `SOCK_DGRAM` + `IPPROTO_ICMP` (1) = ping socket (like Linux's unprivileged ping sockets). Returns an fd; `close` closes it |+| 42 | `connect` | fd, *sockaddr_in, len | **Stage 18**: `AF_UNIX` takes a `sockaddr_un` (path, abstract, or `AF_UNSPEC` to disconnect a datagram socket); `ECONNREFUSED` if nobody listens, `EAGAIN` for a full backlog when nonblocking. TCP: blocks for the handshake (10 s, `ETIMEDOUT`; `ECONNREFUSED` on a reset). UDP: sets the default peer |+| 43 | `accept` | fd, *sockaddr_in, *len | (also `AF_UNIX`, stage 18) blocks for a connection on a listening socket; returns a new fd. Since 15.1 `*len` is read and updated like Linux | | 44 | `sendto` | fd, buf, len, flags, *sockaddr_in, len | address 0 = the connected peer. ICMP: you write the echo header, the kernel fills in the id and checksum. `flags` (so `MSG_NOSIGNAL`) and `len` are ignored | | 45 | `recvfrom` | fd, buf, len, flags, *sockaddr_in, *len | blocks (wait queue) until data; 0 = the peer closed (TCP); `EAGAIN` after `SO_RCVTIMEO`. Since 15.1 the address honours `*len` |+| 46 | `sendmsg` | fd, *msghdr, flags | **stage 18**: `struct msghdr` (56 bytes): a name, iovecs and control messages. `AF_UNIX`: `SCM_RIGHTS` (up to 253 fds) and `SCM_CREDENTIALS`; flags `MSG_DONTWAIT`, `MSG_NOSIGNAL`. INET sockets: name + iovecs |+| 47 | `recvmsg` | fd, *msghdr, flags | **stage 18**: fills the iovecs, the sender's name, control messages (fds become new fds; `MSG_CMSG_CLOEXEC`; `MSG_CTRUNC` if they don't fit, the rest are closed) and `msg_flags` (`MSG_TRUNC`); flags `MSG_PEEK`, `MSG_WAITALL`, `MSG_TRUNC`, `MSG_DONTWAIT` | | 48 | `shutdown` | fd, how | **stage 17**: `SHUT_RD`/`SHUT_WR`/`SHUT_RDWR`; `SHUT_WR` sends a FIN (the peer reads 0); `ENOTCONN` if not connected |-| 49 | `bind` | fd, *sockaddr_in, len | local port (address must be 0 or ours) |+| 49 | `bind` | fd, *sockaddr_in, len | local port (address must be 0 or ours). **Stage 18** `AF_UNIX`: makes a socket file (`EADDRINUSE` if the name exists, `EPERM` on FAT/exFAT), an abstract name, or with only the family an automatic abstract name | | 50 | `listen` | fd, backlog | TCP; up to 4 connections can be waiting | | 51 | `getsockname` | fd, *sockaddr_in, *len | **stage 17**: my address and port (after `bind`, `connect` or `accept`) | | 52 | `getpeername` | fd, *sockaddr_in, *len | **stage 17**: the other end; `ENOTCONN` if none |-| 53 | `socketpair` | domain, type, protocol, *[u32; 2] | **stage 17**: needs `AF_UNIX`, which we don't have: `EAFNOSUPPORT` (`EOPNOTSUPP` for `AF_INET`) |+| 53 | `socketpair` | domain, type, protocol, *[u32; 2] | **stage 18**: `AF_UNIX`, any of the three types: two connected sockets (stage 17 had no `AF_UNIX`: `EAFNOSUPPORT`); `EOPNOTSUPP` for `AF_INET` | | 54 | `setsockopt` | fd, level, name, *val, len | `SO_RCVTIMEO` (a `timeval`) is real; **stage 17**: `SO_REUSEADDR`/`SO_REUSEPORT`/`SO_KEEPALIVE`, `SO_SNDTIMEO`, buffer sizes and `TCP_NODELAY` are accepted and remembered; anything else `ENOPROTOOPT`, like Linux |-| 55 | `getsockopt` | fd, level, name, *val, *len | **stage 17**: `SO_TYPE`, `SO_ERROR` (once), `SO_ACCEPTCONN`, `SO_DOMAIN`, `SO_PROTOCOL`, the timeouts, buffer sizes and remembered flags |+| 55 | `getsockopt` | fd, level, name, *val, *len | **stage 17**: `SO_TYPE`, `SO_ERROR` (once), `SO_ACCEPTCONN`, `SO_DOMAIN`, `SO_PROTOCOL`, the timeouts, buffer sizes and remembered flags. **Stage 18** `AF_UNIX`: `SO_PEERCRED` (pid, uid, gid of the other end), `SO_PASSCRED`, and buffer sizes that really limit the queue (`setsockopt` doubles them, like Linux) | | 56 | `clone` | flags, stack, *ptid, *ctid, tls | fork-like (SIGCHLD, optional CLONE_VFORK), or since **stage 15** a thread: `CLONE_VM|FS|FILES|SIGHAND|THREAD|SYSVSEM` plus optional `SETTLS`, `PARENT_SETTID`, `CHILD_SETTID`, `CHILD_CLEARTID`. The child starts on `stack` with rax = 0 | | 57 | `fork` |  | copy-on-write; parent gets the pid, child 0 | | 58 | `vfork` |  | fork + the parent waits until the child execs or exits |@@ -1614,7 +1626,7 @@ Numbers are Linux's wherever Linux has the same call, so man pages and | 86 | `link` | old, new | **stage 17**: a hard link on ramfs and ext2 (`EMLINK` past 32000); `EPERM` on FAT/exFAT, `EROFS` on btrfs, `EXDEV` across file systems | | 87 | `unlink` | path | files and symlinks (the link, not its target); directories give `EISDIR` | | 88 | `symlink` | target, path | **stage 17**: ramfs and ext2; the target needn't exist |-| 89 | `readlink` | path, buf, len | a symlink's target (no NUL); returns its length, cut to `len` like Linux; `EINVAL` if not a symlink. Stage 15: `/proc/self/exe` is the running program's path |+| 89 | `readlink` | path, buf, len | a symlink's target (no NUL); returns its length, cut to `len` like Linux; `EINVAL` if not a symlink. Stage 15: `/proc/self/exe` is the running program's path. Stage 18: `/proc/self/fd/N` too (`pipe:[N]`, `socket:[N]` for things without a path) | | 90 | `chmod` | path, mode | **stage 16**: the 12 permission bits, stored on ramfs and ext2; `EPERM` on FAT/exFAT (nowhere to keep them), `EROFS` on btrfs. Never checked: everyone is root | | 91 | `fchmod` | fd, mode | **stage 16** | | 92 | `chown` | path, uid, gid | **stage 17**: -1 = unchanged; stored on ramfs and ext2 (FAT/exFAT: `EPERM` unless nothing changes); not enforced; `fchown` 93, `lchown` 94 |@@ -1664,7 +1676,7 @@ Numbers are Linux's wherever Linux has the same call, so man pages and | 232 | `epoll_wait` | epfd, *epoll_event, max, timeout_ms | **stage 15**: level-triggered; EPOLLONESHOT supported | | 233 | `epoll_ctl` | epfd, op, fd, *epoll_event | **stage 15**: ADD 1, DEL 2, MOD 3 | | 234 | `tgkill` | tgid, tid, sig | **stage 15**: a signal for one thread |-| 257 | `openat` | dirfd, path, flags, mode | **stage 15.1**: like `open`, relative to `dirfd` or `AT_FDCWD` (-100); musl's `open` calls this |+| 257 | `openat` | dirfd, path, flags, mode | **stage 15.1**: like `open`, relative to `dirfd` or `AT_FDCWD` (-100); musl's `open` calls this. Stage 18: `/proc/self/fd/N` opens fd N's file again (pipes and ttys: the same open file); a socket file is `ENXIO` | | 258 | `mkdirat` | dirfd, path, mode | **stage 15.1** | | 260 | `fchownat` | dirfd, path, uid, gid, flags | **stage 17**: `AT_SYMLINK_NOFOLLOW`, `AT_EMPTY_PATH` | | 262 | `newfstatat` | dirfd, path, *stat, flags | **stage 15.1**: `AT_SYMLINK_NOFOLLOW` 0x100 (= lstat), `AT_EMPTY_PATH` 0x1000 (= `fstat(dirfd)`); musl's `stat`/`fstat`/`lstat` all end up here |@@ -1680,7 +1692,7 @@ Numbers are Linux's wherever Linux has the same call, so man pages and | 273 | `set_robust_list` | *head, 24 | **stage 16**: pthreads' robust mutexes: when the thread dies, each lock it still holds gets `FUTEX_OWNER_DIED` and a waiter is woken. `get_robust_list` 274 | | 280 | `utimensat` | dirfd, path, *[atime, mtime], flags | **stage 16**: set file times (`UTIME_NOW`, `UTIME_OMIT`; NULL = now); path NULL = the fd itself (`futimens`). ext2 keeps whole seconds; btrfs `EROFS` | | 281 | `epoll_pwait` | epfd, *events, max, timeout, *mask, 8 | **stage 15** |-| 288 | `accept4` | fd, *addr, *len, flags | **stage 15**: flags: SOCK_NONBLOCK, SOCK_CLOEXEC |+| 288 | `accept4` | fd, *addr, *len, flags | **stage 15**: flags: SOCK_NONBLOCK, SOCK_CLOEXEC (others `EINVAL` for `AF_UNIX`, stage 18) | | 291 | `epoll_create1` | flags | **stage 15**: EPOLL_CLOEXEC | | 292 | `dup3` | old, new, flags | flags: O_CLOEXEC | | 293 | `pipe2` | *[u32; 2], flags | flags: O_CLOEXEC |@@ -4459,6 +4471,233 @@ Doesn't (yet): - **Rustbelt's own `sysinfo` stays at 502** (as `rb_sysinfo`), next to   Linux's 99. +## Unix sockets and /proc/self (stage 18)++Stage 18 is about programs talking to *each other* on the same machine.+A Wayland compositor and its windows, D-Bus, X11 and most local servers+do that through **Unix domain sockets** (`AF_UNIX`), so this stage is+the plumbing a compositor will need. It also adds a small `/proc`:+`/proc/self/fd` and `/proc/self/exe`, which musl's `ttyname` and+`realpath` read. The code is in `src/net/unix.rs` and `src/fs/procfs.rs`.++Before that, a small first commit fixed what Clarke's review of 0.17.0+found; see "Small fixes from Clarke's review" below.++### What a Unix socket is++You've met two ways for programs to talk already: **pipes** (stage 7)+and **network sockets** (stage 12). A Unix socket sits between them. It+uses the same calls as a network socket (`socket`, `bind`, `listen`,+`accept`, `connect`, `send`, `recv`), but the "network" is just the+kernel: sending means "copy the bytes into the other socket's queue and+wake whoever is waiting for them". Compared with a pipe, a Unix socket++- goes **both ways** (a pipe is one-way);+- has a **name** other programs can find (see below), so one server can+  `accept` many clients, like a web server does;+- can carry **open files** from one program to another (`SCM_RIGHTS`);+- can tell you **who is on the other end** (`SO_PEERCRED`).++There are three kinds, picked by `socket()`'s second argument:++| Type | Like | Message borders |+|------|------|-----------------|+| `SOCK_STREAM` (1) | TCP, or a two-way pipe | not kept: two `write`s can come out as one `read` |+| `SOCK_DGRAM` (2) | UDP, but reliable and in order | kept: one `send`, one `recv`; no connection needed |+| `SOCK_SEQPACKET` (5) | a stream that keeps borders | kept, and connected like a stream |++Wayland uses `SOCK_STREAM`; `SOCK_SEQPACKET` is what you pick when you+want a connection *and* messages that stay whole.++### Names: a file, an abstract name, or none++A Unix socket's address is a `struct sockaddr_un`: the family (1) and up+to 108 bytes of name. There are three kinds of name:++- **A path**, like `/tmp/echo.sock` or Wayland's+  `$XDG_RUNTIME_DIR/wayland-0`. `bind` creates a **socket file** there+  (`ls -l` would show an `s`; `stat` says `S_IFSOCK`). The file is only+  a signpost: the data never touches the disk, and `connect` uses the+  file to find the listening socket. That works on the ramfs and on+  ext2, which both have an inode type for it. FAT and exFAT have no+  such type, so `bind` there is `EPERM`. You can't `open` a socket file+  (`ENXIO`), and it stays behind after the server exits until someone+  `unlink`s it, just like on Linux (that's why servers delete the old+  one before `bind`).+- **An abstract name**: the path starts with a zero byte. It lives only+  in the kernel, so nothing has to be cleaned up; it disappears when+  its socket closes. Tools print it with a leading `@`, like+  `@rustbelt-test`. `bind` with only the family and no name at all+  ("autobind") picks a free abstract name of five hex digits.+- **Unnamed**: a socket that never called `bind`, such as both ends of a+  `socketpair`. `getpeername` on those returns just the family.++`socketpair(AF_UNIX, type, 0, fds)` makes two sockets already connected+to each other, with no names at all. It's the two-way pipe a parent+makes before `fork` so it can talk to its child.++### Connecting, and the "embryo"++`listen` turns a bound stream (or seqpacket) socket into a server.+`connect` then builds a brand-new socket for the server side right away,+links it to the client, and puts it in the listener's queue of waiting+connections (the *backlog*). `accept` just takes the next one out of+that queue and gives it an fd. Linux calls the not-yet-accepted socket+an "embryo". So a client can `connect` and even `send` before the server+gets round to `accept`; if the server closes without accepting, the+client gets `ECONNRESET`. When the backlog is full, `connect` waits (or+fails with `EAGAIN` on a nonblocking socket), as on Linux.++### Backpressure: why a full queue makes you wait++Every socket has a receive queue. If a fast writer could add to it+forever, it would eat all the kernel's memory, so each queue has a+limit: the smaller of the sender's `SO_SNDBUF` and the receiver's+`SO_RCVBUF` (208 KiB by default, like Linux). When the queue is full,+`write` sleeps on a wait queue until the reader takes something, or+returns `EAGAIN` when the socket is nonblocking, and `poll` stops+reporting `POLLOUT`. That's called **backpressure**. As on Linux,+`setsockopt(SO_SNDBUF, 4096)` reads back as 8192: the kernel doubles+what you ask for to leave room for its own bookkeeping.++For datagram and seqpacket sockets every message also costs 256 bytes+on top of its data, so a flood of empty messages fills the queue too.+(Linux instead also limits a datagram queue to a number of messages;+we only count bytes.)++One rule keeps it deadlock-free: the kernel never holds two sockets'+locks at the same time. Two programs writing to each other at once+can't each be waiting for the other's lock.++### Passing open files: SCM_RIGHTS++This is the surprising one. `sendmsg` takes, next to the data, some+**control messages** ("ancillary data"). With `SCM_RIGHTS` you put fd+numbers in there, and the receiver's `recvmsg` gets **new fds in its own+table for the same open files**: the same file position, the same pipe,+the same socket. An fd number only means something inside one process;+what travels is the open file behind it. A Wayland client hands the+compositor shared-memory buffers this way.++Some details that matter, all Linux's behaviour:++- While a message is in a queue its files are **in flight**: they stay+  open even if the sender closes its own fds. If the receiving socket is+  closed with the message still unread, the kernel closes them, so+  nothing leaks (the tests check memory frames to be sure).+- If the receiver's control buffer is too small for all the fds, it+  gets the ones that fit, `MSG_CTRUNC` is set in `msg_flags`, and the+  rest are closed.+- `MSG_CMSG_CLOEXEC` in `recvmsg`'s flags marks the new fds+  close-on-exec, like `O_CLOEXEC`.+- On a stream socket a `read` never runs past a message that carries+  fds: the bytes before it come out first, and the fds come with the+  next read.++The control buffer has a fixed layout: a `struct cmsghdr` (length,+level, type: 16 bytes) and then the data, padded to 8 bytes. That's why+room for one fd is 20 bytes (`CMSG_LEN(4)`) but `CMSG_SPACE(4)` is 24,+which, since the padding counts, is also enough for two.++### Who's there: credentials++- `getsockopt(SO_PEERCRED)` gives the pid, uid and gid of the program at+  the other end, as they were when it connected. A server can use it to+  decide whom to trust.+- `SCM_CREDENTIALS` is a control message with the same three numbers.+  The sender may send its own (the kernel checks); with `SO_PASSCRED` on,+  the receiver gets them with every message even if the sender sent none.++### poll, epoll and shutdown++Unix sockets work with `poll`, `select` and `epoll` like every other fd.+`POLLIN` means there's data (or, on a listener, a connection to+accept); `POLLOUT` means there's room. `shutdown(SHUT_WR)` tells the+other end "no more data": its reads return 0 (end of file) and its+`poll` reports `POLLRDHUP`; when the other end closes completely it's+`POLLHUP`. Writing to a socket whose peer is gone is `EPIPE` plus+SIGPIPE (or just `EPIPE` with `MSG_NOSIGNAL`).++### /proc/self++On Linux `/proc` is a file system whose files are made up on the spot+by the kernel. Rustbelt now mounts a tiny one at `/proc` (`statfs`+magic `0x9fa0`, like Linux). Every answer depends on *who asks*: the+same path shows each process its own files.++- `/proc/self/exe` is a symlink to the running program (it used to be a+  special case inside `readlink`).+- `/proc/self/fd/` lists your open fds, and each `/proc/self/fd/N` is a+  symlink to what fd N is: a path for a file, `/dev/null` or `/dev/pts/0`+  for a device, and for things with no path Linux's made-up names+  `pipe:[4001]`, `socket:[123]` and `anon_inode:[eventpoll]`. The number+  in brackets is the same as `st_ino` from `fstat`, so two fds for the+  same pipe can be recognised.+- **Opening** `/proc/self/fd/N` opens fd N's file again: a regular file+  gets a fresh open (its own position); a pipe or a tty gives you the+  same open file again. A socket can't be opened that way (`ENXIO`).++There's no `/proc/<pid>` for other processes yet, and `/proc/self` is a+real directory rather than Linux's symlink to `/proc/<pid>`.++### Small fixes from Clarke's review++Clarke's probes against 0.17.0 found three things; they went in as a+separate commit before the sockets:++- **POSIX timer overruns.** While a timer's signal was still waiting to+  be delivered, further expirations *replaced* the overrun count instead+  of adding to it. Now they add up, and the count is latched when the+  signal is delivered, so `timer_getoverrun` reports the overrun of the+  signal you just got.+- **The headless frame report moved.** The `timerd` thread (stage 17)+  was started by the first timer, so its 16 frames showed up as a+  difference between "before the shell" and "after". It now starts at+  boot and the two numbers match again.+- **"/tmp has /'s st_dev"** turned out not to be a kernel bug. Clarke's+  C probe printed `st.st_dev` in the same `printf` call that also ran+  `stat("/", &st)`, and C doesn't say in which order a function's+  arguments are evaluated. The kernel was already right; `coretest` now+  checks `stat`, `lstat` and `fstat` of mount points so it stays that+  way.++A known flake, for the record: about 3% of runs of `tests/libc.rs`'s+`threads_and_futexes` fail. It happened before this stage too (at+0.17.0) and rerunning passes; it's on the list to track down.++### Testing it++`user/unixtest.rs` has one group of checks per argument: `stream`,+`pair`, `dgram`, `rights`, `nonblock` and `proc`.+`tests/unix_sockets.rs` runs each group twice; the second run must+leave threads, processes, memory frames and bound socket names exactly+where they were (so an fd stuck "in flight" would show up as a leak).+From the shell: `unixtest rights`.++### What stage 18 does and doesn't do++Does: everything above, with Linux's numbers, structures and error+codes.++Doesn't (yet):++- **No garbage collector for socket cycles.** If you send a Unix+  socket's fd *through that same socket* (or two sockets through each+  other) and then close everything, the messages keep the sockets+  alive and they're never freed. Linux has a special garbage collector+  for exactly this; real programs almost never do it.+- **Datagram queues are limited by bytes only**, not also by a message+  count like Linux's.+- **`SO_PASSCRED` doesn't autobind** an unnamed socket (Linux gives it+  an abstract name).+- **Permissions on socket files aren't checked**, like every other+  permission so far.+- **`sendmsg`/`recvmsg` on network sockets** carry the name and data,+  but no control messages.+- **`/proc`** has only `self/fd` and `self/exe`: no `/proc/<pid>`, no+  `/proc/mounts` (the `mounts` call 501 still does that job), and+  `/proc/self` is a directory, not a symlink.+ ## Roadmap  Done: all 12 chapters of the 2nd edition, plus preemptive kernel threads,@@ -4474,7 +4713,7 @@ threads and futexes, ttys and ptys), musl's prerequisites (per-thread FPU/SSE/AVX state, Linux C-string path calls and `struct stat`), the first batch of the calls musl and dash use at run time, and a second batch for coreutils and busybox (links, owners, `statfs`, CPU times,-timers, user ids).+timers, user ids), and Unix sockets with `/proc/self` (stage 18).  - [x] Freestanding binary — [phil-opp](https://os.phil-opp.com/freestanding-rust-binary/) - [x] Minimal kernel booting in QEMU — [phil-opp](https://os.phil-opp.com/minimal-rust-kernel/), [OSDev: Bare Bones](https://wiki.osdev.org/Bare_Bones)@@ -4513,6 +4752,7 @@ timers, user ids). - [x] musl prerequisites (stage 15.1): per-thread FPU/SSE/AVX state with XSAVE/FXSAVE (eager switching, cleared on exec), Linux's fpstate in signal frames, #MF/#XM -> SIGFPE, C-string paths with `PATH_MAX` and `EFAULT`, Linux `struct stat`, the `*at()` calls, `rename`, a syscall-number audit, and `rawabi` (a no-libc gcc binary) — [Intel SDM vol. 1 ch. 13: XSAVE](https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html), [OSDev: SSE](https://wiki.osdev.org/SSE), [man 2 openat](https://man7.org/linux/man-pages/man2/openat.2.html), [man 2 stat](https://man7.org/linux/man-pages/man2/stat.2.html), [LWN: LazyFP](https://lwn.net/Articles/757246/) - [x] libc batch 1 (stage 16): `rt_sigsuspend`/`rt_sigpending`/`rt_sigtimedwait`/`rt_sigqueueinfo`, queued real-time signals, `sigaltstack`, per-process `umask` and rlimits (`prlimit64`), `fcntl` record locks with deadlock detection and honest `F_GETFL`, `truncate`/`ftruncate`/`fsync`/`chmod`/`utimensat` with real file times, `prctl`, `sched_getaffinity`, `madvise`, robust futex lists, subreapers and self-reaping orphans, `--trace` naming unknown calls by number — [man 2 sigsuspend](https://man7.org/linux/man-pages/man2/sigsuspend.2.html), [man 2 fcntl](https://man7.org/linux/man-pages/man2/fcntl.2.html), [man 2 sigaltstack](https://man7.org/linux/man-pages/man2/sigaltstack.2.html), [man 2 prlimit](https://man7.org/linux/man-pages/man2/prlimit.2.html), [man 2 get_robust_list](https://man7.org/linux/man-pages/man2/get_robust_list.2.html) - [x] coreutils batch 2 (stage 17): `link`/`symlink` and their `*at` forms, native `rename` of full directories and `RENAME_NOREPLACE`, the `chown` family with owners on ramfs and ext2, `statfs`/`fstatfs`/`statx` with Linux's magic numbers and a real `st_dev`/`st_nlink`, Linux's `sysinfo` (99), `times`/`getrusage`/CPU-time clocks/`clock_getres`, `getsockname`/`getpeername`/`getsockopt`/`shutdown` and remembered socket options, `alarm`/`setitimer`/POSIX timers, real (unenforced) user and group ids, nice values, `sethostname`, `flock`, `preadv`/`pwritev`, `sendfile`, and a `getdents64` that `rm -r` can trust — [man 2 link](https://man7.org/linux/man-pages/man2/link.2.html), [man 2 statfs](https://man7.org/linux/man-pages/man2/statfs.2.html), [man 2 timer_create](https://man7.org/linux/man-pages/man2/timer_create.2.html), [man 7 credentials](https://man7.org/linux/man-pages/man7/credentials.7.html), [man 2 flock](https://man7.org/linux/man-pages/man2/flock.2.html)+- [x] Unix sockets (stage 18): `AF_UNIX` stream/dgram/seqpacket with path, abstract and unnamed names, `socketpair`, `sendmsg`/`recvmsg` with `SCM_RIGHTS` (in-flight fds, `MSG_CTRUNC`, `MSG_CMSG_CLOEXEC`) and `SCM_CREDENTIALS`, `SO_PEERCRED`/`SO_PASSCRED`, real buffer limits with backpressure, poll/epoll with `POLLRDHUP`/`POLLHUP`, and `/proc/self/fd` + `/proc/self/exe` — [man 7 unix](https://man7.org/linux/man-pages/man7/unix.7.html), [man 3 cmsg](https://man7.org/linux/man-pages/man3/cmsg.3.html), [man 2 sendmsg](https://man7.org/linux/man-pages/man2/sendmsg.2.html), [man 5 proc](https://man7.org/linux/man-pages/man5/proc.5.html), [Wayland protocol: wire format](https://wayland.freedesktop.org/docs/html/ch04.html) - [ ] The kernel/OS split and a musl port (next; waits for Tyler's go; see the missing-syscall list in "musl prerequisites") - [ ] A Hyprland-style tiling compositor on the framebuffer (later) 
@@ -80,6 +80,7 @@ const PROGRAMS: &[&str] = &[     "fputest", "pathtest", // stage 15.1: musl prerequisites     "libctest", // stage 16: libc batch 1     "coretest", // stage 17: coreutils batch 2+    "unixtest", // stage 18: Unix sockets, /proc/self ];  /// C programs in /bin, built with the host's C compiler and no libc at all:
@@ -88,7 +88,7 @@ set -e # Lines the shell self-test must have printed (only for a plain boot). if [ -z "${1:-}" ] && [ "$code" = 33 ]; then     expect=(-        "Rustbelt 0.17.0 (stage 17: coreutils batch 2) x86_64" # ver+        "Rustbelt 0.18.0 (stage 18: Unix sockets) x86_64" # ver         "[smp] 4 CPUs online"                          # smp.rs started 3 more cores         "[fpu] "                                       # XSAVE (-cpu max) or FXSAVE (qemu64)         "B:  /mnt/disk0   fat32"                       # drives@@ -159,7 +159,7 @@ if [ -z "${1:-}" ] && [ "$code" = 33 ]; then         "[nbtest] all checks passed"         "[fputest] all checks passed"                  # stage 15.1: musl prerequisites         "[pathtest] all checks passed"-        "rawabi: uname: Rustbelt 0.17.0"               # a no-libc gcc binary+        "rawabi: uname: Rustbelt 0.18.0"               # a no-libc gcc binary         "rawabi: all checks passed"         "sh: nosuchprogram: command not found"         "cd: there is no drive Q:"@@ -169,11 +169,11 @@ if [ -z "${1:-}" ] && [ "$code" = 33 ]; then     for line in "${expect[@]}"; do         grep -qF -- "$line" "$LOG" || { echo "[run-headless] MISSING: $line"; missing=1; }     done-    # Pipe results: `... | grep -i EXT | wc -l` -> 1, `dir bin | wc -l` -> 38 (the programs),+    # Pipe results: `... | grep -i EXT | wc -l` -> 1, `dir bin | wc -l` -> 39 (the programs),     # `cat C:/chunks.txt | wc -l` (copied from E:) -> 3,     # `type D:/COW.TXT | wc -l` (copied from E:) -> 4, and     # `cat D:/seq.txt | wc -l` (downloaded by wget) -> 20000.-    for n in 1 38 3 4 20000; do+    for n in 1 39 3 4 20000; do         grep -qx -- "$n" "$LOG" || { echo "[run-headless] MISSING pipe result: $n"; missing=1; }     done     # No memory leaks: once the shell and all ~60 programs it ran have@@ -184,7 +184,7 @@ if [ -z "${1:-}" ] && [ "$code" = 33 ]; then         echo "[run-headless] MISSING: frames back at the baseline after the shell"; missing=1     fi     # The other direction: the host fetched a page from httpd in the guest.-    if grep -qF "Hello from Rustbelt! Rustbelt 0.17.0" "$CURL_OUT" 2>/dev/null; then+    if grep -qF "Hello from Rustbelt! Rustbelt 0.18.0" "$CURL_OUT" 2>/dev/null; then         echo "[run-headless] host curl got httpd's page: $(head -1 "$CURL_OUT")"     else         echo "[run-headless] MISSING: host curl through hostfwd to httpd"; missing=1@@ -196,7 +196,7 @@ if [ -z "${1:-}" ] && [ "$code" = 33 ]; then     # drop the '\r's before comparing whole lines.     tr -d '\r' < "$HVC_OUT" > "$HVC_OUT.txt" 2>/dev/null && mv "$HVC_OUT.txt" "$HVC_OUT"     # The shell on hvc0 ran the host's commands and answered over hvc0.-    for line in "Rustbelt getty: shell on this terminal" "Rustbelt 0.17.0 (stage 17" \+    for line in "Rustbelt getty: shell on this terminal" "Rustbelt 0.18.0 (stage 18" \                 "$ echo hi over hvc0" "hi over hvc0" "fb0" "hvc0"; do         grep -qF -- "$line" "$HVC_OUT" 2>/dev/null || { echo "[run-headless] MISSING on hvc0: $line"; missing=1; }     done
@@ -45,7 +45,7 @@ fputest | grep passed pathtest | grep passed rawabi | grep -v "ok   " TXT-    set -- "Rustbelt 0.17.0 (stage 17: coreutils batch 2) x86_64" "tile: 4 windows on 1024x768, console 61x46" \+    set -- "Rustbelt 0.18.0 (stage 18: Unix sockets) x86_64" "tile: 4 windows on 1024x768, console 61x46" \         "^C" "(interrupted: SIGINT)" "threads: 4 threads summed 1..=40000000 = 800000020000000" \         "echoserver: epoll: 3 clients, 9 echoes" "[threads] all checks passed" \         "[fputest] all checks passed" "[pathtest] all checks passed" \
@@ -157,6 +157,9 @@ impl File for Null {     fn stat(&self) -> Result<Stat, Errno> {         device_stat(0)     }+    fn proc_link(&self) -> Option<String> {+        Some(String::from("/dev/null"))+    } }  struct Random;@@ -175,6 +178,9 @@ impl File for Random {     fn stat(&self) -> Result<Stat, Errno> {         device_stat(0)     }+    fn proc_link(&self) -> Option<String> {+        Some(String::from("/dev/urandom")) // (or /dev/random: the same device)+    } }  /// /dev/fb0: the framebuffer as a file of `stride * height * 4` bytes.@@ -266,6 +272,9 @@ impl File for Framebuffer {     fn stat(&self) -> Result<Stat, Errno> {         device_stat(self.info.size_bytes())     }+    fn proc_link(&self) -> Option<String> {+        Some(String::from("/dev/fb0"))+    }     fn as_framebuffer(&self) -> Option<&Framebuffer> {         Some(self)     }
@@ -45,15 +45,19 @@ pub enum Errno {     ENOTSOCK = 88,        // socket call on something that isn't a socket     EDESTADDRREQ = 89,    // send on an unconnected socket without an address     EMSGSIZE = 90,        // datagram too big+    EPROTOTYPE = 91,      // wrong socket type (stage 18: connect a stream socket to a dgram one)+    ESOCKTNOSUPPORT = 94, // no such socket type (stage 18)     ENOPROTOOPT = 92,     // unknown socket option (setsockopt)     EPROTONOSUPPORT = 93, // unsupported protocol (socket())     EAFNOSUPPORT = 97,    // unsupported address family (only AF_INET)-    EADDRINUSE = 98,      // port already in use+    EADDRINUSE = 98,      // port already in use (or a unix socket path that exists)+    EADDRNOTAVAIL = 99,   // stage 18: no such address to bind to     ENETDOWN = 100,       // no network card     ENETUNREACH = 101,    // no IP address (yet): DHCP didn't answer     ECONNRESET = 104,     // connection reset by the other side     EISCONN = 106,        // already connected     ENOTCONN = 107,       // not connected+    ETOOMANYREFS = 109,   // stage 18: too many fds in one SCM_RIGHTS message     ETIMEDOUT = 110,      // connect/DNS timed out     ECONNREFUSED = 111,   // nobody listens on that port (got a RST)     EINPROGRESS = 115,    // nonblocking connect: started, not done yet@@ -68,13 +72,13 @@ impl Errno {     /// The errno for a negative syscall result like -2, if we know it.     pub fn from_ret(ret: i64) -> Option<Errno> {         use Errno::*;-        const ALL: [Errno; 54] = [+        const ALL: [Errno; 58] = [             EPERM, ESRCH, EINTR, ERESTARTSYS, ERESTARTNOHAND, ENOENT, EIO, E2BIG, ENOEXEC, EBADF, ECHILD, EAGAIN, ENOMEM, EFAULT, EBUSY, EEXIST, ENODEV,             ENOTDIR, EISDIR, EINVAL, EMFILE, ENOTTY, EFBIG, ENOSPC, ESPIPE, EROFS, EPIPE, ERANGE,             ENAMETOOLONG, ENOSYS, ENOTEMPTY, ELOOP, EOPNOTSUPP, ENOTSOCK, EDESTADDRREQ, EMSGSIZE,             EPROTONOSUPPORT, EAFNOSUPPORT, EADDRINUSE, ENETDOWN, ENETUNREACH, ECONNRESET, EISCONN,             ENOTCONN, ETIMEDOUT, ECONNREFUSED, ENXIO, EINPROGRESS, EXDEV, ENOPROTOOPT, EDEADLK, ENOLCK, EMLINK,-            EACCES,+            EACCES, EPROTOTYPE, EADDRNOTAVAIL, ETOOMANYREFS, ESOCKTNOSUPPORT,         ];         ALL.into_iter().find(|&e| -(e as i64) == ret)     }
@@ -727,7 +727,7 @@ impl Volume {         let attrs = match kind {             InodeKind::File => ATTR_ARCHIVE,             InodeKind::Directory => ATTR_DIRECTORY,-            InodeKind::Symlink | InodeKind::Device => return Err(Errno::EOPNOTSUPP), // exFAT has neither+            InodeKind::Symlink | InodeKind::Device | InodeKind::Socket => return Err(Errno::EOPNOTSUPP), // exFAT has none of these         };         let mut dir = self.resolve(dir_path)?;         if !dir.is_dir() {
@@ -151,11 +151,13 @@ const S_IFMT: u16 = 0o170000; const S_IFREG: u16 = 0o100000; const S_IFDIR: u16 = 0o040000; const S_IFLNK: u16 = 0o120000;+const S_IFSOCK: u16 = 0o140000; // stage 18: a Unix socket's name  // Directory entry file types (with INCOMPAT_FILETYPE). const FT_REG: u8 = 1; const FT_DIR: u8 = 2; const FT_SYMLINK: u8 = 7;+const FT_SOCK: u8 = 6;  const INDEX_FL: u32 = 0x1000; // directory has an htree index @@ -202,6 +204,7 @@ impl DiskInode {         match self.mode() & S_IFMT {             S_IFDIR => InodeKind::Directory,             S_IFLNK => InodeKind::Symlink,+            S_IFSOCK => InodeKind::Socket,             _ => InodeKind::File, // regular files (devices, fifos: treated as files)         }     }@@ -263,6 +266,7 @@ impl DiskInode {         match self.kind() {             InodeKind::Directory => FT_DIR,             InodeKind::Symlink => FT_SYMLINK,+            InodeKind::Socket => FT_SOCK,             _ => FT_REG,         }     }@@ -854,10 +858,11 @@ impl Volume {             return Err(Errno::EEXIST);         }         let dir = match kind {-            InodeKind::File => false,+            InodeKind::File | InodeKind::Socket => false,             InodeKind::Directory => true,             InodeKind::Symlink | InodeKind::Device => return Err(Errno::EOPNOTSUPP), // no symlink/mknod syscall yet         };+        let sock = kind == InodeKind::Socket;         let mut inode = self.alloc_inode(self.group_of(parent_ino), dir)?;         let now = self.now();         inode.set_times(&[I_ATIME, I_CTIME, I_MTIME], now);@@ -865,7 +870,8 @@ impl Volume {             put16(&mut inode.raw, I_MODE, S_IFDIR | 0o755);             inode.set_links(2); // the parent's entry + its own "."         } else {-            put16(&mut inode.raw, I_MODE, S_IFREG | 0o644);+            // A socket's name (stage 18) is an inode with no data blocks.+            put16(&mut inode.raw, I_MODE, if sock { S_IFSOCK | 0o755 } else { S_IFREG | 0o644 });             inode.set_links(1);         }         let result = (|| {@@ -879,7 +885,7 @@ impl Volume {                 inode.set_size(self.block_size as u64);             }             self.write_inode(&inode)?;-            self.add_entry(&mut parent, name, inode.ino, if dir { FT_DIR } else { FT_REG })?;+            self.add_entry(&mut parent, name, inode.ino, if dir { FT_DIR } else if sock { FT_SOCK } else { FT_REG })?;             if dir {                 parent.set_links(parent.links() + 1); // the new ".." points at it                 self.write_inode(&parent)?;@@ -1296,6 +1302,7 @@ impl Inode for Ext2Inode {                 1 => InodeKind::File,                 2 => InodeKind::Directory,                 7 => InodeKind::Symlink,+                6 => InodeKind::Socket,                 _ => vol.read_inode(e.ino)?.kind(), // revision 0: no type in the entry             };             out.push(DirEntry { name: e.name, kind, ino: e.ino as u64 });
@@ -360,8 +360,8 @@ impl Inode for FatInode {         if self.lookup(name).is_ok() {             return Err(Errno::EEXIST);         }-        if kind == InodeKind::Symlink {-            return Err(Errno::EOPNOTSUPP); // FAT has no symlinks+        if matches!(kind, InodeKind::Symlink | InodeKind::Socket | InodeKind::Device) {+            return Err(Errno::EOPNOTSUPP); // FAT has no symlinks or special files         }         self.with_dir(|dir| {             match kind {
@@ -38,6 +38,7 @@ impl Stat {             InodeKind::Directory => KIND_DIR,             InodeKind::Symlink => KIND_SYMLINK,             InodeKind::Device => KIND_DEVICE,+            InodeKind::Socket => KIND_SOCKET,         };         Stat { kind, size: inode.size() as u64, mode: inode.mode() as u64 }     }@@ -232,6 +233,23 @@ pub trait File: Send + Sync {     fn as_tty(&self) -> Option<&crate::tty::TtyFile> {         None     }+    /// Stage 18: a Unix (AF_UNIX) socket? See net/unix.rs.+    fn as_unix(&self) -> Option<&crate::net::unix::UnixSocket> {+        None+    }+    /// Stage 18: for files without a path (pipes, sockets), the number+    /// fstat's st_ino and /proc/self/fd show ("pipe:[N]", "socket:[N]").+    /// Both ends of one pipe share it, like Linux. None: the file's own+    /// address is used.+    fn anon_ino(&self) -> Option<u64> {+        None+    }+    /// Stage 18: what /proc/self/fd/N points to, for files that know a+    /// better name than their path (devices and terminals: "/dev/null",+    /// "/dev/pts/0"). See fs::procfs::fd_link for the rest.+    fn proc_link(&self) -> Option<String> {+        None+    } }  /// A file or directory opened through the VFS: inode + current position.@@ -424,6 +442,7 @@ impl File for InodeFile {                 InodeKind::File => 8,      // DT_REG                 InodeKind::Symlink => 10,  // DT_LNK                 InodeKind::Device => 2,    // DT_CHR (a "character device")+                InodeKind::Socket => 12,   // DT_SOCK (stage 18)             };             rec[19..19 + entry.name.len()].copy_from_slice(entry.name.as_bytes());             used += reclen;@@ -432,3 +451,9 @@ impl File for InodeFile {         Ok(used)     } }++/// The inode number of a file without a path: its `anon_ino`, or else+/// its address (unique while it's open). Stage 18 (/proc/self/fd).+pub fn anon_ino(file: &Arc<dyn File>) -> u64 {+    file.anon_ino().unwrap_or(Arc::as_ptr(file) as *const u8 as u64)+}
@@ -36,6 +36,7 @@ pub mod ext2; pub mod fat; pub mod file; pub mod pipe;+pub mod procfs; pub mod ramfs;  use crate::errno::Errno;@@ -50,6 +51,10 @@ pub enum InodeKind {     Symlink,     /// A device file in /dev (opening it gives the driver's own `File`).     Device,+    /// Stage 18: a Unix socket's name in the file system, made by `bind`.+    /// It holds no data: `connect` uses it to find the listening socket+    /// (net/unix.rs); `open` on it fails with ENXIO, like Linux.+    Socket, }  /// One entry of a directory listing.@@ -332,13 +337,21 @@ pub fn normalize(cwd: &str, path: &str) -> Result<String, Errno> {  /// Resolve an absolute path to an inode, following symlinks. pub fn lookup(path: &str) -> Result<Arc<dyn Inode>, Errno> {-    walk(path, true, 0)+    walk(path, true, 0).map(|(inode, _)| inode)+}++/// Like [`lookup`] (or `lookup_nofollow`), but also say where the inode+/// really is: the path with every symlink on the way replaced by its+/// target (stage 18). stat uses it for st_dev (a link on one file system+/// can point into another), and Unix sockets for their key.+pub fn lookup_resolved(path: &str, follow_last: bool) -> Result<(Arc<dyn Inode>, String), Errno> {+    walk(path, follow_last, 0) }  /// Like [`lookup`], but if the *last* component is a symlink, return the /// link itself (for `readlink`, and for deleting a link, not its target). pub fn lookup_nofollow(path: &str) -> Result<Arc<dyn Inode>, Errno> {-    walk(path, false, 0)+    walk(path, false, 0).map(|(inode, _)| inode) }  /// The target of the symlink at absolute `path`.@@ -349,7 +362,7 @@ pub fn readlink(path: &str) -> Result<String, Errno> { /// How many symlinks one lookup may follow (Linux allows 40). const MAX_SYMLINKS: usize = 8; -fn walk(path: &str, follow_last: bool, links_followed: usize) -> Result<Arc<dyn Inode>, Errno> {+fn walk(path: &str, follow_last: bool, links_followed: usize) -> Result<(Arc<dyn Inode>, String), Errno> {     if !path.starts_with('/') {         return Err(Errno::EINVAL); // callers resolve relative paths first     }@@ -387,7 +400,7 @@ fn walk(path: &str, follow_last: bool, links_followed: usize) -> Result<Arc<dyn          dir = if dir == "/" { format!("/{}", name) } else { format!("{}/{}", dir, name) };         inode = child;     }-    Ok(inode)+    Ok((inode, dir)) }  /// Split a normalized path into (parent directory inode, last component).@@ -417,7 +430,7 @@ pub fn remove(path: &str, want_dir: bool) -> Result<(), Errno> {     let inode = lookup_nofollow(&normalized)?;     match (inode.kind(), want_dir) {         (InodeKind::Directory, false) => return Err(Errno::EISDIR),-        (InodeKind::File | InodeKind::Symlink, true) => return Err(Errno::ENOTDIR),+        (InodeKind::File | InodeKind::Symlink | InodeKind::Socket, true) => return Err(Errno::ENOTDIR),         _ => {}     }     let (parent, name) = parent_of(&normalized)?;@@ -595,9 +608,11 @@ pub fn init() {     dir.create("tmp", InodeKind::Directory).unwrap(); // mount point     dir.create("mnt", InodeKind::Directory).unwrap(); // disks go to /mnt/diskN     dir.create("dev", InodeKind::Directory).unwrap(); // device files (dev.rs)+    dir.create("proc", InodeKind::Directory).unwrap(); // stage 18: /proc/self (procfs.rs)     mount("/", root).unwrap();     mount("/tmp", ramfs::RamFs::new()).unwrap();     mount("/dev", Arc::new(crate::dev::DevFs)).unwrap();+    mount("/proc", Arc::new(procfs::ProcFs)).unwrap(); }  /// Try to mount every registered partition (and unpartitioned disk) at
@@ -26,6 +26,8 @@ use x86_64::instructions::interrupts::without_interrupts; pub const PIPE_CAPACITY: usize = 64 * 1024;  struct Pipe {+    /// Its number, for fstat's st_ino and /proc/self/fd ("pipe:[N]").+    id: u64,     // Locked with interrupts off only (it's touched in `wait_until`'s     // condition, see the locking rule in thread.rs).     state: Mutex<PipeState>,@@ -44,7 +46,9 @@ pub struct PipeWriter(Arc<Pipe>, AtomicBool);  /// A new pipe: (read end, write end). pub fn new() -> (PipeReader, PipeWriter) {+    static NEXT_ID: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(4000);     let pipe = Arc::new(Pipe {+        id: NEXT_ID.fetch_add(1, Ordering::Relaxed),         state: Mutex::new(PipeState { buffer: VecDeque::new(), write_closed: false, read_closed: false }),         readers_wait: WaitQueue::new(),         writers_wait: WaitQueue::new(),@@ -53,6 +57,9 @@ pub fn new() -> (PipeReader, PipeWriter) { }  impl File for PipeReader {+    fn anon_ino(&self) -> Option<u64> {+        Some(self.0.id) // both ends: one pipe, one number+    }     fn status_flags(&self) -> u64 {         0 // O_RDONLY: the read end     }@@ -112,6 +119,9 @@ impl File for PipeReader { }  impl File for PipeWriter {+    fn anon_ino(&self) -> Option<u64> {+        Some(self.0.id) // both ends: one pipe, one number+    }     fn status_flags(&self) -> u64 {         1 // O_WRONLY: the write end     }
@@ -0,0 +1,177 @@+//! /proc (stage 18): a window into the running process, made up on the fly.+//!+//! Linux's /proc has a directory for every process and lots of files. We+//! have just the corner that programs (and C libraries) lean on most:+//!+//!   /proc/self/exe     a symlink to the program that's running+//!   /proc/self/fd/     one symlink per open file descriptor:+//!   /proc/self/fd/N      to the file's path ("/tmp/notes.txt"), or a+//!                        name like "pipe:[4123]" or "socket:[1007]" for+//!                        things that have no path, or "/dev/pts/0"+//!+//! Nothing here is stored anywhere. Every lookup and every listing asks+//! the process that is *asking* (`current()`) for its fd table, so two+//! programs listing /proc/self/fd at once each see their own files. That+//! is the whole trick of "self".+//!+//! Following one of the fd links (open, stat) is special-cased in+//! syscall.rs, as on Linux: opening /proc/self/fd/N opens the file that's+//! behind fd N again (a pipe or terminal: the same open file, like dup),+//! even when the link's text isn't a real path ("pipe:[...]").+//!+//! Differences from Linux: /proc/self is a directory, not a symlink to+//! /proc/<pid>, and there are no /proc/<pid> directories yet.++use super::file::File;+use super::{DirEntry, FileSystem, Inode, InodeKind, StatFs};+use crate::errno::Errno;+use alloc::{format, string::String, sync::Arc, vec::Vec};++/// f_type of a mounted /proc (PROC_SUPER_MAGIC in <linux/magic.h>).+pub const PROC_MAGIC: u64 = 0x9fa0;++pub struct ProcFs;++impl FileSystem for ProcFs {+    fn name(&self) -> &'static str {+        "proc"+    }+    fn root(&self) -> Arc<dyn Inode> {+        Arc::new(Node::Root)+    }+    fn statfs(&self) -> StatFs {+        StatFs { magic: PROC_MAGIC, block_size: 4096, name_max: 255, ..Default::default() }+    }+}++/// The handful of things in our /proc.+#[derive(Clone, Copy, PartialEq, Eq, Debug)]+enum Node {+    Root,+    SelfDir,+    FdDir,+    Exe,+    /// /proc/self/fd/N+    Fd(u64),+}++/// The open files of the process asking, by fd number.+fn my_fds() -> Vec<(u64, Arc<dyn File>)> {+    crate::syscall::current().map(|p| p.open_files()).unwrap_or_default()+}++/// What /proc/self/fd/N says for this open file (readlink's answer).+pub fn fd_link(file: &Arc<dyn File>) -> String {+    if let Some(name) = file.proc_link() {+        return name; // devices and terminals know their /dev name+    }+    if let Some(path) = file.path() {+        return path;+    }+    let ino = super::file::anon_ino(file);+    if file.as_socket().is_some() || file.as_unix().is_some() {+        format!("socket:[{}]", ino)+    } else if file.as_epoll().is_some() {+        String::from("anon_inode:[eventpoll]")+    } else if file.stat().is_ok_and(|s| s.kind == super::file::KIND_PIPE) {+        format!("pipe:[{}]", ino)+    } else {+        String::from("anon_inode:[unknown]")+    }+}++/// Is `path` (absolute, normalized) /proc/self/fd/N? Then N.+pub fn fd_number(path: &str) -> Option<u64> {+    let n = path.strip_prefix("/proc/self/fd/")?;+    if n.is_empty() || !n.bytes().all(|b| b.is_ascii_digit()) || (n.len() > 1 && n.starts_with('0')) {+        return None;+    }+    n.parse().ok()+}++impl Inode for Node {+    fn kind(&self) -> InodeKind {+        match self {+            Node::Root | Node::SelfDir | Node::FdDir => InodeKind::Directory,+            Node::Exe | Node::Fd(_) => InodeKind::Symlink,+        }+    }+    fn size(&self) -> usize {+        match self {+            Node::Root => 1,+            Node::SelfDir => 2,+            Node::FdDir => my_fds().len(),+            _ => self.readlink().map(|t| t.len()).unwrap_or(0),+        }+    }+    fn mode(&self) -> u32 {+        match self {+            Node::Root | Node::SelfDir => 0o555,+            Node::FdDir => 0o500,+            Node::Exe => 0o777,+            Node::Fd(_) => 0o700, // lrwx------, like Linux+        }+    }+    fn ino(&self) -> Option<u64> {+        Some(match *self {+            Node::Root => 1,+            Node::SelfDir => 2,+            Node::FdDir => 3,+            Node::Exe => 4,+            Node::Fd(n) => 1000 + n,+        })+    }+    fn owner(&self) -> (u32, u32) {+        // Everything under /proc/self belongs to whoever is looking.+        match (self, crate::syscall::current()) {+            (Node::Root, _) | (_, Err(_)) => (0, 0),+            (_, Ok(p)) => {+                let c = *p.creds.lock();+                (c.euid, c.egid)+            }+        }+    }+    fn readlink(&self) -> Result<String, Errno> {+        match *self {+            Node::Exe => Ok(crate::syscall::current()?.exe.lock().clone()),+            Node::Fd(n) => {+                let file = crate::syscall::current()?.file(n).map_err(|_| Errno::ENOENT)?;+                Ok(fd_link(&file))+            }+            _ => Err(Errno::EINVAL),+        }+    }+    fn lookup(&self, name: &str) -> Result<Arc<dyn Inode>, Errno> {+        let node = match (self, name) {+            (Node::Root, "self") => Node::SelfDir,+            (Node::SelfDir, "fd") => Node::FdDir,+            (Node::SelfDir, "exe") => Node::Exe,+            (Node::FdDir, n) => {+                let fd = fd_number(&format!("/proc/self/fd/{}", n)).ok_or(Errno::ENOENT)?;+                crate::syscall::current()?.file(fd).map_err(|_| Errno::ENOENT)?;+                Node::Fd(fd)+            }+            (Node::Root | Node::SelfDir, _) => return Err(Errno::ENOENT),+            _ => return Err(Errno::ENOTDIR),+        };+        Ok(Arc::new(node))+    }+    fn list(&self) -> Result<Vec<DirEntry>, Errno> {+        let entry = |name: String, kind, ino| DirEntry { name, kind, ino };+        Ok(match self {+            Node::Root => alloc::vec![entry(String::from("self"), InodeKind::Directory, 2)],+            Node::SelfDir => alloc::vec![+                entry(String::from("exe"), InodeKind::Symlink, 4),+                entry(String::from("fd"), InodeKind::Directory, 3),+            ],+            Node::FdDir => my_fds().into_iter().map(|(fd, _)| entry(format!("{}", fd), InodeKind::Symlink, 1000 + fd)).collect(),+            _ => return Err(Errno::ENOTDIR),+        })+    }+    fn create(&self, _name: &str, _kind: InodeKind) -> Result<Arc<dyn Inode>, Errno> {+        Err(if self.kind() == InodeKind::Directory { Errno::EACCES } else { Errno::ENOTDIR })+    }+    fn remove(&self, _name: &str) -> Result<(), Errno> {+        Err(if self.kind() == InodeKind::Directory { Errno::EACCES } else { Errno::ENOTDIR })+    }+}
@@ -51,6 +51,8 @@ pub enum Node {     Dir(Mutex<BTreeMap<String, Arc<RamInode>>>),     /// A symlink's target (stage 17).     Symlink(String),+    /// A Unix socket's name (stage 18): no contents.+    Socket, }  /// What `stat` shows besides the contents (stage 16).@@ -113,6 +115,7 @@ impl Inode for RamInode {             Node::File(_) => InodeKind::File,             Node::Dir(_) => InodeKind::Directory,             Node::Symlink(_) => InodeKind::Symlink,+            Node::Socket => InodeKind::Socket,         }     } @@ -121,6 +124,7 @@ impl Inode for RamInode {             Node::File(data) => data.lock().len(),             Node::Dir(entries) => entries.lock().len(),             Node::Symlink(target) => target.len(),+            Node::Socket => 0,         }     } @@ -171,6 +175,7 @@ impl Inode for RamInode {         let node = Arc::new(RamInode::new(match kind {             InodeKind::File => Node::File(Mutex::new(Vec::new())),             InodeKind::Directory => Node::Dir(Mutex::new(BTreeMap::new())),+            InodeKind::Socket => Node::Socket,             InodeKind::Symlink | InodeKind::Device => return Err(Errno::EOPNOTSUPP), // symlinks: `symlink`         }));         entries.insert(String::from(name), node.clone());
@@ -11,6 +11,7 @@ //!                    the `netd` kernel thread that keeps smoltcp going, and //!                    reacts to DHCP (configure our IP address and route). //!   * socket.rs      the BSD-style sockets user programs get as fds.+//!   * unix.rs        AF_UNIX sockets (stage 18): local only, no network. //! //! How packets keep flowing: smoltcp does nothing by itself. Someone must //! call `Interface::poll` whenever a frame arrives, whenever a program sent@@ -26,6 +27,7 @@  pub mod device; pub mod socket;+pub mod unix; pub mod virtio_net;  use crate::thread::WaitQueue;
@@ -0,0 +1,1513 @@+//! Unix domain sockets (stage 18): AF_UNIX, sockets between programs on+//! the same machine.+//!+//! A network socket (net/socket.rs) talks to another computer through the+//! network card. A Unix socket talks to another *program on this+//! computer*, and the kernel just moves the bytes from one socket's queue+//! to the other's. Wayland, X11, D-Bus, systemd, Docker and most local+//! servers use them. Why not a pipe? A Unix socket+//!+//!   * goes both ways (a pipe is one-way),+//!   * has a *name* other programs can find: a file in the file system+//!     ("/tmp/.X11-unix/X0", made by `bind`), or an "abstract" name that+//!     lives only in the kernel, or none at all (`socketpair`),+//!   * lets one server `accept` many clients, like TCP,+//!   * can carry *open files* from one program to another (SCM_RIGHTS):+//!     the receiver gets new fds for the same open files. Wayland sends+//!     shared-memory buffers that way.+//!   * says who is on the other end (SO_PEERCRED: pid, uid, gid).+//!+//! The three kinds, picked by socket()'s type:+//!+//!   SOCK_STREAM     a byte stream, like TCP or a pipe: message borders+//!                   are not kept (two writes may come out as one read)+//!   SOCK_DGRAM      separate messages ("datagrams"), like UDP, but+//!                   reliable and in order; no connection needed+//!   SOCK_SEQPACKET  connected like STREAM, but keeps message borders+//!+//! How it's built. Every socket has an `Endpoint`: a queue of messages+//! that were sent *to* it, plus its name, its peer and flags. Sending+//! means "lock the peer's endpoint, append to its queue, wake its+//! readers". Each queue has a size limit (SO_RCVBUF, and the sender's+//! SO_SNDBUF); a sender that finds the queue full waits on the peer's+//! `writers` queue, or gets EAGAIN when non-blocking. That waiting is+//! called *backpressure*: a fast writer can't use up all the memory.+//!+//! Connecting: `connect` makes a brand-new endpoint for the server's side+//! and puts it in the listening socket's queue of pending connections;+//! `accept` takes it out and gives it an fd. (Linux calls the not yet+//! accepted one an "embryo".)+//!+//! Lock rule: an endpoint's `state` is locked with interrupts off (it is+//! read in wait conditions, see thread.rs), and we never hold two+//! endpoints' locks at once, so two sockets sending to each other can't+//! deadlock.+//!+//! Known gap: a socket that is sent through *itself* (its own fd, in its+//! own queue) keeps itself alive forever. Linux has a garbage collector+//! for such cycles; we don't (yet).++use crate::errno::Errno;+use crate::fs::{self, file::File, file::Stat, file::KIND_SOCKET, Inode, InodeKind};+use crate::poll::{self, POLLERR, POLLHUP, POLLIN, POLLOUT, POLLRDHUP};+use crate::process::Process;+use crate::signal;+use crate::syscall::{copy_to_user, current, read_iovecs, user_slice, user_slice_mut};+use crate::thread::WaitQueue;+use alloc::{+    collections::VecDeque,+    sync::{Arc, Weak},+    vec::Vec,+};+use core::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};+use spin::Mutex;+use x86_64::instructions::interrupts::without_interrupts;++pub const AF_UNSPEC: u64 = 0;+pub const AF_UNIX: u64 = 1;+const SOCK_STREAM: u64 = 1;+const SOCK_DGRAM: u64 = 2;+const SOCK_RAW: u64 = 3;+const SOCK_SEQPACKET: u64 = 5;+const SOCK_NONBLOCK: u64 = 0o4000;+const SOCK_CLOEXEC: u64 = 0o2000000;++// send/recv flags (Linux's values).+const MSG_OOB: u64 = 0x1;+const MSG_PEEK: u64 = 0x2;+const MSG_CTRUNC: u32 = 0x8;+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;++// Socket options.+const SOL_SOCKET: u64 = 1;+const SO_REUSEADDR: u64 = 2;+const SO_TYPE: u64 = 3;+const SO_ERROR: u64 = 4;+const SO_SNDBUF: u64 = 7;+const SO_RCVBUF: u64 = 8;+const SO_KEEPALIVE: u64 = 9;+const SO_PASSCRED: u64 = 16;+const SO_PEERCRED: u64 = 17;+const SO_RCVTIMEO: u64 = 20;+const SO_SNDTIMEO: u64 = 21;+const SO_ACCEPTCONN: u64 = 30;+const SO_SNDBUFFORCE: u64 = 32;+const SO_RCVBUFFORCE: u64 = 33;+const SO_PROTOCOL: u64 = 38;+const SO_DOMAIN: u64 = 39;++// Control messages ("ancillary data") in sendmsg/recvmsg.+const SCM_RIGHTS: u32 = 1;+const SCM_CREDENTIALS: u32 = 2;+/// At most this many fds in one message (Linux's SCM_MAX_FD).+const SCM_MAX_FD: usize = 253;++/// Buffer sizes, as on a default Linux: 208 KiB each way; setsockopt+/// doubles what it's given (Linux keeps room for bookkeeping) within+/// these limits.+const DEFAULT_BUF: usize = 212_992;+const MAX_BUF: usize = 2 * 212_992;+const MIN_SNDBUF: usize = 4608;+const MIN_RCVBUF: usize = 2304;+/// What a datagram costs on top of its bytes (Linux's sk_buff header is+/// about that big): so a queue of empty messages can fill up too.+const MSG_OVERHEAD: usize = 256;+/// The most sendmsg/recvmsg copies in one call.+const MAX_IO: usize = 4 << 20;+/// Longest sockaddr_un: 2 bytes family + 108 path bytes.+const SUN_LEN: usize = 110;++#[derive(Clone, Copy, PartialEq, Eq, Debug)]+pub enum Type {+    Stream,+    Dgram,+    SeqPacket,+}++impl Type {+    fn number(self) -> u32 {+        match self {+            Type::Stream => SOCK_STREAM as u32,+            Type::Dgram => SOCK_DGRAM as u32,+            Type::SeqPacket => SOCK_SEQPACKET as u32,+        }+    }+}++/// A socket's address.+#[derive(Clone, PartialEq, Eq, Debug)]+pub enum Name {+    /// Never bound (socketpair, most clients).+    Unnamed,+    /// A file system name, as given to bind (getsockname shows it as is).+    Path(Vec<u8>),+    /// Linux's abstract names: sun_path starts with a 0 byte; the name+    /// is the rest and exists only in the kernel (no file, gone when the+    /// socket closes).+    Abstract(Vec<u8>),+}++/// struct ucred: who sent a message / is on the other end.+#[repr(C)]+#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]+pub struct Ucred {+    pub pid: u32,+    pub uid: u32,+    pub gid: u32,+}++/// The calling process: (the credentials a message carries by default:+/// pid + *real* ids, the ones SO_PEERCRED shows: pid + *effective* ids),+/// as Linux.+fn my_creds(p: &Process) -> (Ucred, Ucred) {+    let c = *p.creds.lock();+    let pid = p.pid as u32;+    (Ucred { pid, uid: c.ruid, gid: c.rgid }, Ucred { pid, uid: c.euid, gid: c.egid })+}++/// One message in a socket's queue. On a stream socket, chunks without+/// fds from the same sender are merged into the last one.+struct Msg {+    data: Vec<u8>,+    /// How much of `data` has been read (stream sockets read in pieces).+    start: usize,+    /// SCM_RIGHTS: open files on their way to the receiver ("in flight").+    /// Dropping the message closes them.+    fds: Vec<Arc<dyn File>>,+    cred: Ucred,+    from: Name,+}++impl Msg {+    fn len(&self) -> usize {+        self.data.len() - self.start+    }+}++enum Role {+    /// Just made (or a datagram socket).+    Fresh,+    /// listen() was called: `pending` are connections not accepted yet.+    Listening { backlog: usize, pending: VecDeque<Arc<Endpoint>> },+    /// A stream/seqpacket socket with a peer (it may have gone since).+    Connected,+}++struct State {+    role: Role,+    /// The other end (stream: the connection; dgram: the default+    /// destination set by connect).+    peer: Option<Weak<Endpoint>>,+    name: Name,+    /// The registry key we're bound under (to remove it on close).+    key: Option<Key>,+    queue: VecDeque<Msg>,+    /// What the queue costs: bytes (stream), bytes + MSG_OVERHEAD per+    /// message (dgram, seqpacket). Senders wait while it's at the limit.+    queued: usize,+    rcv_shut: bool,+    snd_shut: bool,+    /// SO_PASSCRED: receive SCM_CREDENTIALS with every message.+    passcred: bool,+    /// SO_PEERCRED: who connected (server side) or listened (client).+    peercred: Option<Ucred>,+    /// A pending error for the next call (ECONNRESET: the peer closed+    /// with data we never read... or one it never read).+    error: Option<Errno>,+    closed: bool,+    rcvtimeo: Option<u64>,+    sndtimeo: Option<u64>,+}++pub struct Endpoint {+    /// Its number: fstat's st_ino and "socket:[N]" in /proc/self/fd.+    id: u64,+    ty: Type,+    state: Mutex<State>,+    /// Waiting for something to read in our queue (or a connection).+    readers: WaitQueue,+    /// Waiting for room in our queue (senders to us, connectors).+    writers: WaitQueue,+    sndbuf: AtomicUsize,+    rcvbuf: AtomicUsize,+}++static NEXT_ID: AtomicU64 = AtomicU64::new(1000);++impl Endpoint {+    fn new(ty: Type) -> Arc<Endpoint> {+        Arc::new(Endpoint {+            id: NEXT_ID.fetch_add(1, Ordering::Relaxed),+            ty,+            state: Mutex::new(State {+                role: Role::Fresh,+                peer: None,+                name: Name::Unnamed,+                key: None,+                queue: VecDeque::new(),+                queued: 0,+                rcv_shut: false,+                snd_shut: false,+                passcred: false,+                peercred: None,+                error: None,+                closed: false,+                rcvtimeo: None,+                sndtimeo: None,+            }),+            readers: WaitQueue::new(),+            writers: WaitQueue::new(),+            sndbuf: AtomicUsize::new(DEFAULT_BUF),+            rcvbuf: AtomicUsize::new(DEFAULT_BUF),+        })+    }++    /// Run `f` on our state, locked (with interrupts off: see the top).+    fn with<R>(&self, f: impl FnOnce(&mut State) -> R) -> R {+        without_interrupts(|| f(&mut self.state.lock()))+    }++    fn wake(&self) {+        self.readers.wake_all();+        self.writers.wake_all();+        poll::notify();+    }++    fn peer(&self) -> Option<Arc<Endpoint>> {+        self.with(|s| s.peer.as_ref().and_then(|w| w.upgrade()))+    }++    /// What a message of `len` bytes costs in a queue.+    fn cost(&self, len: usize) -> usize {+        if self.ty == Type::Stream { len } else { len + MSG_OVERHEAD }+    }++    /// The socket is closed (its last fd is gone), or a pending connection+    /// is thrown away (`embryo`). Tell the peer, empty the queue: the+    /// fds in flight in it are closed too, once our lock is released.+    fn close(self: &Arc<Self>, embryo: bool) {+        let (queue, pending, peer, key, unread) = self.with(|s| {+            s.closed = true;+            s.rcv_shut = true;+            s.snd_shut = true;+            let unread = !s.queue.is_empty();+            let pending = match core::mem::replace(&mut s.role, Role::Fresh) {+                Role::Listening { pending, .. } => pending,+                _ => VecDeque::new(),+            };+            s.queued = 0;+            (core::mem::take(&mut s.queue), pending, s.peer.take(), s.key.take(), unread)+        });+        if let Some(key) = key {+            unregister(&key, self);+        }+        if let Some(peer) = peer.and_then(|w| w.upgrade()) {+            if self.ty != Type::Dgram {+                // The connection is over: the peer reads EOF after its+                // data, and gets EPIPE writing. If we die with unread data+                // (or before being accepted), it gets ECONNRESET first.+                peer.with(|s| {+                    s.rcv_shut = true;+                    s.snd_shut = true;+                    if unread || embryo {+                        s.error = Some(Errno::ECONNRESET);+                    }+                });+            }+            peer.wake();+        }+        for p in pending {+            p.close(true);+        }+        self.wake();+        drop(queue); // closes the in-flight fds (maybe other sockets: no locks held)+    }+}++// --- names: the registry ----------------------------------------------------++/// How a bound socket is found: by the socket file's inode (path names:+/// whichever path leads to the file, even after a rename), or by its+/// abstract name.+#[derive(Clone, PartialEq, Eq, Debug)]+enum Key {+    Inode(u64, u64), // (st_dev, st_ino)+    Abstract(Vec<u8>),+}++struct Bound {+    key: Key,+    ep: Weak<Endpoint>,+    /// Keep the socket file's inode alive while bound (ramfs inode+    /// numbers are addresses: this way none can be reused meanwhile).+    _inode: Option<Arc<dyn Inode>>,+}++static REGISTRY: Mutex<Vec<Bound>> = Mutex::new(Vec::new());++fn registry<R>(f: impl FnOnce(&mut Vec<Bound>) -> R) -> R {+    without_interrupts(|| f(&mut REGISTRY.lock()))+}++fn unregister(key: &Key, ep: &Arc<Endpoint>) {+    registry(|r| r.retain(|b| !(b.key == *key && b.ep.as_ptr() == Arc::as_ptr(ep))));+}++fn find_key(key: &Key) -> Option<Arc<Endpoint>> {+    registry(|r| r.iter().rev().find(|b| b.key == *key).and_then(|b| b.ep.upgrade()))+}++/// The key of the socket file at `path` (and the inode), if it is one.+fn inode_key(path: &str) -> Result<(Key, Arc<dyn Inode>), Errno> {+    let (inode, real) = fs::lookup_resolved(path, true)?;+    if inode.kind() != InodeKind::Socket {+        return Err(Errno::ECONNREFUSED); // a file, but no socket's+    }+    let ino = inode.ino().unwrap_or_else(|| fs::file::ino_hash(real.as_bytes()));+    Ok((Key::Inode(fs::dev_of(&real), ino), inode))+}++/// A sockaddr_un from user space.+#[derive(Debug)]+enum Addr {+    /// AF_UNSPEC: connect() to "nothing" = disconnect (datagrams).+    Unspec,+    /// Just the family: bind picks an abstract name ("autobind").+    Auto,+    Path(Vec<u8>),+    Abstract(Vec<u8>),+}++fn read_addr(ptr: u64, len: u64) -> Result<Addr, Errno> {+    if len < 2 || len as usize > SUN_LEN {+        return Err(Errno::EINVAL);+    }+    let raw = user_slice(ptr, len)?;+    match u16::from_ne_bytes([raw[0], raw[1]]) as u64 {+        AF_UNIX => {}+        AF_UNSPEC => return Ok(Addr::Unspec),+        _ => return Err(Errno::EINVAL),+    }+    let path = &raw[2..];+    Ok(match path.first() {+        None => Addr::Auto,+        Some(0) => Addr::Abstract(path[1..].to_vec()),+        Some(_) => Addr::Path(path.iter().take_while(|&&b| b != 0).copied().collect()),+    })+}++/// The absolute path of a Path address (relative ones: from the cwd).+fn abs_path(raw: &[u8]) -> Result<alloc::string::String, Errno> {+    crate::syscall::path_at(crate::syscall::AT_FDCWD as u64, raw)+}++/// Find the socket an address names (connect, sendto).+fn lookup(addr: &Addr) -> Result<Arc<Endpoint>, Errno> {+    let key = match addr {+        Addr::Path(raw) => inode_key(&abs_path(raw)?)?.0,+        Addr::Abstract(name) => Key::Abstract(name.clone()),+        Addr::Auto | Addr::Unspec => return Err(Errno::EINVAL),+    };+    find_key(&key).ok_or(Errno::ECONNREFUSED)+}++/// Encode a name as a sockaddr_un: (bytes, length).+fn encode(name: &Name) -> Vec<u8> {+    let mut out = (AF_UNIX as u16).to_ne_bytes().to_vec();+    match name {+        Name::Unnamed => {}+        Name::Path(p) => {+            out.extend_from_slice(p);+            out.push(0);+        }+        Name::Abstract(a) => {+            out.push(0);+            out.extend_from_slice(a);+        }+    }+    out+}++/// Write an address like Linux: *len_ptr says how much room there is;+/// we write at most that and set it to the real length.+fn write_addr(ptr: u64, len_ptr: u64, bytes: &[u8]) -> Result<(), Errno> {+    if ptr == 0 || len_ptr == 0 {+        return Ok(());+    }+    let room = i32::from_ne_bytes(user_slice(len_ptr, 4)?.try_into().unwrap());+    if room < 0 {+        return Err(Errno::EINVAL);+    }+    let n = bytes.len().min(room as usize);+    user_slice_mut(ptr, n as u64)?.copy_from_slice(&bytes[..n]);+    copy_to_user(len_ptr, &(bytes.len() as u32))+}++impl Endpoint {+    fn bind(self: &Arc<Self>, addr: Addr) -> Result<(), Errno> {+        if self.with(|s| s.name != Name::Unnamed || s.closed) {+            return Err(Errno::EINVAL); // already has a name+        }+        let (key, name, inode) = match addr {+            Addr::Unspec => return Err(Errno::EINVAL),+            Addr::Auto => return self.autobind(),+            Addr::Abstract(a) => (Key::Abstract(a.clone()), Name::Abstract(a), None),+            Addr::Path(raw) => {+                if raw.is_empty() {+                    return Err(Errno::EINVAL);+                }+                let path = abs_path(&raw)?;+                // The name is a new file of type "socket". If anything+                // is there already (even an old socket file nobody is+                // bound to), that's EADDRINUSE: servers unlink first.+                let inode = match fs::create(&path, InodeKind::Socket) {+                    Err(Errno::EEXIST) => return Err(Errno::EADDRINUSE),+                    Err(Errno::EOPNOTSUPP) => return Err(Errno::EPERM), // FAT, exFAT: no such files+                    other => other?,+                };+                crate::syscall::apply_create_mode(&*inode, 0o777); // srwxr-xr-x after the umask+                let (key, inode) = inode_key(&path)?;+                (key, Name::Path(raw), Some(inode))+            }+        };+        registry(|r| {+            r.retain(|b| b.ep.strong_count() > 0);+            if let Key::Abstract(_) = key {+                if r.iter().any(|b| b.key == key) {+                    return Err(Errno::EADDRINUSE);+                }+            }+            // (A path key can only repeat if the old file was deleted and+            // its inode number reused: the old entry is stale then.)+            r.retain(|b| b.key != key);+            r.push(Bound { key: key.clone(), ep: Arc::downgrade(self), _inode: inode });+            Ok(())+        })?;+        self.with(|s| {+            s.name = name;+            s.key = Some(key);+        });+        Ok(())+    }++    /// bind with just the family: a free abstract name of 5 hex digits,+    /// like Linux.+    fn autobind(self: &Arc<Self>) -> Result<(), Errno> {+        static NEXT: AtomicU64 = AtomicU64::new(0);+        for _ in 0..0x10_0000 {+            let n = NEXT.fetch_add(1, Ordering::Relaxed) & 0xf_ffff;+            let name = alloc::format!("{:05x}", n).into_bytes();+            match self.bind(Addr::Abstract(name)) {+                Err(Errno::EADDRINUSE) => continue,+                other => return other,+            }+        }+        Err(Errno::EADDRINUSE)+    }++    fn listen(&self, backlog: i64) -> Result<(), Errno> {+        if self.ty == Type::Dgram {+            return Err(Errno::EOPNOTSUPP);+        }+        let (_, peer_cred) = my_creds(&*current()?);+        let backlog = if backlog < 0 { 4096 } else { (backlog as usize).min(4096) };+        self.with(|s| {+            if s.name == Name::Unnamed {+                return Err(Errno::EINVAL); // nobody could find it+            }+            match &mut s.role {+                Role::Connected => Err(Errno::EINVAL),+                Role::Listening { backlog: b, .. } => {+                    *b = backlog;+                    Ok(())+                }+                Role::Fresh => {+                    s.role = Role::Listening { backlog, pending: VecDeque::new() };+                    s.peercred = Some(peer_cred); // what connecting clients will see+                    Ok(())+                }+            }+        })?;+        self.wake();+        Ok(())+    }++    fn connect(self: &Arc<Self>, addr: Addr, nonblock: bool) -> Result<(), Errno> {+        if self.ty == Type::Dgram {+            // Datagrams: just remember the default destination.+            let target = match addr {+                Addr::Unspec => None,+                addr => Some(lookup(&addr)?),+            };+            if let Some(t) = &target {+                if t.ty != self.ty {+                    return Err(Errno::EPROTOTYPE);+                }+                if t.with(|s| s.peer.as_ref().is_some_and(|w| w.as_ptr() != Arc::as_ptr(self))) {+                    return Err(Errno::EPERM); // it only talks to someone else+                }+            }+            self.with(|s| s.peer = target.as_ref().map(Arc::downgrade));+            self.wake();+            return Ok(());+        }+        self.with(|s| match s.role {+            Role::Fresh => Ok(()),+            Role::Connected => Err(Errno::EISCONN),+            Role::Listening { .. } => Err(Errno::EINVAL),+        })?;+        if let Addr::Unspec = addr {+            return Err(Errno::EAFNOSUPPORT);+        }+        let listener = lookup(&addr)?;+        if listener.ty != self.ty {+            return Err(Errno::EPROTOTYPE);+        }+        let (_, me) = my_creds(&*current()?);+        // The server's end of the new connection.+        let server = Endpoint::new(self.ty);+        let (lname, lcred, lpass) = listener.with(|s| (s.name.clone(), s.peercred, s.passcred));+        server.with(|s| {+            s.role = Role::Connected;+            s.peer = Some(Arc::downgrade(self));+            s.name = lname;+            s.peercred = Some(me);+            s.passcred = lpass;+        });+        server.sndbuf.store(listener.sndbuf.load(Ordering::Relaxed), Ordering::Relaxed);+        server.rcvbuf.store(listener.rcvbuf.load(Ordering::Relaxed), Ordering::Relaxed);+        let timeout = self.with(|s| s.sndtimeo);+        let mut item = Some(server.clone());+        // Into the listener's queue of pending connections; when that's+        // full (backlog), wait like Linux (or EAGAIN when non-blocking).+        wait_on(&listener.writers, timeout, || {+            let mut s = listener.state.lock();+            if s.closed {+                return Some(Err(Errno::ECONNREFUSED));+            }+            match &mut s.role {+                Role::Listening { backlog, pending } => {+                    if pending.len() <= *backlog {+                        pending.push_back(item.take().unwrap());+                        Some(Ok(()))+                    } else if nonblock {+                        Some(Err(Errno::EAGAIN))+                    } else {+                        None+                    }+                }+                _ => Some(Err(Errno::ECONNREFUSED)), // bound, but not listening+            }+        })??;+        self.with(|s| {+            s.role = Role::Connected;+            s.peer = Some(Arc::downgrade(&server));+            s.peercred = lcred;+        });+        listener.readers.wake_all();+        poll::notify();+        Ok(())+    }++    fn accept(&self, nonblock: bool) -> Result<Arc<Endpoint>, Errno> {+        let timeout = self.with(|s| s.rcvtimeo);+        let ep = wait_on(&self.readers, timeout, || {+            let mut s = self.state.lock();+            match &mut s.role {+                Role::Listening { pending, .. } => match pending.pop_front() {+                    Some(ep) => Some(Ok(ep)),+                    None if nonblock => Some(Err(Errno::EAGAIN)),+                    None => None,+                },+                _ => Some(Err(Errno::EINVAL)),+            }+        })??;+        self.writers.wake_all(); // room in the backlog+        poll::notify();+        Ok(ep)+    }++    fn shutdown(&self, how: u64) -> Result<(), Errno> {+        let (rd, wr) = match how {+            0 => (true, false),+            1 => (false, true),+            2 => (true, true),+            _ => return Err(Errno::EINVAL),+        };+        let peer = self.with(|s| {+            s.rcv_shut |= rd;+            s.snd_shut |= wr;+            s.peer.as_ref().and_then(|w| w.upgrade())+        });+        if let Some(peer) = peer.filter(|_| self.ty != Type::Dgram) {+            // The other side sees the mirror image: we stop writing, it+            // reads EOF; we stop reading, its writes fail (EPIPE).+            peer.with(|s| {+                s.rcv_shut |= wr;+                s.snd_shut |= rd;+            });+            peer.wake();+        }+        self.wake();+        Ok(())+    }+}++/// Wait on `q` until `f` says Some, with an optional timeout+/// (SO_RCVTIMEO/SO_SNDTIMEO: then EAGAIN, like Linux).+fn wait_on<T>(q: &WaitQueue, timeout_us: Option<u64>, f: impl FnMut() -> Option<T>) -> Result<T, Errno> {+    match timeout_us {+        None => q.wait_until_intr(f),+        Some(us) => q.wait_until_deadline_intr(crate::time::deadline_after_us(us), f)?.ok_or(Errno::EAGAIN),+    }+}++// --- sending and receiving --------------------------------------------------++/// What one recv found.+struct Received {+    data: Vec<u8>,+    /// The message's whole length (datagrams: more than `data` when it+    /// didn't fit, and the rest is thrown away: MSG_TRUNC).+    full: usize,+    fds: Vec<Arc<dyn File>>,+    cred: Option<Ucred>,+    from: Name,+}++impl Endpoint {+    /// Send `data` (+ `fds`, SCM_RIGHTS) to our peer, or to `to`.+    /// Returns how many bytes went; EPIPE is for the caller to turn into+    /// SIGPIPE.+    fn send(self: &Arc<Self>, data: &[u8], fds: Vec<Arc<dyn File>>, cred: Ucred, to: Option<Addr>, nonblock: bool) -> Result<usize, Errno> {+        let (name, timeout) = self.with(|s| (s.name.clone(), s.sndtimeo));+        if self.ty == Type::Dgram {+            return self.send_dgram(data, fds, cred, to, nonblock, name, timeout);+        }+        // Stream and seqpacket: only ever to the connected peer.+        let (connected, snd_shut) = self.with(|s| (matches!(s.role, Role::Connected), s.snd_shut));+        if to.is_some() {+            return Err(if connected { Errno::EISCONN } else { Errno::EOPNOTSUPP });+        }+        if !connected {+            return Err(Errno::ENOTCONN);+        }+        let peer = match self.peer() {+            Some(p) if !snd_shut => p,+            _ => return Err(Errno::EPIPE),+        };+        let limit = self.sndbuf.load(Ordering::Relaxed).min(peer.rcvbuf.load(Ordering::Relaxed));+        if self.ty == Type::SeqPacket && self.cost(data.len()) > limit {+            return Err(Errno::EMSGSIZE);+        }+        if data.is_empty() && self.ty == Type::Stream && fds.is_empty() {+            return Ok(0);+        }+        let mut fds = Some(fds);+        let mut sent = 0;+        loop {+            // Put as much as fits into the peer's queue; wait for room.+            let r = wait_on(&peer.writers, timeout, || {+                let mut s = peer.state.lock();+                if s.rcv_shut || s.closed {+                    return Some(Err(Errno::EPIPE));+                }+                let room = limit.saturating_sub(s.queued);+                let n = if self.ty == Type::Stream {+                    room.min(data.len() - sent)+                } else if s.queue.is_empty() || room >= self.cost(data.len()) {+                    data.len()+                } else {+                    0+                };+                if room == 0 || (n == 0 && data.len() > sent) {+                    return if nonblock { Some(Err(Errno::EAGAIN)) } else { None };+                }+                let chunk = &data[sent..sent + n];+                let new_fds = fds.take().unwrap_or_default();+                s.queued += self.cost(n);+                match s.queue.back_mut() {+                    // Stream: glue onto the last chunk if nothing tells them apart.+                    Some(last) if self.ty == Type::Stream && new_fds.is_empty() && last.fds.is_empty() && last.cred == cred && last.start == 0 => {+                        last.data.extend_from_slice(chunk)+                    }+                    _ => s.queue.push_back(Msg { data: chunk.to_vec(), start: 0, fds: new_fds, cred, from: name.clone() }),+                }+                drop(s);+                peer.readers.wake_all();+                poll::notify();+                Some(Ok(n))+            });+            match r {+                Ok(Ok(n)) => {+                    sent += n;+                    if sent >= data.len() {+                        return Ok(sent);+                    }+                }+                // Partly sent, then full (non-blocking), a signal or a+                // timeout: say what went.+                Ok(Err(Errno::EAGAIN)) | Err(_) if sent > 0 => return Ok(sent),+                Ok(Err(e)) | Err(e) => return Err(e),+            }+        }+    }++    #[allow(clippy::too_many_arguments)]+    fn send_dgram(self: &Arc<Self>, data: &[u8], fds: Vec<Arc<dyn File>>, cred: Ucred, to: Option<Addr>, nonblock: bool, name: Name, timeout: Option<u64>) -> Result<usize, Errno> {+        if self.with(|s| s.snd_shut) {+            return Err(Errno::EPIPE);+        }+        let target = match to {+            Some(addr) => lookup(&addr)?,+            None => match self.with(|s| s.peer.clone()) {+                None => return Err(Errno::ENOTCONN),+                Some(w) => w.upgrade().ok_or(Errno::ECONNREFUSED)?,+            },+        };+        if target.ty != Type::Dgram {+            return Err(Errno::EPROTOTYPE);+        }+        let limit = self.sndbuf.load(Ordering::Relaxed).min(target.rcvbuf.load(Ordering::Relaxed));+        if self.cost(data.len()) > limit {+            return Err(Errno::EMSGSIZE);+        }+        let me = Arc::as_ptr(self);+        let mut msg = Some(Msg { data: data.to_vec(), start: 0, fds, cred, from: name });+        wait_on(&target.writers, timeout, || {+            let mut s = target.state.lock();+            if s.closed {+                return Some(Err(Errno::ECONNREFUSED));+            }+            if s.peer.as_ref().is_some_and(|w| w.as_ptr() != me) {+                return Some(Err(Errno::EPERM)); // it only listens to its own peer+            }+            if s.rcv_shut {+                return Some(Err(Errno::EPIPE));+            }+            if !s.queue.is_empty() && s.queued + self.cost(data.len()) > limit {+                return if nonblock { Some(Err(Errno::EAGAIN)) } else { None };+            }+            s.queued += self.cost(data.len());+            s.queue.push_back(msg.take().unwrap());+            drop(s);+            target.readers.wake_all();+            poll::notify();+            Some(Ok(data.len()))+        })?+    }++    /// Receive up to `want` bytes.+    fn recv(&self, want: usize, flags: u64, nonblock: bool) -> Result<Received, Errno> {+        let peek = flags & MSG_PEEK != 0;+        let timeout = self.with(|s| s.rcvtimeo);+        let r = wait_on(&self.readers, timeout, || {+            let mut s = self.state.lock();+            match (&s.role, self.ty) {+                (Role::Listening { .. }, _) | (Role::Fresh, Type::Stream) => return Some(Err(Errno::EINVAL)),+                (Role::Fresh, Type::SeqPacket) => return Some(Err(Errno::ENOTCONN)),+                _ => {}+            }+            if s.queue.is_empty() {+                if let Some(e) = s.error.take() {+                    return Some(Err(e));+                }+                if s.rcv_shut {+                    return Some(Ok(Received { data: Vec::new(), full: 0, fds: Vec::new(), cred: None, from: Name::Unnamed }));+                }+                return if nonblock { Some(Err(Errno::EAGAIN)) } else { None };+            }+            let passcred = s.passcred;+            let st = &mut *s;+            let first = st.queue.front_mut().unwrap();+            let cred = first.cred;+            let from = first.from.clone();+            let fds = if peek { first.fds.clone() } else { core::mem::take(&mut first.fds) };+            let mut data = Vec::new();+            let full;+            if self.ty == Type::Stream {+                // Gather from as many chunks as wanted, but not past one+                // carrying fds (they belong with its first byte) or from+                // another sender when credentials are passed.+                let had_fds = !fds.is_empty();+                let mut used = 0;+                let mut i = 0;+                while data.len() < want && i < st.queue.len() {+                    let m = &mut st.queue[i];+                    if i > 0 && (had_fds || !m.fds.is_empty() || (passcred && m.cred != cred)) {+                        break;+                    }+                    let n = m.len().min(want - data.len());+                    data.extend_from_slice(&m.data[m.start..m.start + n]);+                    if !peek {+                        m.start += n;+                        used += n;+                    }+                    i += 1;+                }+                full = data.len();+                if !peek {+                    st.queued -= used;+                    while st.queue.front().is_some_and(|m| m.len() == 0 && m.fds.is_empty()) {+                        st.queue.pop_front();+                    }+                }+            } else {+                // A whole message; what doesn't fit is lost.+                full = first.len();+                let n = full.min(want);+                data.extend_from_slice(&first.data[first.start..first.start + n]);+                if !peek {+                    st.queued -= self.cost(full);+                    st.queue.pop_front(); // (its fds were taken above)+                }+            }+            Some(Ok(Received { data, full, fds, cred: passcred.then_some(cred), from }))+        })??;+        // Room again in our queue: wake senders waiting for it.+        if !peek {+            self.writers.wake_all();+            poll::notify();+        }+        Ok(r)+    }++    fn poll(&self) -> u16 {+        let (mut ready, peer) = self.with(|s| {+            let mut ready = 0;+            if s.error.is_some() {+                ready |= POLLERR;+            }+            if let Role::Listening { pending, .. } = &s.role {+                return (if pending.is_empty() { 0 } else { POLLIN }, None);+            }+            if self.ty == Type::Stream && matches!(s.role, Role::Fresh) {+                return (POLLOUT | POLLHUP, None);+            }+            if s.rcv_shut && s.snd_shut {+                ready |= POLLHUP;+            }+            if s.rcv_shut {+                ready |= POLLIN | POLLRDHUP;+            }+            if !s.queue.is_empty() {+                ready |= POLLIN;+            }+            let peer = s.peer.as_ref().map(|w| w.upgrade());+            if s.snd_shut {+                ready |= POLLOUT; // a write fails right away (EPIPE): not blocked+            }+            (ready, peer)+        });+        if ready & POLLOUT == 0 {+            match peer {+                // Unconnected datagram socket: sendto never waits on us.+                None => ready |= POLLOUT,+                Some(None) => ready |= POLLOUT, // peer gone: writing fails at once+                Some(Some(p)) => {+                    let limit = self.sndbuf.load(Ordering::Relaxed).min(p.rcvbuf.load(Ordering::Relaxed));+                    if p.with(|s| s.closed || s.rcv_shut || s.queued < limit) {+                        ready |= POLLOUT;+                    }+                }+            }+        }+        ready+    }+}++// --- the open file ----------------------------------------------------------++/// An AF_UNIX socket as an open file (what an fd points to). When the+/// last fd (or in-flight copy) is gone, it's dropped and the socket+/// closes.+pub struct UnixSocket {+    ep: Arc<Endpoint>,+    nonblock: AtomicBool,+}++impl UnixSocket {+    fn new(ep: Arc<Endpoint>, nonblock: bool) -> Arc<dyn File> {+        Arc::new(UnixSocket { ep, nonblock: AtomicBool::new(nonblock) })+    }+    fn nb(&self, flags: u64) -> bool {+        self.nonblock.load(Ordering::Relaxed) || flags & MSG_DONTWAIT != 0+    }+    pub fn id(&self) -> u64 {+        self.ep.id+    }+}++impl Drop for UnixSocket {+    fn drop(&mut self) {+        self.ep.close(false);+    }+}++impl File for UnixSocket {+    fn read(&self, buf: &mut [u8]) -> Result<usize, Errno> {+        let r = self.ep.recv(buf.len(), 0, self.nb(0))?;+        buf[..r.data.len()].copy_from_slice(&r.data);+        Ok(r.data.len()) // (fds sent along are closed: read can't take them)+    }+    fn write(&self, buf: &[u8]) -> Result<usize, Errno> {+        let (cred, _) = my_creds(&*current()?);+        self.ep.send(buf, Vec::new(), cred, None, self.nb(0))+    }+    fn stat(&self) -> Result<Stat, Errno> {+        Ok(Stat { kind: KIND_SOCKET, size: 0, mode: 0o777 })+    }+    fn as_unix(&self) -> Option<&UnixSocket> {+        Some(self)+    }+    fn anon_ino(&self) -> Option<u64> {+        Some(self.ep.id)+    }+    fn set_nonblocking(&self, on: bool) {+        self.nonblock.store(on, Ordering::Relaxed);+    }+    fn nonblocking(&self) -> bool {+        self.nonblock.load(Ordering::Relaxed)+    }+    fn poll(&self) -> u16 {+        self.ep.poll()+    }+    fn ioctl(&self, cmd: u64, arg: u64) -> Result<u64, Errno> {+        const FIONREAD: u64 = 0x541B;+        if cmd != FIONREAD {+            return Err(Errno::ENOTTY);+        }+        // Bytes waiting (stream) or the next message's size.+        let n = self.ep.with(|s| match self.ep.ty {+            Type::Stream => s.queue.iter().map(|m| m.len()).sum::<usize>(),+            _ => s.queue.front().map_or(0, |m| m.len()),+        });+        copy_to_user(arg, &(n as i32))?;+        Ok(0)+    }+}++// --- the system calls -------------------------------------------------------++const SYS_SOCKET: u64 = 41;+const SYS_CONNECT: u64 = 42;+const SYS_ACCEPT: u64 = 43;+const SYS_SENDTO: u64 = 44;+const SYS_RECVFROM: u64 = 45;+const SYS_SENDMSG: u64 = 46;+const SYS_RECVMSG: u64 = 47;+const SYS_SHUTDOWN: u64 = 48;+const SYS_BIND: u64 = 49;+const SYS_LISTEN: u64 = 50;+const SYS_GETSOCKNAME: u64 = 51;+const SYS_GETPEERNAME: u64 = 52;+const SYS_SOCKETPAIR: u64 = 53;+const SYS_SETSOCKOPT: u64 = 54;+const SYS_GETSOCKOPT: u64 = 55;+const SYS_ACCEPT4: u64 = 288;++/// Called before the normal socket calls: handles AF_UNIX ones, and+/// sendmsg/recvmsg for every kind of socket. None = not ours.+pub fn dispatch(nr: u64, a: [u64; 6]) -> Option<Result<u64, Errno>> {+    match nr {+        SYS_SOCKET if a[0] == AF_UNIX => return Some(sys_socket(a[1], a[2])),+        SYS_SOCKETPAIR => return Some(sys_socketpair(a[0], a[1], a[2], a[3])),+        SYS_SENDMSG => return Some(sys_sendmsg(a[0], a[1], a[2])),+        SYS_RECVMSG => return Some(sys_recvmsg(a[0], a[1], a[2])),+        SYS_CONNECT | SYS_ACCEPT | SYS_ACCEPT4 | SYS_SENDTO | SYS_RECVFROM | SYS_SHUTDOWN | SYS_BIND | SYS_LISTEN+        | SYS_GETSOCKNAME | SYS_GETPEERNAME | SYS_SETSOCKOPT | SYS_GETSOCKOPT => {}+        _ => return None,+    }+    // The rest: only if the fd is a Unix socket.+    let file = current().ok()?.file(a[0]).ok()?;+    let sock = file.as_unix()?;+    let ep = &sock.ep;+    Some(match nr {+        SYS_BIND => read_addr(a[1], a[2]).and_then(|addr| ep.bind(addr)).map(|_| 0),+        SYS_LISTEN => ep.listen(a[1] as i32 as i64).map(|_| 0),+        SYS_CONNECT => read_addr(a[1], a[2]).and_then(|addr| ep.connect(addr, sock.nb(0))).map(|_| 0),+        SYS_ACCEPT => sys_accept(sock, a[1], a[2], 0),+        SYS_ACCEPT4 => sys_accept(sock, a[1], a[2], a[3]),+        SYS_SENDTO => {+            let to = if a[4] == 0 { Ok(None) } else { read_addr(a[4], a[5]).map(Some) };+            to.and_then(|to| user_slice(a[1], a[2]).and_then(|data| send(sock, data, Vec::new(), None, to, a[3])))+        }+        SYS_RECVFROM => sys_recvfrom(sock, a[1], a[2], a[3], a[4], a[5]),+        SYS_SHUTDOWN => ep.shutdown(a[1]).map(|_| 0),+        SYS_GETSOCKNAME => write_addr(a[1], a[2], &encode(&ep.with(|s| s.name.clone()))).map(|_| 0),+        SYS_GETPEERNAME => match ep.with(|s| (matches!(s.role, Role::Connected) || ep.ty == Type::Dgram, s.peer.clone())) {+            (true, Some(peer)) => {+                let name = peer.upgrade().map_or(Name::Unnamed, |p| p.with(|s| s.name.clone()));+                write_addr(a[1], a[2], &encode(&name)).map(|_| 0)+            }+            _ => Err(Errno::ENOTCONN),+        },+        SYS_SETSOCKOPT => setsockopt(ep, a[1], a[2], a[3], a[4]),+        SYS_GETSOCKOPT => getsockopt(sock, a[1], a[2], a[3], a[4]),+        _ => unreachable!(),+    })+}++fn sys_socket(ty: u64, protocol: u64) -> Result<u64, Errno> {+    if ty & !(0xf | SOCK_NONBLOCK | SOCK_CLOEXEC) != 0 {+        return Err(Errno::EINVAL);+    }+    let kind = match ty & 0xf {+        SOCK_STREAM => Type::Stream,+        SOCK_DGRAM | SOCK_RAW => Type::Dgram, // (Linux: raw = dgram here)+        SOCK_SEQPACKET => Type::SeqPacket,+        _ => return Err(Errno::ESOCKTNOSUPPORT),+    };+    if protocol > 1 {+        return Err(Errno::EPROTONOSUPPORT);+    }+    let p = current()?;+    p.add_file_with(UnixSocket::new(Endpoint::new(kind), ty & SOCK_NONBLOCK != 0), 0, ty & SOCK_CLOEXEC != 0)+}++/// socketpair: two sockets connected to each other, nameless. The+/// simplest way to get a two-way channel to a child process.+fn sys_socketpair(domain: u64, ty: u64, protocol: u64, out: u64) -> Result<u64, Errno> {+    match domain {+        AF_UNIX => {}+        2 => return Err(Errno::EOPNOTSUPP), // AF_INET: Linux says this too+        _ => return Err(Errno::EAFNOSUPPORT),+    }+    if ty & !(0xf | SOCK_NONBLOCK | SOCK_CLOEXEC) != 0 {+        return Err(Errno::EINVAL);+    }+    let kind = match ty & 0xf {+        SOCK_STREAM => Type::Stream,+        SOCK_DGRAM | SOCK_RAW => Type::Dgram,+        SOCK_SEQPACKET => Type::SeqPacket,+        _ => return Err(Errno::ESOCKTNOSUPPORT),+    };+    if protocol > 1 {+        return Err(Errno::EPROTONOSUPPORT);+    }+    user_slice_mut(out, 8)?; // check before making anything+    let p = current()?;+    let (_, me) = my_creds(&p);+    let (a, b) = (Endpoint::new(kind), Endpoint::new(kind));+    for (x, y) in [(&a, &b), (&b, &a)] {+        x.with(|s| {+            if kind != Type::Dgram {+                s.role = Role::Connected;+            }+            s.peer = Some(Arc::downgrade(y));+            s.peercred = Some(me);+        });+    }+    let nb = ty & SOCK_NONBLOCK != 0;+    let cloexec = ty & SOCK_CLOEXEC != 0;+    let fd0 = p.add_file_with(UnixSocket::new(a, nb), 0, cloexec)?;+    let fd1 = match p.add_file_with(UnixSocket::new(b, nb), 0, cloexec) {+        Ok(fd) => fd,+        Err(e) => {+            let _ = p.close(fd0);+            return Err(e);+        }+    };+    copy_to_user(out, &[fd0 as u32, fd1 as u32])?;+    Ok(0)+}++fn sys_accept(sock: &UnixSocket, addr: u64, len_ptr: u64, flags: u64) -> Result<u64, Errno> {+    if flags & !(SOCK_NONBLOCK | SOCK_CLOEXEC) != 0 {+        return Err(Errno::EINVAL);+    }+    if sock.ep.ty == Type::Dgram {+        return Err(Errno::EOPNOTSUPP);+    }+    let ep = sock.ep.accept(sock.nb(0))?;+    // The address of who connected (usually unnamed).+    let name = ep.peer().map_or(Name::Unnamed, |c| c.with(|s| s.name.clone()));+    let fd = current()?.add_file_with(UnixSocket::new(ep, flags & SOCK_NONBLOCK != 0), 0, flags & SOCK_CLOEXEC != 0)?;+    write_addr(addr, len_ptr, &encode(&name))?;+    Ok(fd)+}++/// Send, with SIGPIPE for EPIPE unless MSG_NOSIGNAL.+fn send(sock: &UnixSocket, data: &[u8], fds: Vec<Arc<dyn File>>, cred: Option<Ucred>, to: Option<Addr>, flags: u64) -> Result<u64, Errno> {+    if flags & MSG_OOB != 0 {+        return Err(Errno::EOPNOTSUPP);+    }+    let p = current()?;+    let cred = cred.unwrap_or_else(|| my_creds(&p).0);+    let r = sock.ep.send(data, fds, cred, to, sock.nb(flags));+    if r == Err(Errno::EPIPE) && flags & MSG_NOSIGNAL == 0 {+        signal::send(&p, signal::SIGPIPE, signal::SigInfo::from_pid(signal::SIGPIPE, p.pid));+    }+    r.map(|n| n as u64)+}++/// Receive into one buffer of `want` bytes: MSG_WAITALL (stream: keep+/// going until it's full) and MSG_TRUNC (datagrams: say the real size).+fn recv(sock: &UnixSocket, want: usize, flags: u64) -> Result<(Received, usize), Errno> {+    if flags & MSG_OOB != 0 {+        return Err(Errno::EOPNOTSUPP);+    }+    let nb = sock.nb(flags);+    let mut r = sock.ep.recv(want, flags, nb)?;+    if flags & MSG_WAITALL != 0 && flags & MSG_PEEK == 0 && sock.ep.ty == Type::Stream && !nb {+        while r.data.len() < want && r.fds.is_empty() {+            match sock.ep.recv(want - r.data.len(), flags, nb) {+                Ok(more) if !more.data.is_empty() && more.fds.is_empty() && more.cred == r.cred => {+                    r.data.extend_from_slice(&more.data);+                }+                Ok(more) if more.data.is_empty() => break, // EOF+                Ok(_) => break, // (can't happen: fds stop at a message border)+                Err(_) => break, // a signal: return what we have+            }+        }+        r.full = r.data.len();+    }+    let ret = if flags & MSG_TRUNC != 0 && sock.ep.ty != Type::Stream { r.full } else { r.data.len() };+    Ok((r, ret))+}++fn sys_recvfrom(sock: &UnixSocket, buf: u64, len: u64, flags: u64, addr: u64, len_ptr: u64) -> Result<u64, Errno> {+    let out = user_slice_mut(buf, len)?;+    let (r, ret) = recv(sock, out.len(), flags)?;+    out[..r.data.len()].copy_from_slice(&r.data);+    if addr != 0 {+        if r.from == Name::Unnamed {+            copy_to_user(len_ptr, &0u32)?;+        } else {+            write_addr(addr, len_ptr, &encode(&r.from))?;+        }+    }+    Ok(ret as u64) // (fds can't be taken with recvfrom: they're closed)+}++// --- sendmsg / recvmsg ------------------------------------------------------++/// struct msghdr (56 bytes on x86_64).+#[repr(C)]+#[derive(Clone, Copy)]+struct MsgHdr {+    name: u64,+    namelen: u32,+    _pad: u32,+    iov: u64,+    iovlen: u64,+    control: u64,+    controllen: u64,+    flags: i32,+    _pad2: u32,+}++const CMSG_HDR: usize = 16; // struct cmsghdr: len u64, level i32, type i32++fn cmsg_align(n: usize) -> usize {+    (n + 7) & !7+}++fn read_msghdr(ptr: u64) -> Result<MsgHdr, Errno> {+    let raw = user_slice(ptr, 56)?;+    Ok(unsafe { core::ptr::read_unaligned(raw.as_ptr() as *const MsgHdr) })+}++/// Gather the iovecs into one buffer (at most MAX_IO bytes).+fn gather(h: &MsgHdr) -> Result<Vec<u8>, Errno> {+    let mut data = Vec::new();+    for (base, len) in read_iovecs(h.iov, h.iovlen)? {+        let room = MAX_IO - data.len();+        data.extend_from_slice(user_slice(base, len.min(room as u64))?);+    }+    Ok(data)+}++/// Spread `data` over the iovecs; the room there.+fn iov_room(h: &MsgHdr) -> Result<usize, Errno> {+    let iov = read_iovecs(h.iov, h.iovlen)?;+    Ok(iov.iter().map(|&(_, l)| l as usize).sum::<usize>().min(MAX_IO))+}++fn scatter(h: &MsgHdr, mut data: &[u8]) -> Result<(), Errno> {+    for (base, len) in read_iovecs(h.iov, h.iovlen)? {+        if data.is_empty() {+            break;+        }+        let n = (len as usize).min(data.len());+        user_slice_mut(base, n as u64)?.copy_from_slice(&data[..n]);+        data = &data[n..];+    }+    Ok(())+}++/// The control messages of a sendmsg: fds (SCM_RIGHTS) and credentials.+fn read_control(h: &MsgHdr, p: &Process) -> Result<(Vec<Arc<dyn File>>, Option<Ucred>), Errno> {+    let mut fds = Vec::new();+    let mut cred = None;+    if h.control == 0 || h.controllen == 0 {+        return Ok((fds, cred));+    }+    let ctl = user_slice(h.control, h.controllen)?;+    let mut off = 0;+    while off + CMSG_HDR <= ctl.len() {+        let len = u64::from_ne_bytes(ctl[off..off + 8].try_into().unwrap()) as usize;+        let level = u32::from_ne_bytes(ctl[off + 8..off + 12].try_into().unwrap());+        let ty = u32::from_ne_bytes(ctl[off + 12..off + 16].try_into().unwrap());+        if len < CMSG_HDR || off + len > ctl.len() {+            return Err(Errno::EINVAL);+        }+        let body = &ctl[off + CMSG_HDR..off + len];+        match (level as u64, ty) {+            (SOL_SOCKET, SCM_RIGHTS) => {+                if body.len() % 4 != 0 || fds.len() + body.len() / 4 > SCM_MAX_FD {+                    return Err(Errno::EINVAL);+                }+                for fd in body.chunks(4) {+                    let fd = i32::from_ne_bytes(fd.try_into().unwrap());+                    if fd < 0 {+                        return Err(Errno::EBADF);+                    }+                    fds.push(p.file(fd as u64)?);+                }+            }+            (SOL_SOCKET, SCM_CREDENTIALS) => {+                if body.len() < 12 {+                    return Err(Errno::EINVAL);+                }+                let c = Ucred {+                    pid: u32::from_ne_bytes(body[0..4].try_into().unwrap()),+                    uid: u32::from_ne_bytes(body[4..8].try_into().unwrap()),+                    gid: u32::from_ne_bytes(body[8..12].try_into().unwrap()),+                };+                // Only root may claim to be someone else.+                let me = *p.creds.lock();+                let root = me.euid == 0;+                let uid_ok = root || [me.ruid, me.euid, me.suid].contains(&c.uid);+                let gid_ok = root || [me.rgid, me.egid, me.sgid].contains(&c.gid);+                if !(root || c.pid as u64 == p.pid) || !uid_ok || !gid_ok {+                    return Err(Errno::EPERM);+                }+                cred = Some(c);+            }+            _ => return Err(Errno::EINVAL),+        }+        off += cmsg_align(len);+    }+    Ok((fds, cred))+}++fn sys_sendmsg(fd: u64, msg: u64, flags: u64) -> Result<u64, Errno> {+    let p = current()?;+    let file = p.file(fd)?;+    let h = read_msghdr(msg)?;+    let data = gather(&h)?;+    if let Some(s) = file.as_socket() {+        // A network socket: no control messages, just the data.+        let to = if h.name == 0 { None } else { Some(crate::syscall::read_sockaddr(h.name)?) };+        return s.send(&data, to).map(|n| n as u64);+    }+    let sock = file.as_unix().ok_or(Errno::ENOTSOCK)?;+    let (fds, cred) = read_control(&h, &p)?;+    let to = if h.name == 0 || h.namelen == 0 { None } else { Some(read_addr(h.name, h.namelen as u64)?) };+    drop(p);+    send(sock, &data, fds, cred, to, flags)+}++fn sys_recvmsg(fd: u64, msg: u64, flags: u64) -> Result<u64, Errno> {+    let p = current()?;+    let file = p.file(fd)?;+    let h = read_msghdr(msg)?;+    let flags_ptr = msg + 48;+    if let Some(s) = file.as_socket() {+        let mut buf = alloc::vec![0u8; iov_room(&h)?];+        let (n, from) = s.recv(&mut buf)?;+        scatter(&h, &buf[..n])?;+        if let (Some(from), true) = (from, h.name != 0) {+            crate::syscall::write_sockaddr(h.name, msg + 8, from)?;+        } else {+            copy_to_user(msg + 8, &0u32)?;+        }+        copy_to_user(msg + 40, &0u64)?; // no control data+        copy_to_user(flags_ptr, &0i32)?;+        return Ok(n as u64);+    }+    let sock = file.as_unix().ok_or(Errno::ENOTSOCK)?;+    let (r, ret) = recv(sock, iov_room(&h)?, flags)?;+    scatter(&h, &r.data)?;+    let mut out_flags = 0u32;+    if r.full > r.data.len() {+        out_flags |= MSG_TRUNC as u32;+    }+    // The sender's address.+    if h.name != 0 {+        if r.from == Name::Unnamed {+            copy_to_user(msg + 8, &0u32)?;+        } else {+            let bytes = encode(&r.from);+            let n = bytes.len().min(h.namelen as usize);+            user_slice_mut(h.name, n as u64)?.copy_from_slice(&bytes[..n]);+            copy_to_user(msg + 8, &(bytes.len() as u32))?;+        }+    } else {+        copy_to_user(msg + 8, &0u32)?;+    }+    // Control messages: credentials, then as many fds as fit (the rest+    // are closed, and MSG_CTRUNC says so).+    let mut ctl: Vec<u8> = Vec::new();+    let room = if h.control == 0 { 0 } else { h.controllen as usize };+    if let Some(c) = r.cred {+        let len = CMSG_HDR + 12;+        if room >= len {+            ctl.extend_from_slice(&(len as u64).to_ne_bytes());+            ctl.extend_from_slice(&(SOL_SOCKET as u32).to_ne_bytes());+            ctl.extend_from_slice(&SCM_CREDENTIALS.to_ne_bytes());+            for v in [c.pid, c.uid, c.gid] {+                ctl.extend_from_slice(&v.to_ne_bytes());+            }+            ctl.resize(cmsg_align(len).min(room), 0);+        } else {+            out_flags |= MSG_CTRUNC;+        }+    }+    if !r.fds.is_empty() {+        let left = room.saturating_sub(ctl.len());+        let fit = if left < CMSG_HDR + 4 { 0 } else { ((left - CMSG_HDR) / 4).min(r.fds.len()) };+        let cloexec = flags & MSG_CMSG_CLOEXEC != 0;+        let mut installed = Vec::new();+        for f in r.fds.iter().take(fit) {+            match p.add_file_with(f.clone(), 0, cloexec) {+                Ok(fd) => installed.push(fd as i32),+                Err(_) => break, // out of fds (EMFILE): the rest are lost+            }+        }+        if installed.len() < r.fds.len() {+            out_flags |= MSG_CTRUNC;+        }+        if !installed.is_empty() {+            let len = CMSG_HDR + 4 * installed.len();+            ctl.extend_from_slice(&(len as u64).to_ne_bytes());+            ctl.extend_from_slice(&(SOL_SOCKET as u32).to_ne_bytes());+            ctl.extend_from_slice(&SCM_RIGHTS.to_ne_bytes());+            for fd in &installed {+                ctl.extend_from_slice(&fd.to_ne_bytes());+            }+            let end = ctl.len() - len + cmsg_align(len);+            ctl.resize(end.min(room), 0);+        }+    }+    drop(r.fds); // fds that didn't fit are closed here+    if !ctl.is_empty() {+        user_slice_mut(h.control, ctl.len() as u64)?.copy_from_slice(&ctl);+    }+    copy_to_user(msg + 40, &(ctl.len() as u64))?;+    copy_to_user(flags_ptr, &(out_flags as i32))?;+    Ok(ret as u64)+}++// --- socket options -----------------------------------------------------------++fn read_int(ptr: u64, len: u64) -> Result<i32, Errno> {+    if len < 4 {+        return Err(Errno::EINVAL);+    }+    Ok(i32::from_ne_bytes(user_slice(ptr, 4)?.try_into().unwrap()))+}++/// struct timeval -> microseconds (None = 0 = no timeout).+fn read_timeval(ptr: u64, len: u64) -> Result<Option<u64>, Errno> {+    if len < 16 {+        return Err(Errno::EINVAL);+    }+    let raw = user_slice(ptr, 16)?;+    let sec = i64::from_ne_bytes(raw[0..8].try_into().unwrap());+    let usec = i64::from_ne_bytes(raw[8..16].try_into().unwrap());+    if sec < 0 || !(0..1_000_000).contains(&usec) {+        return Err(Errno::EINVAL);+    }+    let us = sec as u64 * 1_000_000 + usec as u64;+    Ok((us != 0).then_some(us))+}++fn setsockopt(ep: &Endpoint, level: u64, name: u64, val: u64, len: u64) -> Result<u64, Errno> {+    if level != SOL_SOCKET {+        return Err(Errno::ENOPROTOOPT);+    }+    match name {+        SO_SNDBUF | SO_SNDBUFFORCE => {+            let v = (read_int(val, len)?.max(0) as usize).saturating_mul(2).clamp(MIN_SNDBUF, MAX_BUF);+            ep.sndbuf.store(v, Ordering::Relaxed);+            ep.wake();+        }+        SO_RCVBUF | SO_RCVBUFFORCE => {+            let v = (read_int(val, len)?.max(0) as usize).saturating_mul(2).clamp(MIN_RCVBUF, MAX_BUF);+            ep.rcvbuf.store(v, Ordering::Relaxed);+            ep.wake();+        }+        SO_PASSCRED => {+            let on = read_int(val, len)? != 0;+            ep.with(|s| s.passcred = on);+        }+        SO_RCVTIMEO => {+            let t = read_timeval(val, len)?;+            ep.with(|s| s.rcvtimeo = t);+        }+        SO_SNDTIMEO => {+            let t = read_timeval(val, len)?;+            ep.with(|s| s.sndtimeo = t);+        }+        SO_REUSEADDR | SO_KEEPALIVE => {+            read_int(val, len)?; // accepted, means nothing here+        }+        _ => return Err(Errno::ENOPROTOOPT),+    }+    Ok(0)+}++fn getsockopt(sock: &UnixSocket, level: u64, name: u64, val: u64, len_ptr: u64) -> Result<u64, Errno> {+    if level != SOL_SOCKET {+        return Err(Errno::ENOPROTOOPT);+    }+    let ep = &sock.ep;+    let room = i32::from_ne_bytes(user_slice(len_ptr, 4)?.try_into().unwrap());+    if room < 0 {+        return Err(Errno::EINVAL);+    }+    let int = |v: i64| (v as i32).to_ne_bytes().to_vec();+    let timeval = |t: Option<u64>| {+        let us = t.unwrap_or(0);+        let mut v = ((us / 1_000_000) as i64).to_ne_bytes().to_vec();+        v.extend_from_slice(&((us % 1_000_000) as i64).to_ne_bytes());+        v+    };+    let bytes = match name {+        SO_TYPE => int(ep.ty.number() as i64),+        SO_ERROR => int(ep.with(|s| s.error.take()).map_or(0, |e| e as i64)),+        SO_SNDBUF => int(ep.sndbuf.load(Ordering::Relaxed) as i64),+        SO_RCVBUF => int(ep.rcvbuf.load(Ordering::Relaxed) as i64),+        SO_PASSCRED => int(ep.with(|s| s.passcred) as i64),+        SO_ACCEPTCONN => int(ep.with(|s| matches!(s.role, Role::Listening { .. })) as i64),+        SO_DOMAIN => int(AF_UNIX as i64),+        SO_PROTOCOL => int(0),+        SO_REUSEADDR | SO_KEEPALIVE => int(0),+        SO_RCVTIMEO => timeval(ep.with(|s| s.rcvtimeo)),+        SO_SNDTIMEO => timeval(ep.with(|s| s.sndtimeo)),+        SO_PEERCRED => {+            // Nobody there: pid 0, uid/gid -1 (Linux's "overflow" ids).+            let c = ep.with(|s| s.peercred).unwrap_or(Ucred { pid: 0, uid: u32::MAX, gid: u32::MAX });+            let mut v = c.pid.to_ne_bytes().to_vec();+            v.extend_from_slice(&c.uid.to_ne_bytes());+            v.extend_from_slice(&c.gid.to_ne_bytes());+            v+        }+        _ => return Err(Errno::ENOPROTOOPT),+    };+    let n = bytes.len().min(room as usize);+    user_slice_mut(val, n as u64)?.copy_from_slice(&bytes[..n]);+    copy_to_user(len_ptr, &(n as u32))?;+    Ok(0)+}++/// How many sockets have a name right now (tests: nothing left bound).+pub fn bound_count() -> usize {+    registry(|r| r.iter().filter(|b| b.ep.strong_count() > 0).count())+}
@@ -721,6 +721,12 @@ impl Process {     }      /// The fd limit (RLIMIT_NOFILE's soft value).+    /// Every open fd and its file, lowest first (stage 18: /proc/self/fd).+    pub fn open_files(&self) -> Vec<(u64, Arc<dyn File>)> {+        let fds = self.fds.lock();+        fds.files.iter().enumerate().filter_map(|(fd, f)| f.clone().map(|f| (fd as u64, f))).collect()+    }+     pub fn fd_limit(&self) -> usize {         self.fds.lock().limit     }
@@ -51,12 +51,14 @@ //! |  34 | pause       |                                     | sleeps until a signal; returns EINTR | //! |  35 | nanosleep   | *timespec, *remaining               | EINTR on a signal (remaining time written back) | //! |  39 | getpid      |                                     | |-//! |  41 | socket      | domain, type, protocol              | AF_INET (2) only; type may add SOCK_NONBLOCK/SOCK_CLOEXEC; SOCK_STREAM (1) = TCP, SOCK_DGRAM (2) = UDP, SOCK_DGRAM + IPPROTO_ICMP (1) = ping socket |+//! |  41 | socket      | domain, type, protocol              | AF_INET (2), stage 18: AF_UNIX (1) STREAM/DGRAM/SEQPACKET (src/net/unix.rs); type may add SOCK_NONBLOCK/SOCK_CLOEXEC; SOCK_STREAM (1) = TCP, SOCK_DGRAM (2) = UDP, SOCK_DGRAM + IPPROTO_ICMP (1) = ping socket | //! |  42 | connect     | fd, *sockaddr_in, len               | TCP: handshake, blocks (10 s timeout; nonblocking: EINPROGRESS); UDP/ICMP: set default destination. 127.x = loopback | //! |  43 | accept      | fd, *sockaddr_in or 0, *u32 len or 0 | blocks for a connection (SO_RCVTIMEO applies); returns the new fd. Like Linux, *len is read and updated |-//! |  44 | sendto      | fd, buf, len, flags, *sockaddr_in or 0, addrlen | flags and addrlen are ignored (a sockaddr_in is always 16 bytes), so MSG_NOSIGNAL is too |+//! |  44 | sendto      | fd, buf, len, flags, *sockaddr_in or 0, addrlen | INET: flags and addrlen are ignored (a sockaddr_in is always 16 bytes), so MSG_NOSIGNAL is too; AF_UNIX (stage 18) honors them | //! |  45 | recvfrom    | fd, buf, len, flags, *sockaddr_in or 0, *addrlen | blocks (SO_RCVTIMEO -> EAGAIN); 0 = TCP peer closed. The address is cut to *addrlen |-//! |  49 | bind        | fd, *sockaddr_in, len               | sets the local port (address ignored: we have one) |+//! |  46 | sendmsg     | fd, *msghdr, flags                  | stage 18: iovecs + name; AF_UNIX: SCM_RIGHTS (<= 253 fds), SCM_CREDENTIALS, MSG_NOSIGNAL/DONTWAIT |+//! |  47 | recvmsg     | fd, *msghdr, flags                  | stage 18: AF_UNIX control data (MSG_CTRUNC, MSG_CMSG_CLOEXEC), MSG_PEEK/WAITALL/TRUNC/DONTWAIT |+//! |  49 | bind        | fd, *sockaddr_in, len               | sets the local port (address ignored: we have one); AF_UNIX: a socket file, an abstract name, or autobind | //! |  50 | listen      | fd, backlog                         | TCP; backlog 1..4 | //! |  54 | setsockopt  | fd, level, name, *value, len        | stage 17 (src/syscall_ext.rs): SO_RCVTIMEO is real; SO_REUSEADDR & co. stored; others ENOPROTOOPT | //! |  56 | clone       | flags, stack, *ptid, *ctid, tls     | fork-like (SIGCHLD [+ CLONE_VFORK]) or a thread: CLONE_VM|FS|FILES|SIGHAND|THREAD|SYSVSEM [+ SETTLS, PARENT_SETTID, CHILD_CLEARTID] |@@ -528,6 +530,8 @@ pub fn name(nr: u64) -> &'static str {         SYS_ACCEPT4 => "accept4",         SYS_SENDTO => "sendto",         SYS_RECVFROM => "recvfrom",+        46 => "sendmsg",+        47 => "recvmsg",         SYS_BIND => "bind",         SYS_LISTEN => "listen",         SYS_SETSOCKOPT => "setsockopt",@@ -651,7 +655,10 @@ extern "C" fn syscall_dispatch(frame: &mut TrapFrame) {         signal::deliver(frame); // signals unblocked by the restored mask         return;     }+    // Stage 18: AF_UNIX sockets (and sendmsg/recvmsg) first; None = not one.+    let mut unix = crate::net::unix::dispatch(nr, [a1, a2, a3, a4, a5, frame.r9]);     let result = match nr {+        _ if unix.is_some() => unix.take().unwrap(),         SYS_READ => sys_read(a1, a2, a3),         SYS_WRITE => sys_write(a1, a2, a3),         SYS_OPEN => sys_openat(AT_FDCWD as u64, a1, a2, a3),@@ -999,6 +1006,9 @@ pub(crate) fn user_path(ptr: u64) -> Result<String, Errno> { /// files get the file system's default permissions). fn sys_openat(dirfd: u64, path_ptr: u64, flags: u64, mode: u64) -> Result<u64, Errno> {     let path = user_path_at(dirfd, path_ptr)?;+    if let (0, Some(fd)) = (flags & O_NOFOLLOW, fs::procfs::fd_number(&path)) {+        return open_proc_fd(fd, flags);+    }     let found = if flags & O_NOFOLLOW != 0 { fs::lookup_nofollow(&path) } else { fs::lookup(&path) };     let inode = match found {         Err(Errno::ENOENT) if flags & O_CREAT != 0 => {@@ -1029,6 +1039,9 @@ fn sys_openat(dirfd: u64, path_ptr: u64, flags: u64, mode: u64) -> Result<u64, E         }         return p.add_file_with(device, 0, cloexec);     }+    if inode.kind() == InodeKind::Socket {+        return Err(Errno::ENXIO); // sockets are connect()ed to, not opened+    }     if flags & O_TRUNC != 0 && inode.kind() == InodeKind::File {         inode.truncate()?;     }@@ -1036,6 +1049,39 @@ fn sys_openat(dirfd: u64, path_ptr: u64, flags: u64, mode: u64) -> Result<u64, E     current()?.add_file_with(Arc::new(file), 0, cloexec) } +/// open("/proc/self/fd/N") (stage 18), as on Linux: a file with a path is+/// opened again (its own offset, the new flags); a pipe, terminal or+/// device gives the same open file again, like dup(N); sockets can't be+/// opened (ENXIO).+fn open_proc_fd(fd: u64, flags: u64) -> Result<u64, Errno> {+    let p = current()?;+    let file = p.file(fd).map_err(|_| Errno::ENOENT)?;+    let cloexec = flags & O_CLOEXEC != 0;+    if file.as_unix().is_some() || file.as_socket().is_some() {+        return Err(Errno::ENXIO);+    }+    if let (Some(path), Some(inode)) = (file.path(), file.vfs_inode()) {+        let is_dir = inode.kind() == InodeKind::Directory;+        if flags & O_DIRECTORY != 0 && !is_dir {+            return Err(Errno::ENOTDIR);+        }+        if is_dir && flags & O_ACCMODE != 0 {+            return Err(Errno::EISDIR);+        }+        if inode.open_device()?.is_none() {+            if flags & O_TRUNC != 0 && inode.kind() == InodeKind::File {+                inode.truncate()?;+            }+            let file = InodeFile::open_flags(inode, path, flags)?;+            return p.add_file_with(Arc::new(file), 0, cloexec);+        }+    }+    if flags & O_DIRECTORY != 0 {+        return Err(Errno::ENOTDIR);+    }+    p.add_file_with(file, 0, cloexec)+}+ /// newfstatat(dirfd, path, *stat, flags): stat, lstat (AT_SYMLINK_NOFOLLOW: /// a symlink itself, not what it points to) and, with AT_EMPTY_PATH and /// path "", fstat(dirfd). Writes Linux's `struct stat`.@@ -1051,8 +1097,15 @@ pub(crate) fn stat_at(dirfd: u64, path_ptr: u64, flags: u64) -> Result<LinuxStat         return fd_stat(dirfd);     }     let path = if raw.is_empty() && flags & AT_EMPTY_PATH != 0 { at_base(dirfd)? } else { path_at(dirfd, &raw)? };-    let inode = if flags & AT_SYMLINK_NOFOLLOW != 0 { fs::lookup_nofollow(&path)? } else { fs::lookup(&path)? };-    Ok(fs::file::linux_stat(&*inode, &path))+    let follow = flags & AT_SYMLINK_NOFOLLOW == 0;+    if let (true, Some(fd)) = (follow, fs::procfs::fd_number(&path)) {+        // /proc/self/fd/N: what fd N is, even a pipe or socket (stage 18).+        return fd_stat(fd).map_err(|_| Errno::ENOENT);+    }+    // (The resolved path: st_dev is the file system the file is *on*,+    // even when a symlink on another one led to it.)+    let (inode, real) = fs::lookup_resolved(&path, follow)?;+    Ok(fs::file::linux_stat(&*inode, &real)) }  fn sys_fstat(fd: u64, stat_ptr: u64) -> Result<u64, Errno> {@@ -1070,7 +1123,7 @@ pub(crate) fn fd_stat(fd: u64) -> Result<LinuxStat, Errno> {     }     let ino = match file.path() {         Some(path) => fs::file::ino_hash(path.as_bytes()),-        None => Arc::as_ptr(&file) as *const u8 as u64,+        None => fs::file::anon_ino(&file),     };     Ok(LinuxStat::new(&file.stat()?, ino).with_times(file.times())) }@@ -1143,11 +1196,8 @@ fn sys_readlinkat(dirfd: u64, path_ptr: u64, buf: u64, len: u64) -> Result<u64,      if len as i64 <= 0 {         return Err(Errno::EINVAL);     }-    // There's no /proc yet, but libcs and programs ask "where am I?"-    // with readlink("/proc/self/exe"): answer just that one.-    let target = if path == "/proc/self/exe" {-        current()?.exe.lock().clone()-    } else {+    // (/proc/self/exe and /proc/self/fd/N are symlinks in procfs.rs now.)+    let target = {         let inode = fs::lookup_nofollow(&path)?;         if inode.kind() != InodeKind::Symlink {             return Err(Errno::EINVAL); // not a symlink@@ -1341,7 +1391,7 @@ fn sys_uname(buf: u64) -> Result<u64, Errno> {         "Rustbelt",                     // sysname (`uname -s`)         host.as_str(),                  // nodename (hostname; sethostname changes it)         env!("CARGO_PKG_VERSION"),      // release (`uname -r`)-        "stage 17: coreutils batch 2",  // version+        "stage 18: Unix sockets",      // version         "x86_64",                       // machine         domain.as_str(),                // domainname     ];
@@ -26,7 +26,7 @@ //! | 51  | getsockname          | our own address and port                           | //! | 52  | getpeername          | the other end's address and port                   | //! | 48  | shutdown             | close one direction of a connection                |-//! | 53  | socketpair           | needs AF_UNIX, which we don't have: EAFNOSUPPORT   |+//! | 53  | socketpair           | AF_INET: EOPNOTSUPP (AF_UNIX: net/unix.rs, st. 18) | //! | 37  | alarm                | SIGALRM in N seconds                               | //! | 36  | getitimer            | the ITIMER_REAL timer                              | //! | 38  | setitimer            | same, set (periodic too)                           |
@@ -986,6 +986,9 @@ impl File for TtyFile {     fn as_tty(&self) -> Option<&TtyFile> {         Some(self)     }+    fn proc_link(&self) -> Option<String> {+        Some(if self.master { String::from("/dev/ptmx") } else { format!("/dev/{}", self.tty.name) })+    } }  impl Drop for TtyFile {
@@ -176,7 +176,7 @@ fn scripted_shell_session() {         "piped text\n",         "wc -l\n2\n",         "saved\nmore\n",-        "Rustbelt 0.17.0",+        "Rustbelt 0.18.0",         "B:/ $ exit 3\n",     ] {         assert!(out.contains(expected), "missing {:?} in:\n{}", expected, out);
@@ -0,0 +1,107 @@+//! Stage 18: AF_UNIX sockets and /proc/self. The detailed checks live in+//! user/unixtest.rs (exit code = failed checks); this file runs each+//! group twice and checks nothing is left behind -- threads, processes,+//! memory frames (fds in flight included), and bound names.+#![no_std]+#![no_main]+#![feature(custom_test_frameworks)]+#![test_runner(rustbelt::test_runner)]+#![reexport_test_harness_main = "test_main"]++extern crate alloc;++use alloc::string::String;+use bootloader::{BootInfo, entry_point};+use core::panic::PanicInfo;+use rustbelt::process::{self, ExitStatus, SpawnArgs};+use rustbelt::{fs, memory, net, syscall, thread};++entry_point!(main);++fn main(boot_info: &'static BootInfo) -> ! {+    rustbelt::init();+    rustbelt::init_memory(boot_info);+    rustbelt::init_smp(); // all 4 CPUs run threads+    fs::init();+    rustbelt::init_storage();+    test_main();+    rustbelt::hlt_loop();+}++fn run(args: &[&str]) -> ExitStatus {+    let mut spawn = SpawnArgs::kernel("unixtest");+    spawn.args.extend(args.iter().map(|a| String::from(*a)));+    let p = process::spawn_path("/mnt/disk0/bin/unixtest", spawn).expect("spawn failed");+    process::wait(&p)+}++fn counts() -> (usize, usize, usize, usize) {+    thread::sleep_ticks(3); // let exited threads' stacks be freed+    (thread::thread_count(), process::count(), memory::frames_in_use(), net::unix::bound_count())+}++/// Run one group twice (the first warms caches), check it passes and+/// nothing is left behind.+fn run_without_leaks(group: &str) {+    assert_eq!(run(&[group]), ExitStatus::Exited(0), "unixtest {} failed (warm-up)", group);+    let before = counts();+    assert_eq!(run(&[group]), ExitStatus::Exited(0), "unixtest {} failed", group);+    assert_eq!(counts(), before, "unixtest {} leaked something", group);+    assert_eq!(net::unix::bound_count(), 0, "unixtest {} left bound sockets behind", group);+}++/// A server on a path (ramfs and ext2) echoes for a forked client;+/// abstract names, autobind, ECONNRESET/EPIPE/ECONNREFUSED, SO_PEERCRED.+#[test_case]+fn stream_echo_across_fork() {+    run_without_leaks("stream");+}++/// socketpair: stream (both ways, fork, shutdown, MSG_PEEK), seqpacket,+/// dgram, SOCK_CLOEXEC/SOCK_NONBLOCK.+#[test_case]+fn socketpairs() {+    run_without_leaks("pair");+}++/// Datagrams between named sockets: sender names, MSG_TRUNC, connect,+/// EPERM/EPROTOTYPE/EMSGSIZE/ECONNREFUSED.+#[test_case]+fn datagrams() {+    run_without_leaks("dgram");+}++/// SCM_RIGHTS: a file and a pipe end read through the received fds;+/// MSG_CTRUNC, MSG_CMSG_CLOEXEC, fds in flight closed with the socket+/// (and no frames leaked: see run_without_leaks); SCM_CREDENTIALS.+#[test_case]+fn passing_fds() {+    run_without_leaks("rights");+}++/// Nonblocking EAGAIN, real buffer limits (and a blocked writer), poll,+/// epoll EPOLLRDHUP/EPOLLHUP, nonblocking accept, full backlog.+#[test_case]+fn nonblocking_and_poll() {+    run_without_leaks("nonblock");+}++/// /proc/self/fd: listing, readlink (paths, pipe:[N], socket:[N]),+/// opening entries; /proc/self/exe.+#[test_case]+fn proc_self() {+    run_without_leaks("proc");+}++/// `--trace` knows the new calls by name.+#[test_case]+fn trace_names() {+    assert_eq!(syscall::trace_name(46), "sendmsg");+    assert_eq!(syscall::trace_name(47), "recvmsg");+    assert_eq!(syscall::trace_name(53), "socketpair");+}++#[panic_handler]+fn panic(info: &PanicInfo) -> ! {+    rustbelt::test_panic_handler(info)+}
@@ -62,7 +62,7 @@ fn mounts_are_separate_file_systems() {     fs::mount("/tmp/deeper", RamFs::new()).unwrap();     assert_eq!(fs::lookup("/tmp/deeper").unwrap().list().unwrap().len(), 0);     assert_eq!(fs::mount("/tmp/deeper", RamFs::new()).err(), Some(Errno::EEXIST));-    assert_eq!(fs::mounts().len(), 4); // "/", /dev (devfs), /tmp, /tmp/deeper+    assert_eq!(fs::mounts().len(), 5); // "/", /dev (devfs), /proc, /tmp, /tmp/deeper }  #[test_case]@@ -73,7 +73,7 @@ fn write_grow_and_list() {     assert_eq!(inode.read_at(0, &mut buf), Ok(7));     assert_eq!(&buf[..7], b"\0\0\0\0abc");     let names: Vec<String> = fs::lookup("/").unwrap().list().unwrap().into_iter().map(|e| e.name).collect();-    assert_eq!(names, vec!["dev", "etc", "hello.txt", "mnt", "tmp"]);+    assert_eq!(names, vec!["dev", "etc", "hello.txt", "mnt", "proc", "tmp"]); }  #[test_case]
@@ -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");+}