rust-kernel
Rustbelt: a hobby x86_64 kernel written in Rust
| author | Edward <edward@localhost> | 2026-10-31 17:45:51 -0700 |
|---|---|---|
| commit | dea72d3d03399a30d1eee0627380adfd6a6c888e | |
| parent | a91bf2c1317b4b7dc0c293615e379acf56ae0183 | |
| tree | browse files at this commit | |
Stage 18: Unix sockets and /proc/self (0.18.0) Local IPC good enough for Wayland, in src/net/unix.rs: - AF_UNIX SOCK_STREAM, SOCK_DGRAM, SOCK_SEQPACKET; path (ramfs, ext2 socket inodes, S_IFSOCK; EPERM on FAT/exFAT; open() -> ENXIO), abstract and unnamed addresses, autobind - socket/socketpair/bind/listen/connect/accept/accept4/send(to)/ recv(from)/sendmsg/recvmsg/shutdown/getsockname/getpeername/ get/setsockopt, FIONREAD; poll/select/epoll incl. POLLRDHUP/POLLHUP - SCM_RIGHTS with in-flight refcounts (closed if the receiver dies), MSG_CTRUNC, MSG_CMSG_CLOEXEC, SCM_MAX_FD; SCM_CREDENTIALS (checked), SO_PASSCRED, SO_PEERCRED - byte backpressure: min(SO_SNDBUF, peer SO_RCVBUF), setsockopt doubles like Linux, getsockopt honours *optlen; dgram adds 256 B/message - sendmsg/recvmsg (46/47) for INET sockets too (name + iovecs) - /proc (src/fs/procfs.rs): /proc/self/fd listing, readlink (path, pipe:[N], socket:[N], anon_inode:[eventpoll]), open-as-reopen/ dup, stat following; /proc/self/exe moved here Tests: user/unixtest.rs + tests/unix_sockets.rs (7 new, 173 total). README stage 18 chapter and table updates. Deferred: GC for socket cycles, dgram qlen limit, SO_PASSCRED autobind, /proc/<pid>, control messages on INET sockets.
| M | Cargo.lock | +1 -1 |
| M | Cargo.toml | +1 -1 |
| M | README.md | +252 -12 |
| M | build.rs | +1 -0 |
| M | scripts/run-headless.sh | +6 -6 |
| M | scripts/screenshot.sh | +1 -1 |
| M | src/dev.rs | +9 -0 |
| M | src/errno.rs | +7 -3 |
| M | src/fs/exfat.rs | +1 -1 |
| M | src/fs/ext2.rs | +10 -3 |
| M | src/fs/fat.rs | +2 -2 |
| M | src/fs/file.rs | +25 -0 |
| M | src/fs/mod.rs | +20 -5 |
| M | src/fs/pipe.rs | +10 -0 |
| A | src/fs/procfs.rs | +177 -0 |
| M | src/fs/ramfs.rs | +5 -0 |
| M | src/net/mod.rs | +2 -0 |
| A | src/net/unix.rs | +1513 -0 |
| M | src/process.rs | +6 -0 |
| M | src/syscall.rs | +62 -12 |
| M | src/syscall_ext.rs | +1 -1 |
| M | src/tty.rs | +3 -0 |
| M | tests/shell.rs | +1 -1 |
| A | tests/unix_sockets.rs | +107 -0 |
| M | tests/vfs.rs | +2 -2 |
| M | user/coretest.rs | +4 -1 |
| M | user/nbtest.rs | +1 -1 |
| A | user/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");+}