RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

authorTyler Fowler <tyler@localhost>2026-10-30 11:39:24 -0700
commitc01312bd1a5e452ced8b564a6ae52dca022714f3
parent06676b7883176d6d94f9119e23e8f53dab15cd57
treebrowse files at this commit
WIP 15.1: version bump, selftest/headless/screenshot, README chapter
MCargo.lock+1 -1
MCargo.toml+1 -1
MREADME.md+416 -32
Mdisk/scripts/selftest.sh+6 -0
Mscripts/run-headless.sh+14 -6
Mscripts/screenshot.sh+10 -7
Msrc/errno.rs+3 -2
Msrc/fpu.rs+7 -0
Msrc/main.rs+1 -0
Msrc/syscall.rs+40 -20
Mtests/shell.rs+1 -1
Muser/demo.rs+12 -7
Muser/ulib.rs+2 -0

13 files changed, 514 insertions, 77 deletions

@@ -261,7 +261,7 @@ dependencies = [  [[package]] name = "rustbelt"-version = "0.15.0"+version = "0.15.1" dependencies = [  "bootloader",  "conquer-once",
@@ -1,6 +1,6 @@ [package] name = "rustbelt"-version = "0.15.0"+version = "0.15.1" edition = "2024"  [dependencies]
@@ -32,7 +32,7 @@ Under the shell:   (wait queues) without using any CPU while they wait. - **User programs.** Small Rust ELF binaries run in **ring 3** as processes,   each with its own address space. Their memory is freed when they exit. They-  talk to the kernel through 83 `syscall`s, numbered like Linux's:+  talk to the kernel through 101 `syscall`s, numbered like Linux's:   `read`, `write`, `open`, `stat`, `getdents64`, `pipe2`, `uname`, `socket`,   `connect`, `getrandom`... A few are our own: `spawn`, `mounts`, `sysinfo`,   `resolve`, `netinfo` and `trace_me`.@@ -79,6 +79,12 @@ Under the shell:   ^C/^Z/^\ as signals on every terminal, `/dev/tty`, `/dev/ptmx` and   `/dev/pts/N`; plus `nanosleep`, `clock_gettime(CLOCK_REALTIME)`,   `readv`/`writev`, `pread`/`pwrite` and `readlink /proc/self/exe`.+- **Ready for musl (stage 15.1).** Every thread has its own **FPU/SSE/AVX+  registers** (XSAVE or FXSAVE, also in signal frames, cleared by+  `execve`); the file calls take **C-string paths** with Linux's+  signatures (`openat` and the other `*at()` calls, Linux's `struct+  stat`, `rename`, `access`), and `rawabi`, a C program built by gcc with+  no libc at all, runs unmodified.  It has a `cargo test` suite that runs inside QEMU, and a headless self-test that drives the shell with a script.@@ -90,10 +96,11 @@ threads, user mode, syscalls, fork/exec/mmap/signals, ELF loading, the VFS, PCI, partitions, FAT32, the shell, the ext2, exFAT and btrfs drivers, multi-core (SMP) support, the virtio-net driver and socket layer, the virtio-rng, virtio-console and virtio-gpu drivers with the framebuffer-console, and stage 15's poll/epoll, loopback, threads, futexes and ptys. The 8x16 font is [Spleen](https://github.com/fcambus/spleen) by+console, stage 15's poll/epoll, loopback, threads, futexes and ptys, and+stage 15.1's FPU state switching and Linux path calls. The 8x16 font is [Spleen](https://github.com/fcambus/spleen) by Frederic Cambus (BSD 2-clause, license in `src/font.rs`). -![Rustbelt 0.15.0 (stage 15: libc plumbing) on the 1024x768 virtio-gpu framebuffer: `tile` drew four tiled windows; in the left one the shell interrupts a CPU hog with Ctrl+C ("[exit code 130] (interrupted: SIGINT)"), `threads` sums 1..=40000000 with 4 threads and a futex mutex, `echoserver` echoes over 127.0.0.1 with epoll and poll, and `ptytest` and `nbtest` report "all checks passed"](boot.png)+![Rustbelt 0.15.1 (stage 15.1: musl prerequisites) on the 1024x768 virtio-gpu framebuffer: `tile` drew four tiled windows; in the left one the shell interrupts a CPU hog with Ctrl+C ("[exit code 130] (interrupted: SIGINT)"), `threads` sums 1..=40000000 with 4 threads and a futex mutex, `echoserver` echoes over 127.0.0.1, `fputest` and `pathtest` report "all checks passed", and `rawabi`, a C program built with gcc -nostdlib, prints its uname, pi computed with SSE and "all checks passed"](boot.png)  ## New to Rust and C? Start here @@ -979,6 +986,7 @@ heap_allocation::simple_allocation...               [ok] | `tests/guard_pages.rs` | the guard page sits right below each kernel stack, and the user stack's 64 KiB guard zone has the right bounds; a kernel thread that recurses forever is killed with "kernel stack overflow" three times in a row (the rest of the kernel carries on, and its stack is freed); the user `overflow` program ends with `Killed(SIGSEGV, "stack overflow")` and leaves no frames behind | | `tests/process.rs` | **the process model** (stage 14), mostly by running the user test programs: `cowtest` (globals, heap and stack stay separate after fork in both directions, three generations, `MAP_SHARED` vs `MAP_PRIVATE`); `forktest` (fork/wait4/getppid, 10 children reaped with `wait4(-1)`, `ECHILD`, execve errors, vfork + exec, `pipe2(O_CLOEXEC)`, fork + dup2 + exec through a pipe, a child dying of SIGSEGV); **exec argv/envp/auxv** (`forktest args "two words" ""` with `FOO=bar EMPTY=`: argc, every argv and envp string, `AT_PAGESZ`, `AT_ENTRY` = `_start`, `AT_PHDR` covering the entry, `AT_RANDOM`, `AT_EXECFN`, stack position); `mmaptest` (zeroed anonymous memory, 64 MiB that costs nothing until touched, SIGSEGV handlers that see `si_addr` and `SEGV_ACCERR`/`SEGV_MAPERR` and fix the fault so the access is retried, `PROT_NONE`, file-backed private mappings, `brk`, `EINVAL`s, memory back to the start); an unhandled fault is `Killed(SIGSEGV)` with status 11 and exit code 139; `sigtest` (handler round trip through sigreturn with registers intact, masks and pending signals, SIGKILL can't be caught, default actions, SIGCHLD with `si_pid`/`si_status`, SIGPIPE, stop/continue seen by `wait4(WUNTRACED/WCONTINUED)`, `EINTR` vs `SA_RESTART`, `kill` errors, signal 0, `kill(-pgid)`); wait-status encoding; a **fork bomb** stops at `EAGAIN`, is killed and reaped, and leaves frames and the process count unchanged; **20 fork/exec/exit loops leak no frames** and leave no shared frames; **Ctrl+C** (`tty::input_char('\x03')`) kills `spin` in the foreground group with SIGINT (exit code 130), and does nothing when no group is in the foreground | | `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) |  How it works: `#![feature(custom_test_frameworks)]` collects every `#[test_case]` function and passes them to `rustbelt::test_runner` (in `src/lib.rs`), which@@ -1047,7 +1055,8 @@ src/futex.rs              futex WAIT/WAKE (+ bitsets, private flag, timeouts): w src/fs/mod.rs             VFS: Inode + FileSystem traits, mount table, path normalize/lookup/create/remove,                           symlink following (ELOOP after 8), init (boot files),                           mount_disks (ext2, exFAT, btrfs or FAT partitions/disks -> /mnt/diskN)-src/fs/file.rs            File trait (what an fd points to), InodeFile (inode + offset), Stat, getdents64+src/fs/file.rs            File trait (what an fd points to), InodeFile (inode + offset + path), Stat,+                          LinuxStat (Linux's struct stat), getdents64 src/fs/ramfs.rs           in-memory filesystem src/fs/fat.rs             FAT32 via the fatfs crate: BlockIo adapter + FatFs/FatInode for the VFS src/fs/ext2.rs            hand-written ext2: superblock, groups, bitmaps, inodes, indirect blocks,@@ -1096,6 +1105,10 @@ user/{forktest,cowtest,mmaptest,sigtest}.rs  stage 14: the process-model checks                            lines and "all checks passed"; tests/process.rs runs them) user/{threads,echoserver,ptytest,nbtest}.rs  stage 15: threads + futexes, poll/epoll over loopback,                           ptys, nonblocking I/O (tests/libc.rs runs them)+user/{fputest,pathtest}.rs  stage 15.1: per-thread FPU/SSE/AVX state + signal fpstate; the Linux+                          C-string path calls (tests/musl_prereq.rs runs them, and rawabi)+user/rawabi.c             stage 15.1: a no-libc Linux C program (gcc -nostdlib): raw syscalls, C paths, SSE+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`) scripts/gpu-check.sh      types `tile`, dumps the virtio-gpu screen (PPM), compares pixels scripts/http-reply.sh     the host side of `fetch http://10.0.2.100/`: QEMU runs it per connection@@ -1517,10 +1530,11 @@ Numbers are Linux's wherever Linux has the same call, so man pages and |---|---|---|---| | 0 | `read` | fd, buf, len | console: blocks until Enter; pipe: blocks until data or EOF; socket: like `recv` | | 1 | `write` | fd, buf, len | console = screen + serial; pipe: `EPIPE` **and SIGPIPE** once the reader is gone; socket: like `send` |-| 2 | `open` | path, path_len, flags | `O_CREAT` 0o100, `O_TRUNC` 0o1000, `O_APPEND` 0o2000 (every write goes to the end: `>>`); `O_CLOEXEC` 0o2000000 (closed by `execve`); directories can be opened (for `getdents64`) |+| 2 | `open` | path, flags, mode | **C string since stage 15.1** (= `openat(AT_FDCWD, ...)`). `O_RDONLY`/`O_WRONLY`/`O_RDWR` 0/1/2, `O_CREAT` 0o100, `O_EXCL` 0o200 (`EEXIST`), `O_TRUNC` 0o1000, `O_APPEND` 0o2000 (every write goes to the end: `>>`), `O_DIRECTORY` 0o200000 (`ENOTDIR`), `O_NOFOLLOW` 0o400000 (`ELOOP` on a symlink), `O_CLOEXEC` 0o2000000 (closed by `execve`); directories can be opened (for `getdents64`), but not for writing (`EISDIR`). `mode` is ignored | | 3 | `close` | fd | |-| 4 | `stat` | path, path_len, *Stat | follows symlinks |-| 5 | `fstat` | fd, *Stat | |+| 4 | `stat` | path, *stat | **stage 15.1**: Linux's 144-byte `struct stat`; follows symlinks |+| 5 | `fstat` | fd, *stat | |+| 6 | `lstat` | path, *stat | **stage 15.1**: doesn't follow a final symlink | | 7 | `poll` | *pollfd, n, timeout_ms | **stage 15**: -1 = forever; pipes, sockets, ttys, files, epoll fds | | 8 | `lseek` | fd, offset, whence | `SEEK_SET/CUR/END` = 0/1/2; `ESPIPE` on console/pipes | | 9 | `mmap` | addr, len, prot, flags, fd, offset | MAP_PRIVATE/SHARED, MAP_ANONYMOUS, MAP_FIXED; files: private only |@@ -1535,6 +1549,7 @@ Numbers are Linux's wherever Linux has the same call, so man pages and | 18 | `pwrite64` | fd, buf, len, offset | **stage 15** | | 19 | `readv` | fd, *iovec, n | **stage 15**: `struct iovec {base, len}`, up to 1024 | | 20 | `writev` | fd, *iovec, n | **stage 15** |+| 21 | `access` | path, mode | **stage 15.1**: 0 if the path exists (no permissions to check) | | 22 | `pipe` | *[u32; 2] | [read end, write end]; `pipe2` (293) also takes `O_CLOEXEC` | | 23 | `select` | n, *rd, *wr, *ex, *timeval | **stage 15**: fd bitmaps (n <= 1024) | | 24 | `sched_yield` | | |@@ -1545,27 +1560,30 @@ Numbers are Linux's wherever Linux has the same call, so man pages and | 39 | `getpid` | | | | 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 |-| 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 |-| 45 | `recvfrom` | fd, buf, len, flags, *sockaddr_in, *len | blocks (wait queue) until data; 0 = the peer closed (TCP); `EAGAIN` after `SO_RCVTIMEO` |+| 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 |+| 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` | | 49 | `bind` | fd, *sockaddr_in, len | local port (address must be 0 or ours) | | 50 | `listen` | fd, backlog | TCP; up to 4 connections can be waiting |-| 54 | `setsockopt` | fd, level, name, *val, len | only `SOL_SOCKET`/`SO_RCVTIMEO` (a `timeval`) |+| 54 | `setsockopt` | fd, level, name, *val, len | only `SOL_SOCKET`/`SO_RCVTIMEO` (a `timeval`); anything else `ENOPROTOOPT`, 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 | | 59 | `execve` | path, argv, envp (C strings) | Linux's stack layout: argc, argv, envp, auxv | | 60 | `exit` | code | since **stage 15** ends only the calling *thread*; `exit_group` (231) ends the whole process |-| 61 | `wait4` | pid, *status, options, *rusage | **Linux's `wait4` since stage 14** (stage 13 had a simpler `wait`): pid > 0 one child, -1 any, 0 / < -1 a process group; `*status` gets the packed status (`WIFEXITED`, `WEXITSTATUS`, `WIFSIGNALED`, `WTERMSIG`, `WIFSTOPPED`...); options `WNOHANG` 1 (returns 0 if nobody is done), `WUNTRACED` 2, `WCONTINUED` 8; reaps the child; `ECHILD` if none; rusage must be 0 |+| 61 | `wait4` | pid, *status, options, *rusage | **Linux's `wait4` since stage 14** (stage 13 had a simpler `wait`): pid > 0 one child, -1 any, 0 / < -1 a process group; `*status` gets the packed status (`WIFEXITED`, `WEXITSTATUS`, `WIFSIGNALED`, `WTERMSIG`, `WIFSTOPPED`...); options `WNOHANG` 1 (returns 0 if nobody is done), `WUNTRACED` 2, `WCONTINUED` 8; reaps the child; `ECHILD` if none; since 15.1 a `*rusage` is zero-filled (no usage accounting yet) | | 62 | `kill` | pid, sig | pid 0 = my group, -1 = all, < -1 = group -pid |-| 63 | `uname` | *utsname | Linux's struct: 6 NUL-padded 65-byte fields: "Rustbelt", "qemu", "0.15.0", "stage 15: libc plumbing", "x86_64", "(none)" |+| 63 | `uname` | *utsname | Linux's struct: 6 NUL-padded 65-byte fields: "Rustbelt", "qemu", "0.15.1", "stage 15.1: musl prerequisites", "x86_64", "(none)" | | 72 | `fcntl` | fd, cmd, arg | F_DUPFD 0, F_GETFD 1, F_SETFD 2, F_GETFL 3, F_SETFL 4 (`O_NONBLOCK` on pipes, sockets, ttys), F_DUPFD_CLOEXEC 1030 | | 79 | `getcwd` | buf, len | like Linux since stage 15: writes a NUL and returns the length including it; `ERANGE` if too small |-| 80 | `chdir` | path, path_len | per-process cwd, inherited by forked and spawned children |-| 83 | `mkdir` | path, path_len | |-| 84 | `rmdir` | path, path_len | empty directories only (`ENOTEMPTY`); `EBUSY` on a mount point |-| 87 | `unlink` | path, path_len | files and symlinks (the link, not its target); directories give `EISDIR` |-| 89 | `readlink` | path, path_len, 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 |+| 80 | `chdir` | path | per-process cwd, inherited by forked and spawned children |+| 81 | `fchdir` | fd | **stage 15.1**: an open directory |+| 82 | `rename` | old, new | **stage 15.1**: files (also over an existing file) and empty directories, within one file system (else `EXDEV`). Not atomic: see "Linux paths" |+| 83 | `mkdir` | path, mode | `mode` ignored |+| 84 | `rmdir` | path | empty directories only (`ENOTEMPTY`); `EBUSY` on a mount point |+| 87 | `unlink` | path | files and symlinks (the link, not its target); directories give `EISDIR` |+| 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 |+| 102 | `getuid` |  | **stage 15.1**: always 0, and so are `getgid` 104, `geteuid` 107, `getegid` 108 (no users yet) | | 109 | `setpgid` | pid, pgid | 0 = myself / a new group | | 110 | `getppid` |  | 0 if started by the kernel or orphaned | | 111 | `getpgrp` |  |  |@@ -1580,10 +1598,18 @@ Numbers are Linux's wherever Linux has the same call, so man pages and | 217 | `getdents64` | fd, buf, len | real Linux `struct linux_dirent64` records (ino, off, reclen, type, name\0) | | 218 | `set_tid_address` | *u32 | **stage 15**: cleared + futex-woken when this thread exits; returns the tid | | 228 | `clock_gettime` | clock, *timespec | microsecond resolution (TSC calibrated against the PIT). `CLOCK_MONOTONIC` (1) counts from boot; since stage 15 `CLOCK_REALTIME` (0) is real Unix time (the CMOS clock read once at boot, plus uptime) |+| 230 | `clock_nanosleep` | clock, flags, *timespec, *remaining | **stage 15.1**: `CLOCK_REALTIME`, `MONOTONIC` or `BOOTTIME`; flag `TIMER_ABSTIME` (1) sleeps until that time. musl's `nanosleep` and `sleep` use this | | 231 | `exit_group` | code | ends the process: every thread (stage 15; `exit`, 60, ends one thread) | | 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 |+| 258 | `mkdirat` | dirfd, path, mode | **stage 15.1** |+| 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 |+| 263 | `unlinkat` | dirfd, path, flags | **stage 15.1**: `AT_REMOVEDIR` 0x200 = rmdir |+| 264 | `renameat` | olddirfd, old, newdirfd, new | **stage 15.1**; `renameat2` (316) too, with flags 0 |+| 267 | `readlinkat` | dirfd, path, buf, len | **stage 15.1** |+| 269 | `faccessat` | dirfd, path, mode | **stage 15.1**; `faccessat2` (439) too, its flags ignored | | 270 | `pselect6` | n, *rd, *wr, *ex, *timespec, *{*mask, 8} | **stage 15** | | 271 | `ppoll` | *pollfd, n, *timespec, *mask, 8 | **stage 15**: sets the mask while waiting | | 281 | `epoll_pwait` | epfd, *events, max, timeout, *mask, 8 | **stage 15** |@@ -1592,7 +1618,7 @@ Numbers are Linux's wherever Linux has the same call, so man pages and | 292 | `dup3` | old, new, flags | flags: O_CLOEXEC | | 293 | `pipe2` | *[u32; 2], flags | flags: O_CLOEXEC | | 318 | `getrandom` | buf, len, flags | random bytes from virtio-rng (the host's `/dev/urandom`), up to 64 KiB per call; never blocks, flags ignored. Without virtio-rng: a TSC-seeded xorshift (not secure) |-| 500 | `spawn` | path, path_len, argv, argv_len, *SpawnOptions | **ours**: start a program, returns its pid (fork + exec in one step, like `posix_spawn`; the kernel's first shell and `getty` use it, the shell uses fork + exec) |+| 500 | `spawn` | path, argv, argv_len, *SpawnOptions | **ours**: start a program, returns its pid (fork + exec in one step, like `posix_spawn`; the kernel's first shell and `getty` use it, the shell uses fork + exec) | | 501 | `mounts` | buf, len | **ours** (Linux reads `/proc/mounts`): the mount table as text, one `path fstype` line per mount, in mount order; returns the length, `ERANGE` if too small. The shell assigns drive letters from it | | 502 | `sysinfo` | *SysInfo | **ours**: CPU count, the CPU you're on, thread/process/frame counts, uptime ticks, and per-CPU timer ticks and busy ticks (sample twice for the load) | | 503 | `resolve` | name, name_len, *addrs, max | **ours** (Linux does DNS in libc, in user space): IPv4 addresses for a name via the DHCP-given DNS server; returns how many; `ENOENT` / `ETIMEDOUT` |@@ -1601,14 +1627,22 @@ Numbers are Linux's wherever Linux has the same call, so man pages and  Deviations from Linux worth knowing: -- **Paths are pointer + length**, not NUL-terminated C strings (easier to-  validate, and Rust `&str`s aren't NUL-terminated). Relative paths start at-  the process's cwd; `.` and `..` are resolved in the kernel (`fs::normalize`).-- **`Stat` is tiny**: `#[repr(C)] struct Stat { kind: u64, size: u64, mode: u64 }`,-  kind 1 = file, 2 = directory, 3 = console, 4 = pipe, 5 = symlink,-  6 = socket, 7 = device (`/dev/*`; `getdents64` says `DT_CHR`); `mode`-  is the permission bits (`0o644`), or 0 on file systems without them-  (ramfs, FAT). No inode numbers, owners or times yet.+- **Paths are C strings since stage 15.1** (stages 5-15 passed pointer ++  length, which was easier to validate and matches Rust's `&str`). The+  kernel copies at most `PATH_MAX` (4096) bytes, NUL included, a page at a+  time, so a bad pointer is `EFAULT` and a missing NUL `ENAMETOOLONG`.+  Relative paths start at the process's cwd or at an `*at()` call's+  `dirfd`; `.` and `..` are resolved in the kernel (`fs::normalize`).+- **`stat` is Linux's** (since 15.1): the 144-byte x86_64 `struct stat`.+  `st_mode` has the real type bits (`S_IFREG`, `S_IFDIR`, `S_IFLNK`,+  `S_IFCHR` for the console and `/dev/*`, `S_IFIFO`, `S_IFSOCK`) and the+  file system's permission bits, or a sensible default (0644 files, 0755+  directories) on ramfs and FAT. `st_ino` is a hash of the path (no file+  system here exposes stable inode numbers through the VFS yet), uid/gid+  are 0, and the times are 0.+- **`mode` arguments and permissions are ignored**: everything runs as+  root (uid 0), `access` only checks that a path exists, and `open`'s and+  `mkdir`'s modes are dropped. - **Stage 14's calls are exactly Linux's.** `execve` takes NUL-terminated C   strings and `char **` arrays, `rt_sigaction` takes the kernel's   `struct sigaction`, `wait4` writes Linux's status word, `siginfo` and the@@ -1944,6 +1978,7 @@ Programs in `B:/bin` (`user/*.rs`, each 30-150 lines): | `hello`, `demo`, `evil`, `spin`, `child` | the stage 5/6 test programs: `run demo` runs all the stage 6 syscall checks | | `forktest`, `cowtest`, `mmaptest`, `sigtest` | the stage 14 checks of fork/exec/wait4, copy-on-write, mmap and signals; each ends with "all checks passed" (`forktest bomb` is a bounded fork bomb, `mmaptest crash` dies of SIGSEGV) | | `threads [n]`, `echoserver [port]`, `ptytest`, `nbtest` | the stage 15 checks: a parallel sum on `n` threads with a futex mutex; an epoll + poll echo server over 127.0.0.1 (`echoserver 7070` just serves forever); ptys and the line discipline; nonblocking I/O and the libc extras. Each ends with "all checks passed" |+| `fputest`, `pathtest`, `rawabi [dir]` | the stage 15.1 checks: per-thread FPU/SSE/AVX state and signal frames; the Linux path calls; a C program with no libc doing file calls in `dir` (default `/tmp`), SSE maths, a signal and fork + wait4. Each ends with "all checks passed" |  Built-in aliases are the DOS names: `dir` = `ls`, `type` = `cat`, `del` = `rm`, `md` = `mkdir`, `copy` = `cp`; and for networking,@@ -3506,9 +3541,330 @@ Doesn't (yet):   `eventfd` or `timerfd`. - One global "something changed" counter wakes every poller. - Timeouts are rounded up to 55 ms PIT ticks (never early).-- No FPU/SSE state saving (Rustbelt's programs are soft-float). musl-  needs it, so it comes first in the next stage.-- Paths are still passed as pointer + length, not C strings.+- No FPU/SSE state saving (Rustbelt's programs are soft-float), and paths+  passed as pointer + length: both fixed in stage 15.1, next.++## musl prerequisites (stage 15.1)++Stage 15 gave Rustbelt what a C library needs from a kernel's *behaviour*.+Stage 15.1 fixes what a C library needs from its *interface*. Up to now+Rustbelt's own Rust programs were the only users, so a few of its calls+could take shortcuts that musl would trip over. This stage removes the+three that matter most. It does not port musl; that is the next step.++1. The **FPU/SSE/AVX registers** belong to each thread. They are saved and+   restored on every switch and put into signal frames.+2. **Paths are C strings**. The file calls take Linux's arguments in+   Linux's order, including the `*at()` family, and fill Linux's+   `struct stat`.+3. An **audit** of every syscall number and argument order. A plain Linux+   program, `rawabi`, is built by gcc with no libc and runs unmodified.++### Floating point is part of a thread's registers++Rustbelt's kernel and its Rust programs are built *soft-float*: they do+floating-point maths with integer instructions and never touch the+FPU. A C compiler is different. It keeps every `double` in an SSE+register (xmm0-xmm15), and musl's `memcpy` and `strlen` use them too. For+a C program those 16 registers (32 bytes each with AVX) are as much "its+registers" as `rax` is. Picture two threads sharing a CPU. If the kernel+switches between them without saving these registers, one thread's `x * y`+quietly comes out with the other thread's `x`.++The CPU can save all of this state with one instruction (see `src/fpu.rs`):++- **FXSAVE/FXRSTOR** exist on every x86_64 CPU. They save the x87 stack,+  xmm0-15 and the MXCSR control register into a 512-byte area.+- **XSAVE/XRSTOR** are on newer CPUs. They save the same 512 bytes, a+  64-byte header that says which parts are present, and then one block per+  extra feature, such as AVX's upper ymm halves (256 more bytes). The OS+  chooses which features it saves by writing XCR0, and CPUID then reports+  how big the area is (832 bytes for x87 + SSE + AVX).++Before user code may run an SSE instruction, the OS has to promise that it+saves this state. It does that with control-register bits, set on every+CPU in `fpu::init_cpu`:++- `CR4.OSFXSR`: the OS uses FXSAVE, so SSE is allowed. Without it SSE+  instructions raise #UD.+- `CR4.OSXMMEXCPT`: unmasked SSE maths errors raise #XM.+- `CR4.OSXSAVE`: XSAVE and XCR0 may be used.+- In CR0, `EM` and `TS` are cleared, and `MP` and `NE` are set.++Rustbelt then uses **eager switching**. When `schedule()` leaves a user+thread it saves that thread's state into `Task::fpu`, and it loads the+next thread's state before returning to user mode. Lazy switching (set+`CR0.TS`, take a #NM fault on the first FPU instruction, and save only+then) used to be faster. It is also the source of a CPU side channel+(LazyFP), and Linux dropped it in 2018. Eager switching costs about one+kilobyte copied per switch, and it is ten lines of code.++The state follows a thread through the process model the same way the+other registers do:++| event | FPU state |+|---|---|+| `fork`, `clone` (process or thread) | the child starts with a copy of the parent's |+| `execve` | reset: zeros, x87 control word 0x37f, MXCSR 0x1f80 |+| signal delivery | saved into the signal frame, then reset for the handler |+| `rt_sigreturn` | loaded back from the frame, so a handler may change it |+| x87 error / SSE error (#MF 16 / #XM 19) | SIGFPE; for #XM the `si_code` (`FPE_FLTDIV`, `FPE_FLTINV`...) comes from MXCSR's flags |++### FPU state in signal frames++A signal handler is ordinary C code, so it uses xmm registers. The+interrupted code must find its own values again afterwards. Linux puts the+FPU state in the signal frame, and musl and glibc never look at it. GDB,+and programs that edit the registers in a handler, do look, and use+`ucontext->uc_mcontext.fpregs`. Rustbelt now writes the same layout as+Linux (`signal.rs`, `setup_frame`):++```+higher addresses+  [ 128-byte red zone of the interrupted code ]+  fpstate (64-byte aligned): the XSAVE (or FXSAVE) image+      bytes 464..512 ("sw_bytes", unused by the CPU):+          magic1 0x46505853 "FPXS", extended size, XCR0, XSAVE size+      ...followed by magic2 0x46505845 "FPXE" (XSAVE only)+  rt_sigframe: restorer address, siginfo, ucontext+      ucontext.uc_mcontext.fpregs -> fpstate+lower addresses: the handler's rsp+```++The magic numbers tell a reader that the area is XSAVE format and how big+it is. Without them it is a plain FXSAVE image. Because the area comes+from user memory, `rt_sigreturn` checks it before loading it. An MXCSR with+reserved bits set would make the CPU fault inside the kernel, so the+process gets SIGSEGV ("bad sigreturn FPU state") instead. A null `fpregs`+means "start clean", as on Linux.++### Linux paths: C strings, `struct stat` and the `*at()` calls++Since stage 5 the file calls took a path as pointer + length. That was easy+to check and matched Rust's `&str`, but it is not what Linux does. musl's+`open(path, flags)` puts one pointer in rdi, so on the old ABI it would+have read `flags` as the path length. Every path call now takes a+NUL-terminated string.++The kernel copies the string in **page by page**, with `user_cstr`. Each+page is checked against the process's memory regions before it is read,+so a string that runs into an unmapped page is `EFAULT`, never a kernel+crash. The copy stops at `PATH_MAX` (4096 bytes, NUL included), so a path+with no NUL is `ENAMETOOLONG` rather than an endless read. `pathtest` tries+NULL, a kernel address, an unmapped address and a string that straddles+into an unmapped page, on every path call.++The **`*at()` calls** (`openat`, `newfstatat`, `mkdirat`, `unlinkat`,+`renameat`, `readlinkat`, `faccessat`) resolve a relative path against+an open directory fd instead of the cwd, or against the cwd when+`dirfd = AT_FDCWD` (-100). They exist so a program can work inside a+directory that another process might rename meanwhile. musl calls some of+them even for the plain functions: `stat()` is `newfstatat(AT_FDCWD, path,+&st, 0)`, and `fstat()` may become `newfstatat(fd, "", &st,+AT_EMPTY_PATH)`. The old calls (`open`, `stat`, `mkdir`...) are now one-line+wrappers that pass `AT_FDCWD`. The kernel remembers each open file's path+(`File::path`), so a directory fd can serve as a base.++**`struct stat`** is now Linux's 144-byte x86_64 layout. The kernel keeps+its own small `Stat` internally and converts it at the syscall boundary+(`LinuxStat::new` in `fs/file.rs`):++- `st_mode` combines the type (`S_IFREG`, `S_IFDIR`, `S_IFLNK`,+  `S_IFCHR`, `S_IFIFO`, `S_IFSOCK`) with the permission bits. File systems+  without permissions (ramfs, FAT) get 0644 for files and 0755 for+  directories.+- `st_ino` is a hash of the path. The VFS has no stable inode numbers yet,+  but musl only compares them (for example in `realpath`), and a hash is+  stable and almost always unique.++`ulib` now has `Stat::is_dir()`, `is_char_device()` and `perm()` in place+of the old `kind` numbers, and `isatty()` works the way libc's does, with+`ioctl(TCGETS)`.++**`rename`** is new. The VFS has no "move this directory entry"+operation, so `fs::rename` copies a file, or recreates an empty directory,+and then removes the old name. It refuses what it can't do safely:++| case | result |+|---|---|+| a directory into itself | `EINVAL` |+| a mount point | `EBUSY` |+| a non-empty directory, a symlink, or across file systems | `EXDEV` |++`EXDEV` is the same error Linux gives across file systems, so `mv` falls+back to copying. The rename is **not atomic**: a crash in the middle can+leave both names, or a reader can briefly see neither. Real per-file-system+rename (an ext2 directory-entry move, for example) is future work.++A few calls changed their argument order to match Linux:++- `recvfrom` gets its sixth argument, `*addrlen`, and `accept` honours+  `*addrlen` (the address is cut to the given length, and the full length+  is written back).+- `wait4` accepts a `struct rusage` pointer and zero-fills it.+- `clock_nanosleep` arrived; musl's `nanosleep` and `sleep` use it.+- `spawn`, Rustbelt's own call, now takes a C-string path too.++### The audit++A script compared every `SYS_*` constant in `src/syscall.rs` and+`user/ulib.rs` against the host's `<asm/unistd_64.h>`. The only numbers that+differ from Linux are Rustbelt's own 500-505 (`spawn`, `mounts`,+`sysinfo`, `resolve`, `netinfo`, `trace_me`). These are not Linux calls,+so they can't collide with one. The one catch is the name `sysinfo`: on+Linux it is call 99 with a different struct, and musl's `sysinfo()` would+get `ENOSYS`.++The argument orders that differed were fixed above. The ones that still+differ in meaning, not in order, are:++- `sendto` ignores `flags`, so `MSG_NOSIGNAL` does not stop a SIGPIPE.+- `getrandom` ignores its flags.+- `mode` arguments (`open`, `mkdir`) are dropped, and all uids are 0.+- `setsockopt` knows only `SO_RCVTIMEO`. Anything else now gives Linux's+  `ENOPROTOOPT`.++### rawabi: a Linux program with no libc++`user/rawabi.c` is the test that matters most for musl. It is ordinary C+written against Linux, not against Rustbelt:++- its own `_start`+- a `syscall` wrapper with Linux's numbers+- C-string paths and Linux's `struct stat`+- `double` maths (SSE instructions, plus an explicit `sqrtsd`)+- a signal handler that clobbers xmm registers while the interrupted+  code keeps a `double` live in one++`build.rs` compiles it with the host's gcc:++```+cc -O2 -static -nostdlib -ffreestanding -fno-stack-protector -fpie -no-pie \+   -Wl,-Ttext-segment=0x8000400000 -o rawabi rawabi.c+```++`-nostdlib` means there is no libc and no startup files. `-fpie -no-pie`+looks contradictory, but it isn't. `-fpie` makes gcc address everything+RIP-relative, so the code works at any address. `-no-pie` still links an+ordinary fixed-address `ET_EXEC` file, the only kind Rustbelt's ELF+loader accepts. `-Ttext-segment` puts that file at Rustbelt's user base+(0x80_0000_0000). That address is above 4 GiB, where gcc's default+"absolute 32-bit addresses" code would not link. A normal Linux+machine runs the same file without complaint. Try `./rawabi /tmp` on the+host: it prints the same checks.++What it checks:++- the startup calls musl makes (`arch_prctl` + `%fs:0`, `set_tid_address`,+  `rt_sigprocmask`)+- `uname`, `brk` and an anonymous `mmap`+- `mkdir`, `openat(AT_FDCWD, O_CREAT)` + `writev`, `stat`, `newfstatat`,+  `access` and `stat(NULL)` = `EFAULT`+- `rename`, then `read`, `fstat` and `lseek`+- `getdents64`, `chdir` + `getcwd`, `unlinkat` with and without+  `AT_REMOVEDIR`+- `clock_nanosleep` on `CLOCK_MONOTONIC`+- π from a series, plus the signal test above+- `fork` + `wait4` with a rusage buffer++It runs from the shell (`rawabi`), in the boot self-test, and in+`tests/musl_prereq.rs`.++### Testing it++`tests/musl_prereq.rs` runs the three programs twice each, at `-smp 4`,+and fails if a thread, process or memory frame leaks:++- **`fputest`**: three processes, each with three threads plus `main`,+  load all 16 xmm (or ymm) registers with their own pattern and spin,+  checking it after every yield. The test also covers:+  - a fork child inherits the parent's registers+  - `execve` starts clean+  - a handler starts with clean registers and sees the interrupted values+    in `fpregs` (magic numbers, sizes, xmm0 and the upper ymm half)+  - a handler that edits the saved xmm15 changes what the interrupted+    code sees after `sigreturn`+  - 40 asynchronous `tgkill`s land in the middle of the check loop+  - division by zero with exceptions unmasked gives SIGFPE (skipped+    under QEMU's TCG, which never raises it; see below)+- **`pathtest`**: `EFAULT` for NULL, kernel and unmapped pointers on 13+  calls, and for a path straddling into an unmapped page (the same path+  ending at the page boundary is fine). It also tests:+  - `ENAMETOOLONG` at 4096 bytes, a 4095-byte path accepted, and the empty+    path giving `ENOENT`+  - the `struct stat` fields and file types, `lstat` vs `stat`, and+    `AT_EMPTY_PATH`+  - every `*at()` call with a real directory fd and with a bad one+  - `O_EXCL`, `O_DIRECTORY`, `O_NOFOLLOW`, `fchdir`, and every+    `rename` case+- **`rawabi`**, as above.++QEMU's default CPU model (`qemu64`) has no XSAVE, so `Cargo.toml` now+passes `-cpu max` to QEMU. That gives XSAVE and AVX under QEMU's+software emulation (TCG). The FXSAVE fallback is tested by running the+same tests with `-cpu qemu64`.++### What stage 15.1 does and doesn't do++Does: everything above, at `-smp 4`, with XSAVE (x87 + SSE + AVX) or+FXSAVE. The boot log says which, for example `[fpu] XSAVE: x87 SSE AVX,+832 bytes per thread`.++Doesn't (yet):++- **AVX-512** and other XSAVE features stay off in XCR0. The area would be+  about 2.7 KiB per thread, and QEMU's TCG doesn't emulate AVX-512 anyway.+- **#XM under TCG.** QEMU's TCG never raises the SIMD exception: an+  unmasked divide by zero just sets the flag in MXCSR and gives infinity.+  `fputest` notices this and skips that check. The `#XM` → SIGFPE path is+  written but untested here, because KVM (where the real CPU would raise+  it) isn't available on the build machine.+- **No `XSAVEOPT`/`XSAVES`** (which skip unchanged state) and no+  "init-optimization" tricks: every switch saves the whole area.+- **Rename isn't atomic**, and can't move non-empty directories or+  symlinks.+- **No permissions**: `mode`, `umask`, `chmod` and uids.+- **ELF loading** accepts only static `ET_EXEC` files at or above+  0x80_0000_0000. A stock `musl-gcc -static` binary is linked at+  0x400000, so it needs `-Wl,-Ttext-segment=0x8000400000`, or the loader+  needs to learn static-PIE (`ET_DYN`, loaded at a base of its choosing).++**Syscalls musl will call that Rustbelt still lacks.** These all return+`-ENOSYS` today. Most matter only for the functions that use them.++| musl function(s) | missing syscall (x86_64 number) |+|---|---|+| `malloc` (mallocng) | `mremap` 25, `madvise` 28 (failure is tolerated) |+| `umask`, `chmod`, `fchmod`, `fchmodat`, `chown` family | `umask` 95, `chmod` 90, `fchmod` 91, `fchmodat` 268, `fchownat` 260 (and `chown` 92, `fchown` 93, `lchown` 94) |+| `truncate`, `ftruncate`, `fsync`, `fdatasync` | 76, 77, 74, 75 |+| `utimensat`, `futimens`, `utime` | `utimensat` 280 |+| `link`, `symlink` (`ln`) | `link` 86 / `linkat` 265, `symlink` 88 / `symlinkat` 266 |+| `statvfs`, `flock`, `fcntl` record locks | `statfs` 137 / `fstatfs` 138, `flock` 73, `F_SETLK`/`F_GETLK` |+| `getrlimit`/`setrlimit` | `prlimit64` 302 (falls back to `getrlimit` 97 / `setrlimit` 160) |+| `sysconf(_SC_NPROCESSORS_ONLN)`, `sched_getaffinity` | `sched_getaffinity` 204 |+| `sysinfo` | `sysinfo` 99 (ours is 502, with a different struct) |+| `getrusage`, `times`, `clock()` | `getrusage` 98, `times` 100; `clock_gettime(CLOCK_PROCESS_CPUTIME_ID)` is `EINVAL` |+| `clock_getres` | `clock_getres` 229 |+| `sigaltstack` (also Rust's std, for stack overflow messages) | `sigaltstack` 131 |+| `sigsuspend`, `sigpending`, `sigtimedwait`/`sigwait`, `sigqueue` | `rt_sigsuspend` 130, `rt_sigpending` 127, `rt_sigtimedwait` 128, `rt_sigqueueinfo` 129 |+| `alarm`, `setitimer`, `timer_create` | `alarm` 37, `setitimer` 38, `timer_create` 222 ... |+| pthreads: robust mutexes, `pthread_setname_np`, `dlclose` barriers | `set_robust_list` 273, `prctl` 157, `membarrier` 324 |+| sockets | `shutdown` 48, `getsockname` 51, `getpeername` 52, `socketpair` 53, `getsockopt` 55, `sendmsg` 46, `recvmsg` 47 (the DNS resolver's TCP fallback) |+| `setuid` family, `getgroups` | 105, 106, 113-120 (`getresuid` 118 ...), `getgroups` 115 |+| `nice`, `getpriority` | `getpriority` 140, `setpriority` 141 |+| `preadv`, `pwritev`, `sendfile` | 295, 296, `sendfile` 40 |++Not syscalls, but missing just the same:++- **`AF_UNIX`** sockets (syslog, many daemons), and **`AF_INET6`**. musl's+  resolver falls back to IPv4.+- **`/proc/self/fd/N`**, which musl's `ttyname`, `realpath` and `fchmodat`+  read. Only `/proc/self/exe` exists.+- **The vDSO.** musl looks up `AT_SYSINFO_EHDR` and falls back to the real+  `clock_gettime` syscall when it is missing. That fallback is fine, only+  slower.+- **`/etc/passwd`, `/etc/resolv.conf` and the other files** musl reads.+  Rustbelt does DNS in the kernel (`resolve`, 503).  ## Roadmap @@ -3521,7 +3877,8 @@ guard pages, networking (virtio-net + smoltcp + sockets), and the virtual drivers (virtio-rng, virtio-console, virtio-gpu 2D with a framebuffer console), the process model (fork/exec/mmap/signals), and the kernel side of a libc (poll/epoll, nonblocking I/O, loopback,-threads and futexes, ttys and ptys).+threads and futexes, ttys and ptys), and musl's prerequisites (per-thread+FPU/SSE/AVX state, Linux C-string path calls and `struct stat`).  - [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)@@ -3557,7 +3914,8 @@ threads and futexes, ttys and ptys). - [x] Virtual drivers (stage 13): a modern (1.0) virtio transport shared by virtio-rng (`getrandom`, `/dev/random`, smoltcp's seed), virtio-console (`/dev/hvc0`, `getty`) and virtio-gpu 2D (a 1024x768 framebuffer, a bitmap-font console, `/dev/fb0`, `tile`) — [virtio 1.2 spec](https://docs.oasis-open.org/virtio/virtio/v1.2/virtio-v1.2.html), [OSDev: Virtio](https://wiki.osdev.org/Virtio), [Linux virtio-gpu driver](https://github.com/torvalds/linux/tree/master/drivers/gpu/drm/virtio) - [x] Processes (stage 14): `fork` with copy-on-write (refcounted frames), `execve` with Linux's argv/envp/auxv stack, `mmap`/`munmap`/`mprotect`/`brk` with demand paging, signals with Linux signal frames and `sigreturn`, `wait4`, `dup2`/`pipe2`/`fcntl`, process groups, Ctrl+C and job control in the shell — [man 2 fork](https://man7.org/linux/man-pages/man2/fork.2.html), [man 2 execve](https://man7.org/linux/man-pages/man2/execve.2.html), [man 7 signal](https://man7.org/linux/man-pages/man7/signal.7.html), [LWN: getauxval() and the auxiliary vector](https://lwn.net/Articles/519085/), [OSDev: Paging](https://wiki.osdev.org/Paging) - [x] Libc plumbing (stage 15): `poll`/`ppoll`/`select`/`pselect6`/`epoll`, `O_NONBLOCK` + `EAGAIN`, a loopback interface, `futex`, `clone` threads with TLS (`arch_prctl`), `set_tid_address`, `gettid`/`tgkill`, `exit` vs `exit_group`, ttys and ptys with termios, `/dev/tty`, `/dev/ptmx` + `/dev/pts/N`, a fork+exec `getty`, `nanosleep`, `CLOCK_REALTIME`, `readv`/`writev`, `pread`/`pwrite` — [man 7 epoll](https://man7.org/linux/man-pages/man7/epoll.7.html), [man 2 futex](https://man7.org/linux/man-pages/man2/futex.2.html), [Drepper: Futexes Are Tricky](https://www.akkadia.org/drepper/futex.pdf), [The TTY demystified](https://www.linusakesson.net/programming/tty/), [man 7 pty](https://man7.org/linux/man-pages/man7/pty.7.html)-- [ ] The kernel/OS split and a musl port (next; waits for Tyler's go)+- [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/)+- [ ] 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)  ### Filesystem roadmap (Tyler's plan)@@ -3777,6 +4135,32 @@ and for a UEFI/framebuffer setup later, the newer [`bootloader` 0.11](https://gi   - QEMU's default network card is an e1000. Adding `-netdev`/`-device     virtio-net-pci` replaces it, so the PCI device count stayed 10 (stage 13's     GPU, rng and console make it 12).+- **Pitfalls hit in stage 15.1:**+  - *QEMU's TCG never raises #XM.* With every SSE exception unmasked,+    `1.0 / 0.0` set MXCSR's divide-by-zero flag and quietly gave infinity:+    no exception, no SIGFPE. Real CPUs (and KVM) do raise it. `fputest`+    now checks for that case and says it skipped the test, instead of+    failing.+  - *QEMU's default `qemu64` CPU has no XSAVE*, so the XSAVE/AVX code would+    never have run. `-cpu max` is in Cargo.toml's run and test args, and+    the FXSAVE path is checked by a run with `-cpu qemu64`.+  - *gcc can't link `-no-pie` code above 4 GiB.* Its default code assumes+    every address fits in 32 bits (`mov $sym, %eax`). `-fpie` makes the code+    RIP-relative, but then a `GOTPCREL` load of an asm-defined symbol still+    failed to link ("failed to convert GOTPCREL relocation; relink with+    --no-relax"). Marking that symbol `visibility("hidden")` lets gcc+    address it directly.+  - *`rawabi` failed its own signal check on Linux*, before it ever ran on+    Rustbelt: `sqrt(2) * sqrt(8)` is not exactly 4.0 in floating point.+    The handler now computes `sqrt(16) * sqrt(0.25)`, which is exact. Test+    a portable test on the host first.+  - *One more file in `/bin` broke `demo`.* Its `getdents64` buffer (1 KiB)+    held all of `/bin` until `rawabi` made it 36 names, and the test+    assumed one call reads everything. It now calls until 0, like+    `readdir` does.+  - *`ls -l` and the old `Stat.mode == 0`.* FAT and ramfs files used to+    show no permission bits. With Linux's `struct stat`, `st_mode` always+    has some, so `ls -l` always prints them (`-rw-r--r--`). - **Pitfalls hit in stage 15:**   - *`write` must return what the caller asked for, not what went out.*     With ONLCR a 6-byte `"hello\n"` becomes 7 bytes on the wire. The tty
@@ -107,6 +107,12 @@ threads echoserver ptytest nbtest+# Stage 15.1: musl prerequisites. FPU/SSE/AVX state per thread and in+# signal frames, the Linux C-string path syscalls, and rawabi: a plain+# Linux binary built by gcc with no libc at all.+fputest+pathtest+rawabi nosuchprogram cd Q: cd A:/tmp
@@ -18,6 +18,9 @@ #            fsck.exfat on it) #        BTRFS_IMG=other.img ./scripts/run-headless.sh #            use other.img as drive E: (read-only, so always snapshot=on)+#        QEMU_CPU=qemu64 ./scripts/run-headless.sh+#            QEMU's CPU model (default "max": XSAVE + AVX, like Cargo.toml's+#            run-args); qemu64 has only FXSAVE, so this checks fpu.rs's fallback set -euo pipefail cd "$(dirname "$0")/.." @@ -68,7 +71,7 @@ VIRTIO_DEVICES=(-vga none -device virtio-vga -device virtio-rng-pci  LOG=target/headless-serial.log set +e-timeout 120 qemu-system-x86_64 -smp 4 \+timeout 120 qemu-system-x86_64 -smp 4 -cpu "${QEMU_CPU:-max}" \     -drive format=raw,file="$IMG" \     -drive file=target/disk.img,format=raw,if=virtio,snapshot=on \     -drive "$EXT2_DRIVE" \@@ -85,8 +88,9 @@ set -e # Lines the shell self-test must have printed (only for a plain boot). if [ -z "${1:-}" ] && [ "$code" = 33 ]; then     expect=(-        "Rustbelt 0.15.0 (stage 15: libc plumbing) x86_64" # ver+        "Rustbelt 0.15.1 (stage 15.1: musl prerequisites) 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         "C:  /mnt/disk1   ext2"         "D:  /mnt/disk2   exfat"@@ -153,6 +157,10 @@ if [ -z "${1:-}" ] && [ "$code" = 33 ]; then         "[echoserver] all checks passed"         "[ptytest] all checks passed"         "[nbtest] all checks passed"+        "[fputest] all checks passed"                  # stage 15.1: musl prerequisites+        "[pathtest] all checks passed"+        "rawabi: uname: Rustbelt 0.15.1"               # a no-libc gcc binary+        "rawabi: all checks passed"         "sh: nosuchprogram: command not found"         "cd: there is no drive Q:"         "A:/tmp $ pwd"@@ -161,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` -> 33,+    # Pipe results: `... | grep -i EXT | wc -l` -> 1, `dir bin | wc -l` -> 36 (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 33 3 4 20000; do+    for n in 1 36 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@@ -176,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.15.0" "$CURL_OUT" 2>/dev/null; then+    if grep -qF "Hello from Rustbelt! Rustbelt 0.15.1" "$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@@ -188,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.15.0 (stage 15" \+    for line in "Rustbelt getty: shell on this terminal" "Rustbelt 0.15.1 (stage 15.1" \                 "$ 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
@@ -26,8 +26,9 @@ if [ -z "$SESSION" ]; then     # The default session (what boot.png shows): the tiling demo moves the     # text console into the left window of the virtio-gpu framebuffer, a     # CPU hog stopped with Ctrl+C (the tty turns ^C into SIGINT), then-    # stage 15's libc plumbing: threads summing on all CPUs, the epoll/poll-    # echo server over 127.0.0.1, the pty and nonblocking-I/O tests.+    # stage 15's libc plumbing (threads summing on all CPUs, the epoll/poll+    # echo server over 127.0.0.1) and stage 15.1's musl prerequisites: the+    # FPU and path-syscall tests and rawabi, a no-libc Linux binary.     # "vx{BS}er" types a typo and fixes it.     SESSION=$(mktemp /tmp/session.XXXXXX)     cat > "$SESSION" <<'TXT'@@ -37,18 +38,20 @@ spin 1000 > /tmp/hog.txt {^C} threads | grep threads echoserver | grep echoes-ptytest | grep passed-nbtest | grep passed+fputest | grep passed+pathtest | grep passed+rawabi | grep -v "ok   " TXT-    set -- "Rustbelt 0.15.0 (stage 15: libc plumbing) x86_64" "tile: 4 windows on 1024x768, console 61x46" \+    set -- "Rustbelt 0.15.1 (stage 15.1: musl prerequisites) 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" \-        "[ptytest] all checks passed" "[nbtest] all checks passed"+        "[fputest] all checks passed" "[pathtest] all checks passed" \+        "rawabi: pi ~ 3.141588" "rawabi: all checks passed" fi SOCK=$(mktemp -u /tmp/qemu-mon.XXXXXX) SERIAL_LOG=$(mktemp /tmp/serial.XXXXXX) -qemu-system-x86_64 -smp 4 -drive format=raw,file="$IMG" \+qemu-system-x86_64 -smp 4 -cpu max -drive format=raw,file="$IMG" \     -drive file=target/disk.img,format=raw,if=virtio,snapshot=on \     -drive file=target/ext2.img,format=raw,if=virtio,snapshot=on \     -drive file=target/exfat.img,format=raw,if=virtio,snapshot=on \
@@ -41,6 +41,7 @@ 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+    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@@ -63,12 +64,12 @@ 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; 49] = [+        const ALL: [Errno; 50] = [             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,+            ENOTCONN, ETIMEDOUT, ECONNREFUSED, ENXIO, EINPROGRESS, EXDEV, ENOPROTOOPT,         ];         ALL.into_iter().find(|&e| -(e as i64) == ret)     }
@@ -122,6 +122,13 @@ pub fn uses_xsave() -> bool { pub fn xfeatures() -> u64 {     XCR0.load(Ordering::Relaxed) }+/// One line for the boot log, e.g. "XSAVE: x87 SSE AVX, 832 bytes per thread".+pub fn describe() -> alloc::string::String {+    let avx = if xfeatures() & XFEATURE_AVX != 0 { " AVX" } else { "" };+    let how = if uses_xsave() { "XSAVE" } else { "FXSAVE" };+    alloc::format!("{}: x87 SSE{}, {} bytes per thread", how, avx, size())+}+ /// Bytes of saved state (512 with FXSAVE). pub fn size() -> usize {     SIZE.load(Ordering::Relaxed) as usize
@@ -64,6 +64,7 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! {     // Start the other CPU cores; from now on threads run on all of them.     let cpus = rustbelt::init_smp();     kprintln!("[smp] {} CPU{} online", cpus, if cpus == 1 { "" } else { "s" });+    kprintln!("[fpu] {}", rustbelt::fpu::describe()); // stage 15.1: per-thread FPU/SSE/AVX      // --- Preemptive threads demo (thread.rs) --------------------------------     // From now on the timer interrupt switches between threads every tick.
@@ -10,18 +10,23 @@ //! execve, wait4, mmap, signals, dup, pipe2, fcntl, process groups) follow //! Linux exactly -- C strings, structs, flags -- so a C library can use //! them as they are, and so do stage 15's (poll/epoll, futex, threads,-//! ttys, nanosleep, readv/writev...). The older file calls still take-//! paths as pointer + length (Linux: NUL-terminated); relative paths start-//! at the cwd. Every blocking call honours O_NONBLOCK (-> EAGAIN).+//! ttys, nanosleep, readv/writev...). Since stage 15.1 the file calls do+//! too: paths are NUL-terminated C strings (at most PATH_MAX = 4096 bytes+//! with the NUL; a bad pointer is EFAULT, never a crash), stat fills+//! Linux's 144-byte `struct stat`, and the `*at()` versions take a+//! directory fd or AT_FDCWD (-100). Relative paths start at the cwd.+//! `mode` arguments are accepted and ignored (no permissions yet), and+//! uid/gid are always 0. Every blocking call honours O_NONBLOCK (-> EAGAIN). //! //! | nr  | name        | arguments                           | note | //! |-----|-------------|-------------------------------------|------| //! |   0 | read        | fd, buf, len                        | blocks: tty until a line (canonical) or a byte (raw), pipe until data/EOF | //! |   1 | write       | fd, buf, len                        | to a pipe without readers: EPIPE + SIGPIPE; a full pipe blocks |-//! |   2 | open        | path, path_len, flags               | O_CREAT 0o100, O_NOCTTY 0o400, O_TRUNC 0o1000, O_APPEND, O_NONBLOCK 0o4000, O_CLOEXEC 0o2000000 |+//! |   2 | open        | path, flags, mode                   | = openat(AT_FDCWD, ...). O_RDONLY/WRONLY/RDWR 0/1/2, O_CREAT 0o100, O_EXCL 0o200, O_NOCTTY 0o400, O_TRUNC 0o1000, O_APPEND, O_NONBLOCK 0o4000, O_DIRECTORY 0o200000, O_NOFOLLOW 0o400000, O_CLOEXEC 0o2000000 | //! |   3 | close       | fd                                  | |-//! |   4 | stat        | path, path_len, *Stat               | our own small `Stat {kind, size, mode}`; follows symlinks |-//! |   5 | fstat       | fd, *Stat                           | |+//! |   4 | stat        | path, *stat                         | Linux `struct stat`; follows symlinks. st_ino is a hash of the path |+//! |   5 | fstat       | fd, *stat                           | |+//! |   6 | lstat       | path, *stat                         | doesn't follow a final symlink | //! |   7 | poll        | *pollfd, n, timeout_ms              | -1 = forever; pipes, sockets, ttys, files, epoll fds | //! |   8 | lseek       | fd, offset, whence                  | SEEK_SET/CUR/END = 0/1/2 | //! |   9 | mmap        | addr, len, prot, flags, fd, offset  | MAP_PRIVATE/SHARED, MAP_ANONYMOUS, MAP_FIXED; files: private only |@@ -36,6 +41,7 @@ //! |  18 | pwrite64    | fd, buf, len, offset                | | //! |  19 | readv       | fd, *iovec, n                       | `struct iovec {base, len}`, up to 1024 | //! |  20 | writev      | fd, *iovec, n                       | |+//! |  21 | access      | path, mode                          | = faccessat(AT_FDCWD, ...): 0 if it exists (we have no permissions) | //! |  22 | pipe        | *[u32; 2]                           | [read end, write end] | //! |  23 | select      | n, *rd, *wr, *ex, *timeval          | fd bitmaps (n <= 1024) | //! |  24 | sched_yield |                                     | |@@ -46,27 +52,30 @@ //! |  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 | //! |  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 |-//! |  44 | sendto      | fd, buf, len, flags, *sockaddr_in or 0 | Linux has a 6th arg (addrlen); we only take 5, the address is always 16 bytes |-//! |  45 | recvfrom    | fd, buf, len, flags, *sockaddr_in or 0 | blocks (SO_RCVTIMEO -> EAGAIN); 0 = TCP peer closed. No addrlen pointer |+//! |  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 |+//! |  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) | //! |  50 | listen      | fd, backlog                         | TCP; backlog 1..4 |-//! |  54 | setsockopt  | fd, level, name, *value, len        | only SOL_SOCKET (1) / SO_RCVTIMEO (20) with a `struct timeval` |+//! |  54 | setsockopt  | fd, level, name, *value, len        | only SOL_SOCKET (1) / SO_RCVTIMEO (20) with a `struct timeval`; other options: 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] | //! |  57 | fork        |                                     | copy-on-write; parent gets the pid, child 0 | //! |  58 | vfork       |                                     | fork + the parent waits until the child execs or exits | //! |  59 | execve      | path, argv, envp (C strings)        | Linux's stack layout: argc, argv, envp, auxv | //! |  60 | exit        | code                                | ends this *thread* only (exit_group ends the process) |-//! |  61 | wait4       | pid, *status, options, *rusage      | WNOHANG 1, WUNTRACED 2, WCONTINUED 8; rusage must be 0 |+//! |  61 | wait4       | pid, *status, options, *rusage      | WNOHANG 1, WUNTRACED 2, WCONTINUED 8; *rusage (if not 0) is zero-filled: we don't count usage | //! |  62 | kill        | pid, sig                            | pid 0 = my group, -1 = all, < -1 = group -pid | //! |  63 | uname       | *utsname                            | Linux's `struct utsname` (6 x 65-byte strings) | //! |  72 | fcntl       | fd, cmd, arg                        | F_DUPFD 0, F_GETFD 1, F_SETFD 2, F_GETFL 3, F_SETFL 4 (O_NONBLOCK), F_DUPFD_CLOEXEC 1030 | //! |  79 | getcwd      | buf, len                            | like Linux: writes a NUL, returns the length including it |-//! |  80 | chdir       | path, path_len                      | |-//! |  83 | mkdir       | path, path_len                      | |-//! |  84 | rmdir       | path, path_len                      | only empty directories |-//! |  87 | unlink      | path, path_len                      | files and symlinks (not what a symlink points to) |-//! |  89 | readlink    | path, path_len, buf, len            | a symlink's target; returns its length (cut to `len`, no NUL); /proc/self/exe = my program |+//! |  80 | chdir       | path                                | |+//! |  81 | fchdir      | fd                                  | a directory opened with open() |+//! |  82 | rename      | old, new                            | = renameat(AT_FDCWD, old, AT_FDCWD, new); see `fs::rename` for the limits |+//! |  83 | mkdir       | path, mode                          | = mkdirat(AT_FDCWD, ...) |+//! |  84 | rmdir       | path                                | only empty directories |+//! |  87 | unlink      | path                                | files and symlinks (not what a symlink points to) |+//! |  89 | readlink    | path, buf, len                      | a symlink's target; returns its length (cut to `len`, no NUL); /proc/self/exe = my program |+//! | 102 | getuid      |                                     | always 0 (root), like getgid 104, geteuid 107, getegid 108 | //! | 109 | setpgid     | pid, pgid                           | 0 = myself / a new group | //! | 110 | getppid     |                                     | 0 if started by the kernel or orphaned | //! | 111 | getpgrp     |                                     | |@@ -81,10 +90,18 @@ //! | 217 | getdents64  | fd, buf, len                        | Linux `struct linux_dirent64` records | //! | 218 | set_tid_address | *u32                            | cleared + futex-woken when this thread exits; returns the tid | //! | 228 | clock_gettime | clock, *timespec                  | REALTIME 0 (CMOS clock at boot + uptime), MONOTONIC 1 (since boot) |+//! | 230 | clock_nanosleep | clock, flags, *timespec, *remaining | REALTIME, MONOTONIC or BOOTTIME 7; TIMER_ABSTIME 1 = sleep until that time | //! | 231 | exit_group  | code                                | ends every thread of the process | //! | 232 | epoll_wait  | epfd, *epoll_event, max, timeout_ms | level-triggered; EPOLLONESHOT supported | //! | 233 | epoll_ctl   | epfd, op, fd, *epoll_event          | ADD 1, DEL 2, MOD 3 | //! | 234 | tgkill      | tgid, tid, sig                      | a signal for one thread |+//! | 257 | openat      | dirfd, path, flags, mode            | O_NOFOLLOW on a symlink: ELOOP; O_EXCL: EEXIST; O_DIRECTORY on a file: ENOTDIR; a directory for writing: EISDIR |+//! | 258 | mkdirat     | dirfd, path, mode                   | |+//! | 262 | newfstatat  | dirfd, path, *stat, flags           | AT_SYMLINK_NOFOLLOW 0x100, AT_EMPTY_PATH 0x1000 (= fstat(dirfd)) |+//! | 263 | unlinkat    | dirfd, path, flags                  | AT_REMOVEDIR 0x200 = rmdir |+//! | 264 | renameat    | olddirfd, old, newdirfd, new        | renameat2 316 too: flags 0 only (else EINVAL) |+//! | 267 | readlinkat  | dirfd, path, buf, len               | |+//! | 269 | faccessat   | dirfd, path, mode                   | faccessat2 439 too (its flags are ignored) | //! | 270 | pselect6    | n, *rd, *wr, *ex, *timespec, *{*mask, 8} | | //! | 271 | ppoll       | *pollfd, n, *timespec, *mask, 8     | sets the mask while waiting | //! | 281 | epoll_pwait | epfd, *events, max, timeout, *mask, 8 | |@@ -93,7 +110,7 @@ //! | 292 | dup3        | old, new, flags                     | flags: O_CLOEXEC | //! | 293 | pipe2       | *[u32; 2], flags                    | flags: O_CLOEXEC | //! | 318 | getrandom   | buf, len, flags                     | random bytes (virtio-rng); up to 64 KiB per call; flags ignored |-//! | 500 | spawn       | path, path_len, argv, argv_len, *SpawnOptions | ours: new process running a program (fork+exec in one step, like posix_spawn) |+//! | 500 | spawn       | path, argv, argv_len, *SpawnOptions | ours: new process running a program (fork+exec in one step, like posix_spawn) | //! | 501 | mounts      | buf, len                            | ours: "path fstype\n" per mount (Linux: read /proc/mounts) | //! | 502 | sysinfo     | *SysInfo                            | ours: CPUs, threads, memory, per-CPU busy time (Linux: /proc/stat etc.) | //! | 503 | resolve     | name, name_len, *[[u8; 4]], max     | ours: DNS lookup (A records), returns the count. Linux does DNS in libc |@@ -1151,7 +1168,7 @@ fn sys_uname(buf: u64) -> Result<u64, Errno> {         "Rustbelt",                     // sysname (`uname -s`)         "qemu",                         // nodename (hostname)         env!("CARGO_PKG_VERSION"),      // release (`uname -r`)-        "stage 15: libc plumbing",      // version+        "stage 15.1: musl prerequisites", // version         "x86_64",                       // machine         "(none)",                       // domainname     ];@@ -1308,8 +1325,11 @@ fn sys_recvfrom(fd: u64, buf: u64, len: u64, addr_ptr: u64, len_ptr: u64) -> Res /// Only SO_RCVTIMEO: `struct timeval {i64 seconds, i64 microseconds}`; /// zero means "block forever", like Linux. fn sys_setsockopt(fd: u64, level: u64, name: u64, value: u64, len: u64) -> Result<u64, Errno> {-    if level != SOL_SOCKET || name != SO_RCVTIMEO || len < 16 {-        return Err(Errno::EOPNOTSUPP);+    if level != SOL_SOCKET || name != SO_RCVTIMEO {+        return Err(Errno::ENOPROTOOPT); // what Linux says for an unknown option+    }+    if len < 16 {+        return Err(Errno::EINVAL);     }     let tv = user_slice(value, 16)?;     let secs = u64::from_ne_bytes(tv[..8].try_into().unwrap());
@@ -176,7 +176,7 @@ fn scripted_shell_session() {         "piped text\n",         "wc -l\n2\n",         "saved\nmore\n",-        "Rustbelt 0.15.0",+        "Rustbelt 0.15.1",         "B:/ $ exit 3\n",     ] {         assert!(out.contains(expected), "missing {:?} in:\n{}", expected, out);
@@ -59,18 +59,23 @@ fn main() -> i64 {     // --- list a directory with getdents64 -----------------------------------     let dir = open("bin", 0);     let mut dents = [0u8; 1024];-    let len = getdents64(dir, &mut dents);+    let mut len = getdents64(dir, &mut dents);     check(len > 0, "getdents64 bin");     let (mut saw_demo, mut saw_child) = (false, false);     print!("[demo] /mnt/disk0/bin:");-    for_each_dirent(&dents, len.max(0) as usize, |name, d_type| {-        print!(" {}{}", name, if d_type == 4 { "/" } else { "" });-        saw_demo |= name == "demo";-        saw_child |= name == "child";-    });+    // One call returns as many whole records as fit in the buffer; keep+    // calling until it returns 0 (the end of the directory).+    while len > 0 {+        for_each_dirent(&dents, len as usize, |name, d_type| {+            print!(" {}{}", name, if d_type == 4 { "/" } else { "" });+            saw_demo |= name == "demo";+            saw_child |= name == "child";+        });+        len = getdents64(dir, &mut dents);+    }     println!();     check(saw_demo && saw_child, "bin contains demo and child");-    check(getdents64(dir, &mut dents) == 0, "getdents64 at end returns 0");+    check(len == 0 && getdents64(dir, &mut dents) == 0, "getdents64 at end returns 0");     close(dir);      // --- relative paths, "..", errors ----------------------------------------
@@ -867,6 +867,7 @@ pub fn strerror(errno: i64) -> &'static str {         16 => "Device or resource busy",         19 => "No such device",         17 => "File exists",+        18 => "Invalid cross-device link",         20 => "Not a directory",         21 => "Is a directory",         22 => "Invalid argument",@@ -886,6 +887,7 @@ pub fn strerror(errno: i64) -> &'static str {         88 => "Socket operation on non-socket",         89 => "Destination address required",         90 => "Message too long",+        92 => "Protocol not available",         93 => "Protocol not supported",         97 => "Address family not supported",         98 => "Address already in use",