RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

authorEdward <edward@localhost>2026-10-31 16:03:26 -0700
commit63fcb8621f453f1445fea724b37bf39479a23d9f
parent58fc4fd3656324fc67db37322de2bdd6e563faf8
treebrowse files at this commit
Stage 16: libc batch 1 (0.16.0)

The calls musl and dash make at run time, from Clarke's traces:
rt_sigsuspend (dash's `wait` no longer spins), rt_sigpending,
rt_sigtimedwait, rt_sigqueueinfo/rt_tgsigqueueinfo with queued
real-time signals, sigaltstack + SA_ONSTACK; per-process umask and
rlimits (getrlimit/setrlimit/prlimit64, RLIMIT_NOFILE = the real 64);
fcntl F_GETLK/F_SETLK/F_SETLKW record locks with EDEADLK, honest
F_GETFL; truncate/ftruncate, fsync/fdatasync, chmod/fchmod/fchmodat(2),
utimensat and real stat times; prctl, sched_get/setaffinity, madvise,
set/get_robust_list. Orphans go to a PR_SET_CHILD_SUBREAPER ancestor
(or a real init), else reap themselves. --trace prints unknown calls as
syscall_N.

Tests: tests/libc_batch1.rs + user/libctest.rs (7 new, 157 total).
MCargo.lock+1 -1
MCargo.toml+1 -1
MREADME.md+315 -21
Mbuild.rs+1 -0
Mscripts/run-headless.sh+6 -6
Mscripts/screenshot.sh+1 -1
Msrc/errno.rs+4 -2
Msrc/fs/btrfs.rs+20 -1
Msrc/fs/ext2.rs+40 -1
Msrc/fs/fat.rs+18 -0
Msrc/fs/file.rs+90 -6
Asrc/fs/lock.rs+194 -0
Msrc/fs/mod.rs+66 -0
Msrc/fs/pipe.rs+6 -0
Msrc/fs/ramfs.rs+89 -21
Msrc/mm.rs+40 -0
Msrc/process.rs+283 -11
Msrc/signal.rs+170 -16
Msrc/syscall.rs+631 -19
Atests/libc_batch1.rs+116 -0
Mtests/shell.rs+1 -1
Auser/libctest.rs+655 -0
Muser/nbtest.rs+1 -1

23 files changed, 2749 insertions, 109 deletions

@@ -261,7 +261,7 @@ dependencies = [  [[package]] name = "rustbelt"-version = "0.15.1"+version = "0.16.0" dependencies = [  "bootloader",  "conquer-once",
@@ -1,6 +1,6 @@ [package] name = "rustbelt"-version = "0.15.1"+version = "0.16.0" edition = "2024"  [dependencies]
@@ -85,6 +85,14 @@ Under the shell:   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.+- **libc batch 1 (stage 16).** The calls musl and the `dash` shell make+  once a program is running: `rt_sigsuspend` (so dash's `wait` sleeps+  instead of spinning), `sigtimedwait`, `sigpending`, queued real-time+  signals, `sigaltstack`; a per-process **umask** and **rlimits**; `fcntl`+  **record locks**; `truncate`, `fsync`, `chmod`, `utimensat` and real+  file times; `prctl`, `sched_getaffinity`, `madvise` and robust futex+  lists. Orphaned children are adopted by a subreaper or clean themselves+  up.  It has a `cargo test` suite that runs inside QEMU, and a headless self-test that drives the shell with a script.@@ -97,7 +105,8 @@ 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, 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+stage 15.1's FPU state switching and Linux path calls, and stage 16's+libc batch 1. The 8x16 font is [Spleen](https://github.com/fcambus/spleen) by Frederic Cambus (BSD 2-clause, license in `src/font.rs`).  ![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)@@ -987,6 +996,7 @@ heap_allocation::simple_allocation...               [ok] | `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) |+| `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` |  How it works: `#![feature(custom_test_frameworks)]` collects every `#[test_case]` function and passes them to `rustbelt::test_runner` (in `src/lib.rs`), which@@ -1108,6 +1118,8 @@ user/{threads,echoserver,ptytest,nbtest}.rs  stage 15: threads + futexes, poll/e 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+user/libctest.rs          stage 16: libc batch 1 checks, one group per argument (tests/libc_batch1.rs)+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`) scripts/gpu-check.sh      types `tile`, dumps the virtio-gpu screen (PPM), compares pixels@@ -1530,7 +1542,7 @@ 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, 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 |+| 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`). Since stage 16 a new file gets `mode & ~umask` (ramfs, ext2), and `F_GETFL` reports the access mode | | 3 | `close` | fd | | | 4 | `stat` | path, *stat | **stage 15.1**: Linux's 144-byte `struct stat`; follows symlinks | | 5 | `fstat` | fd, *stat | |@@ -1553,6 +1565,7 @@ Numbers are Linux's wherever Linux has the same call, so man pages and | 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` | | |+| 28 | `madvise` | addr, len, advice | **stage 16**: `MADV_DONTNEED` 4 / `MADV_FREE` 8 free private anonymous pages (they read back as zeros); other known hints return 0; unknown ones `EINVAL`, unmapped memory `ENOMEM` | | 32 | `dup` | fd | lowest free fd | | 33 | `dup2` | old, new | closes `new` first if open | | 34 | `pause` |  | sleeps until a signal; returns EINTR |@@ -1573,16 +1586,23 @@ Numbers are Linux's wherever Linux has the same call, so man pages and | 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; 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.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 |+| 63 | `uname` | *utsname | Linux's struct: 6 NUL-padded 65-byte fields: "Rustbelt", "qemu", "0.16.0", "stage 16: libc batch 1", "x86_64", "(none)" |+| 72 | `fcntl` | fd, cmd, arg | F_DUPFD 0 (below `RLIMIT_NOFILE`, else `EINVAL`), F_GETFD 1, F_SETFD 2, F_GETFL 3 (since stage 16 the real access mode, `O_APPEND`, `O_NONBLOCK`), F_SETFL 4 (`O_NONBLOCK`; `O_APPEND` since 16), F_DUPFD_CLOEXEC 1030. **Stage 16**: record locks F_GETLK 5, F_SETLK 6 (`EAGAIN` on a conflict), F_SETLKW 7 (sleeps; `EDEADLK`, `EINTR`), Linux's `struct flock`; dropped when the process closes *any* fd of the file, or exits |+| 74 | `fsync` | fd | **stage 16**: 0 for files and directories (writes already go straight to the disk); `fdatasync` 75 the same; `EINVAL` for pipes, sockets, ttys |+| 76 | `truncate` | path, length | **stage 16**: grow (zeros) or shrink a file; `EISDIR`, `EINVAL` (negative), `EFBIG` (over 4 GiB), `EROFS` on btrfs |+| 77 | `ftruncate` | fd, length | **stage 16**: the same through an fd opened for writing (else `EINVAL`) | | 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 | 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 |+| 83 | `mkdir` | path, mode | since stage 16 `mode & ~umask` (ramfs, ext2) | | 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 |+| 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** |+| 95 | `umask` | mask | **stage 16**: per process (default 022), inherited by fork; returns the old one |+| 97 | `getrlimit` | resource, *[cur, max] | **stage 16**: = `prlimit64(0, ...)`; `setrlimit` 160 too | | 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 |@@ -1590,10 +1610,18 @@ Numbers are Linux's wherever Linux has the same call, so man pages and | 112 | `setsid` |  | **stage 15**: new session + group, no controlling tty | | 121 | `getpgid` | pid |  | | 124 | `getsid` | pid | **stage 15** |+| 127 | `rt_sigpending` | *set, size | **stage 16**: signals waiting because they're blocked |+| 128 | `rt_sigtimedwait` | *set, *siginfo, *timespec, 8 | **stage 16**: take one of `set` without running a handler (sleeps; `EAGAIN` after the timeout, `{0, 0}` = just look) |+| 129 | `rt_sigqueueinfo` | pid, sig, *siginfo | **stage 16**: send with your own siginfo (`sigqueue`); `si_code` must be negative for another process (`EPERM`). Real-time signals (32-64) queue up, each with its value; `rt_tgsigqueueinfo` 297 for one thread |+| 130 | `rt_sigsuspend` | *mask, 8 | **stage 16**: swap in `mask`, sleep until a handler runs, put the old mask back, `EINTR` (dash's `wait`) |+| 131 | `sigaltstack` | *new, *old | **stage 16**: a separate stack for `SA_ONSTACK` handlers; `SS_DISABLE` 2, `SS_ONSTACK` 1 (read only), `SS_AUTODISARM`; `ENOMEM` below 2 KiB, `EPERM` while on it |+| 157 | `prctl` | option, arg2... | **stage 16**: `PR_SET_NAME`/`PR_GET_NAME` 15/16 (thread name, 15 bytes), `PR_SET_PDEATHSIG`/`GET` 1/2, `PR_SET_DUMPABLE`/`GET` 4/3, `PR_SET_CHILD_SUBREAPER`/`GET` 36/37, `PR_SET_NO_NEW_PRIVS`/`GET` 38/39; others `EINVAL` | | 158 | `arch_prctl` | code, addr | **stage 15**: ARCH_SET_FS 0x1002 / ARCH_GET_FS 0x1003: the TLS pointer |+| 160 | `setrlimit` | resource, *[cur, max] | **stage 16**: see `prlimit64` | | 186 | `gettid` |  | **stage 15**: thread id (= pid for the first thread) | | 200 | `tkill` | tid, sig | **stage 15** | | 202 | `futex` | *u32, op, val, *timespec, *u32, val3 | **stage 15**: FUTEX_WAIT 0, WAKE 1, WAIT_BITSET 9, WAKE_BITSET 10 (+ PRIVATE 128) |+| 204 | `sched_getaffinity` | tid, len, *mask | **stage 16**: one bit per CPU (all of them: we don't pin threads); returns the bytes written (8). `sched_setaffinity` 203 accepts any mask that includes a CPU | | 213 | `epoll_create` | size | **stage 15**: = epoll_create1(0) | | 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 |@@ -1609,14 +1637,18 @@ Numbers are Linux's wherever Linux has the same call, so man pages and | 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** |+| 268 | `fchmodat` | dirfd, path, mode | **stage 16**; `fchmodat2` (452) also takes `AT_SYMLINK_NOFOLLOW` (`EOPNOTSUPP` on a symlink, like Linux) and `AT_EMPTY_PATH` | | 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 |+| 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 | | 291 | `epoll_create1` | flags | **stage 15**: EPOLL_CLOEXEC | | 292 | `dup3` | old, new, flags | flags: O_CLOEXEC | | 293 | `pipe2` | *[u32; 2], flags | flags: O_CLOEXEC |+| 302 | `prlimit64` | pid, resource, *new, *old | **stage 16**: limits are per process and inherited. Real numbers where the kernel has a real limit: `RLIMIT_NOFILE` 64 (the fd table), `RLIMIT_STACK` (the user stack), `RLIMIT_NPROC`; the rest unlimited. Lowering works; raising a hard limit past the real one is `EPERM` | | 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, 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 |@@ -1639,10 +1671,12 @@ Deviations from Linux worth knowing:   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.+  are 0, and (since stage 16) the times are real: ext2 and btrfs keep+  them on disk, ramfs in memory; FAT and exFAT still report 0.+- **Permissions are ignored**: everything runs as root (uid 0) and+  `access` only checks that a path exists. Since stage 16 `open`'s and+  `mkdir`'s modes (minus the umask) and `chmod` are *stored* on ramfs and+  ext2, so `ls -l` and `stat` show them, but nothing checks them. - **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@@ -1979,6 +2013,7 @@ Programs in `B:/bin` (`user/*.rs`, each 30-150 lines): | `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" |+| `libctest wait\|signals\|locks\|files\|misc\|orphans` | the stage 16 checks (libc batch 1), one group at a time; 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,@@ -3831,24 +3866,21 @@ Doesn't (yet):  **Syscalls musl will call that Rustbelt still lacks.** These all return `-ENOSYS` today. Most matter only for the functions that use them.+(Stage 16, below, crossed off the rows for signals, rlimits, `umask` and+`chmod`, `truncate`, `fsync`, `utimensat`, record locks, `prctl`, robust+lists, `sched_getaffinity` and `madvise`.)  | 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 |+| `malloc` (mallocng) | `mremap` 25 (failure is tolerated) |+| `chown` family | `fchownat` 260 (and `chown` 92, `fchown` 93, `lchown` 94) | | `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 |+| `statvfs`, `flock` | `statfs` 137 / `fstatfs` 138, `flock` 73 | | `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 |+| `dlclose` barriers | `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 |@@ -3866,6 +3898,249 @@ Not syscalls, but missing just the same: - **`/etc/passwd`, `/etc/resolv.conf` and the other files** musl reads.   Rustbelt does DNS in the kernel (`resolve`, 503). +## libc batch 1 (stage 16)++Stage 15.1 got a musl program to *start*. Stage 16 is about what it does+next. Clarke ran real musl 1.2.5 programs and the `dash` shell with+`run --trace` and collected every call that came back `ENOSYS`. This+stage is the first batch of them, in the order they hurt:++1. `rt_sigsuspend`, without which dash's `wait` spun forever;+2. `umask` and the resource limits;+3. `sched_getaffinity` (how many CPUs do I have?);+4. orphaned children that were never freed;+5. then the rest: `sigaltstack`, `sigtimedwait`, `fcntl` locks,+   `truncate`, `chmod`, `utimensat`, `prctl` and friends.++Every call uses Linux's number, arguments and error codes, so `man 2+<name>` describes it. Where we can't do the real thing we say so with an+honest error (`ENOSYS`, `EINVAL`, `EROFS`), never a fake success.++### Waiting for a signal without missing it++A shell runs `sleep 5 &` and then `wait`. It must sleep until a SIGCHLD+says a child finished. The obvious code has a bug:++```+if (!child_done)    // (1) no, not yet...+    pause();        // (2) ...so sleep until a signal+```++If the SIGCHLD arrives between (1) and (2), its handler sets `child_done`+*before* we sleep, and `pause` then sleeps forever: nobody will send a+second signal. This is called a lost wake-up.++The fix is to keep SIGCHLD **blocked** (held back, "pending") while you+check, and let one system call unblock it and go to sleep in a single+step:++```+sigprocmask(SIG_BLOCK, {SIGCHLD}, &old);+while (!child_done)+    sigsuspend(&old);   // unblock + sleep, atomically; then block again+```++`rt_sigsuspend` (130) swaps in the given mask, sleeps until a handler has+run, puts the old mask back and returns `EINTR` (it always "fails"; that+is its success). If the signal was already pending it doesn't sleep at+all: the handler runs at once. In the kernel this was short, because+`ppoll` (stage 15) already does "a temporary mask while waiting":+`set_temporary_mask`, then the same sleep as `pause`, and the signal+delivery code restores the mask when the handler is done.++Before stage 16 the call returned `ENOSYS` right away, so dash's loop ran+round and round at 100% CPU. `libctest wait` is that exact loop with a+limit on the rounds, so a broken kernel fails the test instead of hanging+it.++Its relatives:++- **`rt_sigpending`** (127): which blocked signals are waiting.+- **`rt_sigtimedwait`** (128): take a signal from a set *without* running+  its handler, and get its `siginfo`. It is like `read` for signals. The+  caller blocks the signals first; the kernel unblocks them only for the+  wait. A timeout gives `EAGAIN`, and `{0, 0}` means "just look".+- **`rt_sigqueueinfo`** (129) and `rt_tgsigqueueinfo` (297): send a+  signal with your own `siginfo`, which is how `sigqueue()` attaches a+  number to a signal. The kernel won't let you pretend to be the kernel or+  `kill` to someone else: `si_code` must be negative (`EPERM`).++**Standard vs real-time signals.** Signals 1-31 are flags: SIGUSR1 sent+three times while blocked is delivered *once*. Signals 32-64 (the+"real-time" ones) are a queue: sent three times, they arrive three times,+in order, each with its own value. Stage 16 added that queue (up to+1024 per process; then `EAGAIN`).++### A spare stack for signal handlers++A handler normally runs on the thread's own stack, below where the+interrupted code was. What if the signal is SIGSEGV *because* that stack+overflowed? There is no room left to run the handler. **`sigaltstack`**+(131) registers a second stack, and a handler installed with+`SA_ONSTACK` runs there instead. Rust's standard library uses it to print+"thread main has overflowed its stack" instead of dying silently.++The kernel only has to pick a different place for the signal frame+(stage 14's "Signals" section explains frames). Some rules, as on Linux:+the stack must be at least 2 KiB (`ENOMEM`); while a handler is running+on it you can't change it (`EPERM`); and asking about it from inside the+handler reports `SS_ONSTACK`. A handler that interrupts another handler+already on the alternate stack just continues below it.++### umask and resource limits++**umask** (95) is a per-process mask of permission bits that new files+must *not* get. A program asks for `open("x", O_CREAT, 0666)`; with the+usual umask `022` the file gets `0666 & ~022 = 0644` (`rw-r--r--`).+It is inherited across `fork`, so a shell's `umask 077` affects every+command it starts. Rustbelt now stores the result on ramfs and ext2, and+`chmod` changes it, but **nothing checks permissions yet**: everyone is+still root.++**Resource limits** (`getrlimit` 97, `setrlimit` 160, `prlimit64` 302)+are pairs: a *soft* limit (what applies now) and a *hard* limit (the most+the soft one may be raised to). Programs ask, for example, "how many fds+may I open?" and size tables from the answer. Lying would break them, so+where Rustbelt has a real limit it reports it:++| resource | value | why |+|---|---|---|+| `RLIMIT_NOFILE` 7 | 64 / 64 | the fd table has 64 slots |+| `RLIMIT_STACK` 3 | the user stack size | fixed when the stack is mapped |+| `RLIMIT_NPROC` 6 | the process table size | |+| `RLIMIT_CORE` 4 | 0 / unlimited | no core dumps |+| the rest | unlimited | nothing enforces them |++Limits are per process and inherited by `fork`. Lowering one works and+is enforced for `RLIMIT_NOFILE` (`dup` and `open` then fail with `EMFILE`,+`F_DUPFD` with `EINVAL`). Raising a hard limit past the real one is+`EPERM`.++### File locks between processes++`fcntl(F_SETLK)` locks a byte range of a file: a *read* (shared) lock or+a *write* (exclusive) one. Two processes that write the same file use it+to take turns. The kernel keeps a list of `(file, process, range, type)`+in `src/fs/lock.rs`:++- `F_SETLK` takes or releases a lock, or fails at once with `EAGAIN` if+  another process holds a conflicting one.+- `F_SETLKW` ("wait") sleeps until it can take it. If A waits for B while+  B waits for A, both would sleep forever, so the kernel follows the chain+  of "who waits for whom" and refuses the second wait with `EDEADLK`.+- `F_GETLK` asks "would this lock work?" and if not, which process is in+  the way.++They are **POSIX record locks**, with POSIX's famous quirk: closing *any*+fd of a file drops *all* of that process's locks on it, even ones taken+through another fd. Exiting drops them too. `libctest locks` checks both.++`fcntl` also learned to tell the truth with `F_GETFL`: the real access+mode (`O_RDONLY`/`O_WRONLY`/`O_RDWR`, it used to always say `O_RDWR`),+`O_APPEND` and `O_NONBLOCK`.++### truncate, fsync, chmod and file times++- **`truncate`/`ftruncate`** (76/77) set a file's size: shorter cuts off+  the end, longer adds zero bytes. ramfs and FAT do it directly; ext2+  through a generic "copy what stays" fallback. `ftruncate` needs an fd+  opened for writing.+- **`fsync`/`fdatasync`** (74/75) say "make sure this is on the disk".+  Rustbelt writes to the disk straight away (there is no write-back+  cache), so for files they just return 0.+- **`chmod`/`fchmod`/`fchmodat`** (90/91/268) store permission bits on+  ramfs and ext2. FAT and exFAT have nowhere to keep them (`EPERM`).+- **`utimensat`** (280) sets the access and modification times (`touch`+  does this). `stat` used to report every time as 0 (1970); now ramfs,+  ext2 and btrfs report real ones, and new files are stamped with the+  current time. ext2 keeps whole seconds.+- On **btrfs**, which Rustbelt only reads, all of these are `EROFS`+  ("read-only file system"), as on Linux for a read-only mount.++### Processes: prctl, CPUs, madvise, robust locks++- **`prctl`** (157) is Linux's drawer of odds and ends. We do the ones+  libc uses: the thread's name (`pthread_setname_np`, 15 characters),+  "send me this signal when my parent dies" (`PR_SET_PDEATHSIG`),+  `PR_SET_DUMPABLE`, `PR_SET_NO_NEW_PRIVS`, and `PR_SET_CHILD_SUBREAPER`+  (below). Others return `EINVAL`.+- **`sched_getaffinity`** (204) returns a bit mask of the CPUs a thread+  may run on. musl's `sysconf(_SC_NPROCESSORS_ONLN)` counts its bits, so+  this is how a C program learns it has 4 CPUs. Every thread may run+  anywhere, so it's simply one bit per CPU. Its return value is the number+  of bytes written (8), not 0. That is a classic gotcha: the libc wrapper+  zeroes the rest of your buffer and then returns 0.+- **`madvise`** (28) gives hints about memory. Most can be ignored, and+  we do. `MADV_DONTNEED` can't be ignored: it promises the pages read as+  zeros afterwards, and musl's `malloc` relies on that. So we free those+  pages and the next touch gets a fresh zeroed page (stage 14's demand+  paging).+- **Robust futexes** (`set_robust_list`, 273). A pthread mutex lives in+  user memory. If the thread holding it dies, normally nobody ever unlocks+  it. A *robust* mutex is on a list the thread gives the kernel; when the+  thread exits, the kernel walks the list, marks each lock it still holds+  `FUTEX_OWNER_DIED` and wakes one waiter, who can then repair things.++### Orphans: who cleans up?++When a process exits before its children, they become **orphans**. On+Unix, someone must eventually `wait` for every process, or its exit+status (a "zombie") stays in the process table forever. Linux hands+orphans to pid 1, `init`, whose job is to `wait` for anything.++Rustbelt's pid 1 is usually the shell, which only waits for its own+jobs. Handing it orphans would just collect zombies. So:++- An orphan is adopted by the nearest living ancestor that called+  `prctl(PR_SET_CHILD_SUBREAPER, 1)`, as on Linux. That's how service+  managers keep track of the grandchildren of what they start.+- Failing that, by pid 1, *if* pid 1 is a real init: named `init`, or+  a subreaper itself.+- Otherwise it has no parent, and when it exits it reaps itself. Its+  memory is freed straight away.++`libctest orphans` leaves a running orphan and a zombie behind three+times, and the kernel test checks that the process count and the number+of memory frames come back to where they were.++### Tracing calls we don't have++`run --trace prog` printed a call it didn't know as `?(...)`, so you had+to guess which one was missing. Now it prints the number,+`syscall_436(...)`, which you can look up in Linux's+`arch/x86/entry/syscalls/syscall_64.tbl`.++### Testing it++`user/libctest.rs` has one group of checks per argument, and+`tests/libc_batch1.rs` runs each group twice. The second run must leave+the thread, process, memory-frame and file-lock counts exactly where they+were. The groups are `wait`, `signals`, `locks`, `files`, `misc` and+`orphans` (the Tests table above lists what each checks). You can run+them from the shell too, for example `libctest locks`.++### What stage 16 does and doesn't do++Does: everything above, with Linux's numbers and error codes.++Doesn't (yet):++- **Permissions are stored, not checked.** Everyone is uid 0, and+  `read`/`write` don't check the fd's access mode either (only `F_GETFL`,+  `ftruncate` and locks look at it). There is no `chown`.+- **`flock`** (73, whole-file locks), the `F_OFD_*` locks and `F_GETOWN`+  aren't here; nor are `utime`/`utimes`/`futimesat` (musl uses+  `utimensat` anyway).+- **FAT and exFAT times**: they have time fields, but our drivers don't+  write them, so `utimensat` is accepted and forgotten, and `stat` shows 0.+- **`sigtimedwait`'s timeout is rounded up to whole timer ticks** (about+  55 ms each), so a short timeout can take noticeably longer than asked.+- **`madvise`**: `MADV_DONTNEED` frees only private memory (shared and+  file mappings keep their pages), and `MADV_REMOVE`/`MADV_WIPEONFORK`+  are `EINVAL`.+- **The rest of Clarke's list** (static-PIE loading, a hook for running+  disk images) are separate stages.+ ## Roadmap  Done: all 12 chapters of the 2nd edition, plus preemptive kernel threads,@@ -3877,8 +4152,9 @@ 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), and musl's prerequisites (per-thread-FPU/SSE/AVX state, Linux C-string path calls and `struct stat`).+threads and futexes, ttys and ptys), musl's prerequisites (per-thread+FPU/SSE/AVX state, Linux C-string path calls and `struct stat`), and the+first batch of the calls musl and dash use at run time.  - [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)@@ -3915,6 +4191,7 @@ FPU/SSE/AVX state, Linux C-string path calls and `struct stat`). - [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) - [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) - [ ] 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) @@ -4135,6 +4412,23 @@ 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 16:**+  - *Tests that pin a version or a count.* `nbtest` checked that `uname`+    said "stage 15", the shell test looked for "Rustbelt 0.15.1", and+    `run-headless.sh` expected `dir bin | wc -l` to print 36. A new stage+    and one new program (`libctest`) broke all three. Each is now updated,+    but expect it again next stage.+  - *Things that quietly lied.* `F_GETFL` always said `O_RDWR`, every+    `stat` time was 0, ramfs had no modes, and `Inode::truncate` could+    only cut a file to 0. Nothing failed, so nobody noticed until a C+    program asked.+  - *Who is pid 1?* Linux gives orphans to pid 1, but our pid 1 is+    usually the shell, which never calls `wait(-1)`. Handing it orphans+    would have turned a memory leak into a zombie leak. See "Orphans" in+    stage 16.+  - *`sched_getaffinity` returns a size, not 0.* The raw call returns+    how many bytes it wrote; only the libc wrapper returns 0. musl's+    wrapper uses that size to zero the rest of the caller's buffer. - **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:
@@ -78,6 +78,7 @@ const PROGRAMS: &[&str] = &[     "forktest", "cowtest", "mmaptest", "sigtest", // stage 14: processes     "threads", "echoserver", "ptytest", "nbtest", // stage 15: libc plumbing     "fputest", "pathtest", // stage 15.1: musl prerequisites+    "libctest", // stage 16: libc batch 1 ];  /// 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.15.1 (stage 15.1: musl prerequisites) x86_64" # ver+        "Rustbelt 0.16.0 (stage 16: libc batch 1) 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.15.1"               # a no-libc gcc binary+        "rawabi: uname: Rustbelt 0.16.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` -> 36 (the programs),+    # Pipe results: `... | grep -i EXT | wc -l` -> 1, `dir bin | wc -l` -> 37 (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 36 3 4 20000; do+    for n in 1 37 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.15.1" "$CURL_OUT" 2>/dev/null; then+    if grep -qF "Hello from Rustbelt! Rustbelt 0.16.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.15.1 (stage 15.1" \+    for line in "Rustbelt getty: shell on this terminal" "Rustbelt 0.16.0 (stage 16" \                 "$ 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.15.1 (stage 15.1: musl prerequisites) x86_64" "tile: 4 windows on 1024x768, console 61x46" \+    set -- "Rustbelt 0.16.0 (stage 16: libc batch 1) 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" \
@@ -32,7 +32,9 @@ pub enum Errno {     EROFS = 30,        // read-only file system     EPIPE = 32,        // writing to a pipe nobody reads     ERANGE = 34,       // result doesn't fit (getcwd buffer too small)+    EDEADLK = 35,      // waiting for this lock would deadlock (fcntl F_SETLKW)     ENAMETOOLONG = 36, // path too long+    ENOLCK = 37,       // no locks available (the record-lock table is full)     ENOSYS = 38,       // no such system call     ENOTEMPTY = 39,    // directory not empty     ELOOP = 40,        // too many symlinks in a row (a loop?)@@ -64,12 +66,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; 50] = [+        const ALL: [Errno; 52] = [             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,+            ENOTCONN, ETIMEDOUT, ECONNREFUSED, ENXIO, EINPROGRESS, EXDEV, ENOPROTOOPT, EDEADLK, ENOLCK,         ];         ALL.into_iter().find(|&e| -(e as i64) == ret)     }
@@ -333,6 +333,8 @@ struct InodeItem {     size: u64,     mode: u32,     flags: u64,+    /// atime, mtime, ctime as [seconds, nanoseconds] (stage 16: for stat).+    times: [[i64; 2]; 3], }  /// One entry of a DIR_ITEM or DIR_INDEX item.@@ -746,7 +748,11 @@ impl Volume {         if d.len() < 160 {             return Err(Errno::EIO);         }-        Ok(InodeItem { size: get64(&d, INODE_SIZE), mode: get32(&d, INODE_MODE), flags: get64(&d, INODE_FLAGS) })+        // The four btrfs_timespecs (u64 seconds + u32 nanoseconds) at the+        // end: atime 112, ctime 124, mtime 136, otime 148.+        let ts = |at: usize| [get64(&d, at) as i64, get32(&d, at + 8) as i64];+        let times = [ts(112), ts(136), ts(124)];+        Ok(InodeItem { size: get64(&d, INODE_SIZE), mode: get32(&d, INODE_MODE), flags: get64(&d, INODE_FLAGS), times })     }      /// Where a directory entry points: an inode in the same tree, or the@@ -1097,6 +1103,10 @@ impl Inode for BtrfsInode {         self.vol.lock().inode(self.loc).map(|i| i.mode & 0o7777).unwrap_or(0)     } +    fn times(&self) -> Option<[[i64; 2]; 3]> {+        self.vol.lock().inode(self.loc).ok().map(|i| i.times)+    }+     fn readlink(&self) -> Result<String, Errno> {         if self.kind != InodeKind::Symlink {             return Err(Errno::EINVAL);@@ -1147,6 +1157,15 @@ impl Inode for BtrfsInode {     fn truncate(&self) -> Result<(), Errno> {         Err(Errno::EROFS)     }+    fn set_len(&self, _len: usize) -> Result<(), Errno> {+        Err(Errno::EROFS)+    }+    fn set_mode(&self, _mode: u32) -> Result<(), Errno> {+        Err(Errno::EROFS)+    }+    fn set_times(&self, _atime: Option<[i64; 2]>, _mtime: Option<[i64; 2]>) -> Result<(), Errno> {+        Err(Errno::EROFS)+    }     fn create(&self, _name: &str, _kind: InodeKind) -> Result<Arc<dyn Inode>, Errno> {         Err(Errno::EROFS)     }
@@ -1010,7 +1010,46 @@ impl Inode for Ext2Inode {      fn mode(&self) -> u32 {         let vol = self.vol.lock();-        vol.read_inode(self.ino).map(|i| (i.mode() & 0o7777) as u32).unwrap_or(0)+        // (chmod 000 is a real mode here: MODE_SET says so.)+        vol.read_inode(self.ino).map(|i| (i.mode() & 0o7777) as u32 | super::MODE_SET * (i.mode() & 0o7777 == 0) as u32).unwrap_or(0)+    }++    /// chmod: the low 12 bits of i_mode (the type bits above stay).+    fn set_mode(&self, mode: u32) -> Result<(), Errno> {+        let vol = self.vol.lock();+        if !vol.writable {+            return Err(Errno::EROFS);+        }+        let mut inode = vol.read_inode(self.ino)?;+        let new = (inode.mode() & !0o7777) | (mode & 0o7777) as u16;+        put16(&mut inode.raw, I_MODE, new);+        inode.set_times(&[I_CTIME], super::now_timespec()[0] as u32);+        vol.write_inode(&inode)+    }++    /// ext2 keeps whole seconds (32-bit: until 2106).+    fn times(&self) -> Option<[[i64; 2]; 3]> {+        let vol = self.vol.lock();+        let inode = vol.read_inode(self.ino).ok()?;+        let t = |at| [get32(&inode.raw, at) as i64, 0];+        Some([t(I_ATIME), t(I_MTIME), t(I_CTIME)])+    }++    fn set_times(&self, atime: Option<[i64; 2]>, mtime: Option<[i64; 2]>) -> Result<(), Errno> {+        let vol = self.vol.lock();+        if !vol.writable {+            return Err(Errno::EROFS);+        }+        let mut inode = vol.read_inode(self.ino)?;+        let secs = |t: [i64; 2]| t[0].clamp(0, u32::MAX as i64) as u32;+        if let Some(t) = atime {+            inode.set_times(&[I_ATIME], secs(t));+        }+        if let Some(t) = mtime {+            inode.set_times(&[I_MTIME], secs(t));+        }+        inode.set_times(&[I_CTIME], super::now_timespec()[0] as u32);+        vol.write_inode(&inode)     }      fn readlink(&self) -> Result<String, Errno> {
@@ -301,6 +301,24 @@ impl Inode for FatInode {         self.with_file(|file| file.truncate().map_err(errno)) // truncates at the position: 0     } +    /// truncate/ftruncate (stage 16): fatfs cuts a file at its current+    /// position, so seek there first. Growing goes through write_at, which+    /// fills the gap with zeros.+    fn set_len(&self, len: usize) -> Result<(), Errno> {+        if self.kind != InodeKind::File {+            return Err(Errno::EISDIR);+        }+        let size = self.size();+        if len > size {+            self.write_at(len - 1, &[0])?;+            return Ok(());+        }+        self.with_file(|file| {+            file.seek(SeekFrom::Start(len as u64)).map_err(errno)?;+            file.truncate().map_err(errno)+        })+    }+     fn lookup(&self, name: &str) -> Result<Arc<dyn Inode>, Errno> {         if self.kind != InodeKind::Directory {             return Err(Errno::ENOTDIR);
@@ -10,6 +10,7 @@ use super::{Inode, InodeKind}; use crate::errno::Errno; use alloc::{string::String, sync::Arc}; use spin::Mutex;+use core::sync::atomic::{AtomicBool, Ordering};  /// What a `File` or inode tells the kernel about itself. Our own small /// layout; the syscalls turn it into Linux's `struct stat` (below), which@@ -60,9 +61,9 @@ pub struct LinuxStat {     pub st_size: i64,    // bytes     pub st_blksize: i64, // good I/O size: 4096     pub st_blocks: i64,  // 512-byte blocks used-    pub st_atime: [i64; 2], // last access (seconds, nanoseconds): we don't keep times yet+    pub st_atime: [i64; 2], // last access (seconds, nanoseconds); 0 if the file system keeps no times     pub st_mtime: [i64; 2], // last change of the contents-    pub st_ctime: [i64; 2], // last change of the inode+    pub st_ctime: [i64; 2], // last change of the inode (chmod, utimensat, writes)     _unused: [i64; 3], } @@ -99,6 +100,16 @@ impl LinuxStat {             ..Default::default()         }     }++    /// Fill in the times (`Inode::times`, stage 16), if there are any.+    pub fn with_times(mut self, times: Option<[[i64; 2]; 3]>) -> LinuxStat {+        if let Some([a, m, c]) = times {+            self.st_atime = a;+            self.st_mtime = m;+            self.st_ctime = c;+        }+        self+    } }  /// A 64-bit FNV-1a hash: our stand-in for inode numbers (from the path).@@ -176,6 +187,23 @@ pub trait File: Send + Sync {     fn as_epoll(&self) -> Option<&crate::poll::Epoll> {         None     }+    /// fcntl(F_GETFL): the access mode it was opened with (O_RDONLY 0,+    /// O_WRONLY 1, O_RDWR 2) | O_APPEND. (O_NONBLOCK comes from+    /// `nonblocking`.) Files that don't know say O_RDWR.+    fn status_flags(&self) -> u64 {+        2+    }+    /// fcntl(F_SETFL, O_APPEND on/off). Only files opened by path have it.+    fn set_append(&self, _on: bool) {}+    /// The inode's times, for fstat (stage 16).+    fn times(&self) -> Option<[[i64; 2]; 3]> {+        None+    }+    /// The inode behind an fd opened by path, file *or* directory (fchmod,+    /// futimens; `inode` above is regular files only, for mmap).+    fn vfs_inode(&self) -> Option<Arc<dyn Inode>> {+        None+    }     /// The terminal behind the fd, if it is one (tty.rs).     fn as_tty(&self) -> Option<&crate::tty::TtyFile> {         None@@ -188,18 +216,50 @@ pub struct InodeFile {     inode: Arc<dyn Inode>,     offset: Mutex<usize>,     /// O_APPEND: every write goes to the current end of the file (`>>`).-    append: bool,+    /// fcntl(F_SETFL) can switch it, so it's an atomic.+    append: AtomicBool,+    /// O_NONBLOCK: files on a disk never block, so it changes nothing, but+    /// F_GETFL reports what F_SETFL (or open) set, as on Linux.+    nonblock: AtomicBool,+    /// O_RDONLY / O_WRONLY / O_RDWR as passed to open (stage 16). Reads+    /// and writes don't check it yet; F_GETFL, ftruncate and record locks do.+    accmode: u64,     /// The absolute path it was opened by (see `File::path`).     path: String, } +/// open() flag bits InodeFile looks at (Linux's values).+pub const O_ACCMODE: u64 = 3;+pub const O_RDONLY: u64 = 0;+pub const O_WRONLY: u64 = 1;+pub const O_RDWR: u64 = 2;+pub const O_APPEND: u64 = 0o2000;+pub const O_NONBLOCK: u64 = 0o4000;+ impl InodeFile {+    /// Opened read/write (the kernel's own callers, tests).     pub fn open(inode: Arc<dyn Inode>, path: String) -> Result<Self, Errno> {-        Ok(InodeFile { inode, offset: Mutex::new(0), append: false, path })+        Self::open_flags(inode, path, O_RDWR)     }      pub fn open_append(inode: Arc<dyn Inode>, path: String) -> Result<Self, Errno> {-        Ok(InodeFile { inode, offset: Mutex::new(0), append: true, path })+        Self::open_flags(inode, path, O_RDWR | O_APPEND)+    }++    /// With open()'s flags: the access mode and O_APPEND count.+    pub fn open_flags(inode: Arc<dyn Inode>, path: String, flags: u64) -> Result<Self, Errno> {+        let accmode = match flags & O_ACCMODE {+            3 => O_RDWR, // Linux's odd "3": treat as read/write+            m => m,+        };+        Ok(InodeFile {+            inode,+            offset: Mutex::new(0),+            append: AtomicBool::new(flags & O_APPEND != 0),+            nonblock: AtomicBool::new(flags & O_NONBLOCK != 0),+            accmode,+            path,+        })     } } @@ -213,7 +273,7 @@ impl File for InodeFile {      fn write(&self, buf: &[u8]) -> Result<usize, Errno> {         let mut offset = self.offset.lock();-        if self.append {+        if self.append.load(Ordering::Relaxed) {             *offset = self.inode.size();         }         let n = self.inode.write_at(*offset, buf)?;@@ -263,6 +323,30 @@ impl File for InodeFile {         (self.inode.kind() == InodeKind::File).then(|| self.inode.clone())     } +    fn status_flags(&self) -> u64 {+        self.accmode | if self.append.load(Ordering::Relaxed) { O_APPEND } else { 0 }+    }++    fn set_append(&self, on: bool) {+        self.append.store(on, Ordering::Relaxed);+    }++    fn set_nonblocking(&self, on: bool) {+        self.nonblock.store(on, Ordering::Relaxed);+    }++    fn nonblocking(&self) -> bool {+        self.nonblock.load(Ordering::Relaxed)+    }++    fn times(&self) -> Option<[[i64; 2]; 3]> {+        self.inode.times()+    }++    fn vfs_inode(&self) -> Option<Arc<dyn Inode>> {+        Some(self.inode.clone())+    }+     fn getdents(&self, buf: &mut [u8]) -> Result<usize, Errno> {         // Re-list the directory each call and skip what we've returned         // already (simple; a real fs would keep a cursor into the directory).
@@ -0,0 +1,194 @@+//! POSIX record locks: fcntl(F_GETLK / F_SETLK / F_SETLKW), stage 16.+//!+//! A program can lock a byte range of a file: a *read* (shared) lock, which+//! many processes may hold at once, or a *write* (exclusive) lock, which+//! nobody else may overlap. They're "advisory": read() and write() don't+//! look at them, only other fcntl calls do. Databases (SQLite) and mail+//! programs use them so two processes don't change one file at once.+//!+//! The rules, as in Linux:+//!   - locks belong to a *process* (not to an fd): locking a range you+//!     already hold replaces your old lock there (splitting it if needed);+//!     your own locks never conflict with each other;+//!   - closing *any* fd of the file drops all your locks on it, and so does+//!     exiting;+//!   - F_SETLK fails with EAGAIN on a conflict, F_SETLKW waits (a signal+//!     interrupts it), unless waiting would deadlock: EDEADLK.+//!+//! Files are told apart by a key: the path hash stat calls st_ino, or for+//! files without a path (pipes, sockets) the open file's address -- the+//! same number fstat reports as st_ino (`key_of`).+//! Ranges are [start, end) with end = u64::MAX for "to the end of the file+//! and beyond" (l_len = 0).++use crate::errno::Errno;+use crate::thread::WaitQueue;+use alloc::{collections::BTreeMap, vec::Vec};+use spin::Mutex;+use x86_64::instructions::interrupts::without_interrupts;++#[derive(Debug, Clone, Copy, PartialEq, Eq)]+pub struct Lock {+    pub pid: u64,+    pub start: u64,+    pub end: u64,+    pub write: bool,+}++impl Lock {+    fn overlaps(&self, start: u64, end: u64) -> bool {+        self.start < end && start < self.end+    }+}++/// More than this many locks in total: ENOLCK (keeps a runaway program+/// from eating the kernel heap).+const MAX_LOCKS: usize = 1024;++struct Table {+    /// file key -> its locks.+    files: BTreeMap<u64, Vec<Lock>>,+    /// pid -> what it waits for in F_SETLKW (for deadlock detection).+    waiting: BTreeMap<u64, (u64, Lock)>,+}++/// Locked with interrupts off (it's looked at in a wait condition).+static TABLE: Mutex<Table> = Mutex::new(Table { files: BTreeMap::new(), waiting: BTreeMap::new() });+/// F_SETLKW sleepers: woken whenever a lock goes away.+static UNLOCKED: WaitQueue = WaitQueue::new();++fn conflict_in(t: &Table, key: u64, want: &Lock) -> Option<Lock> {+    t.files.get(&key)?.iter().find(|l| l.pid != want.pid && l.overlaps(want.start, want.end) && (l.write || want.write)).copied()+}++/// The key of an open file (see the top).+pub fn key_of(file: &dyn crate::fs::file::File) -> u64 {+    match file.path() {+        Some(path) => crate::fs::file::ino_hash(path.as_bytes()),+        None => file as *const dyn crate::fs::file::File as *const u8 as u64,+    }+}++/// F_GETLK: the first lock that would stop `want`, if any.+pub fn conflict(key: u64, want: Lock) -> Option<Lock> {+    without_interrupts(|| conflict_in(&TABLE.lock(), key, &want))+}++/// Replace `pid`'s locks on [start, end) of file `key` with `new` (None =+/// unlock that range). Doesn't check for conflicts.+fn replace(t: &mut Table, key: u64, pid: u64, start: u64, end: u64, new: Option<bool>) -> Result<(), Errno> {+    let total: usize = t.files.values().map(|v| v.len()).sum();+    let locks = t.files.entry(key).or_default();+    let mut kept = Vec::with_capacity(locks.len() + 2);+    for l in locks.drain(..) {+        if l.pid != pid || !l.overlaps(start, end) {+            kept.push(l);+            continue;+        }+        // Keep the parts of our old lock outside the new range.+        if l.start < start {+            kept.push(Lock { end: start, ..l });+        }+        if l.end > end {+            kept.push(Lock { start: end, ..l });+        }+    }+    if let Some(write) = new {+        if total + 2 > MAX_LOCKS {+            *locks = kept;+            return Err(Errno::ENOLCK);+        }+        kept.push(Lock { pid, start, end, write });+    }+    *locks = kept;+    if locks.is_empty() {+        t.files.remove(&key);+    }+    Ok(())+}++/// F_SETLK: take (Some(write)) or drop (None) a lock. EAGAIN on a conflict.+pub fn set(key: u64, pid: u64, start: u64, end: u64, new: Option<bool>) -> Result<(), Errno> {+    without_interrupts(|| {+        let mut t = TABLE.lock();+        if let Some(write) = new {+            if conflict_in(&t, key, &Lock { pid, start, end, write }).is_some() {+                return Err(Errno::EAGAIN);+            }+        }+        replace(&mut t, key, pid, start, end, new)+    })?;+    UNLOCKED.wake_all(); // (something may have been unlocked)+    Ok(())+}++/// Would `pid` waiting for `want` close a circle of waiters (A waits for+/// B who waits for ... A)?+fn would_deadlock(t: &Table, key: u64, want: &Lock) -> bool {+    let mut next = conflict_in(t, key, want);+    for _ in 0..64 {+        let Some(holder) = next else { return false };+        if holder.pid == want.pid {+            return true;+        }+        next = t.waiting.get(&holder.pid).and_then(|(k, w)| conflict_in(t, *k, w));+    }+    false+}++/// F_SETLKW: like `set`, but wait while somebody else holds a conflicting+/// lock. A signal ends the wait with ERESTARTSYS (EINTR for the program).+pub fn set_wait(key: u64, pid: u64, start: u64, end: u64, write: bool) -> Result<(), Errno> {+    let want = Lock { pid, start, end, write };+    let result = UNLOCKED.wait_until_intr(|| {+        let mut t = TABLE.lock();+        if conflict_in(&t, key, &want).is_none() {+            t.waiting.remove(&pid);+            return Some(replace(&mut t, key, pid, start, end, Some(write)));+        }+        if would_deadlock(&t, key, &want) {+            t.waiting.remove(&pid);+            return Some(Err(Errno::EDEADLK));+        }+        t.waiting.insert(pid, (key, want));+        None+    });+    without_interrupts(|| TABLE.lock().waiting.remove(&pid));+    result?+}++/// `pid` closed an fd of file `key`: all its locks on that file go.+pub fn file_closed(key: u64, pid: u64) {+    let removed = without_interrupts(|| {+        let mut t = TABLE.lock();+        let Some(locks) = t.files.get_mut(&key) else { return false };+        let before = locks.len();+        locks.retain(|l| l.pid != pid);+        let removed = locks.len() != before;+        if locks.is_empty() {+            t.files.remove(&key);+        }+        removed+    });+    if removed {+        UNLOCKED.wake_all();+    }+}++/// Process `pid` exited: drop all its locks.+pub fn process_exited(pid: u64) {+    without_interrupts(|| {+        let mut t = TABLE.lock();+        t.waiting.remove(&pid);+        for locks in t.files.values_mut() {+            locks.retain(|l| l.pid != pid);+        }+        t.files.retain(|_, v| !v.is_empty());+    });+    UNLOCKED.wake_all();+}++/// How many locks exist (tests: nothing left behind).+pub fn count() -> usize {+    without_interrupts(|| TABLE.lock().files.values().map(|v| v.len()).sum())+}
@@ -30,6 +30,7 @@  pub mod btrfs; pub mod exfat;+pub mod lock; pub mod lzo; pub mod ext2; pub mod fat;@@ -109,8 +110,73 @@ pub trait Inode: Send + Sync {     fn open_device(&self) -> Result<Option<Arc<dyn file::File>>, Errno> {         Ok(None)     }++    // --- Stage 16: changing a file's size, permissions and times ---------+    // (truncate/ftruncate, chmod & co., utimensat.)++    /// Make the file exactly `len` bytes long: cut off the end, or grow it+    /// with zero bytes. This default works for any file system that can+    /// truncate to 0 and write: growing writes one zero byte at the new+    /// end (the gap reads as zeros), shrinking keeps the first `len`+    /// bytes, truncates to 0 and writes them back. File systems that can+    /// do better (ramfs, FAT) override it.+    fn set_len(&self, len: usize) -> Result<(), Errno> {+        if self.kind() != InodeKind::File {+            return Err(Errno::EISDIR);+        }+        let size = self.size();+        if len == size {+            return Ok(());+        }+        if len > size {+            self.write_at(len - 1, &[0])?;+            return Ok(());+        }+        let mut keep = alloc::vec![0u8; len];+        let mut done = 0;+        while done < len {+            match self.read_at(done, &mut keep[done..])? {+                0 => break,+                n => done += n,+            }+        }+        self.truncate()?;+        if done > 0 {+            self.write_at(0, &keep[..done])?;+        }+        Ok(())+    }+    /// chmod: set the permission bits (0o7777 part of `mode`). EPERM if the+    /// file system can't store them (FAT, exFAT, /dev), EROFS if it's+    /// read-only (btrfs).+    fn set_mode(&self, _mode: u32) -> Result<(), Errno> {+        Err(Errno::EPERM)+    }+    /// The access, modification and status-change times, each as+    /// [seconds, nanoseconds] since 1970 -- or None if the file system+    /// doesn't keep them (stat then shows 0).+    fn times(&self) -> Option<[[i64; 2]; 3]> {+        None+    }+    /// utimensat: set the access and/or modification time (None = leave+    /// it). The default accepts and forgets them: FAT and exFAT have time+    /// fields, but our drivers don't write them yet. That's what `touch`+    /// needs to not fail.+    fn set_times(&self, _atime: Option<[i64; 2]>, _mtime: Option<[i64; 2]>) -> Result<(), Errno> {+        Ok(())+    } } +/// The wall-clock time as [seconds, nanoseconds] since 1970 (file times).+pub fn now_timespec() -> [i64; 2] {+    let us = crate::time::realtime_us();+    [(us / 1_000_000) as i64, (us % 1_000_000 * 1000) as i64]+}++/// `Inode::mode` of a file whose mode was set to exactly 0 with chmod+/// (plain 0 means "this file system has no permissions").+pub const MODE_SET: u32 = 1 << 31;+ /// A mounted file system. pub trait FileSystem: Send + Sync {     /// Type name, like the `-t` argument of `mount` ("ramfs", "fat32", ...).
@@ -53,6 +53,9 @@ pub fn new() -> (PipeReader, PipeWriter) { }  impl File for PipeReader {+    fn status_flags(&self) -> u64 {+        0 // O_RDONLY: the read end+    }     fn read(&self, buf: &mut [u8]) -> Result<usize, Errno> {         if buf.is_empty() {             return Ok(0);@@ -109,6 +112,9 @@ impl File for PipeReader { }  impl File for PipeWriter {+    fn status_flags(&self) -> u64 {+        1 // O_WRONLY: the write end+    }     fn read(&self, _buf: &mut [u8]) -> Result<usize, Errno> {         Err(Errno::EBADF)     }
@@ -1,8 +1,10 @@ //! ramfs: a file system that lives entirely on the heap (like Linux's ramfs / //! tmpfs without the size limit). The simplest possible `FileSystem`: a tree //! of nodes; files are a `Vec<u8>`, directories a sorted map name -> node.+//! Since stage 16 every node also remembers its permission bits (chmod)+//! and its three times (utimensat, and writes update the modification time). -use super::{DirEntry, FileSystem, Inode, InodeKind};+use super::{now_timespec, DirEntry, FileSystem, Inode, InodeKind, MODE_SET}; use crate::errno::Errno; use alloc::{collections::BTreeMap, string::String, sync::Arc, vec::Vec}; use spin::Mutex;@@ -13,7 +15,7 @@ pub struct RamFs {  impl RamFs {     pub fn new() -> Arc<Self> {-        Arc::new(RamFs { root: Arc::new(RamInode::Dir(Mutex::new(BTreeMap::new()))) })+        Arc::new(RamFs { root: Arc::new(RamInode::new(Node::Dir(Mutex::new(BTreeMap::new())))) })     } } @@ -26,28 +28,57 @@ impl FileSystem for RamFs {     } } -pub enum RamInode {+pub enum Node {     File(Mutex<Vec<u8>>),     Dir(Mutex<BTreeMap<String, Arc<RamInode>>>), } +/// What `stat` shows besides the contents (stage 16).+struct Meta {+    /// 0 = never chmod'ed (stat shows the default 0o644 / 0o755);+    /// otherwise the bits | MODE_SET.+    mode: u32,+    /// atime, mtime, ctime as [seconds, nanoseconds].+    times: [[i64; 2]; 3],+}++pub struct RamInode {+    node: Node,+    meta: Mutex<Meta>,+}++impl RamInode {+    fn new(node: Node) -> RamInode {+        let now = now_timespec();+        RamInode { node, meta: Mutex::new(Meta { mode: 0, times: [now; 3] }) }+    }++    /// The contents changed: new modification and status-change time.+    fn touch(&self) {+        let now = now_timespec();+        let mut meta = self.meta.lock();+        meta.times[1] = now;+        meta.times[2] = now;+    }+}+ impl Inode for RamInode {     fn kind(&self) -> InodeKind {-        match self {-            RamInode::File(_) => InodeKind::File,-            RamInode::Dir(_) => InodeKind::Directory,+        match &self.node {+            Node::File(_) => InodeKind::File,+            Node::Dir(_) => InodeKind::Directory,         }     }      fn size(&self) -> usize {-        match self {-            RamInode::File(data) => data.lock().len(),-            RamInode::Dir(entries) => entries.lock().len(),+        match &self.node {+            Node::File(data) => data.lock().len(),+            Node::Dir(entries) => entries.lock().len(),         }     }      fn read_at(&self, offset: usize, buf: &mut [u8]) -> Result<usize, Errno> {-        let RamInode::File(data) = self else { return Err(Errno::EISDIR) };+        let Node::File(data) = &self.node else { return Err(Errno::EISDIR) };         let data = data.lock();         if offset >= data.len() {             return Ok(0); // EOF@@ -58,24 +89,26 @@ impl Inode for RamInode {     }      fn write_at(&self, offset: usize, buf: &[u8]) -> Result<usize, Errno> {-        let RamInode::File(data) = self else { return Err(Errno::EISDIR) };+        let Node::File(data) = &self.node else { return Err(Errno::EISDIR) };         let mut data = data.lock();         let end = offset.checked_add(buf.len()).ok_or(Errno::EINVAL)?;         if data.len() < end {             data.resize(end, 0); // writing past the end grows the file (gap = zeros)         }         data[offset..end].copy_from_slice(buf);+        drop(data);+        self.touch();         Ok(buf.len())     }      fn lookup(&self, name: &str) -> Result<Arc<dyn Inode>, Errno> {-        let RamInode::Dir(entries) = self else { return Err(Errno::ENOTDIR) };+        let Node::Dir(entries) = &self.node else { return Err(Errno::ENOTDIR) };         let node = entries.lock().get(name).cloned().ok_or(Errno::ENOENT)?;         Ok(node)     }      fn create(&self, name: &str, kind: InodeKind) -> Result<Arc<dyn Inode>, Errno> {-        let RamInode::Dir(entries) = self else { return Err(Errno::ENOTDIR) };+        let Node::Dir(entries) = &self.node else { return Err(Errno::ENOTDIR) };         if name.is_empty() || name.contains('/') || name.len() > 255 {             return Err(Errno::EINVAL);         }@@ -83,32 +116,67 @@ impl Inode for RamInode {         if entries.contains_key(name) {             return Err(Errno::EEXIST);         }-        let node = Arc::new(match kind {-            InodeKind::File => RamInode::File(Mutex::new(Vec::new())),-            InodeKind::Directory => RamInode::Dir(Mutex::new(BTreeMap::new())),+        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::Symlink | InodeKind::Device => return Err(Errno::EOPNOTSUPP), // ramfs has neither-        });+        }));         entries.insert(String::from(name), node.clone());         Ok(node)     }      fn list(&self) -> Result<Vec<DirEntry>, Errno> {-        let RamInode::Dir(entries) = self else { return Err(Errno::ENOTDIR) };+        let Node::Dir(entries) = &self.node else { return Err(Errno::ENOTDIR) };         let entries = entries.lock();         Ok(entries.iter().map(|(name, node)| DirEntry { name: name.clone(), kind: node.kind() }).collect())     }      fn truncate(&self) -> Result<(), Errno> {-        let RamInode::File(data) = self else { return Err(Errno::EISDIR) };+        let Node::File(data) = &self.node else { return Err(Errno::EISDIR) };         data.lock().clear();+        self.touch();+        Ok(())+    }++    fn set_len(&self, len: usize) -> Result<(), Errno> {+        let Node::File(data) = &self.node else { return Err(Errno::EISDIR) };+        data.lock().resize(len, 0); // shorter: cut; longer: zeros+        self.touch();+        Ok(())+    }++    fn mode(&self) -> u32 {+        self.meta.lock().mode+    }++    fn set_mode(&self, mode: u32) -> Result<(), Errno> {+        let mut meta = self.meta.lock();+        meta.mode = (mode & 0o7777) | MODE_SET;+        meta.times[2] = now_timespec();+        Ok(())+    }++    fn times(&self) -> Option<[[i64; 2]; 3]> {+        Some(self.meta.lock().times)+    }++    fn set_times(&self, atime: Option<[i64; 2]>, mtime: Option<[i64; 2]>) -> Result<(), Errno> {+        let mut meta = self.meta.lock();+        if let Some(t) = atime {+            meta.times[0] = t;+        }+        if let Some(t) = mtime {+            meta.times[1] = t;+        }+        meta.times[2] = now_timespec();         Ok(())     }      fn remove(&self, name: &str) -> Result<(), Errno> {-        let RamInode::Dir(entries) = self else { return Err(Errno::ENOTDIR) };+        let Node::Dir(entries) = &self.node else { return Err(Errno::ENOTDIR) };         let mut entries = entries.lock();         let node = entries.get(name).ok_or(Errno::ENOENT)?;-        if let RamInode::Dir(children) = &**node {+        if let Node::Dir(children) = &node.node {             if !children.lock().is_empty() {                 return Err(Errno::ENOTEMPTY);             }
@@ -288,6 +288,46 @@ impl Mm {         Ok(())     } +    /// madvise(MADV_DONTNEED / MADV_FREE), stage 16: "I don't need these+    /// pages' contents any more". The frames of *private* regions in+    /// `start..end` go back to the free pool; touching the page again gives+    /// a fresh zero page (or, for a private file mapping, the file's bytes+    /// again) -- what Linux does, and what malloc implementations rely on+    /// to give memory back. Shared regions keep their pages (their contents+    /// are what the other processes see). ENOMEM if part of the range isn't+    /// mapped (the mapped parts are still done, like Linux).+    pub fn discard(&mut self, start: u64, end: u64) -> Result<(), Errno> {+        let ranges: Vec<(u64, u64)> = self+            .vmas+            .iter()+            .filter(|v| v.end > start && v.start < end && !v.shared)+            .map(|v| (v.start.max(start), v.end.min(end)))+            .collect();+        let mut freed = Vec::new();+        for (from, to) in ranges {+            for addr in (from..to).step_by(PAGE_SIZE as usize) {+                if let Some(frame) = memory::unmap_user_page(self.l4, addr) {+                    memory::flush_user_page(addr);+                    freed.push(frame);+                }+            }+        }+        if !freed.is_empty() {+            self.shootdown();+        }+        for frame in freed {+            memory::release_frame(frame);+        }+        let mut covered = start;+        for v in self.vmas.iter().filter(|v| v.end > start && v.start < end) {+            if v.start > covered {+                return Err(Errno::ENOMEM);+            }+            covered = v.end;+        }+        if covered < end { Err(Errno::ENOMEM) } else { Ok(()) }+    }+     /// A free range of `len` bytes for mmap: the highest gap below MMAP_TOP     /// (and above the heap).     pub fn find_free(&self, len: u64) -> Option<u64> {
@@ -28,7 +28,9 @@ //! child from the process table ("reaps" it -- until then it's a *zombie*). //! If the parent exits first, the children become orphans (parent = //! ORPHAN) and remove themselves when they exit; Unix hands them to-//! `init` instead.+//! `init` instead. Since stage 16 we do too when there's someone to hand+//! them to: an ancestor that called prctl(PR_SET_CHILD_SUBREAPER), or a+//! pid 1 named "init" (see `adopter`). //! //! **Process groups** (job control): every process is in a group, named by //! the pid of its first member. A shell puts each pipeline in its own group@@ -116,11 +118,13 @@ pub struct FdTable {     files: Vec<Option<Arc<dyn File>>>,     /// Bit n: close fd n when the process execs (FD_CLOEXEC / O_CLOEXEC).     cloexec: u64,+    /// RLIMIT_NOFILE's soft limit (stage 16): fds are < this. At most MAX_FDS.+    limit: usize, }  impl FdTable {     fn new(stdio: [Arc<dyn File>; 3]) -> FdTable {-        FdTable { files: stdio.into_iter().map(Some).collect(), cloexec: 0 }+        FdTable { files: stdio.into_iter().map(Some).collect(), cloexec: 0, limit: MAX_FDS }     }      fn get(&self, fd: u64) -> Result<Arc<dyn File>, Errno> {@@ -129,7 +133,7 @@ impl FdTable {      /// Put `file` into the lowest free slot >= `min` (Unix's rule).     fn add(&mut self, file: Arc<dyn File>, min: usize, cloexec: bool) -> Result<u64, Errno> {-        let fd = match (min..MAX_FDS).find(|&i| self.files.get(i).is_none_or(|f| f.is_none())) {+        let fd = match (min..self.limit).find(|&i| self.files.get(i).is_none_or(|f| f.is_none())) {             Some(fd) => fd,             None => return Err(Errno::EMFILE),         };@@ -189,6 +193,21 @@ pub struct Task {     /// scheduler touches it, with interrupts off: saved when the thread is     /// switched out, loaded when it's switched in.     pub fpu: Mutex<crate::fpu::FpuState>,+    /// Its name, prctl(PR_SET_NAME) (stage 16): up to 15 bytes + NUL.+    /// Starts as the program's name; threads inherit their creator's.+    pub comm: Mutex<[u8; 16]>,+    /// set_robust_list (stage 16): user address of its `struct+    /// robust_list_head` (0 = none). When the thread dies, the kernel walks+    /// that list and marks every futex lock it still held "owner died".+    pub robust_list: AtomicU64,+}++/// A thread name as prctl(PR_GET_NAME) returns it: at most 15 bytes, then NULs.+pub fn comm_from(name: &str) -> [u8; 16] {+    let mut comm = [0u8; 16];+    let n = name.len().min(15);+    comm[..n].copy_from_slice(&name.as_bytes()[..n]);+    comm }  impl Task {@@ -251,6 +270,70 @@ pub struct Process {     pub signals: Mutex<SignalState>,     /// vfork: the parent waits until this child execs or exits.     vfork_done: AtomicBool,+    /// umask (stage 16): permission bits taken away from new files'+    /// modes. Inherited by children, kept across exec.+    pub umask: AtomicU32,+    /// getrlimit/setrlimit/prlimit64 (stage 16): [soft, hard] per resource.+    pub rlimits: Mutex<[[u64; 2]; RLIM_NLIMITS]>,+    /// prctl(PR_SET_PDEATHSIG): signal to get when the parent exits (0 = none).+    pub pdeathsig: AtomicU32,+    /// prctl(PR_SET_DUMPABLE) (remembered only: we never write core files).+    pub dumpable: AtomicU32,+    /// prctl(PR_SET_NO_NEW_PRIVS): once set, never cleared (we have no+    /// setuid programs, so it changes nothing yet).+    pub no_new_privs: AtomicBool,+    /// prctl(PR_SET_CHILD_SUBREAPER) (stage 16): this process adopts its+    /// descendants' orphans and promises to reap them (see `adopter`).+    /// Not inherited.+    pub child_subreaper: AtomicBool,+}++// --- Resource limits (stage 16) ------------------------------------------+//+// Linux's RLIMIT_* numbers. Each limit has a soft value (what's enforced)+// and a hard one (the ceiling the soft one may be raised to). We enforce+// RLIMIT_NOFILE (the fd table); the others report what the kernel really+// does (STACK = the fixed stack size, NPROC = the process table) or are+// remembered without being enforced.+pub const RLIMIT_CPU: usize = 0;+pub const RLIMIT_FSIZE: usize = 1;+pub const RLIMIT_DATA: usize = 2;+pub const RLIMIT_STACK: usize = 3;+pub const RLIMIT_CORE: usize = 4;+pub const RLIMIT_NPROC: usize = 6;+pub const RLIMIT_NOFILE: usize = 7;+pub const RLIMIT_MEMLOCK: usize = 8;+pub const RLIMIT_SIGPENDING: usize = 11;+pub const RLIMIT_MSGQUEUE: usize = 12;+pub const RLIMIT_NLIMITS: usize = 16;+pub const RLIM_NLIMITS: usize = RLIMIT_NLIMITS;+pub const RLIM_INFINITY: u64 = u64::MAX;++/// What a process the kernel starts gets (Linux's usual defaults, with our+/// real numbers where the kernel has a fixed limit).+pub fn default_rlimits() -> [[u64; 2]; RLIM_NLIMITS] {+    let mut r = [[RLIM_INFINITY; 2]; RLIM_NLIMITS];+    r[RLIMIT_STACK] = [USER_STACK_SIZE, USER_STACK_SIZE];+    r[RLIMIT_CORE] = [0, RLIM_INFINITY]; // no core files+    r[RLIMIT_NPROC] = [MAX_PROCESSES as u64; 2];+    r[RLIMIT_NOFILE] = [MAX_FDS as u64; 2];+    r[RLIMIT_MEMLOCK] = [8 << 20; 2];+    r[RLIMIT_SIGPENDING] = [1024; 2];+    r[RLIMIT_MSGQUEUE] = [819_200; 2];+    r[13] = [0, 0]; // RLIMIT_NICE+    r[14] = [0, 0]; // RLIMIT_RTPRIO+    r+}++/// The most a hard limit can be raised to (EPERM above): where the kernel+/// has a fixed size, that.+fn rlimit_ceiling(resource: usize) -> u64 {+    match resource {+        RLIMIT_NOFILE => MAX_FDS as u64,+        RLIMIT_STACK => USER_STACK_SIZE,+        RLIMIT_NPROC => MAX_PROCESSES as u64,+        _ => RLIM_INFINITY,+    } }  static NEXT_PID: AtomicU64 = AtomicU64::new(1);@@ -350,22 +433,30 @@ pub fn spawn_image(path: &str, elf_image: &[u8], spawn: SpawnArgs) -> Result<Arc         child_wait: WaitQueue::new(),         signals: Mutex::new(SignalState::new()),         vfork_done: AtomicBool::new(false),+        umask: AtomicU32::new(0o022),+        rlimits: Mutex::new(default_rlimits()),+        pdeathsig: AtomicU32::new(0),+        dumpable: AtomicU32::new(1),+        no_new_privs: AtomicBool::new(false),+        child_subreaper: AtomicBool::new(false),     };     serial_println!("[process] started pid {} ({}), entry {:#x}", pid, name, start.rip);-    Ok(start_process(process, start, 0, 0, crate::fpu::initial()))+    Ok(start_process(process, start, 0, 0, crate::fpu::initial(), signal::AltStack::DISABLED, comm_from(name))) }  /// Put the process in the table and start its first thread (tid = pid).-fn start_process(process: Process, start: TrapFrame, fs_base: u64, blocked: u64, fpu: FpuState) -> Arc<Process> {+fn start_process(process: Process, start: TrapFrame, fs_base: u64, blocked: u64, fpu: FpuState, altstack: signal::AltStack, comm: [u8; 16]) -> Arc<Process> {     let process = Arc::new(process);     without_interrupts(|| PROCESSES.lock().insert(process.pid, process.clone()));-    start_task(&process, process.pid, start, fs_base, blocked, fpu);+    start_task(&process, process.pid, start, fs_base, blocked, fpu, altstack, comm);     process }  /// Start a thread `tid` of `process` that enters user mode with `start`. /// `fpu`: its FPU/SSE registers (a copy of the creator's, or clean).-fn start_task(process: &Arc<Process>, tid: u64, start: TrapFrame, fs_base: u64, blocked: u64, fpu: FpuState) -> Arc<Task> {+/// `altstack`: its sigaltstack (fork keeps the caller's); `comm`: its name.+#[allow(clippy::too_many_arguments)]+fn start_task(process: &Arc<Process>, tid: u64, start: TrapFrame, fs_base: u64, blocked: u64, fpu: FpuState, altstack: signal::AltStack, comm: [u8; 16]) -> Arc<Task> {     let task = Arc::new(Task {         tid: AtomicU64::new(tid),         process: process.clone(),@@ -374,8 +465,10 @@ fn start_task(process: &Arc<Process>, tid: u64, start: TrapFrame, fs_base: u64,          clear_child_tid: AtomicU64::new(0),         start: Mutex::new(Some(start)),         killed: AtomicBool::new(false),-        sig: Mutex::new(ThreadSig::new(blocked)),+        sig: Mutex::new(ThreadSig::with_altstack(blocked, altstack)),         fpu: Mutex::new(fpu),+        comm: Mutex::new(comm),+        robust_list: AtomicU64::new(0),     });     without_interrupts(|| process.tasks.lock().push(task.clone()));     let l4 = process.mm.lock().l4;@@ -522,9 +615,15 @@ impl Process {         }         let replaced = {             let mut fds = self.fds.lock();+            if new as usize >= fds.limit {+                return Err(Errno::EBADF);+            }             let file = fds.get(old)?;             fds.set(new as usize, file, cloexec)         };+        if let Some(f) = &replaced {+            self.drop_locks(&**f);+        }         drop(replaced); // closing the old file may block: after unlocking         Ok(new)     }@@ -550,10 +649,50 @@ impl Process {                 _ => return Err(Errno::EBADF),             }         };+        if let Some(f) = &file {+            self.drop_locks(&**f);+        }         drop(file); // dropping the last Arc closes the file (after unlocking)         Ok(())     } +    /// POSIX: closing any fd of a file drops my record locks on it (fs/lock.rs).+    fn drop_locks(&self, file: &dyn File) {+        fs::lock::file_closed(fs::lock::key_of(file), self.pid);+    }++    /// The fd limit (RLIMIT_NOFILE's soft value).+    pub fn fd_limit(&self) -> usize {+        self.fds.lock().limit+    }++    /// getrlimit / prlimit64: [soft, hard] of `resource`. EINVAL if unknown.+    pub fn rlimit(&self, resource: u64) -> Result<[u64; 2], Errno> {+        let r = usize::try_from(resource).ok().filter(|&r| r < RLIM_NLIMITS).ok_or(Errno::EINVAL)?;+        Ok(self.rlimits.lock()[r])+    }++    /// setrlimit / prlimit64. The soft limit can't be above the hard one+    /// (EINVAL); a hard limit above what the kernel can do is EPERM. (We're+    /// root, so raising a hard limit is otherwise fine, like Linux's+    /// CAP_SYS_RESOURCE.)+    pub fn set_rlimit(&self, resource: u64, new: [u64; 2]) -> Result<(), Errno> {+        let r = usize::try_from(resource).ok().filter(|&r| r < RLIM_NLIMITS).ok_or(Errno::EINVAL)?;+        if new[0] > new[1] {+            return Err(Errno::EINVAL);+        }+        if new[1] > rlimit_ceiling(r) {+            return Err(Errno::EPERM);+        }+        self.rlimits.lock()[r] = new;+        if r == RLIMIT_NOFILE {+            // Fds already open above the new limit stay open (like Linux);+            // only new ones must be below it.+            self.fds.lock().limit = new[0] as usize;+        }+        Ok(())+    }+     pub fn status(&self) -> Option<ExitStatus> {         without_interrupts(|| *self.status.lock())     }@@ -568,6 +707,11 @@ impl Process {         self.name.lock().clone()     } +    /// prctl(PR_SET_NAME) on the main thread renames the process too.+    pub fn set_name(&self, name: &str) {+        *self.name.lock() = String::from(name);+    }+     pub fn parent(&self) -> u64 {         self.parent.load(Ordering::Relaxed)     }@@ -658,6 +802,8 @@ pub fn exit_current(status: ExitStatus) -> ! { pub fn exit_thread(status: ExitStatus) -> ! {     if let Some(task) = thread::current_task() {         let p = task.process.clone();+        // Robust futexes (stage 16): locks it died holding get "owner died".+        exit_robust_list(&p, &task);         // pthread_join sleeps on this word with a futex until it's 0.         let ctid = task.clear_child_tid.swap(0, Ordering::Relaxed);         if ctid != 0 && ctid % 4 == 0 && crate::mm::user_access_ok(ctid, 4, true) {@@ -682,12 +828,101 @@ pub fn exit_thread(status: ExitStatus) -> ! {     thread::exit_current() } +// --- Robust futex lists (set_robust_list, stage 16) ------------------------+//+// A "robust" mutex survives its owner dying. The lock word holds the+// owner's thread id; each thread keeps a linked list of the robust locks it+// holds, in its own memory, and tells the kernel where the list starts:+//+//   struct robust_list_head {+//       struct robust_list *next;   // first entry (the head itself = empty)+//       long futex_offset;          // entry address + this = the lock word+//       struct robust_list *list_op_pending; // lock being taken/released now+//   };+//+// When the thread exits, the kernel walks the list. Every lock word that+// still has the thread's id gets FUTEX_OWNER_DIED set (and the id cleared),+// and if somebody waits (FUTEX_WAITERS) one waiter is woken -- so the next+// pthread_mutex_lock returns EOWNERDEAD instead of hanging forever.++pub const ROBUST_LIST_HEAD_SIZE: u64 = 24;+const FUTEX_WAITERS: u32 = 0x8000_0000;+const FUTEX_OWNER_DIED: u32 = 0x4000_0000;+const FUTEX_TID_MASK: u32 = 0x3fff_ffff;+/// Linux's limit on list entries (a loop in a corrupted list can't hang us).+const ROBUST_LIST_LIMIT: usize = 2048;++fn read_user_u64(addr: u64) -> Option<u64> {+    let b = crate::syscall::user_slice(addr, 8).ok()?;+    Some(u64::from_le_bytes(b.try_into().ok()?))+}++/// Mark one lock word "owner died" if `tid` holds it.+fn handle_futex_death(p: &Process, uaddr: u64, tid: u32) {+    if uaddr % 4 != 0 || !crate::mm::user_access_ok(uaddr, 4, true) {+        return;+    }+    let word = unsafe { &*(uaddr as *const core::sync::atomic::AtomicU32) };+    let mut val = word.load(Ordering::SeqCst);+    loop {+        if val & FUTEX_TID_MASK != tid {+            return; // not (or no longer) ours+        }+        let new = (val & FUTEX_WAITERS) | FUTEX_OWNER_DIED;+        match word.compare_exchange(val, new, Ordering::SeqCst, Ordering::SeqCst) {+            Ok(_) => break,+            Err(now) => val = now,+        }+    }+    if val & FUTEX_WAITERS != 0 {+        crate::futex::wake(p, uaddr, 1);+    }+}++/// Walk the exiting thread's robust list (see above).+fn exit_robust_list(p: &Process, task: &Task) {+    let head = task.robust_list.swap(0, Ordering::Relaxed);+    if head == 0 {+        return;+    }+    let tid = task.tid() as u32;+    let (Some(mut entry), Some(offset), Some(pending)) = (read_user_u64(head), read_user_u64(head + 8), read_user_u64(head + 16)) else {+        return;+    };+    let offset = offset as i64;+    for _ in 0..ROBUST_LIST_LIMIT {+        if entry == head {+            break;+        }+        // (Bit 0 of an entry pointer is a PI-futex flag: ignore it.)+        let next = read_user_u64(entry & !1);+        if entry != pending {+            handle_futex_death(p, (entry & !1).wrapping_add(offset as u64), tid);+        }+        match next {+            Some(n) => entry = n,+            None => break,+        }+    }+    if pending != 0 {+        handle_futex_death(p, (pending & !1).wrapping_add(offset as u64), tid);+    }+}+ /// The last thread of `process` is exiting: close everything, become a /// zombie, tell the parent. fn process_exit(process: Arc<Process>, status: ExitStatus) {     // Close all files (a pipe's reader sees EOF once its writers are gone).     let files = core::mem::take(&mut process.fds.lock().files);     drop(files);+    fs::lock::process_exited(process.pid); // its fcntl record locks+    // prctl(PR_SET_PDEATHSIG): children that asked to hear of our death.+    for child in all().into_iter().filter(|c| c.parent() == process.pid && c.status().is_none()) {+        let sig = child.pdeathsig.load(Ordering::Relaxed);+        if sig != 0 {+            signal::send(&child, sig, SigInfo::from_pid(sig, process.pid));+        }+    }     crate::tty::process_exited(&process); // (raw mode, controlling terminal)     let name = process.name();     match status {@@ -715,11 +950,13 @@ fn process_exit(process: Arc<Process>, status: ExitStatus) {         *process.status.lock() = Some(status);         let mut removed = Vec::new();         let children: Vec<Arc<Process>> = table.values().filter(|c| c.parent() == process.pid).cloned().collect();+        let adopter = adopter(&table, &process);         for child in children {             if child.status().is_some() {                 removed.extend(table.remove(&child.pid)); // exited: reap it             } else {-                child.parent.store(ORPHAN, Ordering::Relaxed); // still running+                // Still running: a reaper adopts it, or it reaps itself.+                child.parent.store(adopter.unwrap_or(ORPHAN), Ordering::Relaxed);             }         }         // Nobody will wait for us if we're an orphan, or if our parent@@ -750,6 +987,26 @@ fn process_exit(process: Arc<Process>, status: ExitStatus) {     drop(process); } +/// Who adopts the running children of `dying` (stage 16)? Like Linux: the+/// nearest living ancestor that said prctl(PR_SET_CHILD_SUBREAPER), else+/// pid 1 -- but only if pid 1 is a real `init` (named "init", or a+/// subreaper), because one that never calls wait(-1) would collect+/// zombies forever. Today pid 1 is usually our own shell, which only waits+/// for its jobs, so most orphans get None: they reap themselves on exit+/// (parent = ORPHAN), which frees their memory just the same.+fn adopter(table: &BTreeMap<u64, Arc<Process>>, dying: &Process) -> Option<u64> {+    let alive = |pid: u64| table.get(&pid).filter(|p| p.status().is_none() && p.pid != dying.pid);+    let mut pid = dying.parent();+    for _ in 0..table.len() {+        match alive(pid) {+            Some(p) if p.child_subreaper.load(Ordering::Relaxed) => return Some(p.pid),+            Some(p) => pid = p.parent(),+            None => break,+        }+    }+    alive(1).filter(|init| init.child_subreaper.load(Ordering::Relaxed) || init.name() == "init").map(|init| init.pid)+}+ /// Block (the calling thread) until `process` has ended and its thread is /// gone, then remove it from the process table. Used by the kernel (tests, /// the shell supervisor). Once the caller drops its own `Arc`, all of the@@ -876,6 +1133,9 @@ pub fn fork(parent: &Arc<Process>, frame: &TrapFrame, stack: u64, vfork: bool) -     let me = thread::current_task();     let fs_base = me.as_ref().map_or(0, |t| t.fs_base.load(Ordering::Relaxed));     let blocked = me.as_ref().map_or(0, |t| without_interrupts(|| t.sig.lock().blocked));+    // ... and its sigaltstack and name (stage 16).+    let altstack = me.as_ref().map_or(signal::AltStack::DISABLED, |t| without_interrupts(|| t.sig.lock().altstack));+    let comm = me.as_ref().map_or_else(|| comm_from(&parent.name()), |t| *t.comm.lock());     let child = Process {         pid,         parent: AtomicU64::new(parent.pid),@@ -899,11 +1159,17 @@ pub fn fork(parent: &Arc<Process>, frame: &TrapFrame, stack: u64, vfork: bool) -         child_wait: WaitQueue::new(),         signals: Mutex::new(signals),         vfork_done: AtomicBool::new(false),+        umask: AtomicU32::new(parent.umask.load(Ordering::Relaxed)),+        rlimits: Mutex::new(*parent.rlimits.lock()),+        pdeathsig: AtomicU32::new(0), // (not inherited, like Linux)+        dumpable: AtomicU32::new(parent.dumpable.load(Ordering::Relaxed)),+        no_new_privs: AtomicBool::new(parent.no_new_privs.load(Ordering::Relaxed)),+        child_subreaper: AtomicBool::new(false),     };     serial_println!("[process] pid {} forked pid {}", parent.pid, pid);     // The child's FPU/SSE registers start as a copy of ours (we're running,     // so ours are in the CPU right now).-    let child = start_process(child, start, fs_base, blocked, crate::fpu::save_live());+    let child = start_process(child, start, fs_base, blocked, crate::fpu::save_live(), altstack, comm);     if vfork {         // The parent sleeps until the child has exec'd or exited (real         // vfork shares the memory meanwhile; we just use copy-on-write).@@ -938,7 +1204,10 @@ pub fn clone_thread(p: &Arc<Process>, frame: &TrapFrame, stack: u64, tls: Option     let blocked = without_interrupts(|| me.sig.lock().blocked);     // Forget scheduler threads of threads that are completely gone.     without_interrupts(|| p.kthreads.lock().retain(|&k| thread::exists(k)));-    let task = start_task(p, tid, start, fs_base, blocked, crate::fpu::save_live());+    // A new thread has no sigaltstack (its creator's stack isn't its own)+    // and starts with its creator's name.+    let comm = *me.comm.lock();+    let task = start_task(p, tid, start, fs_base, blocked, crate::fpu::save_live(), signal::AltStack::DISABLED, comm);     task.clear_child_tid.store(clear_tid, Ordering::Relaxed);     serial_println!("[process] pid {} new thread {}", p.pid, tid);     Ok(tid)@@ -991,6 +1260,9 @@ pub fn execve(p: &Arc<Process>, frame: &mut TrapFrame, path: &str, argv: &[Vec<u         me.fs_base.store(0, Ordering::Relaxed); // no TLS until the new libc sets one         crate::fpu::reset_live(); // clean FPU/SSE/AVX registers for the new program         me.clear_child_tid.store(0, Ordering::Relaxed);+        me.robust_list.store(0, Ordering::Relaxed); // the list was in the old program's memory+        *me.comm.lock() = comm_from(name);+        without_interrupts(|| me.sig.lock().altstack = signal::AltStack::DISABLED);     }     *p.env.lock() = envp.iter().map(|e| String::from_utf8_lossy(e).into_owned()).collect();     without_interrupts(|| p.signals.lock().exec());
@@ -126,6 +126,21 @@ pub const SA_ONSTACK: u64 = 0x0800_0000; pub const SA_RESTART: u64 = 0x1000_0000; pub const SA_NODEFER: u64 = 0x4000_0000; pub const SA_RESETHAND: u64 = 0x8000_0000;+/// Every flag we act on. rt_sigaction stores only these (Linux 5.11+ does+/// the same), so a program can set e.g. SA_UNSUPPORTED 0x400 and read the+/// action back to learn which flags this kernel knows.+pub const SA_KNOWN: u64 = SA_NOCLDSTOP | SA_NOCLDWAIT | SA_SIGINFO | SA_RESTORER | SA_ONSTACK | SA_RESTART | SA_NODEFER | SA_RESETHAND;+// sigaltstack's ss_flags (stage 16).+pub const SS_ONSTACK: u32 = 1; // (read only) we're running on it right now+pub const SS_DISABLE: u32 = 2; // there is none+pub const SS_AUTODISARM: u32 = 1 << 31; // forget it while a handler runs on it+/// The smallest alternate stack sigaltstack accepts (ENOMEM below).+pub const MINSIGSTKSZ: u64 = 2048;+/// First real-time signal. Real-time signals queue: sending one 3 times+/// delivers it 3 times, each with its own siginfo (stage 16).+pub const SIGRTMIN: u32 = 32;+/// At most this many queued signals per pending set (sigqueue: EAGAIN beyond).+const MAX_QUEUED: usize = 1024; // rt_sigprocmask `how`. pub const SIG_BLOCK: u64 = 0; pub const SIG_UNBLOCK: u64 = 1;@@ -133,6 +148,8 @@ pub const SIG_SETMASK: u64 = 2; // siginfo codes. pub const SI_USER: i32 = 0; pub const SI_KERNEL: i32 = 0x80;+pub const SI_QUEUE: i32 = -1; // sigqueue()+pub const SI_TKILL: i32 = -6; // tkill / tgkill pub const SEGV_MAPERR: i32 = 1; // no mapping at that address pub const SEGV_ACCERR: i32 = 2; // mapped, but not allowed (e.g. write to read-only) pub const CLD_EXITED: i32 = 1;@@ -205,8 +222,9 @@ pub enum Report {     Continued, } -/// Pending signals with their siginfo (standard signals don't queue: one-/// pending instance each, like Linux).+/// Pending signals with their siginfo. Standard signals (1-31) don't+/// queue: one pending instance each, like Linux. Real-time ones (32-64)+/// do, in the order they were sent (stage 16). #[derive(Default)] pub struct Pending {     pub set: u64,@@ -214,30 +232,75 @@ pub struct Pending { }  impl Pending {-    /// Mark `sig` pending with this siginfo.-    fn raise(&mut self, sig: u32, info: SigInfo) {+    /// Mark `sig` pending with this siginfo. False if the queue is full+    /// (a real-time signal that couldn't be queued).+    fn raise(&mut self, sig: u32, info: SigInfo) -> bool {+        if sig < SIGRTMIN {+            self.info.retain(|(s, _)| *s != sig);+        } else if self.info.len() >= MAX_QUEUED {+            return false;+        }         self.set |= bit(sig);-        self.info.retain(|(s, _)| *s != sig);         self.info.push((sig, info));+        true     } -    /// Take the lowest signal of `allowed` (SIGKILL first).+    /// Take the lowest signal of `allowed` (SIGKILL first); for a queued+    /// real-time signal, its oldest instance.     fn take(&mut self, allowed: u64) -> Option<(u32, SigInfo)> {         let set = self.set & allowed;         if set == 0 {             return None;         }         let sig = if set & bit(SIGKILL) != 0 { SIGKILL } else { set.trailing_zeros() + 1 };-        self.set &= !bit(sig);         let info = match self.info.iter().position(|(s, _)| *s == sig) {-            Some(i) => self.info.swap_remove(i).1,+            Some(i) => self.info.remove(i).1,             None => SigInfo::new(sig, SI_KERNEL),         };+        if !self.info.iter().any(|(s, _)| *s == sig) {+            self.set &= !bit(sig);+        }         Some((sig, info))     }      fn discard(&mut self, mask: u64) {         self.set &= !mask;+        self.info.retain(|(s, _)| bit(*s) & mask == 0);+    }+}++/// A thread's alternate signal stack (sigaltstack, stage 16): handlers+/// installed with SA_ONSTACK run on it -- the way a program can still+/// handle SIGSEGV after overflowing its normal stack.+#[derive(Debug, Clone, Copy, PartialEq, Eq)]+pub struct AltStack {+    pub sp: u64,   // lowest address+    pub size: u64, // bytes+    /// SS_DISABLE, or 0 / SS_AUTODISARM for a usable one.+    pub flags: u32,+}++impl AltStack {+    pub const DISABLED: AltStack = AltStack { sp: 0, size: 0, flags: SS_DISABLE };++    fn enabled(&self) -> bool {+        self.flags & SS_DISABLE == 0+    }++    /// Is `sp` on it? (Linux's test: the stack grows down from sp + size.)+    pub fn contains(&self, sp: u64) -> bool {+        self.enabled() && sp > self.sp && sp - self.sp <= self.size+    }++    /// stack_t.ss_flags as sigaltstack reports it for a thread at `sp`.+    fn report_flags(&self, sp: u64) -> u32 {+        if !self.enabled() {+            SS_DISABLE+        } else if self.contains(sp) {+            SS_ONSTACK | (self.flags & SS_AUTODISARM)+        } else {+            self.flags & SS_AUTODISARM+        }     } } @@ -251,11 +314,17 @@ pub struct ThreadSig {     /// The real mask while ppoll/pselect6/epoll_pwait run with a temporary     /// one (see `set_temporary_mask`).     saved_mask: Option<u64>,+    /// sigaltstack (stage 16).+    pub altstack: AltStack, }  impl ThreadSig {     pub fn new(blocked: u64) -> ThreadSig {-        ThreadSig { blocked, pending: Pending::default(), reason: None, saved_mask: None }+        ThreadSig::with_altstack(blocked, AltStack::DISABLED)+    }++    pub fn with_altstack(blocked: u64, altstack: AltStack) -> ThreadSig {+        ThreadSig { blocked, pending: Pending::default(), reason: None, saved_mask: None, altstack }     }      /// The signals this thread may take now.@@ -694,14 +763,24 @@ fn setup_frame(t: &Task, frame: &mut TrapFrame, sig: u32, info: &SigInfo, action     // moving rsp) goes the FPU state, then the frame, aligned so the     // handler starts with rsp % 16 == 8, as if it had been called.     let fp_size = fpstate_size();-    let fp = (frame.rsp.wrapping_sub(128).wrapping_sub(fp_size)) & !63;-    let sp = ((fp.wrapping_sub(size)) & !0xf).wrapping_sub(8);     // The mask sigreturn restores: the real one, if a ppoll & co. had-    // swapped in a temporary mask.-    let blocked = without_interrupts(|| {-        let ts = t.sig.lock();-        ts.saved_mask.unwrap_or(ts.blocked)+    // swapped in a temporary mask. And the sigaltstack (stage 16): an+    // SA_ONSTACK handler starts at its top -- unless we're on it already+    // (a nested signal), then we just keep going down it.+    let (blocked, alt) = without_interrupts(|| {+        let mut ts = t.sig.lock();+        let alt = ts.altstack;+        if alt.flags & SS_AUTODISARM != 0 {+            ts.altstack = AltStack::DISABLED; // sigreturn puts it back (from uc_stack)+        }+        (ts.saved_mask.unwrap_or(ts.blocked), alt)     });+    let on_alt = action.flags & SA_ONSTACK != 0 && alt.enabled() && !alt.contains(frame.rsp);+    let top = if on_alt { alt.sp + alt.size } else { frame.rsp.wrapping_sub(128) };+    let fp = (top.wrapping_sub(fp_size)) & !63;+    let sp = ((fp.wrapping_sub(size)) & !0xf).wrapping_sub(8);+    // uc_stack: the sigaltstack as it was (sigreturn restores it from here).+    let uc_stack = [alt.sp, alt.report_flags(frame.rsp) as u64, alt.size];     let s = crate::gdt::selectors();     let mcontext = SigContext {         r8: frame.r8,@@ -735,6 +814,7 @@ fn setup_frame(t: &Task, frame: &mut TrapFrame, sig: u32, info: &SigInfo, action         pretcode: action.restorer,         uc: UContext {             flags: UC_SS_FLAGS | if xsave { UC_FP_XSTATE } else { 0 },+            stack: uc_stack,             mcontext,             sigmask: blocked,             ..Default::default()@@ -818,7 +898,14 @@ pub fn sys_rt_sigreturn(frame: &mut TrapFrame) {         error: 0,     };     if let Some(t) = thread::current_task() {-        without_interrupts(|| t.sig.lock().blocked = rt.uc.sigmask & !UNBLOCKABLE);+        without_interrupts(|| {+            let mut ts = t.sig.lock();+            ts.blocked = rt.uc.sigmask & !UNBLOCKABLE;+            // Put the sigaltstack back as uc_stack says, like Linux (that's+            // how SS_AUTODISARM re-arms; errors, e.g. still on it, are ignored).+            let [ss_sp, ss_flags, ss_size] = rt.uc.stack;+            let _ = set_altstack(&mut ts, ss_sp, ss_flags as u32, ss_size, frame.rsp);+        });     }     if restore_fpstate(m.fpstate).is_err() {         process::exit_group(ExitStatus::Killed(SIGSEGV, "bad sigreturn FPU state"));@@ -881,6 +968,7 @@ pub fn sys_rt_sigaction(p: &Process, sig: u64, new: Option<SigAction>, set_size:         let old = s.action(sig);         if let Some(mut new) = new {             new.mask &= !UNBLOCKABLE;+            new.flags &= SA_KNOWN; // (see SA_KNOWN)             s.actions[sig as usize - 1] = new;             // Setting a signal to "ignored" discards it if pending (POSIX).             if s.ignored(sig) {@@ -936,3 +1024,69 @@ pub fn sys_kill(me: &Process, pid: i64, sig: u64) -> Result<u64, Errno> {     }     Ok(0) }++// --- Stage 16: the rest of the rt_sig* family, and sigaltstack -------------++/// Change a thread's sigaltstack (sigaltstack's "new" half, also used by+/// sigreturn). `user_sp`: where the thread's stack pointer is.+fn set_altstack(ts: &mut ThreadSig, ss_sp: u64, ss_flags: u32, ss_size: u64, user_sp: u64) -> Result<(), Errno> {+    if ts.altstack.contains(user_sp) {+        return Err(Errno::EPERM); // can't change it while running on it+    }+    match ss_flags & !SS_AUTODISARM {+        SS_DISABLE => ts.altstack = AltStack::DISABLED,+        0 | SS_ONSTACK => {+            if ss_size < MINSIGSTKSZ {+                return Err(Errno::ENOMEM);+            }+            ts.altstack = AltStack { sp: ss_sp, size: ss_size, flags: ss_flags & SS_AUTODISARM };+        }+        _ => return Err(Errno::EINVAL),+    }+    Ok(())+}++/// sigaltstack(*new, *old): returns the old one as (ss_sp, ss_flags,+/// ss_size) -- ss_flags says SS_ONSTACK if we're on it, SS_DISABLE if+/// there's none -- then sets `new` (if given).+pub fn sys_sigaltstack(t: &Task, new: Option<(u64, u32, u64)>, user_sp: u64) -> Result<(u64, u32, u64), Errno> {+    without_interrupts(|| {+        let mut ts = t.sig.lock();+        let a = ts.altstack;+        let old = (a.sp, a.report_flags(user_sp), a.size);+        if let Some((sp, flags, size)) = new {+            set_altstack(&mut ts, sp, flags, size, user_sp)?;+        }+        Ok(old)+    })+}++/// rt_sigpending: signals that are pending *and blocked* (the others would+/// have been delivered already), for this thread or its whole process.+pub fn pending_blocked(t: &Task) -> u64 {+    without_interrupts(|| {+        let s = t.process.signals.lock();+        let ts = t.sig.lock();+        (s.pending.set | ts.pending.set) & ts.saved_mask.unwrap_or(ts.blocked)+    })+}++/// rt_sigtimedwait's dequeue: take a pending signal of `set` (the+/// thread's own first, then the process's), without running any handler.+pub fn take_waited(t: &Task, set: u64) -> Option<(u32, SigInfo)> {+    without_interrupts(|| {+        let mut s = t.process.signals.lock();+        let mut ts = t.sig.lock();+        ts.pending.take(set).or_else(|| s.pending.take(set))+    })+}++/// Is the process's queue of pending signals full (sigqueue: EAGAIN)?+pub fn queue_full(p: &Process) -> bool {+    without_interrupts(|| p.signals.lock().pending.info.len() >= MAX_QUEUED)+}++/// All signals but SIGKILL / SIGSTOP (rt_sigtimedwait can't wait for those).+pub const fn blockable(set: u64) -> u64 {+    set & !UNBLOCKABLE+}
@@ -15,8 +15,8 @@ //! 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).+//! `mode` arguments are stored where the filesystem can (stage 16: ramfs,+//! ext2) but never checked -- everyone is root -- and uid/gid are always 0. Every blocking call honours O_NONBLOCK (-> EAGAIN). //! //! | nr  | name        | arguments                           | note | //! |-----|-------------|-------------------------------------|------|@@ -45,6 +45,7 @@ //! |  22 | pipe        | *[u32; 2]                           | [read end, write end] | //! |  23 | select      | n, *rd, *wr, *ex, *timeval          | fd bitmaps (n <= 1024) | //! |  24 | sched_yield |                                     | |+//! |  28 | madvise     | addr, len, advice                   | hints accepted; MADV_DONTNEED 4 / MADV_FREE 8 drop private pages (they read back as zeros) | //! |  32 | dup         | fd                                  | lowest free fd | //! |  33 | dup2        | old, new                            | closes `new` first if open | //! |  34 | pause       |                                     | sleeps until a signal; returns EINTR |@@ -66,7 +67,9 @@ //! |  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 |+//! |  72 | fcntl       | fd, cmd, arg                        | F_DUPFD 0, F_GETFD 1, F_SETFD 2, F_GETFL 3 (access mode, O_APPEND, O_NONBLOCK), F_SETFL 4 (O_NONBLOCK, O_APPEND), F_GETLK 5 / F_SETLK 6 / F_SETLKW 7 (record locks, fs/lock.rs), F_DUPFD_CLOEXEC 1030 |+//! |  74 | fsync       | fd                                  | fdatasync 75 too: writes already went to the disk, so just checks the fd |+//! |  76 | truncate    | path, len                           | cut or zero-extend a file; ftruncate 77 = the same on an fd open for writing | //! |  79 | getcwd      | buf, len                            | like Linux: writes a NUL, returns the length including it | //! |  80 | chdir       | path                                | | //! |  81 | fchdir      | fd                                  | a directory opened with open() |@@ -75,6 +78,9 @@ //! |  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 |+//! |  90 | chmod       | path, mode                          | ramfs and ext2 keep it; FAT: EPERM; btrfs: EROFS. fchmod 91 = on an fd |+//! |  95 | umask       | mask                                | returns the old one; open(O_CREAT)/mkdir take these bits away from `mode` |+//! |  97 | getrlimit   | resource, *rlimit                   | {soft, hard}; setrlimit 160 too. RLIMIT_NOFILE is enforced (<= 64) | //! | 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 |@@ -82,10 +88,17 @@ //! | 112 | setsid      |                                     | new session + group, no controlling tty | //! | 121 | getpgid     | pid                                 | | //! | 124 | getsid      | pid                                 | |+//! | 127 | rt_sigpending | *set, 8                           | blocked signals that are waiting |+//! | 128 | rt_sigtimedwait | *set, *siginfo, *timespec, 8    | take a signal without a handler; EAGAIN on timeout |+//! | 129 | rt_sigqueueinfo | tgid, sig, *siginfo             | sigqueue(); real-time signals (32..64) queue up. rt_tgsigqueueinfo 297: to one thread |+//! | 130 | rt_sigsuspend | *mask, 8                          | sleep with `mask` until a handler ran; EINTR |+//! | 131 | sigaltstack | *new, *old                          | `stack_t` {sp, flags, size}; SS_DISABLE 2, SS_AUTODISARM; SA_ONSTACK handlers run there |+//! | 157 | prctl       | option, ...                         | PR_SET/GET_NAME 15/16, PDEATHSIG 1/2, DUMPABLE 3/4, NO_NEW_PRIVS 38/39; others EINVAL | //! | 158 | arch_prctl  | code, addr                          | ARCH_SET_FS 0x1002 / ARCH_GET_FS 0x1003: the TLS pointer | //! | 186 | gettid      |                                     | thread id (= pid for the first thread) | //! | 200 | tkill       | tid, sig                            | | //! | 202 | futex       | *u32, op, val, *timespec, *u32, val3 | FUTEX_WAIT 0, WAKE 1, WAIT_BITSET 9, WAKE_BITSET 10 (+ PRIVATE 128) |+//! | 204 | sched_getaffinity | tid, len, *mask                 | every online CPU; returns 8 (bytes written). sched_setaffinity 203: accepted, not enforced | //! | 213 | epoll_create | size                               | = epoll_create1(0) | //! | 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 |@@ -101,14 +114,18 @@ //! | 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               | |+//! | 268 | fchmodat    | dirfd, path, mode                   | fchmodat2 452: + flags (AT_SYMLINK_NOFOLLOW on a symlink: EOPNOTSUPP) | //! | 269 | faccessat   | dirfd, path, mode                   | faccessat2 439 too (its flags are ignored) |+//! | 273 | set_robust_list | *head, 24                       | robust mutexes: marked FUTEX_OWNER_DIED when the thread dies. get_robust_list 274 | //! | 270 | pselect6    | n, *rd, *wr, *ex, *timespec, *{*mask, 8} | | //! | 271 | ppoll       | *pollfd, n, *timespec, *mask, 8     | sets the mask while waiting |+//! | 280 | utimensat   | dirfd, path or 0, *[timespec; 2], flags | UTIME_NOW / UTIME_OMIT; FAT ignores times; btrfs: EROFS | //! | 281 | epoll_pwait | epfd, *events, max, timeout, *mask, 8 | | //! | 288 | accept4     | fd, *addr, *len, flags              | flags: SOCK_NONBLOCK, SOCK_CLOEXEC | //! | 291 | epoll_create1 | flags                             | EPOLL_CLOEXEC | //! | 292 | dup3        | old, new, flags                     | flags: O_CLOEXEC | //! | 293 | pipe2       | *[u32; 2], flags                    | flags: O_CLOEXEC |+//! | 302 | prlimit64   | pid, resource, *new, *old           | get and/or set another process's limits (0 = me) | //! | 318 | getrandom   | buf, len, flags                     | random bytes (virtio-rng); up to 64 KiB per call; flags ignored | //! | 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) |@@ -269,6 +286,32 @@ pub const SYS_READLINKAT: u64 = 267; pub const SYS_FACCESSAT: u64 = 269; pub const SYS_RENAMEAT2: u64 = 316; pub const SYS_FACCESSAT2: u64 = 439;+// Stage 16: what a C library (musl) calls besides open/read/write.+pub const SYS_MADVISE: u64 = 28;+pub const SYS_FSYNC: u64 = 74;+pub const SYS_FDATASYNC: u64 = 75;+pub const SYS_TRUNCATE: u64 = 76;+pub const SYS_FTRUNCATE: u64 = 77;+pub const SYS_CHMOD: u64 = 90;+pub const SYS_FCHMOD: u64 = 91;+pub const SYS_UMASK: u64 = 95;+pub const SYS_GETRLIMIT: u64 = 97;+pub const SYS_RT_SIGPENDING: u64 = 127;+pub const SYS_RT_SIGTIMEDWAIT: u64 = 128;+pub const SYS_RT_SIGQUEUEINFO: u64 = 129;+pub const SYS_RT_SIGSUSPEND: u64 = 130;+pub const SYS_SIGALTSTACK: u64 = 131;+pub const SYS_PRCTL: u64 = 157;+pub const SYS_SETRLIMIT: u64 = 160;+pub const SYS_FCHMODAT: u64 = 268;+pub const SYS_SET_ROBUST_LIST: u64 = 273;+pub const SYS_GET_ROBUST_LIST: u64 = 274;+pub const SYS_UTIMENSAT: u64 = 280;+pub const SYS_RT_TGSIGQUEUEINFO: u64 = 297;+pub const SYS_PRLIMIT64: u64 = 302;+pub const SYS_FCHMODAT2: u64 = 452;+pub const SYS_SCHED_SETAFFINITY: u64 = 203;+pub const SYS_SCHED_GETAFFINITY: u64 = 204;  // Socket constants (Linux's values). pub const AF_INET: u64 = 2;@@ -301,7 +344,31 @@ pub const F_GETFD: u64 = 1; pub const F_SETFD: u64 = 2; pub const F_GETFL: u64 = 3; pub const F_SETFL: u64 = 4;+pub const F_GETLK: u64 = 5;+pub const F_SETLK: u64 = 6;+pub const F_SETLKW: u64 = 7; pub const F_DUPFD_CLOEXEC: u64 = 1030;+/// struct flock's l_type.+pub const F_RDLCK: i16 = 0;+pub const F_WRLCK: i16 = 1;+pub const F_UNLCK: i16 = 2;+/// utimensat: "set this time to now" / "leave this time alone" (tv_nsec).+pub const UTIME_NOW: i64 = (1 << 30) - 1;+pub const UTIME_OMIT: i64 = (1 << 30) - 2;+/// prctl options we know.+pub const PR_SET_PDEATHSIG: u64 = 1;+pub const PR_GET_PDEATHSIG: u64 = 2;+pub const PR_GET_DUMPABLE: u64 = 3;+pub const PR_SET_DUMPABLE: u64 = 4;+pub const PR_SET_NAME: u64 = 15;+pub const PR_GET_NAME: u64 = 16;+pub const PR_SET_CHILD_SUBREAPER: u64 = 36;+pub const PR_GET_CHILD_SUBREAPER: u64 = 37;+pub const PR_SET_NO_NEW_PRIVS: u64 = 38;+pub const PR_GET_NO_NEW_PRIVS: u64 = 39;+/// madvise advice values.+pub const MADV_DONTNEED: u64 = 4;+pub const MADV_FREE: u64 = 8; pub const FD_CLOEXEC: u64 = 1; /// ioctl(FIONBIO, *int): O_NONBLOCK on/off, for any file. pub const FIONBIO: u64 = 0x5421;@@ -360,6 +427,31 @@ pub fn name(nr: u64) -> &'static str {         SYS_FACCESSAT => "faccessat",         SYS_RENAMEAT2 => "renameat2",         SYS_FACCESSAT2 => "faccessat2",+        SYS_MADVISE => "madvise",+        SYS_FSYNC => "fsync",+        SYS_FDATASYNC => "fdatasync",+        SYS_TRUNCATE => "truncate",+        SYS_FTRUNCATE => "ftruncate",+        SYS_CHMOD => "chmod",+        SYS_FCHMOD => "fchmod",+        SYS_UMASK => "umask",+        SYS_GETRLIMIT => "getrlimit",+        SYS_RT_SIGPENDING => "rt_sigpending",+        SYS_RT_SIGTIMEDWAIT => "rt_sigtimedwait",+        SYS_RT_SIGQUEUEINFO => "rt_sigqueueinfo",+        SYS_RT_SIGSUSPEND => "rt_sigsuspend",+        SYS_SIGALTSTACK => "sigaltstack",+        SYS_PRCTL => "prctl",+        SYS_SETRLIMIT => "setrlimit",+        SYS_FCHMODAT => "fchmodat",+        SYS_SET_ROBUST_LIST => "set_robust_list",+        SYS_GET_ROBUST_LIST => "get_robust_list",+        SYS_UTIMENSAT => "utimensat",+        SYS_RT_TGSIGQUEUEINFO => "rt_tgsigqueueinfo",+        SYS_PRLIMIT64 => "prlimit64",+        SYS_FCHMODAT2 => "fchmodat2",+        SYS_SCHED_SETAFFINITY => "sched_setaffinity",+        SYS_SCHED_GETAFFINITY => "sched_getaffinity",         SYS_LSEEK => "lseek",         SYS_IOCTL => "ioctl",         SYS_PIPE => "pipe",@@ -557,8 +649,8 @@ extern "C" fn syscall_dispatch(frame: &mut TrapFrame) {     let result = match nr {         SYS_READ => sys_read(a1, a2, a3),         SYS_WRITE => sys_write(a1, a2, a3),-        SYS_OPEN => sys_openat(AT_FDCWD as u64, a1, a2),-        SYS_OPENAT => sys_openat(a1, a2, a3),+        SYS_OPEN => sys_openat(AT_FDCWD as u64, a1, a2, a3),+        SYS_OPENAT => sys_openat(a1, a2, a3, a4),         SYS_CLOSE => current().and_then(|p| p.close(a1)).map(|_| 0),         SYS_STAT => sys_newfstatat(AT_FDCWD as u64, a1, a2, 0),         SYS_LSTAT => sys_newfstatat(AT_FDCWD as u64, a1, a2, AT_SYMLINK_NOFOLLOW),@@ -601,8 +693,8 @@ extern "C" fn syscall_dispatch(frame: &mut TrapFrame) {         SYS_GETCWD => sys_getcwd(a1, a2),         SYS_CHDIR => user_path(a1).and_then(|p| sys_chdir(p)),         SYS_FCHDIR => sys_fchdir(a1),-        SYS_MKDIR => sys_mkdirat(AT_FDCWD as u64, a1),-        SYS_MKDIRAT => sys_mkdirat(a1, a2),+        SYS_MKDIR => sys_mkdirat(AT_FDCWD as u64, a1, a2),+        SYS_MKDIRAT => sys_mkdirat(a1, a2, a3),         SYS_RMDIR => sys_unlinkat(AT_FDCWD as u64, a1, AT_REMOVEDIR),         SYS_UNLINK => sys_unlinkat(AT_FDCWD as u64, a1, 0),         SYS_UNLINKAT => sys_unlinkat(a1, a2, a3),@@ -661,6 +753,31 @@ extern "C" fn syscall_dispatch(frame: &mut TrapFrame) {         SYS_TKILL => sys_tgkill(None, a1, a2),         SYS_SETSID => current().and_then(|p| process::setsid(&p)),         SYS_GETSID => current().and_then(|p| if a1 == 0 { Ok(p.sid()) } else { process::get(a1).map(|t| t.sid()).ok_or(Errno::ESRCH) }),+        // --- stage 16 ---+        SYS_MADVISE => sys_madvise(a1, a2, a3),+        SYS_UMASK => current().map(|p| p.umask.swap(a1 as u32 & 0o777, Ordering::Relaxed) as u64),+        SYS_FSYNC | SYS_FDATASYNC => sys_fsync(a1),+        SYS_TRUNCATE => sys_truncate(a1, a2),+        SYS_FTRUNCATE => sys_ftruncate(a1, a2),+        SYS_CHMOD => sys_fchmodat(AT_FDCWD as u64, a1, a2, 0),+        SYS_FCHMOD => sys_fchmod(a1, a2),+        SYS_FCHMODAT => sys_fchmodat(a1, a2, a3, 0),+        SYS_FCHMODAT2 => sys_fchmodat(a1, a2, a3, a4),+        SYS_UTIMENSAT => sys_utimensat(a1, a2, a3, a4),+        SYS_GETRLIMIT => sys_prlimit64(0, a1, 0, a2),+        SYS_SETRLIMIT => sys_prlimit64(0, a1, a2, 0),+        SYS_PRLIMIT64 => sys_prlimit64(a1, a2, a3, a4),+        SYS_PRCTL => sys_prctl(a1, a2, a3, a4, a5),+        SYS_SET_ROBUST_LIST => sys_set_robust_list(a1, a2),+        SYS_GET_ROBUST_LIST => sys_get_robust_list(a1, a2, a3),+        SYS_SIGALTSTACK => sys_sigaltstack(a1, a2, frame.rsp),+        SYS_RT_SIGPENDING => sys_rt_sigpending(a1, a2),+        SYS_RT_SIGSUSPEND => sys_rt_sigsuspend(a1, a2),+        SYS_RT_SIGTIMEDWAIT => sys_rt_sigtimedwait(a1, a2, a3, a4),+        SYS_RT_SIGQUEUEINFO => sys_rt_sigqueueinfo(a1, None, a2, a3),+        SYS_RT_TGSIGQUEUEINFO => sys_rt_sigqueueinfo(a1, Some(a2), a3, a4),+        SYS_SCHED_GETAFFINITY => sys_sched_affinity(a1, a2, a3, false),+        SYS_SCHED_SETAFFINITY => sys_sched_affinity(a1, a2, a3, true),         _ => Err(Errno::ENOSYS),     };     let ret = match result {@@ -738,8 +855,17 @@ fn trace_line(nr: u64, a: [u64; 4], ret: i64) -> String {         _ => format!("{}, {:#x}, {:#x}", a[0], a[1], a[2]),     };     match Errno::from_ret(ret) {-        Some(e) if ret < 0 => format!("{}({}) = {} {:?}", name(nr), args, ret, e),-        _ => format!("{}({}) = {}", name(nr), args, ret),+        Some(e) if ret < 0 => format!("{}({}) = {} {:?}", trace_name(nr), args, ret, e),+        _ => format!("{}({}) = {}", trace_name(nr), args, ret),+    }+}++/// A syscall's name for the trace; unknown ones by number (`syscall_130`),+/// so a missing call is easy to look up in Linux's table.+pub fn trace_name(nr: u64) -> String {+    match name(nr) {+        "?" => alloc::format!("syscall_{}", nr),+        known => String::from(known),     } } @@ -858,11 +984,15 @@ fn user_path(ptr: u64) -> Result<String, Errno> { /// open / openat(dirfd, path, flags, mode). Everything is readable and /// writable whatever O_RDONLY/O_WRONLY say, and `mode` is ignored (new /// files get the file system's default permissions).-fn sys_openat(dirfd: u64, path_ptr: u64, flags: u64) -> Result<u64, Errno> {+fn sys_openat(dirfd: u64, path_ptr: u64, flags: u64, mode: u64) -> Result<u64, Errno> {     let path = user_path_at(dirfd, path_ptr)?;     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 => fs::create(&path, InodeKind::File)?,+        Err(Errno::ENOENT) if flags & O_CREAT != 0 => {+            let new = fs::create(&path, InodeKind::File)?;+            apply_create_mode(&*new, mode);+            new+        }         Ok(_) if flags & O_CREAT != 0 && flags & O_EXCL != 0 => return Err(Errno::EEXIST),         Ok(i) if flags & O_NOFOLLOW != 0 && i.kind() == InodeKind::Symlink => return Err(Errno::ELOOP),         other => other?,@@ -889,7 +1019,7 @@ fn sys_openat(dirfd: u64, path_ptr: u64, flags: u64) -> Result<u64, Errno> {     if flags & O_TRUNC != 0 && inode.kind() == InodeKind::File {         inode.truncate()?;     }-    let file = if flags & O_APPEND != 0 { InodeFile::open_append(inode, path)? } else { InodeFile::open(inode, path)? };+    let file = InodeFile::open_flags(inode, path, flags)?;     current()?.add_file_with(Arc::new(file), 0, cloexec) } @@ -903,7 +1033,7 @@ fn sys_newfstatat(dirfd: u64, path_ptr: u64, stat_ptr: u64, flags: u64) -> Resul     }     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)? };-    let stat = LinuxStat::new(&Stat::of(&*inode), fs::file::ino_hash(path.as_bytes()));+    let stat = LinuxStat::new(&Stat::of(&*inode), fs::file::ino_hash(path.as_bytes())).with_times(inode.times());     copy_to_user(stat_ptr, &stat)?;     Ok(0) }@@ -916,7 +1046,7 @@ fn sys_fstat(fd: u64, stat_ptr: u64) -> Result<u64, Errno> {         Some(path) => fs::file::ino_hash(path.as_bytes()),         None => Arc::as_ptr(&file) as *const u8 as u64,     };-    copy_to_user(stat_ptr, &LinuxStat::new(&file.stat()?, ino))?;+    copy_to_user(stat_ptr, &LinuxStat::new(&file.stat()?, ino).with_times(file.times()))?;     Ok(0) } @@ -928,11 +1058,21 @@ fn sys_faccessat(dirfd: u64, path_ptr: u64) -> Result<u64, Errno> { }  /// mkdir / mkdirat(dirfd, path, mode); `mode` is ignored.-fn sys_mkdirat(dirfd: u64, path_ptr: u64) -> Result<u64, Errno> {-    fs::create(&user_path_at(dirfd, path_ptr)?, InodeKind::Directory)?;+fn sys_mkdirat(dirfd: u64, path_ptr: u64, mode: u64) -> Result<u64, Errno> {+    let dir = fs::create(&user_path_at(dirfd, path_ptr)?, InodeKind::Directory)?;+    apply_create_mode(&*dir, mode);     Ok(0) } +/// open(O_CREAT) / mkdir (stage 16): the new file gets `mode` minus the+/// umask bits, where the filesystem keeps modes (ramfs, ext2). Elsewhere+/// (FAT, read-only btrfs) it's quietly skipped -- the file exists, which+/// is what the caller asked for.+fn apply_create_mode(inode: &dyn fs::Inode, mode: u64) {+    let umask = current().map(|p| p.umask.load(Ordering::Relaxed)).unwrap_or(0o022);+    let _ = inode.set_mode(mode as u32 & 0o7777 & !umask);+}+ /// unlink / rmdir / unlinkat(dirfd, path, flags): AT_REMOVEDIR = rmdir. fn sys_unlinkat(dirfd: u64, path_ptr: u64, flags: u64) -> Result<u64, Errno> {     if flags & !AT_REMOVEDIR != 0 {@@ -1047,16 +1187,19 @@ fn sys_fcntl(fd: u64, cmd: u64, arg: u64) -> Result<u64, Errno> {     let p = current()?;     let file = p.file(fd)?;     match cmd {-        F_DUPFD | F_DUPFD_CLOEXEC if arg as usize >= process::MAX_FDS => Err(Errno::EINVAL),+        F_DUPFD | F_DUPFD_CLOEXEC if arg as usize >= p.fd_limit() => Err(Errno::EINVAL),         F_DUPFD => p.add_file_with(file, arg as usize, false),         F_DUPFD_CLOEXEC => p.add_file_with(file, arg as usize, true),         F_GETFD => p.cloexec(fd).map(|on| if on { FD_CLOEXEC } else { 0 }),         F_SETFD => p.set_cloexec(fd, arg & FD_CLOEXEC != 0).map(|_| 0),-        F_GETFL => Ok(2 | if file.nonblocking() { O_NONBLOCK } else { 0 }), // O_RDWR+        F_GETFL => Ok(file.status_flags() | if file.nonblocking() { O_NONBLOCK } else { 0 }),         F_SETFL => {+            // Only O_NONBLOCK and O_APPEND can change; the access mode can't.             file.set_nonblocking(arg & O_NONBLOCK != 0);+            file.set_append(arg & O_APPEND != 0);             Ok(0)         }+        F_GETLK | F_SETLK | F_SETLKW => sys_fcntl_lock(&p, &*file, cmd, arg),         _ => Err(Errno::EINVAL),     } }@@ -1168,7 +1311,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.1: musl prerequisites", // version+        "stage 16: libc batch 1",       // version         "x86_64",                       // machine         "(none)",                       // domainname     ];@@ -1732,3 +1875,472 @@ fn sys_mprotect(p: &process::Process, addr: u64, len: u64, prot: u64) -> Result<     }     p.mm.lock().protect(start, end, prot).map(|_| 0) }++// --- Stage 16: libc batch 1 -------------------------------------------------++/// madvise(addr, len, advice): hints about how memory will be used. Most+/// are just hints (we accept and ignore them); MADV_DONTNEED and MADV_FREE+/// really give private pages back (they read as zeros afterwards).+/// MADV_REMOVE / MADV_WIPEONFORK and unknown values: EINVAL (we can't do+/// them, and saying "done" would be a lie a program might rely on).+fn sys_madvise(addr: u64, len: u64, advice: u64) -> Result<u64, Errno> {+    let discard = match advice {+        MADV_DONTNEED | MADV_FREE => true,+        // NORMAL, RANDOM, SEQUENTIAL, WILLNEED; DONTFORK..NOHUGEPAGE,+        // DONTDUMP, DODUMP; KEEPONFORK, COLD, PAGEOUT, POPULATE_(READ|WRITE).+        0..=3 | 10..=17 | 19..=23 => false,+        _ => return Err(Errno::EINVAL),+    };+    if addr % 4096 != 0 {+        return Err(Errno::EINVAL);+    }+    if len == 0 {+        return Ok(0);+    }+    let end = addr.checked_add(len).ok_or(Errno::EINVAL)?.next_multiple_of(4096);+    if !crate::mm::Mm::range_ok(addr, end) {+        return Err(Errno::ENOMEM);+    }+    if discard {+        current()?.mm.lock().discard(addr, end)?;+    }+    Ok(0)+}++/// fsync / fdatasync: our filesystems write through to the disk on every+/// write (there's no page cache yet), so there's nothing left to flush.+/// Pipes, sockets and ttys can't be synced: EINVAL, like Linux.+fn sys_fsync(fd: u64) -> Result<u64, Errno> {+    let file = current()?.file(fd)?;+    if file.vfs_inode().is_some() { Ok(0) } else { Err(Errno::EINVAL) }+}++/// Longest file truncate/ftruncate may make (bigger: EFBIG). 4 GiB - 1 is+/// FAT's limit, and far more than our RAM, so ramfs can't be asked to+/// allocate something silly either.+const MAX_TRUNCATE: u64 = u32::MAX as u64;++fn set_len_checked(inode: &dyn fs::Inode, len: u64) -> Result<u64, Errno> {+    match inode.kind() {+        InodeKind::Directory => return Err(Errno::EISDIR),+        InodeKind::File => {}+        _ => return Err(Errno::EINVAL),+    }+    if (len as i64) < 0 {+        return Err(Errno::EINVAL);+    }+    if len > MAX_TRUNCATE {+        return Err(Errno::EFBIG);+    }+    inode.set_len(len as usize).map(|_| 0)+}++/// truncate(path, len): cut a file to `len` bytes, or grow it with zeros.+fn sys_truncate(path_ptr: u64, len: u64) -> Result<u64, Errno> {+    let inode = fs::lookup(&user_path(path_ptr)?)?;+    set_len_checked(&*inode, len)+}++/// ftruncate(fd, len): the same through an fd, which must be open for writing.+fn sys_ftruncate(fd: u64, len: u64) -> Result<u64, Errno> {+    let file = current()?.file(fd)?;+    let inode = file.inode().ok_or(Errno::EINVAL)?; // regular files only+    if file.status_flags() & O_ACCMODE == 0 {+        return Err(Errno::EINVAL); // opened O_RDONLY (Linux says EINVAL too)+    }+    set_len_checked(&*inode, len)+}++/// fchmodat(dirfd, path, mode) and fchmodat2 (same + flags). chmod 90 is+/// fchmodat(AT_FDCWD, ...). FAT has no permissions (EPERM), btrfs is+/// read-only here (EROFS). Symlinks have no mode of their own: with+/// AT_SYMLINK_NOFOLLOW on one, EOPNOTSUPP (as Linux).+fn sys_fchmodat(dirfd: u64, path_ptr: u64, mode: u64, flags: u64) -> Result<u64, Errno> {+    if flags & !(AT_SYMLINK_NOFOLLOW | AT_EMPTY_PATH) != 0 {+        return Err(Errno::EINVAL);+    }+    let raw = user_cstr(path_ptr, PATH_MAX - 1)?;+    if raw.is_empty() && flags & AT_EMPTY_PATH != 0 && dirfd as i32 as i64 != AT_FDCWD {+        return sys_fchmod(dirfd, mode);+    }+    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)? };+    if inode.kind() == InodeKind::Symlink {+        return Err(Errno::EOPNOTSUPP);+    }+    inode.set_mode(mode as u32 & 0o7777).map(|_| 0)+}++/// fchmod(fd, mode): files and directories opened by path; others EINVAL.+fn sys_fchmod(fd: u64, mode: u64) -> Result<u64, Errno> {+    let inode = current()?.file(fd)?.vfs_inode().ok_or(Errno::EINVAL)?;+    inode.set_mode(mode as u32 & 0o7777).map(|_| 0)+}++/// utimensat(dirfd, path, *[timespec; 2], flags): set the access and+/// modification times. NULL times = both "now"; a tv_nsec of UTIME_NOW /+/// UTIME_OMIT = now / leave alone. A NULL path means the file `dirfd` is+/// open on (that's how musl does futimens). The change time becomes now.+/// FAT accepts and forgets them (it only has a modification date in a+/// format we don't write); btrfs: EROFS.+fn sys_utimensat(dirfd: u64, path_ptr: u64, times_ptr: u64, flags: u64) -> Result<u64, Errno> {+    if flags & !(AT_SYMLINK_NOFOLLOW | AT_EMPTY_PATH) != 0 {+        return Err(Errno::EINVAL);+    }+    let now = fs::now_timespec();+    let (atime, mtime) = if times_ptr == 0 {+        (Some(now), Some(now))+    } else {+        let raw = user_slice(times_ptr, 32)?;+        let word = |i: usize| i64::from_le_bytes(raw[i * 8..i * 8 + 8].try_into().unwrap());+        let one = |sec: i64, nsec: i64| match nsec {+            UTIME_NOW => Ok(Some(now)),+            UTIME_OMIT => Ok(None),+            0..=999_999_999 => Ok(Some([sec, nsec])),+            _ => Err(Errno::EINVAL),+        };+        (one(word(0), word(1))?, one(word(2), word(3))?)+    };+    let inode = if path_ptr == 0 {+        if flags & AT_SYMLINK_NOFOLLOW != 0 {+            return Err(Errno::EINVAL);+        }+        if dirfd as i32 as i64 == AT_FDCWD {+            return Err(Errno::EFAULT); // Linux: a NULL path needs a real fd+        }+        current()?.file(dirfd)?.vfs_inode().ok_or(Errno::EINVAL)?+    } else {+        let raw = user_cstr(path_ptr, PATH_MAX - 1)?;+        if raw.is_empty() && flags & AT_EMPTY_PATH != 0 && dirfd as i32 as i64 != AT_FDCWD {+            current()?.file(dirfd)?.vfs_inode().ok_or(Errno::EINVAL)?+        } else {+            let path = if raw.is_empty() && flags & AT_EMPTY_PATH != 0 { at_base(dirfd)? } else { path_at(dirfd, &raw)? };+            if flags & AT_SYMLINK_NOFOLLOW != 0 { fs::lookup_nofollow(&path)? } else { fs::lookup(&path)? }+        }+    };+    inode.set_times(atime, mtime).map(|_| 0)+}++/// prlimit64(pid, resource, *new, *old), and getrlimit / setrlimit on+/// myself. `struct rlimit` is {u64 soft, u64 hard}; RLIM_INFINITY = !0.+/// What the kernel enforces: RLIMIT_NOFILE (the fd table size, at most 64).+/// The rest are remembered and inherited (fork), so programs that check+/// them see sensible, consistent numbers.+fn sys_prlimit64(pid: u64, resource: u64, new_ptr: u64, old_ptr: u64) -> Result<u64, Errno> {+    let me = current()?;+    let target = if pid == 0 || pid == me.pid {+        me+    } else {+        process::get(pid).filter(|p| p.status().is_none()).ok_or(Errno::ESRCH)?+    };+    let new = if new_ptr == 0 {+        None+    } else {+        let raw = user_slice(new_ptr, 16)?;+        Some([u64::from_le_bytes(raw[..8].try_into().unwrap()), u64::from_le_bytes(raw[8..].try_into().unwrap())])+    };+    if old_ptr != 0 {+        user_slice_mut(old_ptr, 16)?; // EFAULT before changing anything+    }+    let old = target.rlimit(resource)?;+    if let Some(new) = new {+        target.set_rlimit(resource, new)?;+    }+    if old_ptr != 0 {+        copy_to_user(old_ptr, &old)?;+    }+    Ok(0)+}++/// prctl(option, ...): a grab bag of per-process settings. We know:+/// PR_SET_NAME / PR_GET_NAME (the thread's 16-byte name, `comm`; for the+/// main thread also what `ps` shows), PR_SET/GET_PDEATHSIG (a signal to+/// get when my parent dies), PR_SET/GET_DUMPABLE, PR_SET/GET_NO_NEW_PRIVS,+/// PR_SET/GET_CHILD_SUBREAPER (adopt orphaned descendants, process.rs).+/// Everything else: EINVAL, which is what Linux says to unknown options.+fn sys_prctl(option: u64, a2: u64, a3: u64, a4: u64, a5: u64) -> Result<u64, Errno> {+    let p = current()?;+    match option {+        PR_SET_PDEATHSIG => {+            if a2 > signal::NSIG as u64 {+                return Err(Errno::EINVAL);+            }+            p.pdeathsig.store(a2 as u32, Ordering::Relaxed);+            Ok(0)+        }+        PR_GET_PDEATHSIG => copy_to_user(a2, &(p.pdeathsig.load(Ordering::Relaxed) as i32)).map(|_| 0),+        PR_GET_DUMPABLE => Ok(p.dumpable.load(Ordering::Relaxed) as u64),+        PR_SET_DUMPABLE if a2 <= 1 => {+            p.dumpable.store(a2 as u32, Ordering::Relaxed);+            Ok(0)+        }+        PR_SET_NAME => {+            // Like Linux: up to 15 bytes are taken, a longer name is cut.+            let mut name = [0u8; 16];+            for i in 0..15 {+                let b = user_slice(a2 + i as u64, 1)?[0];+                if b == 0 {+                    break;+                }+                name[i] = b;+            }+            let t = task()?;+            *t.comm.lock() = name;+            if t.tid() == p.pid {+                let len = name.iter().position(|&b| b == 0).unwrap_or(16);+                p.set_name(&String::from_utf8_lossy(&name[..len]));+            }+            Ok(0)+        }+        PR_GET_NAME => {+            let name = *task()?.comm.lock();+            copy_to_user(a2, &name).map(|_| 0)+        }+        PR_SET_CHILD_SUBREAPER => {+            p.child_subreaper.store(a2 != 0, Ordering::Relaxed);+            Ok(0)+        }+        PR_GET_CHILD_SUBREAPER => copy_to_user(a2, &(p.child_subreaper.load(Ordering::Relaxed) as i32)).map(|_| 0),+        PR_SET_NO_NEW_PRIVS if a2 == 1 && a3 == 0 && a4 == 0 && a5 == 0 => {+            p.no_new_privs.store(true, Ordering::Relaxed);+            Ok(0)+        }+        PR_GET_NO_NEW_PRIVS if a2 == 0 && a3 == 0 && a4 == 0 && a5 == 0 => Ok(p.no_new_privs.load(Ordering::Relaxed) as u64),+        _ => Err(Errno::EINVAL),+    }+}++/// Size of `struct robust_list_head` (list, futex_offset, list_op_pending).+pub const ROBUST_LIST_HEAD_SIZE: u64 = 24;++/// set_robust_list(head, len): where this thread's list of held robust+/// mutexes lives. When the thread dies, the kernel walks it and marks each+/// one FUTEX_OWNER_DIED (process.rs, `exit_robust_list`), so the next+/// locker learns the owner died instead of waiting forever.+fn sys_set_robust_list(head: u64, len: u64) -> Result<u64, Errno> {+    if len != ROBUST_LIST_HEAD_SIZE {+        return Err(Errno::EINVAL);+    }+    task()?.robust_list.store(head, Ordering::Relaxed);+    Ok(0)+}++/// get_robust_list(tid, *head, *len): 0 = myself.+fn sys_get_robust_list(tid: u64, head_ptr: u64, len_ptr: u64) -> Result<u64, Errno> {+    let t = if tid == 0 { task()? } else { process::all().iter().find_map(|p| p.task(tid)).ok_or(Errno::ESRCH)? };+    copy_to_user(head_ptr, &t.robust_list.load(Ordering::Relaxed))?;+    copy_to_user(len_ptr, &ROBUST_LIST_HEAD_SIZE)?;+    Ok(0)+}++/// sigaltstack(*new, *old): `stack_t` = {void *ss_sp; int ss_flags; size_t+/// ss_size} (24 bytes with padding). Handlers installed with SA_ONSTACK+/// run on this stack -- how a program can still handle SIGSEGV after+/// overflowing its normal stack. `user_sp`: the caller's stack pointer+/// (to tell whether it's running on the alternate stack right now).+fn sys_sigaltstack(new_ptr: u64, old_ptr: u64, user_sp: u64) -> Result<u64, Errno> {+    let new = if new_ptr == 0 {+        None+    } else {+        let raw = user_slice(new_ptr, 24)?;+        let word = |i: usize| u64::from_le_bytes(raw[i * 8..i * 8 + 8].try_into().unwrap());+        Some((word(0), word(1) as u32, word(2)))+    };+    if old_ptr != 0 {+        user_slice_mut(old_ptr, 24)?;+    }+    let (sp, flags, size) = signal::sys_sigaltstack(&*task()?, new, user_sp)?;+    if old_ptr != 0 {+        copy_to_user(old_ptr, &[sp, flags as u64, size])?;+    }+    Ok(0)+}++/// rt_sigpending(*set, size): which signals are waiting because they're+/// blocked.+fn sys_rt_sigpending(set_ptr: u64, size: u64) -> Result<u64, Errno> {+    if size > 8 {+        return Err(Errno::EINVAL);+    }+    let set = signal::pending_blocked(&*task()?);+    let out = user_slice_mut(set_ptr, size)?;+    out.copy_from_slice(&set.to_le_bytes()[..size as usize]);+    Ok(0)+}++fn read_sigset(ptr: u64, size: u64) -> Result<u64, Errno> {+    if size != 8 {+        return Err(Errno::EINVAL);+    }+    Ok(u64::from_le_bytes(user_slice(ptr, 8)?.try_into().unwrap()))+}++/// rt_sigsuspend(*mask, 8): swap in `mask` and sleep until a signal is+/// handled (always "fails" with EINTR); the old mask comes back after the+/// handler, so there's no window where a signal can slip by unnoticed.+fn sys_rt_sigsuspend(mask_ptr: u64, size: u64) -> Result<u64, Errno> {+    let mask = read_sigset(mask_ptr, size)?;+    signal::set_temporary_mask(mask);+    Err(thread::pause())+}++/// rt_sigtimedwait(*set, *siginfo, *timeout, 8): wait for one of `set`+/// (normally blocked) and *take* it -- no handler runs; the signal number+/// is returned and its siginfo copied out. Timeout NULL = forever, {0, 0}+/// = just look; expired: EAGAIN. Another signal with a handler: EINTR.+/// Timing is rounded up to timer ticks (~55 ms).+fn sys_rt_sigtimedwait(set_ptr: u64, info_ptr: u64, timeout_ptr: u64, size: u64) -> Result<u64, Errno> {+    static SIGWAIT: thread::WaitQueue = thread::WaitQueue::new();+    let set = signal::blockable(read_sigset(set_ptr, size)?);+    let timeout = if timeout_ptr == 0 { None } else { Some(read_timespec_us(timeout_ptr)?) };+    let t = task()?;+    let got = match signal::take_waited(&t, set) {+        Some(got) => Ok(got),+        None if timeout == Some(0) => Err(Errno::EAGAIN),+        None => {+            // Let the waited-for signals wake us (they're usually blocked).+            let blocked = without_interrupts_blocked(&t);+            signal::set_temporary_mask(blocked & !set);+            let r = match timeout {+                Some(us) => SIGWAIT+                    .wait_until_deadline_intr(crate::time::deadline_after_us(us), || signal::take_waited(&t, set))+                    .and_then(|got| got.ok_or(Errno::EAGAIN)),+                None => SIGWAIT.wait_until_intr(|| signal::take_waited(&t, set)),+            };+            signal::restore_temporary_mask();+            r.map_err(|e| if e == Errno::EAGAIN { e } else { Errno::EINTR })+        }+    }?;+    let (sig, info) = got;+    if info_ptr != 0 {+        copy_to_user(info_ptr, &info)?;+    }+    Ok(sig as u64)+}++/// The thread's blocked set right now.+fn without_interrupts_blocked(t: &process::Task) -> u64 {+    x86_64::instructions::interrupts::without_interrupts(|| t.sig.lock().blocked)+}++/// rt_sigqueueinfo(tgid, sig, *siginfo) / rt_tgsigqueueinfo(tgid, tid,+/// sig, *siginfo): send a signal with my own siginfo (sigqueue() puts a+/// value in it). Real-time signals (32..64) queue up: sending one 3 times+/// delivers it 3 times, each with its own value. Pretending to be the+/// kernel or kill() (si_code >= 0, or SI_TKILL) is only allowed toward+/// myself: EPERM. A full queue: EAGAIN.+fn sys_rt_sigqueueinfo(tgid: u64, tid: Option<u64>, sig: u64, info_ptr: u64) -> Result<u64, Errno> {+    if sig > signal::NSIG as u64 {+        return Err(Errno::EINVAL);+    }+    let raw = user_slice(info_ptr, core::mem::size_of::<signal::SigInfo>() as u64)?;+    let mut info = unsafe { core::ptr::read_unaligned(raw.as_ptr() as *const signal::SigInfo) };+    let me = task()?;+    let (target_id, my_id) = match tid {+        Some(tid) => (tid, me.tid()),+        None => (tgid, me.process.pid),+    };+    if (info.code >= 0 || info.code == signal::SI_TKILL) && target_id != my_id {+        return Err(Errno::EPERM);+    }+    let p = process::get(tgid).filter(|p| p.status().is_none()).ok_or(Errno::ESRCH)?;+    let thread = match tid {+        Some(tid) => Some(p.task(tid).ok_or(Errno::ESRCH)?),+        None => None,+    };+    if sig == 0 {+        return Ok(0);+    }+    if signal::queue_full(&p) {+        return Err(Errno::EAGAIN);+    }+    info.signo = sig as i32;+    match thread {+        Some(t) => signal::send_thread(&t, sig as u32, info),+        None => signal::send(&p, sig as u32, info),+    }+    Ok(0)+}++/// fcntl(F_GETLK / F_SETLK / F_SETLKW, *flock): POSIX record locks (see+/// fs/lock.rs). `struct flock` = {i16 l_type, i16 l_whence, i64 l_start,+/// i64 l_len, i32 l_pid} (32 bytes with padding).+fn sys_fcntl_lock(p: &Arc<process::Process>, file: &dyn File, cmd: u64, arg: u64) -> Result<u64, Errno> {+    let raw = user_slice(arg, 32)?;+    let l_type = i16::from_le_bytes([raw[0], raw[1]]);+    let l_whence = i16::from_le_bytes([raw[2], raw[3]]);+    let l_start = i64::from_le_bytes(raw[8..16].try_into().unwrap());+    let l_len = i64::from_le_bytes(raw[16..24].try_into().unwrap());+    // Where the range starts from: SEEK_SET, SEEK_CUR or SEEK_END.+    let base = match l_whence {+        0 => 0,+        1 => file.seek(0, 1).unwrap_or(0) as i64,+        2 => file.stat()?.size as i64,+        _ => return Err(Errno::EINVAL),+    };+    let start = base.checked_add(l_start).ok_or(Errno::EINVAL)?;+    let (start, end) = match l_len {+        0 => (start, u64::MAX as i128), // to the end of the file, and beyond+        n if n > 0 => (start, start as i128 + n as i128),+        n => (start.checked_add(n).ok_or(Errno::EINVAL)?, start as i128), // the bytes *before* start+    };+    if start < 0 || end > u64::MAX as i128 {+        return Err(Errno::EINVAL);+    }+    let (start, end) = (start as u64, end as u64);+    let access = file.status_flags() & O_ACCMODE;+    let want = match l_type {+        F_RDLCK if access == 1 => return Err(Errno::EBADF), // reading needs a readable fd+        F_WRLCK if access == 0 => return Err(Errno::EBADF), // writing needs a writable fd+        F_RDLCK => Some(false),+        F_WRLCK => Some(true),+        F_UNLCK => None,+        _ => return Err(Errno::EINVAL),+    };+    let key = fs::lock::key_of(file);+    match cmd {+        F_GETLK => {+            let write = want.ok_or(Errno::EINVAL)?;+            let out = user_slice_mut(arg, 32)?;+            match fs::lock::conflict(key, fs::lock::Lock { pid: p.pid, start, end, write }) {+                Some(c) => {+                    let len = if c.end == u64::MAX { 0 } else { (c.end - c.start) as i64 };+                    out[0..2].copy_from_slice(&(if c.write { F_WRLCK } else { F_RDLCK }).to_le_bytes());+                    out[2..4].copy_from_slice(&0i16.to_le_bytes());+                    out[8..16].copy_from_slice(&(c.start as i64).to_le_bytes());+                    out[16..24].copy_from_slice(&len.to_le_bytes());+                    out[24..28].copy_from_slice(&(c.pid as i32).to_le_bytes());+                }+                None => out[0..2].copy_from_slice(&F_UNLCK.to_le_bytes()),+            }+            Ok(0)+        }+        F_SETLKW if want.is_some() => fs::lock::set_wait(key, p.pid, start, end, want.unwrap()).map(|_| 0),+        _ => fs::lock::set(key, p.pid, start, end, want).map(|_| 0),+    }+}++/// sched_getaffinity(tid, len, *mask) / sched_setaffinity: which CPUs a+/// thread may run on. Ours run on any CPU, so "get" reports every online+/// CPU (that's how musl's sysconf(_SC_NPROCESSORS_ONLN) and `nproc` count+/// them) and "set" only checks the mask names at least one online CPU (we+/// don't pin threads). Like the raw Linux call, "get" returns the number+/// of bytes written (8: one 64-bit word of CPU bits), not 0.+fn sys_sched_affinity(tid: u64, len: u64, mask_ptr: u64, set: bool) -> Result<u64, Errno> {+    if tid != 0 && !process::all().iter().any(|p| p.task(tid).is_some()) {+        return Err(Errno::ESRCH);+    }+    let cpus = crate::smp::cpu_count().min(64);+    let online: u64 = if cpus == 64 { u64::MAX } else { (1u64 << cpus) - 1 };+    if set {+        let raw = user_slice(mask_ptr, len.min(8))?;+        let mut word = [0u8; 8];+        word[..raw.len()].copy_from_slice(raw);+        return if u64::from_le_bytes(word) & online != 0 { Ok(0) } else { Err(Errno::EINVAL) };+    }+    if len < 8 || len % 8 != 0 {+        return Err(Errno::EINVAL); // Linux: room for whole longs, all CPUs+    }+    copy_to_user(mask_ptr, &online)?;+    Ok(8)+}
@@ -0,0 +1,116 @@+//! Stage 16, "libc batch 1": the system calls musl and dash use once a+//! program is running -- signal waiting (rt_sigsuspend, sigtimedwait,+//! sigqueueinfo, sigaltstack), umask and rlimits, fcntl record locks,+//! truncate/fsync/chmod/utimensat, prctl, sched_getaffinity, madvise,+//! robust futex lists, and what happens to orphaned children. The detailed+//! checks live in user/libctest.rs (exit code = failed checks); this file+//! runs each group and checks nothing is left behind.+#![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, 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("libctest");+    spawn.args.extend(args.iter().map(|a| String::from(*a)));+    let p = process::spawn_path("/mnt/disk0/bin/libctest", spawn).expect("spawn failed");+    process::wait(&p)+}++fn counts() -> (usize, usize, usize) {+    thread::sleep_ticks(3); // let exited threads' stacks be freed+    (thread::thread_count(), process::count(), memory::frames_in_use())+}++/// Run one group twice (the first warms caches), check it passes and+/// threads, processes, memory frames and file locks come back to where+/// they were.+fn run_without_leaks(group: &str) {+    assert_eq!(run(&[group]), ExitStatus::Exited(0), "libctest {} failed (warm-up)", group);+    let before = counts();+    assert_eq!(run(&[group]), ExitStatus::Exited(0), "libctest {} failed", group);+    assert_eq!(counts(), before, "libctest {} leaked something", group);+    assert_eq!(fs::lock::count(), 0, "libctest {} left file locks behind", group);+}++/// dash's `wait`: block SIGCHLD, fork, rt_sigsuspend until the handler ran.+/// Before stage 16 rt_sigsuspend was ENOSYS and dash spun forever here.+#[test_case]+fn dash_wait_with_sigsuspend() {+    run_without_leaks("wait");+}++/// rt_sigpending, rt_sigtimedwait, queued real-time signals, sigaltstack.+#[test_case]+fn signal_waiting_and_queueing() {+    run_without_leaks("signals");+}++/// fcntl: F_GETFL/F_SETFL, F_DUPFD vs RLIMIT_NOFILE, record locks between+/// processes (conflicts, waiting, EDEADLK, dropped on close and exit).+#[test_case]+fn fcntl_flags_and_record_locks() {+    run_without_leaks("locks");+}++/// truncate/ftruncate, fsync, chmod family, umask, utimensat on ramfs,+/// ext2 and FAT; EROFS on btrfs.+#[test_case]+fn file_metadata_calls() {+    run_without_leaks("files");+}++/// rlimits, prctl, sched_getaffinity, madvise, robust futex lists.+#[test_case]+fn process_odds_and_ends() {+    run_without_leaks("misc");+}++/// A process exits leaving a running child and a zombie child: both are+/// cleaned up (their frames come back); a PR_SET_CHILD_SUBREAPER ancestor+/// adopts and reaps orphans instead.+#[test_case]+fn orphans_are_freed() {+    assert_eq!(run(&["orphans"]), ExitStatus::Exited(0), "libctest orphans failed (warm-up)");+    thread::sleep_ticks(10);+    let before = counts();+    for _ in 0..3 {+        assert_eq!(run(&["orphans"]), ExitStatus::Exited(0), "libctest orphans failed");+    }+    thread::sleep_ticks(10); // the last orphan is still sleeping 40 ms+    assert_eq!(counts(), before, "orphaned children leaked");+}++/// `--trace` names syscalls it doesn't know by number.+#[test_case]+fn trace_names_unknown_syscalls() {+    assert_eq!(syscall::trace_name(130), "rt_sigsuspend");+    assert_eq!(syscall::trace_name(204), "sched_getaffinity");+    assert_eq!(syscall::trace_name(999), "syscall_999");+}++#[panic_handler]+fn panic(info: &PanicInfo) -> ! {+    rustbelt::test_panic_handler(info)+}
@@ -176,7 +176,7 @@ fn scripted_shell_session() {         "piped text\n",         "wc -l\n2\n",         "saved\nmore\n",-        "Rustbelt 0.15.1",+        "Rustbelt 0.16.0",         "B:/ $ exit 3\n",     ] {         assert!(out.contains(expected), "missing {:?} in:\n{}", expected, out);
@@ -0,0 +1,655 @@+//! libctest -- stage 16, "libc batch 1": the system calls a C library+//! (musl) and its shell (dash) need beyond open/read/write. Run with one+//! group name; exit code = number of failed checks:+//!   wait     dash's `wait`: block SIGCHLD, fork, rt_sigsuspend until the+//!            handler ran (before 16 rt_sigsuspend was ENOSYS: dash spun+//!            forever)+//!   signals  rt_sigpending, rt_sigtimedwait, rt_sigqueueinfo (real-time+//!            signals queue up), sigaltstack + SA_ONSTACK, unknown sa_flags+//!   locks    fcntl: F_GETFL/F_SETFL, F_DUPFD limits, record locks between+//!            two processes (F_GETLK/F_SETLK/F_SETLKW, dropped on close/exit)+//!   files    truncate/ftruncate, fsync, chmod family, umask, utimensat and+//!            real stat times, on ramfs, ext2, FAT and (read-only) btrfs+//!   misc     rlimits, prctl, sched_getaffinity, madvise, robust futex lists+//!   orphans  children whose parent exits without waiting (the kernel test+//!            checks their memory comes back), and PR_SET_CHILD_SUBREAPER+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++extern crate alloc;++use core::sync::atomic::{AtomicI64, AtomicU64, Ordering};+use ulib::*;++// Stage 16 syscall numbers (Linux's).+const SYS_MADVISE: u64 = 28;+const SYS_FSYNC: u64 = 74;+const SYS_FDATASYNC: u64 = 75;+const SYS_TRUNCATE: u64 = 76;+const SYS_FTRUNCATE: u64 = 77;+const SYS_CHMOD: u64 = 90;+const SYS_FCHMOD: u64 = 91;+const SYS_UMASK: u64 = 95;+const SYS_GETRLIMIT: u64 = 97;+const SYS_RT_SIGPENDING: u64 = 127;+const SYS_RT_SIGTIMEDWAIT: u64 = 128;+const SYS_RT_SIGQUEUEINFO: u64 = 129;+const SYS_RT_SIGSUSPEND: u64 = 130;+const SYS_SIGALTSTACK: u64 = 131;+const SYS_PRCTL: u64 = 157;+const SYS_SETRLIMIT: u64 = 160;+const SYS_GETTID: u64 = 186;+const SYS_SCHED_GETAFFINITY: u64 = 204;+const SYS_FCHMODAT: u64 = 268;+const SYS_SET_ROBUST_LIST: u64 = 273;+const SYS_GET_ROBUST_LIST: u64 = 274;+const SYS_UTIMENSAT: u64 = 280;+const SYS_PRLIMIT64: u64 = 302;++const O_RDWR: u64 = 2;+const O_NONBLOCK: u64 = 0o4000;+const F_DUPFD: u64 = 0;+const F_GETFL: u64 = 3;+const F_SETFL: u64 = 4;+const F_GETLK: u64 = 5;+const F_SETLK: u64 = 6;+const F_SETLKW: u64 = 7;+const F_RDLCK: i16 = 0;+const F_WRLCK: i16 = 1;+const F_UNLCK: i16 = 2;+const EROFS: i64 = 30;+const EMFILE: i64 = 24;+const ENOMEM: i64 = 12;+const EDEADLK: i64 = 35;+const EOPNOTSUPP: i64 = 95;+const SA_ONSTACK: u64 = 0x0800_0000;+const SS_ONSTACK: u32 = 1;+const SS_DISABLE: u32 = 2;+const SI_QUEUE: i32 = -1;+const RLIMIT_STACK: u64 = 3;+const RLIMIT_NOFILE: u64 = 7;+const UTIME_NOW: i64 = (1 << 30) - 1;+const UTIME_OMIT: i64 = (1 << 30) - 2;+const FUTEX_OWNER_DIED: u32 = 0x4000_0000;++fn sys4(nr: u64, a: u64, b: u64, c: u64, d: u64) -> i64 {+    syscall5(nr, a, b, c, d, 0)+}++/// `struct flock`.+#[repr(C)]+#[derive(Clone, Copy, Default)]+struct Flock {+    l_type: i16,+    l_whence: i16,+    l_start: i64,+    l_len: i64,+    l_pid: i32,+    _pad: i32,+}++fn lock(fd: i64, cmd: u64, l_type: i16, start: i64, len: i64) -> (i64, Flock) {+    let mut fl = Flock { l_type, l_whence: SEEK_SET as i16, l_start: start, l_len: len, ..Default::default() };+    let r = syscall3(SYS_FCNTL, fd as u64, cmd, &mut fl as *mut Flock as u64);+    (r, fl)+}++fn cstr(s: &str) -> alloc::vec::Vec<u8> {+    let mut v = alloc::vec::Vec::from(s.as_bytes());+    v.push(0);+    v+}++fn path_call(nr: u64, path: &str, b: u64) -> i64 {+    with_cpath(path, |p| syscall3(nr, p, b, 0))+}++fn file_size(path: &str) -> i64 {+    let mut st = Stat::default();+    if stat(path, &mut st) < 0 { -1 } else { st.size }+}++fn perm(path: &str) -> u32 {+    let mut st = Stat::default();+    stat(path, &mut st);+    st.perm()+}++fn write_file(path: &str, data: &[u8]) -> i64 {+    let fd = open(path, O_WRONLY | O_CREAT | O_TRUNC);+    if fd < 0 {+        return fd;+    }+    let n = write(fd, data);+    close(fd);+    n+}++fn umask(mask: u64) -> u64 {+    syscall3(SYS_UMASK, mask, 0, 0) as u64+}++static GOT: AtomicI64 = AtomicI64::new(0);+static SP_SEEN: AtomicU64 = AtomicU64::new(0);+static ONSTACK_FLAGS: AtomicU64 = AtomicU64::new(0);+static CHANGE_ON_STACK: AtomicI64 = AtomicI64::new(0);++extern "C" fn count_handler(_sig: i32) {+    GOT.fetch_add(1, Ordering::SeqCst);+}++/// SA_ONSTACK handler: where is my stack, does sigaltstack say SS_ONSTACK,+/// and may I change the alternate stack while on it (no: EPERM)?+extern "C" fn altstack_handler(_sig: i32) {+    let local = 0u8;+    SP_SEEN.store(&local as *const u8 as u64, Ordering::SeqCst);+    let mut old = [0u64; 3];+    syscall3(SYS_SIGALTSTACK, 0, old.as_mut_ptr() as u64, 0);+    ONSTACK_FLAGS.store(old[1], Ordering::SeqCst);+    let new = [0u64, SS_DISABLE as u64, 0];+    CHANGE_ON_STACK.store(syscall3(SYS_SIGALTSTACK, new.as_ptr() as u64, 0, 0), Ordering::SeqCst);+}++fn main() -> i64 {+    let group = arg(1).unwrap_or("");+    let mut c = Checks::new("libctest");+    match group {+        "wait" => wait_group(&mut c),+        "signals" => signals(&mut c),+        "locks" => locks(&mut c),+        "files" => files(&mut c),+        "misc" => misc(&mut c),+        "orphans" => orphans(&mut c),+        _ => c.check(false, "usage: libctest wait|signals|locks|files|misc|orphans"),+    }+    c.done()+}++// --- wait: what dash does for `wait` / a foreground job ----------------------++fn wait_group(c: &mut Checks) {+    GOT.store(0, Ordering::SeqCst);+    signal(SIGCHLD, count_handler);+    let old = sigprocmask(SIG_BLOCK, sigbit(SIGCHLD)) as u64;+    let pid = fork();+    if pid == 0 {+        sleep_ms(60);+        exit(7);+    }+    // dash: while no child changed state, sigsuspend(old mask) -- each+    // return is a handled signal (EINTR). Bounded so a broken kernel fails+    // the check instead of hanging the test like dash did.+    let empty = old & !sigbit(SIGCHLD);+    let mut rounds = 0;+    let mut all_eintr = true;+    while GOT.load(Ordering::SeqCst) == 0 && rounds < 20 {+        let r = syscall3(SYS_RT_SIGSUSPEND, &empty as *const u64 as u64, 8, 0);+        all_eintr &= r == -EINTR;+        rounds += 1;+    }+    c.check(all_eintr, "rt_sigsuspend returns EINTR after a handler ran");+    c.check(GOT.load(Ordering::SeqCst) == 1 && rounds == 1, "one sigsuspend: woken by the child's SIGCHLD");+    let mask_now = sigprocmask(SIG_BLOCK, 0) as u64;+    c.check(mask_now & sigbit(SIGCHLD) != 0, "the old mask (SIGCHLD blocked) is back afterwards");+    let st = wait4(pid, 0);+    c.check(matches!(st, Ok((p, s)) if p == pid && wexitstatus(s) == 7), "wait4 then reaps the child (status 7)");+    c.check(syscall3(SYS_RT_SIGSUSPEND, &empty as *const u64 as u64, 4, 0) == -EINVAL, "rt_sigsuspend with size 4: EINVAL");+    // SIGCHLD that arrived while blocked: sigsuspend returns at once.+    let pid = fork();+    if pid == 0 {+        exit(0);+    }+    while try_wait(pid) == -EAGAIN && GOT.load(Ordering::SeqCst) == 1 {+        sleep_ms(5); // until the child is a zombie (SIGCHLD pending)+    }+    sleep_ms(20);+    let r = syscall3(SYS_RT_SIGSUSPEND, &empty as *const u64 as u64, 8, 0);+    c.check(r == -EINTR && GOT.load(Ordering::SeqCst) == 2, "a SIGCHLD already pending ends sigsuspend right away");+    wait4(-1, 0).ok();+    sigprocmask(SIG_SETMASK, old);+}++// --- signals -----------------------------------------------------------------++fn timedwait(set: u64, info: &mut [u64; 16], timeout: Option<[i64; 2]>) -> i64 {+    let ts = timeout.unwrap_or([0, 0]);+    let tsp = if timeout.is_some() { &ts as *const [i64; 2] as u64 } else { 0 };+    sys4(SYS_RT_SIGTIMEDWAIT, &set as *const u64 as u64, info.as_mut_ptr() as u64, tsp, 8)+}++fn sigqueue(pid: i64, sig: i32, code: i32, value: u64) -> i64 {+    let mut info = [0u64; 16];+    info[0] = sig as u32 as u64; // si_signo (si_errno = 0)+    info[1] = code as u32 as u64; // si_code (low half of the 2nd word)+    info[2] = getpid() as u64; // si_pid, si_uid+    info[3] = value; // si_value+    syscall3(SYS_RT_SIGQUEUEINFO, pid as u64, sig as u64, info.as_ptr() as u64)+}++fn signals(c: &mut Checks) {+    let mut info = [0u64; 16];+    // rt_sigpending+    signal(SIGUSR1, count_handler);+    GOT.store(0, Ordering::SeqCst);+    let old = sigprocmask(SIG_BLOCK, sigbit(SIGUSR1) | sigbit(SIGUSR2)) as u64;+    kill(getpid(), SIGUSR1);+    let mut pending = 0u64;+    c.check(syscall3(SYS_RT_SIGPENDING, &mut pending as *mut u64 as u64, 8, 0) == 0, "rt_sigpending: 0");+    c.check(pending & sigbit(SIGUSR1) != 0 && GOT.load(Ordering::SeqCst) == 0, "a blocked SIGUSR1 shows as pending, not delivered");+    c.check(syscall3(SYS_RT_SIGPENDING, &mut pending as *mut u64 as u64, 16, 0) == -EINVAL, "rt_sigpending with size 16: EINVAL");+    sigprocmask(SIG_UNBLOCK, sigbit(SIGUSR1));+    c.check(GOT.load(Ordering::SeqCst) == 1, "unblocking delivers it");++    // rt_sigtimedwait: takes a pending signal without running a handler.+    kill(getpid(), SIGUSR2);+    let r = timedwait(sigbit(SIGUSR2), &mut info, Some([0, 0]));+    c.check(r == SIGUSR2 as i64 && info[0] as u32 == SIGUSR2 as u32, "sigtimedwait takes a pending SIGUSR2 (and fills siginfo)");+    c.check(timedwait(sigbit(SIGUSR2), &mut info, Some([0, 0])) == -EAGAIN, "nothing pending, timeout {0,0}: EAGAIN at once");+    let t0 = now_us();+    let r = timedwait(sigbit(SIGUSR2), &mut info, Some([0, 120_000_000]));+    c.check(r == -EAGAIN && now_us() - t0 >= 100_000, "nothing comes within 120 ms: EAGAIN after the timeout");+    let bad = timedwait(sigbit(SIGUSR2), &mut info, Some([0, 1_000_000_000]));+    c.check(bad == -EINVAL, "tv_nsec of 1e9: EINVAL");+    let me = getpid();+    let child = fork();+    if child == 0 {+        sleep_ms(50);+        kill(me, SIGUSR2);+        exit(0);+    }+    let r = timedwait(sigbit(SIGUSR2), &mut info, None);+    c.check(r == SIGUSR2 as i64 && (info[2] & 0xffff_ffff) as i64 == child, "sigtimedwait sleeps until another process sends it (si_pid = sender)");+    wait(child);++    // rt_sigqueueinfo: real-time signals queue, each with its own value.+    let rt = 40;+    sigprocmask(SIG_BLOCK, sigbit(rt));+    for v in [11u64, 22, 33] {+        sigqueue(me, rt, SI_QUEUE, v);+    }+    let mut values = alloc::vec::Vec::new();+    while timedwait(sigbit(rt), &mut info, Some([0, 0])) == rt as i64 {+        values.push(info[3]);+        c.check(info[1] as u32 as i32 == SI_QUEUE, "queued signal keeps si_code SI_QUEUE");+    }+    c.check(values == [11, 22, 33], "a real-time signal sent 3 times arrives 3 times, in order, with its values");+    sigqueue(me, SIGUSR2, SI_QUEUE, 1);+    sigqueue(me, SIGUSR2, SI_QUEUE, 2);+    let first = timedwait(sigbit(SIGUSR2), &mut info, Some([0, 0]));+    let second = timedwait(sigbit(SIGUSR2), &mut info, Some([0, 0]));+    c.check(first == SIGUSR2 as i64 && second == -EAGAIN, "a standard signal sent twice is pending once");+    let child = fork();+    if child == 0 {+        sleep_ms(200);+        exit(0);+    }+    c.check(sigqueue(child, SIGUSR2, 0, 0) == -EPERM, "si_code >= 0 (pretending to be kill) to another process: EPERM");+    c.check(sigqueue(child, 0, SI_QUEUE, 0) == 0, "signal 0 to a live process: 0");+    kill(child, SIGKILL);+    wait(child);+    c.check(sigqueue(99_999, SIGUSR2, SI_QUEUE, 0) == -ESRCH, "to a pid that doesn't exist: ESRCH");++    // sigaltstack+    let mut old_ss = [0u64; 3];+    syscall3(SYS_SIGALTSTACK, 0, old_ss.as_mut_ptr() as u64, 0);+    c.check(old_ss[1] as u32 == SS_DISABLE, "no alternate stack at first (SS_DISABLE)");+    let size = 64 * 1024u64;+    let stack = mmap(0, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) as u64;+    let small = [stack, 0, 1024];+    c.check(syscall3(SYS_SIGALTSTACK, small.as_ptr() as u64, 0, 0) == -ENOMEM, "a 1 KiB alternate stack: ENOMEM");+    let badflags = [stack, 0x10, size];+    c.check(syscall3(SYS_SIGALTSTACK, badflags.as_ptr() as u64, 0, 0) == -EINVAL, "unknown ss_flags: EINVAL");+    let ss = [stack, 0, size];+    c.check(syscall3(SYS_SIGALTSTACK, ss.as_ptr() as u64, 0, 0) == 0, "set a 64 KiB alternate stack");+    sigaction_raw(SIGUSR1, altstack_handler as usize, SA_ONSTACK, 0);+    kill(me, SIGUSR1);+    let sp = SP_SEEN.load(Ordering::SeqCst);+    c.check((stack..stack + size).contains(&sp), "an SA_ONSTACK handler runs on the alternate stack");+    c.check(ONSTACK_FLAGS.load(Ordering::SeqCst) as u32 == SS_ONSTACK, "inside it, sigaltstack reports SS_ONSTACK");+    c.check(CHANGE_ON_STACK.load(Ordering::SeqCst) == -EPERM, "changing the alternate stack while on it: EPERM");+    syscall3(SYS_SIGALTSTACK, 0, old_ss.as_mut_ptr() as u64, 0);+    c.check(old_ss == [stack, 0, size], "afterwards it's still set, flags 0 (not on it)");+    let off = [0u64, SS_DISABLE as u64, 0];+    syscall3(SYS_SIGALTSTACK, off.as_ptr() as u64, 0, 0);+    munmap(stack, size);++    // Unknown sa_flags bits aren't stored (musl checks what it got back).+    let act = SigAction { handler: count_handler as usize, flags: 0x100 | 0x0400_0000, restorer: 0, mask: 0 };+    let mut back = SigAction::default();+    syscall5(SYS_RT_SIGACTION, SIGUSR2 as u64, &act as *const _ as u64, 0, 8, 0);+    syscall5(SYS_RT_SIGACTION, SIGUSR2 as u64, 0, &mut back as *mut _ as u64, 8, 0);+    c.check(back.flags & 0x100 == 0 && back.flags & 0x0400_0000 != 0, "unknown sa_flags bits are dropped, known ones kept");+    sigaction_raw(SIGUSR1, SIG_DFL, 0, 0);+    sigaction_raw(SIGUSR2, SIG_DFL, 0, 0);+    sigprocmask(SIG_SETMASK, old);+}++// --- locks -------------------------------------------------------------------++fn locks(c: &mut Checks) {+    let path = "/tmp/libctest-locks";+    write_file(path, b"0123456789abcdefghijklmnopqrstuvwxyz");+    // F_GETFL tells the access mode and O_APPEND; F_SETFL changes O_APPEND.+    let ro = open(path, O_RDONLY);+    let wa = open(path, O_WRONLY | O_APPEND);+    c.check(fcntl(ro, F_GETFL, 0) & 3 == 0, "F_GETFL of an O_RDONLY fd: O_RDONLY");+    c.check(fcntl(wa, F_GETFL, 0) as u64 & (3 | O_APPEND) == O_WRONLY | O_APPEND, "F_GETFL: O_WRONLY | O_APPEND");+    fcntl(wa, F_SETFL, O_NONBLOCK);+    c.check(fcntl(wa, F_GETFL, 0) as u64 & (O_APPEND | O_NONBLOCK) == O_NONBLOCK, "F_SETFL clears O_APPEND, sets O_NONBLOCK");+    c.check(lock(ro, F_SETLK, F_WRLCK, 0, 1).0 == -EBADF, "a write lock through a read-only fd: EBADF");+    c.check(lock(wa, F_SETLK, F_RDLCK, 0, 1).0 == -EBADF, "a read lock through a write-only fd: EBADF");+    c.check(lock(ro, F_SETLK, 7, 0, 1).0 == -EINVAL, "l_type 7: EINVAL");+    c.check(lock(ro, F_SETLK, F_RDLCK, -5, 1).0 == -EINVAL, "a range starting before 0: EINVAL");+    close(ro);+    close(wa);++    // Two processes.+    let fd = open(path, O_RDWR);+    c.check(lock(fd, F_SETLK, F_WRLCK, 0, 10).0 == 0, "parent: write lock on bytes 0..10");+    let (r, fl) = lock(fd, F_GETLK, F_WRLCK, 0, 10);+    c.check(r == 0 && fl.l_type == F_UNLCK, "F_GETLK: my own locks never conflict");+    let parent = getpid();+    let (rd, wr) = pipe().unwrap();+    let child = fork();+    if child == 0 {+        let mut k = Checks::new("libctest-child");+        let fd = open(path, O_RDWR);+        let (r, fl) = lock(fd, F_GETLK, F_RDLCK, 5, 1);+        k.check(r == 0 && fl.l_type == F_WRLCK && fl.l_pid as i64 == parent && fl.l_start == 0 && fl.l_len == 10, "child: F_GETLK names the parent's lock");+        k.check(lock(fd, F_SETLK, F_RDLCK, 9, 5).0 == -EAGAIN, "child: overlapping F_SETLK: EAGAIN");+        k.check(lock(fd, F_SETLK, F_WRLCK, 10, 10).0 == 0, "child: bytes 10..20 are free");+        write(wr, b"x"); // tell the parent we're about to wait+        let t0 = now_us();+        let r = lock(fd, F_SETLKW, F_WRLCK, 0, 10).0;+        k.check(r == 0 && now_us() - t0 >= 50_000, "child: F_SETLKW waits until the parent's lock goes away");+        // The parent now asks for 10..20 (ours) with F_SETLKW and sleeps;+        // us asking for its 30..40 would close the circle: EDEADLK.+        sleep_ms(100);+        k.check(lock(fd, F_SETLKW, F_WRLCK, 30, 5).0 == -EDEADLK, "child: a lock wait that would deadlock: EDEADLK");+        exit(k.done()); // exiting drops all our locks+    }+    let mut b = [0u8; 1];+    read(rd, &mut b);+    sleep_ms(100);+    c.check(lock(fd, F_SETLK, F_WRLCK, 30, 10).0 == 0, "parent: write lock on 30..40");+    let fd2 = open(path, O_RDONLY);+    close(fd2); // closing *any* fd of the file drops my locks on it+    // (the child now gets 0..10; we wait for its 10..20, which it holds+    // until it exits)+    c.check(lock(fd, F_SETLK, F_WRLCK, 30, 10).0 == 0, "parent: 30..40 again (the close dropped it)");+    let r = lock(fd, F_SETLKW, F_WRLCK, 10, 10).0;+    c.check(r == 0, "parent: F_SETLKW on the child's range succeeds once it exits");+    c.check(matches!(wait4(child, 0), Ok((_, s)) if wexitstatus(s) == 0), "child's checks passed");+    c.check(lock(fd, F_SETLK, F_WRLCK, 0, 0).0 == 0, "the whole file is free after the child exited");+    c.check(lock(fd, F_SETLK, F_UNLCK, 0, 0).0 == 0, "F_UNLCK");+    close(fd);+    close(rd);+    close(wr);+    unlink(path);++    // RLIMIT_NOFILE is the fd table's real size; F_DUPFD honours it.+    let mut lim = [0u64; 2];+    syscall3(SYS_GETRLIMIT, RLIMIT_NOFILE, lim.as_mut_ptr() as u64, 0);+    c.check(lim == [64, 64], "RLIMIT_NOFILE is 64 (the fd table size)");+    c.check(fcntl(0, F_DUPFD, 64) == -EINVAL, "F_DUPFD at 64: EINVAL");+    let small = [8u64, 64];+    syscall3(SYS_SETRLIMIT, RLIMIT_NOFILE, small.as_ptr() as u64, 0);+    c.check(fcntl(0, F_DUPFD, 8) == -EINVAL, "after setrlimit(NOFILE, 8): F_DUPFD at 8 is EINVAL");+    let mut fds = alloc::vec::Vec::new();+    loop {+        let fd = dup(0);+        if fd < 0 {+            c.check(fd == -EMFILE && fds.iter().all(|&f| f < 8), "dup fails with EMFILE at the soft limit");+            break;+        }+        fds.push(fd);+    }+    for fd in fds {+        close(fd);+    }+    let back = [64u64, 64];+    c.check(syscall3(SYS_SETRLIMIT, RLIMIT_NOFILE, back.as_ptr() as u64, 0) == 0, "raising it back up to the hard limit");+}++// --- files -------------------------------------------------------------------++fn files(c: &mut Checks) {+    // truncate / ftruncate on ramfs, ext2, FAT.+    for dir in ["/tmp", "/mnt/disk1", "/mnt/disk0"] {+        let path = alloc::format!("{}/trunc16.txt", dir);+        write_file(&path, b"hello world");+        let fd = open(&path, O_RDWR);+        c.check(syscall3(SYS_FTRUNCATE, fd as u64, 5, 0) == 0 && file_size(&path) == 5, &alloc::format!("{}: ftruncate to 5 bytes", dir));+        c.check(syscall3(SYS_FTRUNCATE, fd as u64, 9000, 0) == 0 && file_size(&path) == 9000, &alloc::format!("{}: ftruncate grows to 9000", dir));+        let mut buf = [0xffu8; 16];+        let n = syscall6(SYS_PREAD64_NR, fd as u64, buf.as_mut_ptr() as u64, 16, 0, 0, 0);+        c.check(n == 16 && &buf[..5] == b"hello" && buf[5..].iter().all(|&b| b == 0), &alloc::format!("{}: the old bytes stay, the new ones are zeros", dir));+        c.check(syscall3(SYS_FSYNC, fd as u64, 0, 0) == 0 && syscall3(SYS_FDATASYNC, fd as u64, 0, 0) == 0, &alloc::format!("{}: fsync, fdatasync: 0", dir));+        close(fd);+        c.check(path_call(SYS_TRUNCATE, &path, 3) == 0 && file_size(&path) == 3, &alloc::format!("{}: truncate(path, 3)", dir));+        let ro = open(&path, O_RDONLY);+        c.check(syscall3(SYS_FTRUNCATE, ro as u64, 0, 0) == -EINVAL, &alloc::format!("{}: ftruncate of a read-only fd: EINVAL", dir));+        c.check(syscall3(SYS_FTRUNCATE, ro as u64, -1i64 as u64, 0) == -EINVAL, &alloc::format!("{}: negative length: EINVAL", dir));+        close(ro);+        unlink(&path);+    }+    c.check(path_call(SYS_TRUNCATE, "/tmp", 0) == -EISDIR, "truncate of a directory: EISDIR");+    c.check(path_call(SYS_TRUNCATE, "/mnt/disk3/hello.txt", 0) == -EROFS, "truncate on btrfs (read-only here): EROFS");+    c.check(path_call(SYS_TRUNCATE, "/tmp/nope16", 0) == -ENOENT, "truncate of a missing file: ENOENT");+    let (rd, wr) = pipe().unwrap();+    c.check(syscall3(SYS_FSYNC, rd as u64, 0, 0) == -EINVAL, "fsync of a pipe: EINVAL");+    c.check(syscall3(SYS_FTRUNCATE, wr as u64, 0, 0) == -EINVAL, "ftruncate of a pipe: EINVAL");+    close(rd);+    close(wr);++    // umask: taken away from open(O_CREAT) and mkdir modes; per process.+    let old = umask(0o077);+    c.check(old == 0o022, "umask starts as 022 and returns the old value");+    write_file("/tmp/mask16", b"x");+    c.check(perm("/tmp/mask16") == 0o600, "with umask 077, open(O_CREAT, 0644) makes 0600");+    with_cpath("/tmp/maskdir16", |p| syscall3(SYS_MKDIR, p, 0o777, 0));+    c.check(perm("/tmp/maskdir16") == 0o700, "and mkdir(0777) makes 0700");+    let child = fork();+    if child == 0 {+        exit(if umask(0) == 0o077 { 0 } else { 1 });+    }+    c.check(matches!(wait4(child, 0), Ok((_, s)) if wexitstatus(s) == 0), "fork inherits the umask");+    c.check(umask(old) == 0o077, "umask(022) gives back 077");+    unlink("/tmp/mask16");+    with_cpath("/tmp/maskdir16", |p| syscall3(SYS_RMDIR, p, 0, 0));++    // chmod family.+    for dir in ["/tmp", "/mnt/disk1"] {+        let path = alloc::format!("{}/mode16", dir);+        write_file(&path, b"m");+        c.check(path_call(SYS_CHMOD, &path, 0o751) == 0 && perm(&path) == 0o751, &alloc::format!("{}: chmod 751", dir));+        let fd = open(&path, O_RDONLY);+        c.check(syscall3(SYS_FCHMOD, fd as u64, 0, 0) == 0 && perm(&path) == 0, &alloc::format!("{}: fchmod 000 (a real 0, not 'unknown')", dir));+        close(fd);+        let name = cstr("mode16");+        let dfd = open(dir, O_RDONLY | O_DIRECTORY);+        c.check(sys4(SYS_FCHMODAT, dfd as u64, name.as_ptr() as u64, 0o4644, 0) == 0 && perm(&path) == 0o4644, &alloc::format!("{}: fchmodat relative to a dir fd, setuid bit kept", dir));+        close(dfd);+        unlink(&path);+    }+    c.check(path_call(SYS_CHMOD, "/mnt/disk0/readme.txt", 0o600) == -EPERM, "chmod on FAT (no permissions to keep): EPERM");+    c.check(path_call(SYS_CHMOD, "/mnt/disk3/hello.txt", 0o600) == -EROFS, "chmod on btrfs: EROFS");+    // (Rustbelt can't make symlinks yet; btrfs has some.)+    let link = cstr("/mnt/disk3/hello-link");+    let r = sys4(452, AT_FDCWD as u64, link.as_ptr() as u64, 0o700, AT_SYMLINK_NOFOLLOW);+    c.check(r == -EOPNOTSUPP, "fchmodat2(AT_SYMLINK_NOFOLLOW) on a symlink: EOPNOTSUPP (like Linux)");++    // utimensat + stat times.+    for (dir, nsec_kept) in [("/tmp", true), ("/mnt/disk1", false)] {+        let path = alloc::format!("{}/times16", dir);+        write_file(&path, b"t");+        let mut st = Stat::default();+        stat(&path, &mut st);+        c.check(st.mtime[0] > 1_000_000_000, &alloc::format!("{}: a new file's mtime is now (not 1970)", dir));+        let p = cstr(&path);+        let times = [[1000i64, 5], [2000, 6]];+        let r = sys4(SYS_UTIMENSAT, AT_FDCWD as u64, p.as_ptr() as u64, times.as_ptr() as u64, 0);+        stat(&path, &mut st);+        let want = if nsec_kept { ([1000, 5], [2000, 6]) } else { ([1000, 0], [2000, 0]) };+        c.check(r == 0 && st.atime == want.0 && st.mtime == want.1, &alloc::format!("{}: utimensat sets atime and mtime", dir));+        c.check(st.ctime[0] > 1_000_000_000, &alloc::format!("{}: ... and ctime becomes now", dir));+        let omit = [[0i64, UTIME_OMIT], [3000, 0]];+        sys4(SYS_UTIMENSAT, AT_FDCWD as u64, p.as_ptr() as u64, omit.as_ptr() as u64, 0);+        stat(&path, &mut st);+        c.check(st.atime[0] == 1000 && st.mtime[0] == 3000, &alloc::format!("{}: UTIME_OMIT leaves atime alone", dir));+        let fd = open(&path, O_RDONLY);+        let now = [[0i64, UTIME_NOW], [0, UTIME_NOW]];+        c.check(sys4(SYS_UTIMENSAT, fd as u64, 0, now.as_ptr() as u64, 0) == 0, &alloc::format!("{}: futimens (NULL path, an fd): 0", dir));+        fstat(fd, &mut st);+        c.check(st.atime[0] > 1_000_000_000 && st.mtime[0] > 1_000_000_000, &alloc::format!("{}: UTIME_NOW, seen by fstat", dir));+        close(fd);+        unlink(&path);+    }+    let p = cstr("/tmp");+    let bad = [[0i64, 1_000_000_000], [0, 0]];+    c.check(sys4(SYS_UTIMENSAT, AT_FDCWD as u64, p.as_ptr() as u64, bad.as_ptr() as u64, 0) == -EINVAL, "utimensat with tv_nsec = 1e9: EINVAL");+    c.check(sys4(SYS_UTIMENSAT, AT_FDCWD as u64, p.as_ptr() as u64, 0, 0x8) == -EINVAL, "unknown flags: EINVAL");+    let h = cstr("/mnt/disk3/hello.txt");+    c.check(sys4(SYS_UTIMENSAT, AT_FDCWD as u64, h.as_ptr() as u64, 0, 0) == -EROFS, "utimensat on btrfs: EROFS");+    let mut st = Stat::default();+    stat("/mnt/disk3/hello.txt", &mut st);+    c.check(st.mtime[0] > 1_000_000_000, "btrfs files report their real mtime");+}++const SYS_PREAD64_NR: u64 = 17;++// --- misc --------------------------------------------------------------------++fn misc(c: &mut Checks) {+    // rlimits+    let mut lim = [0u64; 2];+    c.check(syscall3(SYS_GETRLIMIT, RLIMIT_STACK, lim.as_mut_ptr() as u64, 0) == 0 && lim[0] > 0 && lim[0] == lim[1], "RLIMIT_STACK: the real (fixed) stack size");+    c.check(syscall3(SYS_GETRLIMIT, 99, lim.as_mut_ptr() as u64, 0) == -EINVAL, "an unknown resource: EINVAL");+    let up = [64u64, 1000];+    c.check(syscall3(SYS_SETRLIMIT, RLIMIT_NOFILE, up.as_ptr() as u64, 0) == -EPERM, "raising NOFILE's hard limit past 64: EPERM");+    let inverted = [10u64, 5];+    c.check(syscall3(SYS_SETRLIMIT, RLIMIT_NOFILE, inverted.as_ptr() as u64, 0) == -EINVAL, "soft above hard: EINVAL");+    let new = [32u64, 64];+    let mut old = [0u64; 2];+    let r = sys4(SYS_PRLIMIT64, 0, RLIMIT_NOFILE, new.as_ptr() as u64, old.as_mut_ptr() as u64);+    c.check(r == 0 && old == [64, 64], "prlimit64 sets and returns the old value");+    let child = fork();+    if child == 0 {+        let mut l = [0u64; 2];+        syscall3(SYS_GETRLIMIT, RLIMIT_NOFILE, l.as_mut_ptr() as u64, 0);+        exit(if l == [32, 64] { 0 } else { 1 });+    }+    c.check(matches!(wait4(child, 0), Ok((_, s)) if wexitstatus(s) == 0), "fork inherits rlimits");+    let back = [64u64, 64];+    sys4(SYS_PRLIMIT64, getpid() as u64, RLIMIT_NOFILE, back.as_ptr() as u64, 0);+    c.check(sys4(SYS_PRLIMIT64, 99_999, RLIMIT_NOFILE, 0, old.as_mut_ptr() as u64) == -ESRCH, "prlimit64 of a missing pid: ESRCH");++    // prctl+    let name = cstr("libctest-worker-long");+    c.check(syscall3(SYS_PRCTL, 15, name.as_ptr() as u64, 0) == 0, "PR_SET_NAME");+    let mut got = [0xffu8; 16];+    syscall3(SYS_PRCTL, 16, got.as_mut_ptr() as u64, 0);+    c.check(&got[..15] == b"libctest-worker" && got[15] == 0, "PR_GET_NAME: cut to 15 bytes + NUL");+    let mut sig = -1i32;+    syscall3(SYS_PRCTL, 1, SIGTERM as u64, 0);+    syscall3(SYS_PRCTL, 2, &mut sig as *mut i32 as u64, 0);+    c.check(sig == SIGTERM, "PR_SET/GET_PDEATHSIG");+    c.check(syscall3(SYS_PRCTL, 1, 99, 0) == -EINVAL, "PR_SET_PDEATHSIG 99: EINVAL");+    syscall3(SYS_PRCTL, 1, 0, 0);+    c.check(syscall3(SYS_PRCTL, 3, 0, 0) == 1, "PR_GET_DUMPABLE: 1");+    c.check(syscall3(SYS_PRCTL, 4, 2, 0) == -EINVAL, "PR_SET_DUMPABLE 2: EINVAL");+    c.check(syscall5(SYS_PRCTL, 38, 1, 0, 0, 0) == 0 && syscall5(SYS_PRCTL, 39, 0, 0, 0, 0) == 1, "PR_SET/GET_NO_NEW_PRIVS");+    c.check(syscall3(SYS_PRCTL, 0x5555, 0, 0) == -EINVAL, "an unknown prctl: EINVAL");++    // sched_getaffinity: every CPU, returns the bytes written.+    let mut mask = [0u64; 2];+    let r = syscall3(SYS_SCHED_GETAFFINITY, 0, 16, mask.as_mut_ptr() as u64);+    let cpus = sysinfo().cpus as u32;+    c.check(r == 8 && mask[0].count_ones() == cpus && cpus >= 1, &alloc::format!("sched_getaffinity: {} CPU bits, returns 8", cpus));+    c.check(syscall3(SYS_SCHED_GETAFFINITY, 0, 4, mask.as_mut_ptr() as u64) == -EINVAL, "sched_getaffinity with 4 bytes: EINVAL");++    // madvise+    let page = mmap(0, 8192, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) as u64;+    unsafe { core::ptr::write_bytes(page as *mut u8, 0xaa, 8192) };+    c.check(syscall3(SYS_MADVISE, page, 8192, 3) == 0, "MADV_WILLNEED: 0 (a hint)");+    c.check(syscall3(SYS_MADVISE, page, 8192, 4) == 0, "MADV_DONTNEED: 0");+    let zero = unsafe { core::slice::from_raw_parts(page as *const u8, 8192) }.iter().all(|&b| b == 0);+    c.check(zero, "after MADV_DONTNEED private anonymous pages read as zeros");+    c.check(syscall3(SYS_MADVISE, page + 1, 4096, 4) == -EINVAL, "unaligned address: EINVAL");+    c.check(syscall3(SYS_MADVISE, page, 4096, 999) == -EINVAL, "unknown advice: EINVAL");+    munmap(page, 8192);+    c.check(syscall3(SYS_MADVISE, page, 4096, 4) == -ENOMEM, "on unmapped memory: ENOMEM");++    // Robust futex lists.+    let mut head = [0u64; 3];+    c.check(syscall3(SYS_SET_ROBUST_LIST, head.as_ptr() as u64, 23, 0) == -EINVAL, "set_robust_list with len 23: EINVAL");+    c.check(syscall3(SYS_SET_ROBUST_LIST, head.as_ptr() as u64, 24, 0) == 0, "set_robust_list");+    let (mut h, mut l) = (0u64, 0u64);+    syscall3(SYS_GET_ROBUST_LIST, 0, &mut h as *mut u64 as u64, &mut l as *mut u64 as u64);+    c.check(h == head.as_ptr() as u64 && l == 24, "get_robust_list returns it");+    syscall3(SYS_SET_ROBUST_LIST, 0, 24, 0);+    // A child dies holding a "robust mutex" in shared memory: the kernel+    // marks the lock word FUTEX_OWNER_DIED, so the next locker knows.+    let shared = mmap(0, 4096, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0) as u64;+    let word = shared as *mut u32;+    let entry = shared + 64; // the mutex's list node; lock word at entry - 64+    let child = fork();+    if child == 0 {+        let tid = syscall3(SYS_GETTID, 0, 0, 0) as u32;+        head[0] = entry; // list: head -> entry -> head+        head[1] = (-64i64) as u64; // futex_offset: from the node to the word+        head[2] = 0;+        unsafe {+            *(entry as *mut u64) = head.as_ptr() as u64;+            word.write_volatile(tid); // "locked by me"+        }+        syscall3(SYS_SET_ROBUST_LIST, head.as_ptr() as u64, 24, 0);+        exit(0); // ... and die holding it+    }+    wait(child);+    let w = unsafe { word.read_volatile() };+    c.check(w & FUTEX_OWNER_DIED != 0 && w & 0x3fff_ffff == 0, "a robust lock held by a dead thread: FUTEX_OWNER_DIED, owner 0");+    munmap(shared, 4096);+    head[0] = 0;+}++// --- orphans -----------------------------------------------------------------++fn orphans(c: &mut Checks) {+    // A child that leaves a running grandchild and a zombie grandchild+    // behind and exits without waiting for either.+    let child = fork();+    if child == 0 {+        if fork() == 0 {+            exit(0); // becomes a zombie: its parent never waits+        }+        if fork() == 0 {+            sleep_ms(40); // still running when its parent exits+            exit(0);+        }+        sleep_ms(10);+        exit(0);+    }+    c.check(matches!(wait4(child, 0), Ok((p, _)) if p == child), "the child exits without waiting for its own children");++    // With PR_SET_CHILD_SUBREAPER we adopt the orphan and can wait for it.+    c.check(syscall3(SYS_PRCTL, 36, 1, 0) == 0, "PR_SET_CHILD_SUBREAPER");+    let me = getpid();+    let child = fork();+    if child == 0 {+        if fork() == 0 {+            sleep_ms(40);+            exit(if getppid() == me { 5 } else { 6 }); // adopted by the subreaper?+        }+        exit(0);+    }+    c.check(matches!(wait4(child, 0), Ok((p, s)) if p == child && wexitstatus(s) == 0), "the middle process exits");+    let r = wait4(-1, 0);+    c.check(matches!(r, Ok((p, s)) if p != child && wexitstatus(s) == 5), "the subreaper reaps the orphaned grandchild, which saw getppid() = subreaper");+    c.check(wait4(-1, WNOHANG) == Err(-ECHILD), "no children left");+    sleep_ms(80); // let the first orphan finish before we exit+}
@@ -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 15"), "uname: Rustbelt, stage 15");+    c.check(u[0] == "Rustbelt" && u[3].starts_with("stage 16"), "uname: Rustbelt, stage 16");      let (secs, _) = clock_gettime(0);     c.check(secs > 1_600_000_000, "CLOCK_REALTIME is a real date (after 2020)");