RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

authorEdward <edward@localhost>2026-10-31 16:49:32 -0700
commitfc5e78aac8e1fcad62e645581d205f6b5b391b7a
parent63fcb8621f453f1445fea724b37bf39479a23d9f
treebrowse files at this commit
Stage 17: coreutils batch 2 (0.17.0)

The ENOSYS calls from Clarke's coreutils/busybox (tier 3) traces, in his
priority order. New calls live in src/syscall_ext.rs; timers in src/timer.rs.

- link/linkat, symlink/symlinkat (ramfs, ext2; EPERM on FAT/exFAT, EROFS on
  btrfs, EXDEV across mounts, EMLINK); native rename of full directories,
  renameat2 RENAME_NOREPLACE; real st_ino/st_nlink/st_dev/owner in stat
- chown/fchown/lchown/fchownat (stored on ramfs/ext2; vfat-like on FAT)
- statfs/fstatfs with Linux magic numbers, statx
- Linux sysinfo (99); ours stays 502, traced as rb_sysinfo
- times, getrusage, CPU-time clocks (10 ms sampling), clock_getres
- setsockopt options accepted/remembered, getsockopt, getsockname,
  getpeername, shutdown; socketpair -> EAFNOSUPPORT (no AF_UNIX)
- alarm, getitimer/setitimer (ITIMER_REAL), POSIX timers (timerd thread)
- real (unenforced) uid/gid sets, groups, fs ids, nice values,
  sethostname/setdomainname
- flock, preadv/pwritev, sendfile
- fixes: getdents64 skipped entries while deleting (rm -r); --trace shows
  the user-visible result of an interrupted call; sigsetsize checks for
  ppoll/pselect6/epoll_pwait; the headless frame report waits for all tasks
- user/coretest.rs + tests/coreutils_batch2.rs, README stage 17 chapter

Deferred: socketpair/AF_UNIX, timerfd, mremap, ITIMER_VIRTUAL/PROF.
MCargo.lock+1 -1
MCargo.toml+1 -1
MREADME.md+361 -19
Mbuild.rs+1 -0
Mscripts/run-headless.sh+6 -6
Mscripts/screenshot.sh+1 -1
Msrc/dev.rs+2 -2
Msrc/errno.rs+5 -2
Msrc/fs/btrfs.rs+60 -1
Msrc/fs/exfat.rs+17 -0
Msrc/fs/ext2.rs+230 -4
Msrc/fs/fat.rs+21 -1
Msrc/fs/file.rs+56 -5
Msrc/fs/lock.rs+70 -2
Msrc/fs/mod.rs+180 -23
Msrc/fs/ramfs.rs+183 -8
Msrc/interrupts.rs+8 -0
Msrc/lib.rs+2 -0
Msrc/main.rs+14 -1
Msrc/memory.rs+15 -0
Msrc/net/socket.rs+105 -3
Msrc/poll.rs+18 -3
Msrc/process.rs+71 -0
Msrc/signal.rs+21 -5
Msrc/syscall.rs+77 -62
Asrc/syscall_ext.rs+1134 -0
Asrc/timer.rs+278 -0
Atests/coreutils_batch2.rs+129 -0
Mtests/shell.rs+1 -1
Auser/coretest.rs+885 -0
Muser/nbtest.rs+1 -1
Muser/ulib.rs+2 -2

32 files changed, 3956 insertions, 154 deletions

@@ -261,7 +261,7 @@ dependencies = [  [[package]] name = "rustbelt"-version = "0.16.0"+version = "0.17.0" dependencies = [  "bootloader",  "conquer-once",
@@ -1,6 +1,6 @@ [package] name = "rustbelt"-version = "0.16.0"+version = "0.17.0" edition = "2024"  [dependencies]
@@ -997,6 +997,7 @@ heap_allocation::simple_allocation...               [ok] | `tests/libc.rs` | **libc plumbing** (stage 15), by running four user test programs twice each (a warm-up, then a run that must leave the thread, process, futex-waiter, pty, socket and frame counts exactly as they were): `threads` (4 threads sum 1..=40,000,000 on several CPUs; a futex mutex keeps a shared counter exact while an unlocked one loses updates; each thread sees its own TLS value through `fs:`; `gettid`; `FUTEX_WAIT` with a stale value is `EAGAIN`, times out, `FUTEX_WAKE` with no waiters; `CLONE_CHILD_CLEARTID` + futex wake for join; `exit` ends one thread, `exit_group` all; `tgkill` and `ESRCH`); `echoserver` (an epoll server and a poll server on 127.0.0.1, three forked clients each, plus UDP over loopback); `ptytest` (canonical mode holds a half line back, backspace editing, CR -> NL, echo, ONLCR; raw mode passes bytes through without echo; `TCGETS`/`TCSETS`; ^C and ^\ kill the child that made the pty its controlling terminal with SIGINT/SIGQUIT and echo "^C"; `/dev/tty` with and without a controlling terminal; closing the master gives the slave EOF); `nbtest` (`EAGAIN` from pipes, sockets and ptys; 64 KiB pipe capacity; poll bits; `fcntl(F_SETFL, O_NONBLOCK)` and `FIONBIO`; nonblocking `connect` is `EINPROGRESS` then `POLLOUT`; `select`, `ppoll`, `readv`/`writev`, `pread`/`pwrite`, `getcwd`, `uname`, `CLOCK_REALTIME`, `nanosleep`, `readlink /proc/self/exe`) | | `tests/musl_prereq.rs` | **musl prerequisites** (stage 15.1), each program twice with no leaks: the FPU is set up (XSAVE with x87 + SSE (+ AVX) under `-cpu max`); `fputest` (16 xmm/ymm registers with a per-thread pattern in 4 threads x 3 processes at once, fork copies them, exec clears them, a signal handler gets clean registers and Linux's fpstate in its frame and may edit it, 40 async signals mid-check, SIGFPE when TCG raises it); `pathtest` (`EFAULT` for NULL/kernel/unmapped/straddling path pointers, `ENAMETOOLONG`, `struct stat` fields and types, `lstat`, `AT_EMPTY_PATH`, every `*at()` call, `O_EXCL`/`O_DIRECTORY`/`O_NOFOLLOW`, `fchdir`, `rename`); `rawabi` (a gcc-built no-libc C program, unmodified) | | `tests/libc_batch1.rs` | **libc batch 1** (stage 16): `libctest` runs one group per test, twice, and nothing (threads, processes, frames, file locks) may be left over: `wait` (dash's `wait`: SIGCHLD blocked, fork, `rt_sigsuspend` returns `EINTR` once the handler ran and puts the old mask back; a SIGCHLD already pending ends it at once); `signals` (`rt_sigpending`, `rt_sigtimedwait` with and without a timeout and from another process, real-time signals queued 3 times with their values, a standard signal only once, `EPERM`/`ESRCH` for `rt_sigqueueinfo`, `sigaltstack` + `SA_ONSTACK`, `EPERM` on the stack, unknown `sa_flags` dropped); `locks` (`F_GETFL` access modes, `F_SETFL`, `F_GETLK`/`F_SETLK`/`F_SETLKW` between two processes, `EDEADLK`, locks dropped on close and on exit, `RLIMIT_NOFILE` vs `dup` and `F_DUPFD`); `files` (`truncate`/`ftruncate`/`fsync` on ramfs, ext2 and FAT, `umask` on `open` and `mkdir` and across fork, `chmod`/`fchmod`/`fchmodat`, `utimensat` with `UTIME_NOW`/`UTIME_OMIT`, `EROFS` on btrfs); `misc` (rlimits, `prctl`, `sched_getaffinity`, `madvise(MADV_DONTNEED)` reads zeros, a robust futex gets `FUTEX_OWNER_DIED`); `orphans` (running and zombie orphans are freed, a `PR_SET_CHILD_SUBREAPER` process adopts and reaps a grandchild); `--trace` names unknown calls `syscall_N` |+| `tests/coreutils_batch2.rs` | **coreutils batch 2** (stage 17): `coretest` runs one group per test, twice, and nothing (threads, processes, frames, file locks, timers) may be left over: `links` (`link`/`linkat`/`symlink`/`symlinkat` on ramfs and ext2 with `st_nlink` and `st_ino`, a dangling symlink, renaming a full directory and over a second name, `EPERM` on FAT/exFAT, `EROFS` on btrfs, `EXDEV`); `owners` (the `chown` family on ramfs and ext2, `-1`, `lchown` vs `chown` on a symlink, FAT's `EPERM` unless nothing changes); `statfs` (`statfs`/`fstatfs` magic numbers and sizes per mount, `ST_RDONLY`, pipes, `statx` vs `stat`, `st_dev` per mount); `info` (`sysinfo` 99 and 502, `times`, `getrusage` for self and children, the CPU-time clocks, `clock_getres`); `sockets` (`getsockname`/`getpeername`, `SO_REUSEADDR` & co., `SO_TYPE`, `SO_ERROR` after a refused connect, `SHUT_WR` = end of file for the peer, `EPIPE` after it, `socketpair` refused); `timers` (`alarm`, one-shot and periodic `setitimer`, `pause`, POSIX timers with `SI_TIMER` + `si_value`, `TIMER_ABSTIME`, `SIGEV_NONE`, not inherited by fork); `ids` (`setuid` & co. with the saved id, groups, new files owned by their creator, nice values, `sethostname` -> `uname`); `misc` (`flock` between open files and processes, `preadv`/`pwritev`, `sendfile`, mask sizes for `ppoll`/`pselect6`, a `ppoll` mask letting a signal in, `getdents64` while deleting on ramfs and ext2); `--trace` names (`link`, `statx`, `rb_sysinfo`) |  How it works: `#![feature(custom_test_frameworks)]` collects every `#[test_case]` function and passes them to `rustbelt::test_runner` (in `src/lib.rs`), which@@ -1119,6 +1120,9 @@ user/{fputest,pathtest}.rs  stage 15.1: per-thread FPU/SSE/AVX state + signal fp                           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)+user/coretest.rs          stage 17: coreutils batch 2 checks, one group per argument (tests/coreutils_batch2.rs)+src/syscall_ext.rs        stage 17: links, chown, statfs/statx, CPU times, socket names/options, timers, ids, flock... (table at the top)+src/timer.rs              stage 17: alarm, setitimer and POSIX timers; the timerd thread that sends their signals 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`)@@ -1570,15 +1574,24 @@ Numbers are Linux's wherever Linux has the same call, so man pages and | 33 | `dup2` | old, new | closes `new` first if open | | 34 | `pause` |  | sleeps until a signal; returns EINTR | | 35 | `nanosleep` | *timespec, *remaining | **Linux's `nanosleep` since stage 15** (stages 2-14 had our own `sleep_ms(ms)` here). Sleeps whole PIT ticks, then yields until the TSC clock says the time is up; a signal ends it early with `EINTR` and the time left in `*remaining` |+| 36 | `getitimer` | which, *itimerval | **stage 17**: `ITIMER_REAL` only (others `EINVAL`) |+| 37 | `alarm` | seconds | **stage 17**: SIGALRM after N seconds; returns the seconds left on the old one; 0 cancels |+| 38 | `setitimer` | which, *new, *old | **stage 17**: `ITIMER_REAL`, one-shot or periodic, in microseconds; fires up to ~110 ms late, never early | | 39 | `getpid` | | |+| 40 | `sendfile` | out_fd, in_fd, *offset, count | **stage 17**: in-kernel copy; the input must be seekable (`EINVAL` for a pipe); with `*offset` the input position doesn't move | | 41 | `socket` | domain, type, protocol | `AF_INET` (2) only; `SOCK_STREAM` (1) = TCP, `SOCK_DGRAM` (2) = UDP, or `SOCK_DGRAM` + `IPPROTO_ICMP` (1) = ping socket (like Linux's unprivileged ping sockets). Returns an fd; `close` closes it | | 42 | `connect` | fd, *sockaddr_in, len | TCP: blocks for the handshake (10 s, `ETIMEDOUT`; `ECONNREFUSED` on a reset). UDP: sets the default peer | | 43 | `accept` | fd, *sockaddr_in, *len | blocks for a connection on a listening socket; returns a new fd. Since 15.1 `*len` is read and updated like Linux | | 44 | `sendto` | fd, buf, len, flags, *sockaddr_in, len | address 0 = the connected peer. ICMP: you write the echo header, the kernel fills in the id and checksum. `flags` (so `MSG_NOSIGNAL`) and `len` are ignored | | 45 | `recvfrom` | fd, buf, len, flags, *sockaddr_in, *len | blocks (wait queue) until data; 0 = the peer closed (TCP); `EAGAIN` after `SO_RCVTIMEO`. Since 15.1 the address honours `*len` |+| 48 | `shutdown` | fd, how | **stage 17**: `SHUT_RD`/`SHUT_WR`/`SHUT_RDWR`; `SHUT_WR` sends a FIN (the peer reads 0); `ENOTCONN` if not connected | | 49 | `bind` | fd, *sockaddr_in, len | local port (address must be 0 or ours) | | 50 | `listen` | fd, backlog | TCP; up to 4 connections can be waiting |-| 54 | `setsockopt` | fd, level, name, *val, len | only `SOL_SOCKET`/`SO_RCVTIMEO` (a `timeval`); anything else `ENOPROTOOPT`, like Linux |+| 51 | `getsockname` | fd, *sockaddr_in, *len | **stage 17**: my address and port (after `bind`, `connect` or `accept`) |+| 52 | `getpeername` | fd, *sockaddr_in, *len | **stage 17**: the other end; `ENOTCONN` if none |+| 53 | `socketpair` | domain, type, protocol, *[u32; 2] | **stage 17**: needs `AF_UNIX`, which we don't have: `EAFNOSUPPORT` (`EOPNOTSUPP` for `AF_INET`) |+| 54 | `setsockopt` | fd, level, name, *val, len | `SO_RCVTIMEO` (a `timeval`) is real; **stage 17**: `SO_REUSEADDR`/`SO_REUSEPORT`/`SO_KEEPALIVE`, `SO_SNDTIMEO`, buffer sizes and `TCP_NODELAY` are accepted and remembered; anything else `ENOPROTOOPT`, like Linux |+| 55 | `getsockopt` | fd, level, name, *val, *len | **stage 17**: `SO_TYPE`, `SO_ERROR` (once), `SO_ACCEPTCONN`, `SO_DOMAIN`, `SO_PROTOCOL`, the timeouts, buffer sizes and remembered flags | | 56 | `clone` | flags, stack, *ptid, *ctid, tls | fork-like (SIGCHLD, optional CLONE_VFORK), or since **stage 15** a thread: `CLONE_VM|FS|FILES|SIGHAND|THREAD|SYSVSEM` plus optional `SETTLS`, `PARENT_SETTID`, `CHILD_SETTID`, `CHILD_CLEARTID`. The child starts on `stack` with rax = 0 | | 57 | `fork` |  | copy-on-write; parent gets the pid, child 0 | | 58 | `vfork` |  | fork + the parent waits until the child execs or exits |@@ -1588,36 +1601,53 @@ Numbers are Linux's wherever Linux has the same call, so man pages and | 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.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 |+| 73 | `flock` | fd, op | **stage 17**: whole-file `LOCK_SH`/`LOCK_EX`, `LOCK_NB`, `LOCK_UN`; owned by the open file, dropped when its last fd closes | | 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" |+| 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". **Stage 17**: ramfs and ext2 move the entry itself (full directories, files with several names); `renameat2` 316 adds `RENAME_NOREPLACE` | | 83 | `mkdir` | path, mode | since stage 16 `mode & ~umask` (ramfs, ext2) | | 84 | `rmdir` | path | empty directories only (`ENOTEMPTY`); `EBUSY` on a mount point |+| 86 | `link` | old, new | **stage 17**: a hard link on ramfs and ext2 (`EMLINK` past 32000); `EPERM` on FAT/exFAT, `EROFS` on btrfs, `EXDEV` across file systems | | 87 | `unlink` | path | files and symlinks (the link, not its target); directories give `EISDIR` |+| 88 | `symlink` | target, path | **stage 17**: ramfs and ext2; the target needn't exist | | 89 | `readlink` | path, buf, len | a symlink's target (no NUL); returns its length, cut to `len` like Linux; `EINVAL` if not a symlink. Stage 15: `/proc/self/exe` is the running program's path | | 90 | `chmod` | path, mode | **stage 16**: the 12 permission bits, stored on ramfs and ext2; `EPERM` on FAT/exFAT (nowhere to keep them), `EROFS` on btrfs. Never checked: everyone is root | | 91 | `fchmod` | fd, mode | **stage 16** |+| 92 | `chown` | path, uid, gid | **stage 17**: -1 = unchanged; stored on ramfs and ext2 (FAT/exFAT: `EPERM` unless nothing changes); not enforced; `fchown` 93, `lchown` 94 | | 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) |+| 98 | `getrusage` | who, *rusage | **stage 17**: user/system CPU time (10 ms samples) for `RUSAGE_SELF`, `RUSAGE_CHILDREN`, `RUSAGE_THREAD`; `ru_maxrss` |+| 99 | `sysinfo` | *sysinfo | **stage 17**: Linux's 112-byte struct: uptime, memory, process count, "load" = busy fraction since boot |+| 100 | `times` | *tms | **stage 17**: CPU times of me and my waited-for children in 1/100 s; returns ticks since boot |+| 102 | `getuid` |  | **stage 17**: the real user id (0 = root unless changed); `getgid` 104, `geteuid` 107, `getegid` 108 likewise |+| 105 | `setuid` | uid | **stage 17**: root sets all three ids; others only the effective one, to their real or saved id (`EPERM`); `setgid` 106 likewise | | 109 | `setpgid` | pid, pgid | 0 = myself / a new group | | 110 | `getppid` |  | 0 if started by the kernel or orphaned | | 111 | `getpgrp` |  |  | | 112 | `setsid` |  | **stage 15**: new session + group, no controlling tty |+| 113 | `setreuid` | ruid, euid | **stage 17**: Linux's rules for the saved id; `setregid` 114 |+| 115 | `getgroups` | size, *list | **stage 17**: supplementary groups; `setgroups` 116 (root only) |+| 117 | `setresuid` | ruid, euid, suid | **stage 17**: not root: each new value must be one of the current three; `getresuid` 118, `setresgid`/`getresgid` 119/120 | | 121 | `getpgid` | pid |  |+| 122 | `setfsuid` | uid | **stage 17**: the id new files get; returns the old one; `setfsgid` 123 | | 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 |+| 137 | `statfs` | path, *statfs | **stage 17**: Linux's magic numbers (ramfs, msdos, ext2, exfat, btrfs), sizes, free blocks and inodes, `ST_RDONLY` |+| 138 | `fstatfs` | fd, *statfs | **stage 17**; pipes and sockets get `PIPEFS`/`SOCKFS` |+| 140 | `getpriority` | which, who | **stage 17**: returns 20 - nice (the raw call; libc subtracts) |+| 141 | `setpriority` | which, who, nice | **stage 17**: clamped to -20..19; lowering it needs root (`EACCES`); the scheduler ignores 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` |+| 170 | `sethostname` | name, len | **stage 17**: what `uname` reports; root only, at most 64 bytes; `setdomainname` 171 | | 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) |@@ -1625,7 +1655,10 @@ Numbers are Linux's wherever Linux has the same call, so man pages and | 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 |-| 228 | `clock_gettime` | clock, *timespec | microsecond resolution (TSC calibrated against the PIT). `CLOCK_MONOTONIC` (1) counts from boot; since stage 15 `CLOCK_REALTIME` (0) is real Unix time (the CMOS clock read once at boot, plus uptime) |+| 222 | `timer_create` | clock, *sigevent, *id | **stage 17**: `CLOCK_REALTIME`/`MONOTONIC`/`BOOTTIME`; `SIGEV_SIGNAL`, `SIGEV_NONE`, `SIGEV_THREAD_ID`; signals carry `SI_TIMER` and `si_value` |+| 223 | `timer_settime` | id, flags, *new, *old | **stage 17**: `TIMER_ABSTIME`; periodic; `timer_gettime` 224, `timer_getoverrun` 225, `timer_delete` 226 |+| 228 | `clock_gettime` | clock, *timespec | microsecond resolution (TSC calibrated against the PIT). `CLOCK_MONOTONIC` (1) counts from boot; since stage 15 `CLOCK_REALTIME` (0) is real Unix time (the CMOS clock read once at boot, plus uptime); **stage 17**: `CLOCK_PROCESS_CPUTIME_ID` (2) and `CLOCK_THREAD_CPUTIME_ID` (3), in 10 ms steps |+| 229 | `clock_getres` | clock, *timespec | **stage 17**: 1 µs for the wall clocks, 10 ms for the CPU-time ones | | 230 | `clock_nanosleep` | clock, flags, *timespec, *remaining | **stage 15.1**: `CLOCK_REALTIME`, `MONOTONIC` or `BOOTTIME`; flag `TIMER_ABSTIME` (1) sleeps until that time. musl's `nanosleep` and `sleep` use this | | 231 | `exit_group` | code | ends the process: every thread (stage 15; `exit`, 60, ends one thread) | | 232 | `epoll_wait` | epfd, *epoll_event, max, timeout_ms | **stage 15**: level-triggered; EPOLLONESHOT supported |@@ -1633,9 +1666,12 @@ Numbers are Linux's wherever Linux has the same call, so man pages and | 234 | `tgkill` | tgid, tid, sig | **stage 15**: a signal for one thread | | 257 | `openat` | dirfd, path, flags, mode | **stage 15.1**: like `open`, relative to `dirfd` or `AT_FDCWD` (-100); musl's `open` calls this | | 258 | `mkdirat` | dirfd, path, mode | **stage 15.1** |+| 260 | `fchownat` | dirfd, path, uid, gid, flags | **stage 17**: `AT_SYMLINK_NOFOLLOW`, `AT_EMPTY_PATH` | | 262 | `newfstatat` | dirfd, path, *stat, flags | **stage 15.1**: `AT_SYMLINK_NOFOLLOW` 0x100 (= lstat), `AT_EMPTY_PATH` 0x1000 (= `fstat(dirfd)`); musl's `stat`/`fstat`/`lstat` all end up here | | 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 |+| 264 | `renameat` | olddirfd, old, newdirfd, new | **stage 15.1**; `renameat2` (316) too, with flags 0 (stage 17: or `RENAME_NOREPLACE`) |+| 265 | `linkat` | olddirfd, old, newdirfd, new, flags | **stage 17**: `AT_SYMLINK_FOLLOW`, `AT_EMPTY_PATH` |+| 266 | `symlinkat` | target, dirfd, path | **stage 17** | | 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 |@@ -1648,11 +1684,13 @@ Numbers are Linux's wherever Linux has the same call, so man pages and | 291 | `epoll_create1` | flags | **stage 15**: EPOLL_CLOEXEC | | 292 | `dup3` | old, new, flags | flags: O_CLOEXEC | | 293 | `pipe2` | *[u32; 2], flags | flags: O_CLOEXEC |+| 295 | `preadv` | fd, *iovec, n, offset | **stage 17**: the file position doesn't move; `pwritev` 296 | | 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) |+| 332 | `statx` | dirfd, path, flags, mask, *statx | **stage 17**: the 256-byte `struct statx`, from the same data as `stat` | | 500 | `spawn` | path, argv, argv_len, *SpawnOptions | **ours**: start a program, returns its pid (fork + exec in one step, like `posix_spawn`; the kernel's first shell and `getty` use it, the shell uses fork + exec) | | 501 | `mounts` | buf, len | **ours** (Linux reads `/proc/mounts`): the mount table as text, one `path fstype` line per mount, in mount order; returns the length, `ERANGE` if too small. The shell assigns drive letters from it |-| 502 | `sysinfo` | *SysInfo | **ours**: CPU count, the CPU you're on, thread/process/frame counts, uptime ticks, and per-CPU timer ticks and busy ticks (sample twice for the load) |+| 502 | `rb_sysinfo` | *SysInfo | **ours** (called `sysinfo` until stage 17; Linux's `sysinfo` is 99): CPU count, the CPU you're on, thread/process/frame counts, uptime ticks, and per-CPU timer ticks and busy ticks (sample twice for the load) | | 503 | `resolve` | name, name_len, *addrs, max | **ours** (Linux does DNS in libc, in user space): IPv4 addresses for a name via the DHCP-given DNS server; returns how many; `ENOENT` / `ETIMEDOUT` | | 504 | `netinfo` | *NetInfo | **ours** (Linux has `ioctl`s and netlink): MAC, IRQ, address/prefix, gateway, DNS, packet and byte counters | | 505 | `trace_me` | on | ours: print my syscalls from now on (survives execve; `run --trace`) |@@ -2014,6 +2052,7 @@ Programs in `B:/bin` (`user/*.rs`, each 30-150 lines): | `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" |+| `coretest links\|owners\|statfs\|info\|sockets\|timers\|ids\|misc` | the stage 17 checks (coreutils batch 2), 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,@@ -3869,22 +3908,17 @@ Doesn't (yet): (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`.)+(Stage 17 crossed off `chown`, `link`/`symlink`, `statfs`/`flock`,+`sysinfo`, `getrusage`/`times`/`clock()`, `clock_getres`, the timers, the+`setuid` family, `nice`, `preadv`/`pwritev`/`sendfile`, and most of the+socket row.)  | musl function(s) | missing syscall (x86_64 number) | |---|---| | `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` | `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 |-| `alarm`, `setitimer`, `timer_create` | `alarm` 37, `setitimer` 38, `timer_create` 222 ... | | `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 |-| `preadv`, `pwritev`, `sendfile` | 295, 296, `sendfile` 40 |+| sockets | `socketpair` 53 (needs `AF_UNIX`; `EAFNOSUPPORT` now), `sendmsg` 46, `recvmsg` 47 (the DNS resolver's TCP fallback) |+| `timerfd_create` | `timerfd_create` 283, `timerfd_settime` 286, `timerfd_gettime` 287 |  Not syscalls, but missing just the same: @@ -4141,6 +4175,290 @@ Doesn't (yet): - **The rest of Clarke's list** (static-PIE loading, a hook for running   disk images) are separate stages. +## coreutils batch 2 (stage 17)++Stage 16 was what a C library needs to *run*. Stage 17 is what the+everyday tools need: `ln`, `chown`, `df`, `uptime`, `time`, `sleep`+with an alarm, `id`, `nice`, `hostname`, `rm -r`, `cp`, and the socket+calls a small server makes. Clarke ran coreutils and busybox (tier 3)+with `run --trace`, wrote down every `ENOSYS`, and put them in order:++1. `link` and `symlink` (and their `*at` forms);+2. `setsockopt` options that may be ignored, `times()`, CPU-time clocks;+3. the `chown` family;+4. `statfs` / `fstatfs` (`df`);+5. Linux's `sysinfo` (99), `getrusage`, `clock_getres`;+6. `getsockopt`, `getsockname`, `shutdown`, `socketpair`;+7. `alarm`, interval timers and POSIX timers;+8. user and group ids, priority, `sethostname`;+9. then, if there was room: `flock`, `preadv`/`pwritev`, `sendfile`+   (and `mremap`, which didn't make it).++As before, every call uses Linux's number, arguments and error codes, so+`man 2 <name>` describes it. The new calls live in their own file,+`src/syscall_ext.rs`, with a table at the top, because `syscall.rs` had+grown past 2000 lines. The dispatcher in `syscall.rs` tries its own+table first and then asks `syscall_ext::dispatch`.++### Hard links and symbolic links++A file's *name* and the file itself are different things. On ext2 (and+on Linux's ramfs) the file is an **inode**, with a number, and a+directory is just a list of `name -> inode number`. So one file can have+two names:++```+ln a.txt b.txt      # link(86): a second name for the same inode+ls -i a.txt b.txt   # the same inode number twice+stat a.txt          # "Links: 2"+```++Change the file through one name and the other sees it; delete one name+and the file lives on until the last name (and the last open fd) is+gone. That count is `st_nlink`, and `stat` now reports the real one.++A **symbolic link** (`symlink` 88, `ln -s`) is different: a tiny file+whose content is a path. Opening it follows the path; the target doesn't+even have to exist (a *dangling* link). `readlink` (stage 15.1) reads it.++Where it works and where it says no:++| file system | `link` | `symlink` |+|---|---|---|+| ramfs (`/`, `/tmp`) | yes | yes |+| ext2 (`/mnt/disk1`) | yes, up to 32000 links (`EMLINK`) | yes |+| FAT, exFAT | `EPERM` (no inodes: a name *is* the file) | `EPERM` |+| btrfs (read-only) | `EROFS` | `EROFS` |+| two different file systems | `EXDEV` (an inode number only means something on its own disk) | works (it's just a path) |++`linkat` (265) adds `AT_SYMLINK_FOLLOW` (link to what a symlink points+at, not the symlink) and `AT_EMPTY_PATH` (link an open fd). `symlinkat`+(266) is `symlink` relative to a directory fd.++**rename learned to move things.** Before, `rename` copied and deleted,+which can't work for a file with two names (the copy would be a third,+separate file) or for a full directory. ramfs and ext2 now move the+directory entry itself (`Inode::rename_entry`), so `mv dir1 dir2/`+works, and so does renaming over a second name of the same file.+`renameat2`'s `RENAME_NOREPLACE` ("fail with `EEXIST` rather than+overwrite") is supported too.++### Owners: chown++Every file has an owner (a user id) and a group (a group id).+`chown` (92), `fchown` (93), `lchown` (94, the symlink itself) and+`fchownat` (260) change them; `-1` means "leave this one alone".+`stat` reports them. ramfs and ext2 store them; FAT and exFAT have+nowhere to put them, so a real change is `EPERM`, but "change it to+what it already is" (root:root) succeeds, as Linux's vfat does. That+matters: `cp -p` and `tar` call `chown` on every file they copy.++New files belong to their creator (the process's *fs* uid and gid, see+below).++### How big is the disk? statfs++`df` asks `statfs` (137) or `fstatfs` (138) for each mount: the block+size, how many blocks there are and how many are free, how many inodes,+the longest name, and a **magic number** saying which kind of file+system it is. Linux uses the same magic numbers, so `stat -f` prints the+right name:++| mount | magic | type |+|---|---|---|+| `/`, `/tmp` | `0x858458f6` | ramfs |+| `/mnt/disk0` | `0x4d44` | msdos (FAT) |+| `/mnt/disk1` | `0xEF53` | ext2 |+| `/mnt/disk2` | `0x2011BAB0` | exfat |+| `/mnt/disk3` | `0x9123683E` | btrfs (flagged read-only, `ST_RDONLY`) |++A pipe or socket fd gets `PIPEFS`/`SOCKFS`, like Linux. Each mount also+has its own `st_dev` now (it used to be 0 everywhere), which is how `cp+-r` and `find -xdev` tell "same file system" from "crossed a mount".++**statx** (332) is the newer stat: a 256-byte struct with a mask saying+which fields are filled in. coreutils' `ls` and `stat` try it first, so+it was cheaper to implement it (from the same data as `stat`) than to+send them down a fallback path.++### What time is it for *me*? CPU time++`time ls` prints three numbers: *real* (wall clock), *user* (CPU time+running the program's own code) and *sys* (CPU time in the kernel on its+behalf). The kernel learns those by looking at what each CPU was doing+at every timer interrupt (every 10 ms): if a thread was running, it gets+10 ms of user or system time, depending on whether the interrupt arrived+in ring 3 or ring 0. It's sampling, not exact, but it's what Linux did+for decades.++From those counters:++- **`times`** (100) gives user and system time of the process and of its+  waited-for children, in "clock ticks" of 1/100 s.+- **`getrusage`** (98) gives the same as `timeval`s, for `RUSAGE_SELF`,+  `RUSAGE_CHILDREN` or `RUSAGE_THREAD`, plus the memory high-water mark+  (here: the resident memory now).+- **`clock_gettime(CLOCK_PROCESS_CPUTIME_ID)`** (clock 2) and+  `CLOCK_THREAD_CPUTIME_ID` (3) used to be `EINVAL`. C's `clock()` uses+  clock 2.+- **`clock_getres`** (229) says how fine each clock is: 1 µs for the+  wall clocks, 10 ms for the CPU-time ones. Honest numbers, not 1 ns.++**sysinfo, the Linux one.** Rustbelt has had its own `sysinfo` since+stage 11, but it took number 502 and a different struct (per-CPU load+bars for `user/sysinfo.rs`). Linux's is 99, and musl's `sysinfo()`,+`uptime` and `free` use it. Both exist now: 99 fills Linux's `struct+sysinfo` (uptime, total/free memory, process count, load averages), and+ours stays at 502, renamed `rb_sysinfo` in `--trace` so the two can't be+confused. The "load average" is the busy fraction of the CPUs since boot+(Rustbelt doesn't keep a 1/5/15-minute history yet).++### Sockets: names, options, half-closing++- **`getsockname`** (51) and **`getpeername`** (52): "which address and+  port am I?" and "who am I talking to?". A server that binds to port 0+  uses the first to learn which port it got.+- **`setsockopt`** (54) used to know only `SO_RCVTIMEO`. Servers set+  `SO_REUSEADDR` (and `SO_KEEPALIVE`, `TCP_NODELAY`, buffer sizes)+  before `bind` and treat an error as fatal, so these are now accepted+  and remembered (smoltcp doesn't need them). Unknown options are still+  `ENOPROTOOPT`, not a fake success.+- **`getsockopt`** (55) reads them back, plus `SO_TYPE`, `SO_ERROR`+  (a failed nonblocking `connect` reports `ECONNREFUSED` here, once) and+  `SO_ACCEPTCONN`.+- **`shutdown`** (48) closes one direction: `SHUT_WR` sends the "no more+  data from me" (a TCP FIN) while you can still read the answer; the+  other side's `read` returns 0. Writing after it is `EPIPE` (and a+  SIGPIPE), reading after `SHUT_RD` returns 0.+- **`socketpair`** (53) makes two connected sockets, which needs+  `AF_UNIX` sockets. Rustbelt doesn't have those yet, so it says so:+  `EAFNOSUPPORT` (`EOPNOTSUPP` for `AF_INET`, as on Linux).++### Alarms and timers++Three generations of "send me a signal later":++- **`alarm(seconds)`** (37): SIGALRM once, after N seconds. Returns how+  many seconds were left on the previous alarm; `alarm(0)` cancels.+- **`setitimer`/`getitimer`** (38/36): the same timer (`ITIMER_REAL`) in+  microseconds, optionally repeating every *interval*.+- **POSIX timers** (`timer_create` 222 to `timer_delete` 226): as many as+  you like, on `CLOCK_REALTIME`, `CLOCK_MONOTONIC` or `CLOCK_BOOTTIME`,+  any signal, an absolute or relative start, and a value (`si_value`)+  that arrives in the handler's `siginfo` with `si_code = SI_TIMER`.+  `SIGEV_NONE` makes a timer you only read with `timer_gettime`.+  If a periodic timer fires while its last signal is still pending, the+  missed periods are counted as *overruns* (`timer_getoverrun`).++They all live in `src/timer.rs`: one table, and one kernel thread+(`timerd`) that sleeps until the nearest deadline and sends what's due.+Why not send signals from the timer interrupt? Sending one takes locks+and may wake threads; doing that in a normal thread keeps the interrupt+handler tiny.++As on Linux: `fork` doesn't copy timers, `execve` keeps the alarm but+deletes POSIX timers, and exiting deletes them all.++### Who am I? User ids++A process has three user ids, and the same three for groups:++- **real**: who started it;+- **effective**: whose permissions it has right now (0 = root);+- **saved**: a spare, so a program that dropped privileges can get them+  back.++Plus an *fs* id (what new files get) and a list of supplementary groups.+`setuid`, `setreuid`, `setresuid`, `setfsuid`, the group versions,+`getgroups`/`setgroups` and the getters (102-123) follow Linux's rules:+root may set anything; anyone else may only switch between the ids they+already have. So a root daemon that does `setuid(1000)` has given up root+for good, while `setresuid(1000, 1000, 0)` keeps 0 in the saved id and+can come back with `setuid(0)`. Ids are inherited by `fork` and kept by+`execve`.++They are **stored, not enforced**: nothing checks permissions yet+(stage 16 said the same about modes). `id` prints the truth, `chown`+uses them, and new files get the fs ids, but uid 1000 can still read+everything.++**Priority.** `getpriority`/`setpriority` (140/141) and `nice` keep a+nice value per process, -20 (most important) to 19. Only root may lower+it (`EACCES`). The scheduler doesn't look at it yet: every thread gets+the same share.++**Host name.** `sethostname`/`setdomainname` (170/171) change what+`uname` reports (the node name was always "qemu"); root only, at most 64+bytes.++### Whole-file locks, vectors and sendfile++- **`flock`** (73) is the other kind of file lock (stage 16 did+  `fcntl`'s). It locks the whole file, shared (`LOCK_SH`) or exclusive+  (`LOCK_EX`), with `LOCK_NB` for "don't wait" and `LOCK_UN` to let go.+  Its big difference from `fcntl` locks: it belongs to the **open file**+  (what one `open()` made, shared by `dup` and `fork`), not to the+  process, and it goes away when the *last* fd of that open file is+  closed. Two `open()`s of the same file by the same process conflict.+  This is what `flock(1)` and many package managers use.+- **`preadv`/`pwritev`** (295/296): `readv`/`writev` at an offset,+  without moving the file position.+- **`sendfile`** (40) copies from one fd to another inside the kernel,+  no user buffer in between. `cp` and web servers use it. The input must+  be something with a position (a file); the output can be anything.++### Small fixes Clarke's traces found++- **`rm -r` left files behind.** `getdents64` re-listed the directory on+  every call and skipped the entries it had already returned *by count*.+  `rm -r` deletes entries between calls, so the count was off and every+  other file was skipped. Now an open directory takes its listing once+  (until it's rewound) and `rm` sees every name. `getdents64` also+  reports the same inode number `stat` does.+- **`--trace` and interrupted calls.** A call cut short by a signal used+  to be traced as `= -512` or `-514` (`ERESTARTSYS`, `ERESTARTNOHAND`),+  kernel-internal codes no program ever sees.+  It's now traced after the signal is delivered, with what the program+  really gets: `= -4 EINTR`, or `= ? (interrupted, restarted)` when+  `SA_RESTART` makes the kernel run it again.+- **`ppoll`, `pselect6`, `epoll_pwait`** now check the signal-mask size+  (must be 8, else `EINVAL`), as Linux does.+- **The headless frame report** (`run-headless.sh`) is printed after+  every process and thread has finished, not while the last ones are+  still exiting, so the numbers no longer jump around.++`unlinkat`, `mkdirat` and `readlinkat` were audited and were already+right.++### Testing it++`user/coretest.rs` has one group of checks per argument: `links`,+`owners`, `statfs`, `info`, `sockets`, `timers`, `ids` and `misc`.+`tests/coreutils_batch2.rs` runs each group twice; the second run must+leave threads, processes, memory frames, file locks (`flock` ones too)+and timers exactly where they were. From the shell: `coretest timers`.++### What stage 17 does and doesn't do++Does: everything above, with Linux's numbers and error codes.++Doesn't (yet):++- **Ids, modes and priorities are stored, not enforced.** No permission+  checks, and the scheduler ignores nice values.+- **Timers are coarse.** `timerd` sleeps in PIT ticks (~55 ms), so a+  timer fires up to ~110 ms late (never early). A 10 ms periodic timer+  will mostly count overruns.+- **Only `ITIMER_REAL`.** `ITIMER_VIRTUAL` and `ITIMER_PROF` (CPU-time+  interval timers) are `EINVAL`, and so is `timer_create` on a CPU-time+  clock.+- **`socketpair`**, which needs `AF_UNIX` sockets; **`timerfd`**; and+  **`mremap`** (musl's `malloc` copes with `ENOSYS`) are left for later.+- **`link`/`symlink` on FAT and exFAT** are `EPERM`: those formats have+  nowhere to keep them.+- **Rustbelt's own `sysinfo` stays at 502** (as `rb_sysinfo`), next to+  Linux's 99.+ ## Roadmap  Done: all 12 chapters of the 2nd edition, plus preemptive kernel threads,@@ -4153,8 +4471,10 @@ 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), 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.+FPU/SSE/AVX state, Linux C-string path calls and `struct stat`), the+first batch of the calls musl and dash use at run time, and a second+batch for coreutils and busybox (links, owners, `statfs`, CPU times,+timers, user ids).  - [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)@@ -4192,6 +4512,7 @@ first batch of the calls musl and dash use at run time. - [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)+- [x] coreutils batch 2 (stage 17): `link`/`symlink` and their `*at` forms, native `rename` of full directories and `RENAME_NOREPLACE`, the `chown` family with owners on ramfs and ext2, `statfs`/`fstatfs`/`statx` with Linux's magic numbers and a real `st_dev`/`st_nlink`, Linux's `sysinfo` (99), `times`/`getrusage`/CPU-time clocks/`clock_getres`, `getsockname`/`getpeername`/`getsockopt`/`shutdown` and remembered socket options, `alarm`/`setitimer`/POSIX timers, real (unenforced) user and group ids, nice values, `sethostname`, `flock`, `preadv`/`pwritev`, `sendfile`, and a `getdents64` that `rm -r` can trust — [man 2 link](https://man7.org/linux/man-pages/man2/link.2.html), [man 2 statfs](https://man7.org/linux/man-pages/man2/statfs.2.html), [man 2 timer_create](https://man7.org/linux/man-pages/man2/timer_create.2.html), [man 7 credentials](https://man7.org/linux/man-pages/man7/credentials.7.html), [man 2 flock](https://man7.org/linux/man-pages/man2/flock.2.html) - [ ] 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) @@ -4412,6 +4733,27 @@ 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 17:**+  - *A number with two names.* Rustbelt's own `sysinfo` had taken 502+    while Linux's `sysinfo` is 99 with a different struct. Both exist now,+    and 502 is traced as `rb_sysinfo` so a trace never says "sysinfo" for+    two different calls.+  - *Listing a directory while deleting from it.* `getdents64` skipped+    entries by count, so `rm -r` silently missed every other file. Tests+    that only list (never delete while listing) can't see this bug.+  - *`nanosleep` ends early when a signal arrives.* A test that sleeps+    500 ms to count a periodic timer's signals woke after the first one+    and counted 1. Sleep in a loop until the time is really up.+  - *A forked child keeps the parent's open files.* With `flock`, the+    lock belongs to the open file, so the child's inherited copy kept it+    alive and a waiting child blocked forever (Linux does the same). The+    child has to close its copies first.+  - *Linux's saved-id rule is subtle.* `setreuid(-1, x)` only copies `x`+    into the saved id if `x` isn't the old real id. Read `man 2 setreuid`+    before writing the test, not after.+  - *Version and count pins again*: `nbtest` wanted "stage 16", the shell+    test "0.16.0", `run-headless.sh` 37 programs in `bin` (now 38, with+    `coretest`). - **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
@@ -79,6 +79,7 @@ const PROGRAMS: &[&str] = &[     "threads", "echoserver", "ptytest", "nbtest", // stage 15: libc plumbing     "fputest", "pathtest", // stage 15.1: musl prerequisites     "libctest", // stage 16: libc batch 1+    "coretest", // stage 17: coreutils batch 2 ];  /// 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.16.0 (stage 16: libc batch 1) x86_64" # ver+        "Rustbelt 0.17.0 (stage 17: coreutils batch 2) 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.16.0"               # a no-libc gcc binary+        "rawabi: uname: Rustbelt 0.17.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` -> 37 (the programs),+    # Pipe results: `... | grep -i EXT | wc -l` -> 1, `dir bin | wc -l` -> 38 (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 37 3 4 20000; do+    for n in 1 38 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.16.0" "$CURL_OUT" 2>/dev/null; then+    if grep -qF "Hello from Rustbelt! Rustbelt 0.17.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.16.0 (stage 16" \+    for line in "Rustbelt getty: shell on this terminal" "Rustbelt 0.17.0 (stage 17" \                 "$ 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.16.0 (stage 16: libc batch 1) x86_64" "tile: 4 windows on 1024x768, console 61x46" \+    set -- "Rustbelt 0.17.0 (stage 17: coreutils batch 2) 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" \
@@ -68,7 +68,7 @@ impl Inode for DevDir {     }     fn list(&self) -> Result<Vec<DirEntry>, Errno> {         let kind = |n| if n == "pts" { InodeKind::Directory } else { InodeKind::Device };-        Ok(names().into_iter().map(|n| DirEntry { name: String::from(n), kind: kind(n) }).collect())+        Ok(names().into_iter().map(|n| DirEntry { name: String::from(n), kind: kind(n), ino: 0 }).collect())     } } @@ -90,7 +90,7 @@ impl Inode for PtsDir {         Ok(Arc::new(PtsNode(n)))     }     fn list(&self) -> Result<Vec<DirEntry>, Errno> {-        Ok(tty::pty_numbers().into_iter().map(|n| DirEntry { name: alloc::format!("{}", n), kind: InodeKind::Device }).collect())+        Ok(tty::pty_numbers().into_iter().map(|n| DirEntry { name: alloc::format!("{}", n), kind: InodeKind::Device, ino: 0 }).collect())     } } 
@@ -30,6 +30,8 @@ pub enum Errno {     EFBIG = 27,        // file too large     ESPIPE = 29,       // can't seek (pipe, console)     EROFS = 30,        // read-only file system+    EACCES = 13,       // permission denied (setpriority: only root may raise priority)+    EMLINK = 31,       // too many hard links to one file     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)@@ -66,12 +68,13 @@ impl Errno {     /// The errno for a negative syscall result like -2, if we know it.     pub fn from_ret(ret: i64) -> Option<Errno> {         use Errno::*;-        const ALL: [Errno; 52] = [+        const ALL: [Errno; 54] = [             EPERM, ESRCH, EINTR, ERESTARTSYS, ERESTARTNOHAND, ENOENT, EIO, E2BIG, ENOEXEC, EBADF, ECHILD, EAGAIN, ENOMEM, EFAULT, EBUSY, EEXIST, ENODEV,             ENOTDIR, EISDIR, EINVAL, EMFILE, ENOTTY, EFBIG, ENOSPC, ESPIPE, EROFS, EPIPE, ERANGE,             ENAMETOOLONG, ENOSYS, ENOTEMPTY, ELOOP, EOPNOTSUPP, ENOTSOCK, EDESTADDRREQ, EMSGSIZE,             EPROTONOSUPPORT, EAFNOSUPPORT, EADDRINUSE, ENETDOWN, ENETUNREACH, ECONNRESET, EISCONN,-            ENOTCONN, ETIMEDOUT, ECONNREFUSED, ENXIO, EINPROGRESS, EXDEV, ENOPROTOOPT, EDEADLK, ENOLCK,+            ENOTCONN, ETIMEDOUT, ECONNREFUSED, ENXIO, EINPROGRESS, EXDEV, ENOPROTOOPT, EDEADLK, ENOLCK, EMLINK,+            EACCES,         ];         ALL.into_iter().find(|&e| -(e as i64) == ret)     }
@@ -332,6 +332,10 @@ struct Loc { struct InodeItem {     size: u64,     mode: u32,+    /// Hard links, owner and group (stage 17: stat shows them).+    nlink: u32,+    uid: u32,+    gid: u32,     flags: u64,     /// atime, mtime, ctime as [seconds, nanoseconds] (stage 16: for stat).     times: [[i64; 2]; 3],@@ -752,7 +756,15 @@ impl Volume {         // 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 })+        Ok(InodeItem {+            size: get64(&d, INODE_SIZE),+            mode: get32(&d, INODE_MODE),+            nlink: get32(&d, 40),+            uid: get32(&d, 44),+            gid: get32(&d, 48),+            flags: get64(&d, INODE_FLAGS),+            times,+        })     }      /// Where a directory entry points: an inode in the same tree, or the@@ -1072,6 +1084,30 @@ impl FileSystem for BtrfsFs {     fn root(&self) -> Arc<dyn Inode> {         Arc::new(BtrfsInode { vol: self.vol.clone(), loc: self.root, kind: InodeKind::Directory })     }+    /// From the superblock's totals (in sectors). btrfs doesn't count+    /// inodes (it makes them as needed), so files = 0, like Linux.+    fn statfs(&self) -> super::StatFs {+        let v = self.vol.lock();+        let bs = v.sectorsize.max(1) as u64;+        let free = v.total_bytes.saturating_sub(v.bytes_used) / bs;+        super::StatFs {+            magic: super::BTRFS_MAGIC,+            block_size: bs,+            blocks: v.total_bytes / bs,+            free_blocks: free,+            avail_blocks: free,+            files: 0,+            free_files: 0,+            name_max: 255,+            read_only: true, // our driver only reads+        }+    }+}++/// An inode number unique across subvolumes: each subvolume numbers its+/// inodes from 256, so other subvolumes' get their tree id on top.+fn ino_of(loc: Loc) -> u64 {+    if loc.root == FS_TREE { loc.ino } else { loc.ino | loc.root << 40 } }  /// A file, directory or symlink: (subvolume, inode number).@@ -1140,6 +1176,7 @@ impl Inode for BtrfsInode {         Ok(entries             .into_iter()             .map(|e| DirEntry {+                ino: if e.location.kind == ROOT_ITEM { 256 | e.location.objectid << 40 } else { ino_of(Loc { root: self.loc.root, ino: e.location.objectid }) },                 name: String::from_utf8_lossy(&e.name).into_owned(),                 kind: match e.file_type {                     2 => InodeKind::Directory,@@ -1172,6 +1209,28 @@ impl Inode for BtrfsInode {     fn remove(&self, _name: &str) -> Result<(), Errno> {         Err(Errno::EROFS)     }+    fn set_owner(&self, _uid: Option<u32>, _gid: Option<u32>) -> Result<(), Errno> {+        Err(Errno::EROFS)+    }+    fn symlink(&self, _name: &str, _target: &str) -> Result<Arc<dyn Inode>, Errno> {+        Err(Errno::EROFS)+    }+    fn link(&self, _name: &str, _target: &Arc<dyn Inode>) -> Result<(), Errno> {+        Err(Errno::EROFS)+    }+    fn rename_entry(&self, _old: &str, _new_dir: &Arc<dyn Inode>, _new: &str) -> Result<(), Errno> {+        Err(Errno::EROFS)+    }++    fn ino(&self) -> Option<u64> {+        Some(ino_of(self.loc))+    }+    fn nlink(&self) -> Option<u64> {+        self.vol.lock().inode(self.loc).ok().map(|i| i.nlink as u64)+    }+    fn owner(&self) -> (u32, u32) {+        self.vol.lock().inode(self.loc).map(|i| (i.uid, i.gid)).unwrap_or((0, 0))+    } }  #[cfg(test)]
@@ -902,6 +902,22 @@ impl FileSystem for ExfatFs {     fn root(&self) -> Arc<dyn Inode> {         Arc::new(ExfatInode { vol: self.vol.clone(), path: Vec::new(), kind: InodeKind::Directory })     }+    /// statfs (stage 17): blocks are clusters, counted by the bitmap.+    fn statfs(&self) -> super::StatFs {+        let v = self.vol.lock();+        let free = v.cluster_count.saturating_sub(v.used) as u64;+        super::StatFs {+            magic: super::EXFAT_MAGIC,+            block_size: v.cluster_size as u64,+            blocks: v.cluster_count as u64,+            free_blocks: free,+            avail_blocks: free,+            files: 0,+            free_files: 0,+            name_max: 255,+            read_only: false,+        }+    } }  /// A file or directory on an exFAT volume. exFAT has no inode numbers (a@@ -980,6 +996,7 @@ impl Inode for ExfatInode {         Ok(sets             .into_iter()             .map(|s| DirEntry {+                ino: 0,                 name: String::from_utf16_lossy(&s.name),                 kind: if s.attrs & ATTR_DIRECTORY != 0 { InodeKind::Directory } else { InodeKind::File },             })
@@ -97,6 +97,7 @@ const ROOT_INO: u32 = 2; // Superblock fields (byte offsets inside the 1024-byte superblock). const SB_INODES_COUNT: usize = 0; const SB_BLOCKS_COUNT: usize = 4;+const SB_R_BLOCKS: usize = 8; // blocks reserved for root const SB_FREE_BLOCKS: usize = 12; const SB_FREE_INODES: usize = 16; const SB_FIRST_DATA_BLOCK: usize = 20; // block holding the superblock: 1 with 1 KiB blocks, else 0@@ -129,6 +130,10 @@ const GD_USED_DIRS: usize = 16; // u16  // Inode fields (the classic first 128 bytes). const I_MODE: usize = 0; // u16+const I_UID: usize = 2; // u16 (low half; the high half is at I_UID_HIGH)+const I_GID: usize = 24; // u16+const I_UID_HIGH: usize = 120; // in the Linux part of "osd2"+const I_GID_HIGH: usize = 122; const I_SIZE: usize = 4; const I_ATIME: usize = 8; const I_CTIME: usize = 12;@@ -150,6 +155,7 @@ const S_IFLNK: u16 = 0o120000; // Directory entry file types (with INCOMPAT_FILETYPE). const FT_REG: u8 = 1; const FT_DIR: u8 = 2;+const FT_SYMLINK: u8 = 7;  const INDEX_FL: u32 = 0x1000; // directory has an htree index @@ -237,6 +243,29 @@ impl DiskInode {     fn set_times(&mut self, which: &[usize], t: u32) {         which.iter().for_each(|&at| put32(&mut self.raw, at, t));     }+    fn owner(&self) -> (u32, u32) {+        let uid = get16(&self.raw, I_UID) as u32 | (get16(&self.raw, I_UID_HIGH) as u32) << 16;+        let gid = get16(&self.raw, I_GID) as u32 | (get16(&self.raw, I_GID_HIGH) as u32) << 16;+        (uid, gid)+    }+    fn set_owner(&mut self, uid: Option<u32>, gid: Option<u32>) {+        if let Some(uid) = uid {+            put16(&mut self.raw, I_UID, uid as u16);+            put16(&mut self.raw, I_UID_HIGH, (uid >> 16) as u16);+        }+        if let Some(gid) = gid {+            put16(&mut self.raw, I_GID, gid as u16);+            put16(&mut self.raw, I_GID_HIGH, (gid >> 16) as u16);+        }+    }+    /// The directory-entry type byte for this inode.+    fn file_type(&self) -> u8 {+        match self.kind() {+            InodeKind::Directory => FT_DIR,+            InodeKind::Symlink => FT_SYMLINK,+            _ => FT_REG,+        }+    } }  /// One directory entry, as read from disk.@@ -334,10 +363,10 @@ impl Volume {         get32(&self.sb, at)     } -    /// Timestamp for new and changed inodes. We have no clock driver yet,-    /// so we reuse the superblock's "last written" time (set by mke2fs).+    /// Timestamp for new and changed inodes: the wall clock (stage 17;+    /// before, with no clock driver, the superblock's "last written" time).     fn now(&self) -> u32 {-        self.sb32(SB_WTIME).max(1)+        (super::now_timespec()[0].clamp(0, u32::MAX as i64) as u32).max(self.sb32(SB_WTIME)).max(1)     }      // ---- blocks ----------------------------------------------------------@@ -899,6 +928,141 @@ impl Volume {         self.free_inode(ino, is_dir)     } +    /// Checks every new name goes through: writable, a valid name, the+    /// parent a directory without that name yet. Returns the parent.+    fn check_new_entry(&mut self, parent_ino: u32, name: &str) -> Result<DiskInode, Errno> {+        if !self.writable {+            return Err(Errno::EROFS);+        }+        if name.is_empty() || name == "." || name == ".." || name.contains('/') {+            return Err(Errno::EINVAL);+        }+        if name.len() > 255 {+            return Err(Errno::ENAMETOOLONG);+        }+        let parent = self.read_inode(parent_ino)?;+        if parent.kind() != InodeKind::Directory {+            return Err(Errno::ENOTDIR);+        }+        if self.find_entry(&parent, name)?.is_some() {+            return Err(Errno::EEXIST);+        }+        Ok(parent)+    }++    /// symlink (stage 17). Like Linux, a target of up to 60 bytes is a+    /// "fast" symlink: stored in the inode's block pointers themselves, no+    /// data block. Longer targets (up to one block) get a data block.+    fn symlink(&mut self, parent_ino: u32, name: &str, target: &str) -> Result<u32, Errno> {+        let mut parent = self.check_new_entry(parent_ino, name)?;+        if target.len() > self.block_size.min(4095) {+            return Err(Errno::ENAMETOOLONG);+        }+        let mut inode = self.alloc_inode(self.group_of(parent_ino), false)?;+        let now = self.now();+        inode.set_times(&[I_ATIME, I_CTIME, I_MTIME], now);+        put16(&mut inode.raw, I_MODE, S_IFLNK | 0o777);+        inode.set_links(1);+        let result = (|| {+            if target.len() <= 60 {+                inode.raw[I_BLOCK..I_BLOCK + target.len()].copy_from_slice(target.as_bytes());+                inode.set_size(target.len() as u64);+                self.write_inode(&inode)?;+            } else {+                self.write_file(&mut inode, 0, target.as_bytes())?; // (writes the inode)+            }+            self.add_entry(&mut parent, name, inode.ino, FT_SYMLINK)+        })();+        if let Err(e) = result {+            if target.len() > 60 {+                let _ = self.free_data(&mut inode);+            }+            let _ = self.free_inode(inode.ino, false);+            return Err(e);+        }+        Ok(inode.ino)+    }++    /// Hard link (stage 17): one more entry for inode `ino`, one more in+    /// its link count.+    fn link(&mut self, parent_ino: u32, name: &str, ino: u32) -> Result<(), Errno> {+        let mut parent = self.check_new_entry(parent_ino, name)?;+        let mut child = self.read_inode(ino)?;+        if child.kind() == InodeKind::Directory {+            return Err(Errno::EPERM);+        }+        if child.links() >= 32000 {+            return Err(Errno::EMLINK); // Linux's EXT2_LINK_MAX+        }+        self.add_entry(&mut parent, name, ino, child.file_type())?;+        child.set_links(child.links() + 1);+        let now = self.now();+        child.set_times(&[I_CTIME], now);+        self.write_inode(&child)+    }++    /// rename (stage 17): add the new entry, drop the old one. A replaced+    /// file loses a link (and is freed if that was its last); a moved+    /// directory gets its ".." pointed at the new parent. The VFS checked+    /// the kinds and that a replaced directory is empty.+    fn rename(&mut self, old_parent: u32, old: &str, new_parent: u32, new: &str) -> Result<(), Errno> {+        if !self.writable {+            return Err(Errno::EROFS);+        }+        if new.is_empty() || new == "." || new == ".." || new.contains('/') {+            return Err(Errno::EINVAL);+        }+        if new.len() > 255 {+            return Err(Errno::ENAMETOOLONG);+        }+        let dir = self.read_inode(old_parent)?;+        let ino = self.find_entry(&dir, old)?.ok_or(Errno::ENOENT)?;+        let mut child = self.read_inode(ino)?;+        let is_dir = child.kind() == InodeKind::Directory;+        let target_dir = self.read_inode(new_parent)?;+        if let Some(existing) = self.find_entry(&target_dir, new)? {+            if existing == ino {+                return Ok(());+            }+            self.remove(new_parent, new)?;+        }+        let mut target_dir = self.read_inode(new_parent)?;+        self.add_entry(&mut target_dir, new, ino, child.file_type())?;+        let mut dir = self.read_inode(old_parent)?;+        self.remove_entry(&mut dir, old)?;+        if is_dir && old_parent != new_parent {+            self.set_dotdot(&mut child, new_parent)?;+            let mut from = self.read_inode(old_parent)?;+            from.set_links(from.links() - 1);+            self.write_inode(&from)?;+            let mut to = self.read_inode(new_parent)?;+            to.set_links(to.links() + 1);+            self.write_inode(&to)?;+        }+        let now = self.now();+        child.set_times(&[I_CTIME], now);+        self.write_inode(&child)+    }++    /// Point a directory's ".." entry at `parent`.+    fn set_dotdot(&mut self, dir: &mut DiskInode, parent: u32) -> Result<(), Errno> {+        let block = self.bmap(dir, 0, false)?;+        if block == 0 {+            return Err(Errno::EIO);+        }+        let mut data = self.read_block(block)?;+        let mut at = 0;+        while at < data.len() {+            let (e_ino, rec_len, name_len, _) = self.parse_entry(&data, at)?;+            if e_ino != 0 && &data[at + 8..at + 8 + name_len] == b".." {+                put32(&mut data, at, parent);+                return self.write_block(block, &data);+            }+            at += rec_len;+        }+        Err(Errno::EIO) // a directory without ".."+    }+     fn truncate(&mut self, ino: u32) -> Result<(), Errno> {         if !self.writable {             return Err(Errno::EROFS);@@ -976,6 +1140,21 @@ impl FileSystem for Ext2Fs {     fn root(&self) -> Arc<dyn Inode> {         Arc::new(Ext2Inode { vol: self.vol.clone(), ino: ROOT_INO, kind: InodeKind::Directory })     }+    fn statfs(&self) -> super::StatFs {+        let v = self.vol.lock();+        let free = v.sb32(SB_FREE_BLOCKS) as u64;+        super::StatFs {+            magic: super::EXT2_MAGIC,+            block_size: v.block_size as u64,+            blocks: v.sb32(SB_BLOCKS_COUNT) as u64,+            free_blocks: free,+            avail_blocks: free.saturating_sub(v.sb32(SB_R_BLOCKS) as u64),+            files: v.sb32(SB_INODES_COUNT) as u64,+            free_files: v.sb32(SB_FREE_INODES) as u64,+            name_max: 255,+            read_only: !v.writable,+        }+    } }  /// A file, directory or symlink on an ext2 volume: just its inode number.@@ -1119,7 +1298,7 @@ impl Inode for Ext2Inode {                 7 => InodeKind::Symlink,                 _ => vol.read_inode(e.ino)?.kind(), // revision 0: no type in the entry             };-            out.push(DirEntry { name: e.name, kind });+            out.push(DirEntry { name: e.name, kind, ino: e.ino as u64 });         }         Ok(out)     }@@ -1128,4 +1307,51 @@ impl Inode for Ext2Inode {         self.dir_only()?;         self.vol.lock().remove(self.ino, name)     }++    // --- stage 17 ----------------------------------------------------------++    fn ino(&self) -> Option<u64> {+        Some(self.ino as u64)+    }++    fn nlink(&self) -> Option<u64> {+        self.vol.lock().read_inode(self.ino).ok().map(|i| i.links() as u64)+    }++    fn owner(&self) -> (u32, u32) {+        self.vol.lock().read_inode(self.ino).map(|i| i.owner()).unwrap_or((0, 0))+    }++    fn set_owner(&self, uid: Option<u32>, gid: Option<u32>) -> Result<(), Errno> {+        let vol = self.vol.lock();+        if !vol.writable {+            return Err(Errno::EROFS);+        }+        let mut inode = vol.read_inode(self.ino)?;+        inode.set_owner(uid, gid);+        inode.set_times(&[I_CTIME], vol.now());+        vol.write_inode(&inode)+    }++    fn symlink(&self, name: &str, target: &str) -> Result<Arc<dyn Inode>, Errno> {+        self.dir_only()?;+        let ino = self.vol.lock().symlink(self.ino, name, target)?;+        Ok(self.child(ino, InodeKind::Symlink))+    }++    fn link(&self, name: &str, target: &Arc<dyn Inode>) -> Result<(), Errno> {+        self.dir_only()?;+        let other = super::downcast::<Ext2Inode>(target).filter(|o| Arc::ptr_eq(&o.vol, &self.vol)).ok_or(Errno::EXDEV)?;+        self.vol.lock().link(self.ino, name, other.ino)+    }++    fn rename_entry(&self, old: &str, new_dir: &Arc<dyn Inode>, new: &str) -> Result<(), Errno> {+        self.dir_only()?;+        let other = super::downcast::<Ext2Inode>(new_dir).filter(|o| Arc::ptr_eq(&o.vol, &self.vol)).ok_or(Errno::EXDEV)?;+        self.vol.lock().rename(self.ino, old, other.ino, new)+    }++    fn as_any(self: Arc<Self>) -> Option<Arc<dyn core::any::Any + Send + Sync>> {+        Some(self)+    } }
@@ -207,6 +207,26 @@ impl FileSystem for FatFs {     fn root(&self) -> Arc<dyn Inode> {         Arc::new(FatInode { fs: self.fs.clone(), path: String::new(), kind: InodeKind::Directory })     }+    /// statfs (stage 17): FAT counts clusters, so a "block" is a cluster.+    /// There's no inode table, so files = 0 (Linux's vfat does the same).+    fn statfs(&self) -> super::StatFs {+        let fs = self.fs.lock();+        let (bs, total, free) = match fs.stats() {+            Ok(st) => (st.cluster_size() as u64, st.total_clusters() as u64, st.free_clusters() as u64),+            Err(_) => (fs.cluster_size() as u64, 0, 0),+        };+        super::StatFs {+            magic: super::MSDOS_MAGIC,+            block_size: bs,+            blocks: total,+            free_blocks: free,+            avail_blocks: free,+            files: 0,+            free_files: 0,+            name_max: 255,+            read_only: false,+        }+    } }  /// A file or directory on a FAT volume, identified by its path from the@@ -366,7 +386,7 @@ impl Inode for FatInode {                     continue; // the VFS doesn't list these (like our ramfs)                 }                 let kind = if entry.is_dir() { InodeKind::Directory } else { InodeKind::File };-                out.push(DirEntry { name, kind });+                out.push(DirEntry { name, kind, ino: 0 });             }             Ok(out)         })
@@ -112,6 +112,30 @@ impl LinuxStat {     } } +/// `struct stat` for an inode reached by `path` (stage 17): the real+/// inode number, link count and owner where the file system has them+/// (ramfs, ext2, btrfs), the path hash and made-up values where it doesn't+/// (FAT, exFAT, /dev), and which mounted file system it's on (st_dev).+pub fn linux_stat(inode: &dyn Inode, path: &str) -> LinuxStat {+    let mut st = LinuxStat::new(&Stat::of(inode), inode.ino().unwrap_or_else(|| ino_hash(path.as_bytes()))).with_times(inode.times());+    if let Some(n) = inode.nlink() {+        st.st_nlink = n.max(1);+    }+    (st.st_uid, st.st_gid) = inode.owner();+    st.st_dev = super::dev_of(path);+    st+}++/// The inode number getdents reports for entry `name` of the directory at+/// `dir`: the file system's own, or the same path hash `linux_stat` uses.+fn entry_ino(dir: &str, entry: &super::DirEntry) -> u64 {+    if entry.ino != 0 {+        return entry.ino;+    }+    let path = if dir == "/" { alloc::format!("/{}", entry.name) } else { alloc::format!("{}/{}", dir, entry.name) };+    ino_hash(path.as_bytes()).max(1)+}+ /// A 64-bit FNV-1a hash: our stand-in for inode numbers (from the path). pub fn ino_hash(bytes: &[u8]) -> u64 {     bytes.iter().fold(0xcbf2_9ce4_8422_2325, |h, &b| (h ^ b as u64).wrapping_mul(0x100_0000_01b3))@@ -226,6 +250,17 @@ pub struct InodeFile {     accmode: u64,     /// The absolute path it was opened by (see `File::path`).     path: String,+    /// Directories (stage 17): the listing, taken at the first getdents+    /// and kept until a rewind, so deleting entries while reading the+    /// directory (`rm -r`) doesn't make us skip any.+    listing: Mutex<Option<alloc::vec::Vec<super::DirEntry>>>,+}++impl Drop for InodeFile {+    /// The last fd for this open file is gone: drop its flock (stage 17).+    fn drop(&mut self) {+        super::lock::flock_release(self as *const InodeFile as usize);+    } }  /// open() flag bits InodeFile looks at (Linux's values).@@ -259,6 +294,7 @@ impl InodeFile {             nonblock: AtomicBool::new(flags & O_NONBLOCK != 0),             accmode,             path,+            listing: Mutex::new(None),         })     } }@@ -283,7 +319,17 @@ impl File for InodeFile {      fn seek(&self, offset: i64, whence: u64) -> Result<u64, Errno> {         if self.inode.kind() == InodeKind::Directory {-            return if offset == 0 && whence == SEEK_SET { *self.offset.lock() = 0; Ok(0) } else { Err(Errno::EINVAL) };+            // The offset is an entry index (what getdents puts in d_off),+            // so seekdir/telldir work. Back to 0 (rewinddir) re-reads it.+            if whence != SEEK_SET || offset < 0 {+                return if whence == SEEK_CUR && offset == 0 { Ok(*self.offset.lock() as u64) } else { Err(Errno::EINVAL) };+            }+            let mut listing = self.listing.lock();+            if offset == 0 {+                *listing = None;+            }+            *self.offset.lock() = offset as usize;+            return Ok(offset as u64);         }         let mut current = self.offset.lock();         let base = match whence {@@ -348,9 +394,14 @@ impl File for InodeFile {     }      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).-        let entries = self.inode.list()?;+        // List the directory once (stage 17; before, each call re-listed+        // it and skipped by index, which skipped entries when files were+        // deleted in between) and hand it out from that copy.+        let mut listing = self.listing.lock();+        if listing.is_none() {+            *listing = Some(self.inode.list()?);+        }+        let entries = listing.as_ref().unwrap();         let mut index = self.offset.lock();         let mut used = 0;         for entry in entries.iter().skip(*index) {@@ -365,7 +416,7 @@ impl File for InodeFile {             }             let rec = &mut buf[used..used + reclen];             rec.fill(0);-            rec[0..8].copy_from_slice(&(*index as u64 + 1).to_le_bytes()); // fake inode number+            rec[0..8].copy_from_slice(&entry_ino(&self.path, entry).to_le_bytes()); // the inode number stat gives             rec[8..16].copy_from_slice(&(*index as i64 + 1).to_le_bytes()); // offset of next             rec[16..18].copy_from_slice(&(reclen as u16).to_le_bytes());             rec[18] = match entry.kind {
@@ -188,7 +188,75 @@ pub fn process_exited(pid: u64) {     UNLOCKED.wake_all(); } -/// How many locks exist (tests: nothing left behind).+/// How many locks exist, record locks and flocks (tests: nothing left behind). pub fn count() -> usize {-    without_interrupts(|| TABLE.lock().files.values().map(|v| v.len()).sum())+    without_interrupts(|| TABLE.lock().files.values().map(|v| v.len()).sum::<usize>()) + flock_count()+}++// --- flock (stage 17) ------------------------------------------------------+//+// flock(fd, LOCK_SH / LOCK_EX / LOCK_UN, maybe | LOCK_NB) locks a *whole*+// file, BSD style. Unlike the record locks above, a flock belongs to the+// *open file* (what open() made; dup() and fork() share it), not to a+// process: two open()s of the same file in one process do conflict, and the+// lock goes away when the last fd sharing that open file is closed+// (`Drop for InodeFile` calls `flock_release`).+//+// Many holders may share LOCK_SH; LOCK_EX allows nobody else. Asking again+// on an open file that already holds one *converts* it (SH <-> EX).++/// file key -> the open files holding a flock on it: (owner, exclusive).+static FLOCKS: Mutex<BTreeMap<u64, Vec<(usize, bool)>>> = Mutex::new(BTreeMap::new());++fn flock_conflict(t: &BTreeMap<u64, Vec<(usize, bool)>>, key: u64, owner: usize, exclusive: bool) -> bool {+    t.get(&key).is_some_and(|v| v.iter().any(|&(o, ex)| o != owner && (ex || exclusive)))+}++/// Remove `owner`'s flock (any file). True if it had one.+fn flock_drop(t: &mut BTreeMap<u64, Vec<(usize, bool)>>, owner: usize) -> bool {+    let mut found = false;+    for v in t.values_mut() {+        let before = v.len();+        v.retain(|&(o, _)| o != owner);+        found |= v.len() != before;+    }+    t.retain(|_, v| !v.is_empty());+    found+}++/// Take (or convert to) a shared / exclusive flock on file `key` for open+/// file `owner`. `wait`: sleep until it's free (a signal interrupts:+/// ERESTARTSYS); else EAGAIN (= EWOULDBLOCK) at once.+pub fn flock(key: u64, owner: usize, exclusive: bool, wait: bool) -> Result<(), Errno> {+    let try_take = || {+        let mut t = FLOCKS.lock();+        if flock_conflict(&t, key, owner, exclusive) {+            return None;+        }+        let v = t.entry(key).or_default();+        v.retain(|&(o, _)| o != owner);+        v.push((owner, exclusive));+        Some(())+    };+    if !wait {+        return without_interrupts(try_take).ok_or(Errno::EAGAIN);+    }+    // Linux drops the old lock before waiting for a conversion (so two+    // holders upgrading at once can't deadlock); so do we.+    if without_interrupts(|| flock_drop(&mut FLOCKS.lock(), owner)) {+        UNLOCKED.wake_all();+    }+    UNLOCKED.wait_until_intr(try_take)+}++/// LOCK_UN, or the open file went away: drop its flock, if any.+pub fn flock_release(owner: usize) {+    if without_interrupts(|| flock_drop(&mut FLOCKS.lock(), owner)) {+        UNLOCKED.wake_all();+    }+}++/// How many flocks are held (tests: nothing left behind).+pub fn flock_count() -> usize {+    without_interrupts(|| FLOCKS.lock().values().map(|v| v.len()).sum()) }
@@ -57,6 +57,9 @@ pub enum InodeKind { pub struct DirEntry {     pub name: String,     pub kind: InodeKind,+    /// The child's inode number (stage 17: what getdents64 reports as+    /// d_ino), or 0 if the file system has none (FAT, exFAT, /dev).+    pub ino: u64, }  /// A file or directory inside a file system.@@ -165,8 +168,83 @@ pub trait Inode: Send + Sync {     fn set_times(&self, _atime: Option<[i64; 2]>, _mtime: Option<[i64; 2]>) -> Result<(), Errno> {         Ok(())     }++    // --- Stage 17: links, owners, renames (coreutils / busybox) ----------++    /// A real inode number (ext2's, btrfs's, a ramfs node's address), or+    /// None: stat then hashes the path instead (FAT, exFAT, /dev).+    fn ino(&self) -> Option<u64> {+        None+    }+    /// How many names (hard links) the inode has; None = "1, or 2 for a+    /// directory" (file systems without hard links).+    fn nlink(&self) -> Option<u64> {+        None+    }+    /// Owner (uid, gid). File systems without owners say root (0, 0).+    fn owner(&self) -> (u32, u32) {+        (0, 0)+    }+    /// chown: set the owner and/or group (None = leave it). EPERM where+    /// the file system can't store them (FAT, exFAT, /dev, like Linux's+    /// vfat), EROFS on read-only btrfs.+    fn set_owner(&self, _uid: Option<u32>, _gid: Option<u32>) -> Result<(), Errno> {+        Err(Errno::EPERM)+    }+    /// Create a symlink `name` -> `target` in this directory. EPERM where+    /// the file system has no symlinks (FAT and exFAT: Linux says EPERM too).+    fn symlink(&self, _name: &str, _target: &str) -> Result<Arc<dyn Inode>, Errno> {+        Err(Errno::EPERM)+    }+    /// Hard link: add `name` in this directory for the existing `target`+    /// (same file system, checked by the caller). EPERM where there are no+    /// hard links (FAT, exFAT).+    fn link(&self, _name: &str, _target: &Arc<dyn Inode>) -> Result<(), Errno> {+        Err(Errno::EPERM)+    }+    /// Move entry `old` of this directory to `new` in `new_dir` (same file+    /// system), replacing what's there (the caller checked that's allowed).+    /// ENOSYS = "can't do it directly": `rename` then copies and deletes.+    fn rename_entry(&self, _old: &str, _new_dir: &Arc<dyn Inode>, _new: &str) -> Result<(), Errno> {+        Err(Errno::ENOSYS)+    }+    /// The concrete type behind the trait object, so a file system can+    /// recognise its own inodes (for link and rename). None = don't care.+    fn as_any(self: Arc<Self>) -> Option<Arc<dyn core::any::Any + Send + Sync>> {+        None+    }+}++/// Downcast an inode to a file system's own type (stage 17).+pub fn downcast<T: Send + Sync + 'static>(inode: &Arc<dyn Inode>) -> Option<Arc<T>> {+    inode.clone().as_any()?.downcast::<T>().ok()+}++/// What statfs/fstatfs report about a file system (stage 17).+#[derive(Debug, Clone, Copy, Default)]+pub struct StatFs {+    /// The file system's magic number (f_type), as in <linux/magic.h>.+    pub magic: u64,+    /// Block size in bytes, and the counts in those blocks.+    pub block_size: u64,+    pub blocks: u64,+    pub free_blocks: u64,+    /// Free blocks an ordinary user may use (ext2 keeps some for root).+    pub avail_blocks: u64,+    /// Inodes (0 = the file system doesn't count them).+    pub files: u64,+    pub free_files: u64,+    pub name_max: u64,+    pub read_only: bool, } +pub const RAMFS_MAGIC: u64 = 0x8584_58f6;+pub const TMPFS_MAGIC: u64 = 0x0102_1994;+pub const EXT2_MAGIC: u64 = 0xEF53;+pub const MSDOS_MAGIC: u64 = 0x4d44;+pub const EXFAT_MAGIC: u64 = 0x2011_BAB0;+pub const BTRFS_MAGIC: u64 = 0x9123_683E;+ /// The wall-clock time as [seconds, nanoseconds] since 1970 (file times). pub fn now_timespec() -> [i64; 2] {     let us = crate::time::realtime_us();@@ -182,6 +260,11 @@ pub trait FileSystem: Send + Sync {     /// Type name, like the `-t` argument of `mount` ("ramfs", "fat32", ...).     fn name(&self) -> &'static str;     fn root(&self) -> Arc<dyn Inode>;+    /// Sizes and counts for statfs (stage 17). The default suits a+    /// memory-only file system that keeps no counts (/dev: devtmpfs).+    fn statfs(&self) -> StatFs {+        StatFs { magic: TMPFS_MAGIC, block_size: 4096, name_max: 255, ..Default::default() }+    } }  struct Mount {@@ -347,38 +430,117 @@ fn mount_of(path: &str) -> Option<String> {     mounts.iter().filter(|m| mount_contains(&m.path, path)).max_by_key(|m| m.path.len()).map(|m| m.path.clone()) } -/// rename(old, new). Our `Inode` trait has no "move this entry" operation-/// yet, so we do it the long way, which is enough for what programs use-/// rename for (save to a temp file, then rename it over the real one):+/// The file system `path` lives on and its place in the mount table+/// (stage 17: statfs, and st_dev = place + 1 so that inode numbers of+/// different file systems never look like the same file).+pub fn fs_of(path: &str) -> Option<(usize, Arc<dyn FileSystem>)> {+    let path = normalize("/", path).ok()?;+    let mounts = MOUNTS.lock();+    mounts+        .iter()+        .enumerate()+        .filter(|(_, m)| mount_contains(&m.path, &path))+        .max_by_key(|(_, m)| m.path.len())+        .map(|(i, m)| (i, m.fs.clone()))+}++/// st_dev for a path: which mounted file system (see `fs_of`).+pub fn dev_of(path: &str) -> u64 {+    fs_of(path).map(|(i, _)| i as u64 + 1).unwrap_or(0)+}++/// symlink(target, path): a new symlink at absolute `path` holding+/// `target` exactly as given (it's resolved when the link is followed).+pub fn symlink(target: &str, path: &str) -> Result<Arc<dyn Inode>, Errno> {+    if target.is_empty() {+        return Err(Errno::ENOENT);+    }+    if lookup_nofollow(path).is_ok() {+        return Err(Errno::EEXIST);+    }+    let (parent, name) = parent_of(path)?;+    parent.symlink(&name, target)+}++/// link(old, new): a second name for the file at `old` (`follow`: if `old`+/// is a symlink, link what it points to, like linkat's AT_SYMLINK_FOLLOW).+pub fn link(old: &str, new: &str, follow: bool) -> Result<(), Errno> {+    let (old, new) = (normalize("/", old)?, normalize("/", new)?);+    let src = if follow { lookup(&old)? } else { lookup_nofollow(&old)? };+    link_inode(&src, &old, &new)+}++/// Hard-link the inode `src` (found at `src_path`) as `new`.+pub fn link_inode(src: &Arc<dyn Inode>, src_path: &str, new: &str) -> Result<(), Errno> {+    if src.kind() == InodeKind::Directory {+        return Err(Errno::EPERM); // no hard links to directories, ever+    }+    let new = normalize("/", new)?;+    if lookup_nofollow(&new).is_ok() {+        return Err(Errno::EEXIST);+    }+    if mount_of(src_path) != mount_of(&new) {+        return Err(Errno::EXDEV);+    }+    let (parent, name) = parent_of(&new)?;+    parent.link(&name, src)+}++/// rename(old, new), and renameat2's RENAME_NOREPLACE (`noreplace`: fail+/// with EEXIST instead of replacing `new`).+///+/// ramfs, ext2 and FAT move the directory entry itself (stage 17,+/// `Inode::rename_entry`): any file, symlink or directory, full or not,+/// and hard links stay links. Other file systems (exFAT) get the long way+/// round, enough for "save to a temp file, rename it over the real one": ///   - a file: copy its bytes to `new` (replacing a file there), then ///     delete `old`; ///   - an empty directory: create `new` (or reuse an empty one), delete `old`;-///   - anything else (a non-empty directory, a symlink, a device), or-///     across file systems: EXDEV, which tells `mv` to copy and delete-///     itself, like Linux does across file systems.-/// Not atomic, unlike Linux's: a crash in the middle can leave both.-pub fn rename(old: &str, new: &str) -> Result<(), Errno> {+///   - anything else (a non-empty directory, a symlink), or across file+///     systems: EXDEV, which tells `mv` to copy and delete itself, like+///     Linux does across file systems.+/// Neither is atomic like Linux's: a crash in the middle can leave both.+pub fn rename(old: &str, new: &str, noreplace: bool) -> Result<(), Errno> {     let (old, new) = (normalize("/", old)?, normalize("/", new)?);     let src = lookup_nofollow(&old)?;-    if old == new {-        return Ok(());-    }     if new.starts_with(&old) && new.as_bytes().get(old.len()) == Some(&b'/') {         return Err(Errno::EINVAL); // a directory into itself     }+    let existing = lookup_nofollow(&new).ok();+    if noreplace && existing.is_some() {+        return Err(Errno::EEXIST); // (even renaming a file onto itself)+    }+    if old == new {+        return Ok(());+    }     if MOUNTS.lock().iter().any(|m| m.path == old || m.path == new) {         return Err(Errno::EBUSY);     }     if mount_of(&old) != mount_of(&new) {         return Err(Errno::EXDEV);     }-    let existing = lookup_nofollow(&new).ok();+    // What may be replaced: a directory only by a directory, and only an+    // empty one; anything else only by a non-directory.+    if let Some(dst) = &existing {+        if src.ino().is_some() && src.ino() == dst.ino() {+            return Ok(()); // two hard links of one file: nothing to do (POSIX)+        }+        match (src.kind() == InodeKind::Directory, dst.kind() == InodeKind::Directory) {+            (true, false) => return Err(Errno::ENOTDIR),+            (false, true) => return Err(Errno::EISDIR),+            (true, true) if !dst.list()?.is_empty() => return Err(Errno::ENOTEMPTY),+            _ => {}+        }+    }+    let (old_parent, old_name) = parent_of(&old)?;+    let (new_parent, new_name) = parent_of(&new)?;+    match old_parent.rename_entry(&old_name, &new_parent, &new_name) {+        Err(Errno::ENOSYS) => {} // not supported here: the long way below+        other => return other,+    }     match src.kind() {         InodeKind::File => {-            if let Some(dst) = &existing {-                if dst.kind() == InodeKind::Directory {-                    return Err(Errno::EISDIR);-                }+            if existing.is_some() {                 remove(&new, false)?;             }             let data = read_all(&old)?;@@ -393,13 +555,8 @@ pub fn rename(old: &str, new: &str) -> Result<(), Errno> {             if !src.list()?.is_empty() {                 return Err(Errno::EXDEV);             }-            match &existing {-                Some(dst) if dst.kind() != InodeKind::Directory => return Err(Errno::ENOTDIR),-                Some(dst) if !dst.list()?.is_empty() => return Err(Errno::ENOTEMPTY),-                Some(_) => {}-                None => {-                    create(&new, InodeKind::Directory)?;-                }+            if existing.is_none() {+                create(&new, InodeKind::Directory)?;             }             remove(&old, true)         }
@@ -3,8 +3,10 @@ //! 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).+//! Stage 17 adds symlinks, hard links (two directory entries holding the+//! same `Arc`), owners (chown) and renames that just move the `Arc`. -use super::{now_timespec, DirEntry, FileSystem, Inode, InodeKind, MODE_SET};+use super::{now_timespec, DirEntry, FileSystem, Inode, InodeKind, StatFs, MODE_SET, RAMFS_MAGIC}; use crate::errno::Errno; use alloc::{collections::BTreeMap, string::String, sync::Arc, vec::Vec}; use spin::Mutex;@@ -26,11 +28,29 @@ impl FileSystem for RamFs {     fn root(&self) -> Arc<dyn Inode> {         self.root.clone()     }+    /// A ramfs can grow until memory runs out, so that's its "size" (Linux's+    /// ramfs says 0 for everything; numbers are friendlier to `df`).+    fn statfs(&self) -> StatFs {+        let (total, free) = crate::memory::frame_totals();+        StatFs {+            magic: RAMFS_MAGIC,+            block_size: 4096,+            blocks: total,+            free_blocks: free,+            avail_blocks: free,+            files: total,+            free_files: free,+            name_max: 255,+            read_only: false,+        }+    } }  pub enum Node {     File(Mutex<Vec<u8>>),     Dir(Mutex<BTreeMap<String, Arc<RamInode>>>),+    /// A symlink's target (stage 17).+    Symlink(String), }  /// What `stat` shows besides the contents (stage 16).@@ -40,6 +60,12 @@ struct Meta {     mode: u32,     /// atime, mtime, ctime as [seconds, nanoseconds].     times: [[i64; 2]; 3],+    /// Owner and group (stage 17: chown; new files get the creator's).+    uid: u32,+    gid: u32,+    /// Directory entries naming this node (hard links; directories count+    /// their subdirectories' ".." on top, see `nlink`).+    links: u32, }  pub struct RamInode {@@ -50,7 +76,26 @@ pub struct RamInode { impl RamInode {     fn new(node: Node) -> RamInode {         let now = now_timespec();-        RamInode { node, meta: Mutex::new(Meta { mode: 0, times: [now; 3] }) }+        RamInode { node, meta: Mutex::new(Meta { mode: 0, times: [now; 3], uid: 0, gid: 0, links: 1 }) }+    }++    fn entries(&self) -> Result<&Mutex<BTreeMap<String, Arc<RamInode>>>, Errno> {+        match &self.node {+            Node::Dir(entries) => Ok(entries),+            _ => Err(Errno::ENOTDIR),+        }+    }++    /// A directory entry pointing at `node` went away.+    fn unlinked(node: &RamInode) {+        let mut meta = node.meta.lock();+        meta.links = meta.links.saturating_sub(1);+        meta.times[2] = now_timespec();+    }++    /// The status-change time (ctime) is now.+    fn changed(&self) {+        self.meta.lock().times[2] = now_timespec();     }      /// The contents changed: new modification and status-change time.@@ -67,6 +112,7 @@ impl Inode for RamInode {         match &self.node {             Node::File(_) => InodeKind::File,             Node::Dir(_) => InodeKind::Directory,+            Node::Symlink(_) => InodeKind::Symlink,         }     } @@ -74,6 +120,14 @@ impl Inode for RamInode {         match &self.node {             Node::File(data) => data.lock().len(),             Node::Dir(entries) => entries.lock().len(),+            Node::Symlink(target) => target.len(),+        }+    }++    fn readlink(&self) -> Result<String, Errno> {+        match &self.node {+            Node::Symlink(target) => Ok(target.clone()),+            _ => Err(Errno::EINVAL),         }     } @@ -109,9 +163,7 @@ impl Inode for RamInode {      fn create(&self, name: &str, kind: InodeKind) -> Result<Arc<dyn Inode>, Errno> {         let Node::Dir(entries) = &self.node else { return Err(Errno::ENOTDIR) };-        if name.is_empty() || name.contains('/') || name.len() > 255 {-            return Err(Errno::EINVAL);-        }+        check_name(name)?;         let mut entries = entries.lock();         if entries.contains_key(name) {             return Err(Errno::EEXIST);@@ -119,16 +171,18 @@ impl Inode for RamInode {         let node = Arc::new(RamInode::new(match kind {             InodeKind::File => Node::File(Mutex::new(Vec::new())),             InodeKind::Directory => Node::Dir(Mutex::new(BTreeMap::new())),-            InodeKind::Symlink | InodeKind::Device => return Err(Errno::EOPNOTSUPP), // ramfs has neither+            InodeKind::Symlink | InodeKind::Device => return Err(Errno::EOPNOTSUPP), // symlinks: `symlink`         }));         entries.insert(String::from(name), node.clone());+        drop(entries);+        self.touch();         Ok(node)     }      fn list(&self) -> Result<Vec<DirEntry>, Errno> {         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())+        Ok(entries.iter().map(|(name, node)| DirEntry { name: name.clone(), kind: node.kind(), ino: node.ino_number() }).collect())     }      fn truncate(&self) -> Result<(), Errno> {@@ -181,7 +235,128 @@ impl Inode for RamInode {                 return Err(Errno::ENOTEMPTY);             }         }-        entries.remove(name);+        if let Some(node) = entries.remove(name) {+            Self::unlinked(&node);+        }+        drop(entries);+        self.touch();+        Ok(())+    }++    // --- stage 17 ----------------------------------------------------------++    fn ino(&self) -> Option<u64> {+        Some(self.ino_number())+    }++    fn nlink(&self) -> Option<u64> {+        let links = self.meta.lock().links as u64;+        Some(match &self.node {+            // "." plus each subdirectory's ".." (like a disk file system)+            Node::Dir(entries) => links + 1 + entries.lock().values().filter(|n| matches!(n.node, Node::Dir(_))).count() as u64,+            _ => links,+        })+    }++    fn owner(&self) -> (u32, u32) {+        let meta = self.meta.lock();+        (meta.uid, meta.gid)+    }++    fn set_owner(&self, uid: Option<u32>, gid: Option<u32>) -> Result<(), Errno> {+        let mut meta = self.meta.lock();+        if let Some(uid) = uid {+            meta.uid = uid;+        }+        if let Some(gid) = gid {+            meta.gid = gid;+        }+        meta.times[2] = now_timespec();+        Ok(())+    }++    fn symlink(&self, name: &str, target: &str) -> Result<Arc<dyn Inode>, Errno> {+        check_name(name)?;+        let mut entries = self.entries()?.lock();+        if entries.contains_key(name) {+            return Err(Errno::EEXIST);+        }+        let node = Arc::new(RamInode::new(Node::Symlink(String::from(target))));+        entries.insert(String::from(name), node.clone());+        drop(entries);+        self.touch();+        Ok(node)+    }++    fn link(&self, name: &str, target: &Arc<dyn Inode>) -> Result<(), Errno> {+        check_name(name)?;+        let node = super::downcast::<RamInode>(target).ok_or(Errno::EXDEV)?;+        let mut entries = self.entries()?.lock();+        if entries.contains_key(name) {+            return Err(Errno::EEXIST);+        }+        {+            let mut meta = node.meta.lock();+            meta.links += 1;+            meta.times[2] = now_timespec();+        }+        entries.insert(String::from(name), node);+        drop(entries);+        self.touch();+        Ok(())+    }++    /// Move the `Arc` from one map to the other. With two directories, the+    /// two locks are taken in address order, so two renames going opposite+    /// ways can't each hold one lock and wait for the other.+    fn rename_entry(&self, old: &str, new_dir: &Arc<dyn Inode>, new: &str) -> Result<(), Errno> {+        check_name(new)?;+        let target = super::downcast::<RamInode>(new_dir).ok_or(Errno::EXDEV)?;+        let same = core::ptr::eq(self, &*target);+        let (src, dst) = (self.entries()?, target.entries()?);+        let replaced;+        if same {+            let mut map = src.lock();+            let node = map.remove(old).ok_or(Errno::ENOENT)?;+            replaced = map.insert(String::from(new), node);+        } else {+            let self_first = (self as *const RamInode) < Arc::as_ptr(&target);+            let (mut a, mut b) = if self_first {+                let a = src.lock();+                (a, dst.lock())+            } else {+                let b = dst.lock();+                (src.lock(), b)+            };+            let node = a.remove(old).ok_or(Errno::ENOENT)?;+            node.changed();+            replaced = b.insert(String::from(new), node);+        }+        if let Some(gone) = replaced {+            Self::unlinked(&gone);+        }+        self.touch();+        target.touch();         Ok(())     }++    fn as_any(self: Arc<Self>) -> Option<Arc<dyn core::any::Any + Send + Sync>> {+        Some(self)+    }+}++impl RamInode {+    /// Inode number: the node's address (unique while it exists).+    fn ino_number(&self) -> u64 {+        self as *const RamInode as u64+    }+}++fn check_name(name: &str) -> Result<(), Errno> {+    match name {+        "" | "." | ".." => Err(Errno::EINVAL),+        _ if name.contains('/') => Err(Errno::EINVAL),+        _ if name.len() > 255 => Err(Errno::ENAMETOOLONG),+        _ => Ok(()),+    } }
@@ -123,6 +123,14 @@ pub extern "C" fn trap_handler(frame: &mut TrapFrame) {             // Every CPU's APIC timer, ~100 times a second: time's up for the             // running thread, let the next one have a go (see thread.rs).             crate::apic::eoi(); // before switching: the next thread may be new+            // CPU-time accounting (stage 17): charge this tick to the+            // thread it interrupted, as user or system time.+            if let Some(task) = crate::thread::current_task() {+                let user = frame.cs & 3 == 3;+                task.cpu_ticks.fetch_add(1, core::sync::atomic::Ordering::Relaxed);+                let cpu = &task.process.cpu;+                (if user { &cpu.user } else { &cpu.system }).fetch_add(1, core::sync::atomic::Ordering::Relaxed);+            }             crate::thread::timer_tick();         }         14 => page_fault(frame),
@@ -46,9 +46,11 @@ pub mod signal; // signals: kill, handlers, signal frames, default actions pub mod smp; // multi-core: start the other CPUs, per-CPU data pub mod stack; // kernel stacks with guard pages pub mod syscall; // syscall/sysret entry + system call implementations+pub mod syscall_ext; // stage 17's syscalls for coreutils/busybox: links, owners, statfs, ids, timers... pub mod task; // cooperative multitasking: async tasks + executor pub mod trap; // TrapFrame: saved user registers, the common return to ring 3 pub mod time; // microsecond clock (TSC calibrated against the PIT)+pub mod timer; // alarm, setitimer, POSIX timers: signals at a deadline (stage 17) pub mod thread; // threads + the multi-core scheduler (sleep/wake, wait queues) pub mod tty; // terminals: line discipline, termios, console/hvc0/ptys pub mod vga_buffer; // the console: VGA text mode, or fbcon on a GPU
@@ -147,7 +147,7 @@ fn shell_supervisor() {     }     loop {         #[cfg(feature = "headless")]-        let frames_before = rustbelt::memory::frames_in_use();+        let (frames_before, threads_before) = (rustbelt::memory::frames_in_use(), rustbelt::thread::thread_count());         #[allow(unused_mut)]         let mut args = process::SpawnArgs::kernel("sh");         #[cfg(feature = "headless")]@@ -165,6 +165,19 @@ fn shell_supervisor() {         {             let ok = status == process::ExitStatus::Exited(0);             serial_println!("[{}] shell self-test finished: {:?}", if ok { "ok" } else { "failed" }, status);+            // The shell is gone, but programs it left running in the+            // background (or orphans still exiting) may not be yet: wait+            // until every process and thread has finished -- up to 5+            // seconds -- before counting frames (stage 17).+            let deadline = rustbelt::time::now_us() + 5_000_000;+            while (process::count() > 0 || rustbelt::thread::thread_count() > threads_before)+                && rustbelt::time::now_us() < deadline+            {+                rustbelt::thread::sleep_ticks(1);+            }+            if process::count() > 0 {+                serial_println!("[memory] {} process(es) still running after 5 s", process::count());+            }             // Every process the self-test ran is gone: all their memory             // (page tables, user pages, kernel stacks) must be back.             serial_println!(
@@ -142,6 +142,10 @@ pub fn usable_memory_bytes(memory_map: &MemoryMap) -> u64 { /// Hand the frame allocator over to the kernel (after the heap is set up), /// so processes can get page tables and pages later. pub fn init_frame_allocator(frame_allocator: BootInfoFrameAllocator) {+    // For sysinfo/statfs (stage 17): how much RAM there is, and how much of+    // it was gone (kernel heap, early page tables) before the hand-over.+    TOTAL_FRAMES.store(usable_memory_bytes(frame_allocator.memory_map) / 4096, Ordering::Relaxed);+    EARLY_FRAMES.store(frame_allocator.next as u64, Ordering::Relaxed);     without_interrupts(|| *FRAME_ALLOCATOR.lock() = Some(frame_allocator));     init_kernel_stack_area(); // must happen before any process exists }@@ -195,6 +199,17 @@ pub fn free_frame(frame: PhysFrame) {     }) } +/// Usable RAM in 4 KiB frames, and frames used before `init_frame_allocator`.+static TOTAL_FRAMES: AtomicU64 = AtomicU64::new(0);+static EARLY_FRAMES: AtomicU64 = AtomicU64::new(0);++/// (total, free) RAM in 4 KiB frames, for sysinfo and the ramfs's statfs.+pub fn frame_totals() -> (u64, u64) {+    let total = TOTAL_FRAMES.load(Ordering::Relaxed);+    let used = EARLY_FRAMES.load(Ordering::Relaxed) + frames_in_use() as u64;+    (total, total.saturating_sub(used))+}+ /// How many frames are in use right now (handed out and not freed). pub fn frames_in_use() -> usize {     without_interrupts(|| BOOT_FRAMES_USED.load(Ordering::Relaxed) as usize - FREE_FRAMES.lock().len)
@@ -58,6 +58,20 @@ struct State {     remote: Option<IpEndpoint>,     /// SO_RCVTIMEO: how long recv/accept may block (None = forever).     timeout_us: Option<u64>,+    /// Stage 17: shutdown(SHUT_RD) / shutdown(SHUT_WR) done.+    rd_shut: bool,+    wr_shut: bool,+    /// SO_ERROR already reported a refused connection (it's said once).+    error_taken: bool,+    /// Boolean options set with setsockopt (bit n = option n, e.g.+    /// SO_REUSEADDR = 2): remembered so getsockopt reads them back.+    flags: u32,+}++impl State {+    fn new(handle: Option<SocketHandle>, local_port: u16) -> State {+        State { handle, listeners: Vec::new(), local_port, remote: None, timeout_us: None, rd_shut: false, wr_shut: false, error_taken: false, flags: 0 }+    } }  pub struct Socket {@@ -97,7 +111,7 @@ impl Socket {         if with_net(|_| ()).is_none() {             return Err(Errno::ENETDOWN); // net::init not run         }-        let mut state = State { handle: None, listeners: Vec::new(), local_port: 0, remote: None, timeout_us: None };+        let mut state = State::new(None, 0);         match kind {             Kind::Tcp => {} // created by connect/listen             Kind::Udp => {@@ -262,8 +276,7 @@ impl Socket {                 st.listeners.remove(i);             }             let peer = n.sockets.get::<tcp::Socket>(handle).remote_endpoint().map(addr).unwrap_or(([0; 4], 0));-            let state =-                State { handle: Some(handle), listeners: Vec::new(), local_port: st.local_port, remote: None, timeout_us: None };+            let state = State::new(Some(handle), st.local_port);             Some(Ok((Socket::with_state(Kind::Tcp, state), peer)))         })     }@@ -277,6 +290,9 @@ impl Socket {             self.bind(0)?; // first send picks a local port, like Linux         }         let sent = self.wait(None, |st, n| {+            if st.wr_shut {+                return Some(Err(Errno::EPIPE)); // shutdown(SHUT_WR)+            }             let Some(h) = st.handle else { return Some(Err(Errno::ENOTCONN)) };             match self.kind {                 Kind::Tcp => {@@ -319,6 +335,9 @@ impl Socket {     /// closed). A datagram bigger than `buf` is cut short (like Linux).     pub fn recv(&self, buf: &mut [u8]) -> Result<(usize, Option<Addr>), Errno> {         let result = self.wait(self.recv_timeout(), |st, n| {+            if st.rd_shut {+                return Some(Ok((0, None))); // shutdown(SHUT_RD): end of stream+            }             let Some(h) = st.handle else {                 return Some(Err(if self.kind == Kind::Tcp { Errno::ENOTCONN } else { Errno::EINVAL }));             };@@ -357,6 +376,89 @@ impl Socket {         Ok(result)     } +    /// getsockname (stage 17): our address and port. Connected TCP knows+    /// its local IP; otherwise it's 0.0.0.0 ("any of ours"), like an+    /// unconnected socket on Linux.+    pub fn local(&self) -> Addr {+        self.locked(|st, n| match (self.kind, st.handle) {+            (Kind::Tcp, Some(h)) => n.sockets.get::<tcp::Socket>(h).local_endpoint().map(addr).unwrap_or(([0; 4], st.local_port)),+            _ => ([0; 4], st.local_port),+        })+        .unwrap_or(([0; 4], 0))+    }++    /// shutdown (stage 17): 0 = SHUT_RD (reads say end-of-file), 1 =+    /// SHUT_WR (TCP sends its FIN; writes get EPIPE), 2 = both.+    pub fn shutdown(&self, how: u64) -> Result<(), Errno> {+        if how > 2 {+            return Err(Errno::EINVAL);+        }+        let kind = self.kind;+        self.locked(|st, n| {+            let connected = match kind {+                Kind::Tcp => st.handle.is_some(),+                _ => st.remote.is_some(),+            };+            if !connected {+                return Err(Errno::ENOTCONN);+            }+            if how != 1 {+                st.rd_shut = true;+            }+            if how != 0 {+                st.wr_shut = true;+                if let (Kind::Tcp, Some(h)) = (kind, st.handle) {+                    n.sockets.get_mut::<tcp::Socket>(h).close(); // FIN; we can still receive+                }+            }+            Ok(())+        })??;+        poll();+        Ok(())+    }++    /// SO_ERROR (stage 17): a pending error, reported once. That's how a+    /// program learns that a nonblocking connect failed.+    pub fn take_error(&self) -> i32 {+        let kind = self.kind;+        self.locked(|st, n| match (kind, st.handle) {+            (Kind::Tcp, Some(h)) if !st.error_taken && !st.wr_shut && n.sockets.get::<tcp::Socket>(h).state() == tcp::State::Closed => {+                st.error_taken = true;+                Errno::ECONNREFUSED as i32+            }+            _ => 0,+        })+        .unwrap_or(0)+    }++    /// Is it listening (SO_ACCEPTCONN)?+    pub fn listening(&self) -> bool {+        without_interrupts(|| !self.state.lock().listeners.is_empty())+    }++    /// The buffer sizes (SO_RCVBUF / SO_SNDBUF): fixed here.+    pub fn buffer_size(&self) -> usize {+        if self.kind == Kind::Tcp { TCP_BUFFER } else { DGRAM_BUFFER }+    }++    /// Remember / read back a boolean option (SO_REUSEADDR & co.). They+    /// change nothing: smoltcp lets a port be reused at once anyway, and+    /// there are no keepalives to send.+    pub fn set_flag(&self, opt: u32, on: bool) {+        without_interrupts(|| {+            let mut st = self.state.lock();+            if on { st.flags |= 1 << opt } else { st.flags &= !(1 << opt) }+        });+    }+    pub fn flag(&self, opt: u32) -> bool {+        without_interrupts(|| self.state.lock().flags & 1 << opt != 0)+    }++    /// The SO_RCVTIMEO set, if any (µs).+    pub fn recv_timeout_us(&self) -> Option<u64> {+        self.recv_timeout()+    }+     /// Local and remote address (for `accept` callers and tests).     pub fn peer(&self) -> Option<Addr> {         self.locked(|st, n| match (self.kind, st.handle) {
@@ -179,7 +179,10 @@ pub fn sys_poll_ms(fds_ptr: u64, nfds: u64, timeout_ms: i64) -> Result<u64, Errn     sys_poll(fds_ptr, nfds, ms_deadline(timeout_ms)) } -pub fn sys_ppoll(fds_ptr: u64, nfds: u64, ts_ptr: u64, mask_ptr: u64) -> Result<u64, Errno> {+/// ppoll(fds, nfds, *timespec, *sigmask, sigsetsize). Stage 17: like+/// Linux, a mask whose size isn't 8 bytes (64 signals) is EINVAL.+pub fn sys_ppoll(fds_ptr: u64, nfds: u64, ts_ptr: u64, mask_ptr: u64, setsize: u64) -> Result<u64, Errno> {+    check_setsize(mask_ptr, setsize)?;     let deadline = timespec_deadline(ts_ptr)?;     with_sigmask(mask_ptr, || sys_poll(fds_ptr, nfds, deadline)) }@@ -264,7 +267,13 @@ pub fn sys_select_tv(nfds: u64, r: u64, w: u64, e: u64, tv_ptr: u64) -> Result<u /// pselect6(nfds, r, w, e, *timespec, *{sigmask ptr, size}) (270). pub fn sys_pselect6(nfds: u64, r: u64, w: u64, e: u64, ts_ptr: u64, mask_arg: u64) -> Result<u64, Errno> {     let deadline = timespec_deadline(ts_ptr)?;-    let mask_ptr = if mask_arg == 0 { 0 } else { u64::from_le_bytes(user_slice(mask_arg, 8)?.try_into().unwrap()) };+    let (mask_ptr, setsize) = if mask_arg == 0 {+        (0, 8)+    } else {+        let raw = user_slice(mask_arg, 16)?;+        (u64::from_le_bytes(raw[..8].try_into().unwrap()), u64::from_le_bytes(raw[8..].try_into().unwrap()))+    };+    check_setsize(mask_ptr, setsize)?;     with_sigmask(mask_ptr, || sys_select(nfds, [r, w, e], deadline)) } @@ -396,7 +405,13 @@ pub fn sys_epoll_ctl(epfd: u64, op: u64, fd: u64, event_ptr: u64) -> Result<u64,  /// epoll_wait(epfd, *events, max, timeout_ms) (232) and epoll_pwait(..., /// *sigmask, 8) (281).-pub fn sys_epoll_pwait(epfd: u64, events_ptr: u64, max: i64, timeout_ms: i64, mask_ptr: u64) -> Result<u64, Errno> {+/// The sigset size check of ppoll / pselect6 / epoll_pwait (stage 17).+fn check_setsize(mask_ptr: u64, setsize: u64) -> Result<(), Errno> {+    if mask_ptr != 0 && setsize != 8 { Err(Errno::EINVAL) } else { Ok(()) }+}++pub fn sys_epoll_pwait(epfd: u64, events_ptr: u64, max: i64, timeout_ms: i64, mask_ptr: u64, setsize: u64) -> Result<u64, Errno> {+    check_setsize(mask_ptr, setsize)?;     if max <= 0 || max > 1024 {         return Err(Errno::EINVAL);     }
@@ -200,6 +200,54 @@ pub struct Task {     /// 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,+    /// CPU time it used (stage 17), in timer ticks (`CPU_TICK_US` each):+    /// for CLOCK_THREAD_CPUTIME_ID.+    pub cpu_ticks: AtomicU64,+}++/// One APIC timer tick (apic.rs programs ~100 a second), in microseconds:+/// the unit of CPU-time accounting (stage 17).+pub const CPU_TICK_US: u64 = 10_000;++/// CPU time, Linux style (stage 17): every timer tick is charged to+/// whatever thread it interrupted, as "user" time if the CPU was running+/// the program and "system" time if it was in the kernel for it. That's+/// what Linux does without fine-grained accounting too: a sample, not a+/// stopwatch. Children's times are added to `child_*` when they're reaped+/// with wait4, which is what times() and getrusage(RUSAGE_CHILDREN) show.+#[derive(Default)]+pub struct CpuTimes {+    pub user: AtomicU64,+    pub system: AtomicU64,+    pub child_user: AtomicU64,+    pub child_system: AtomicU64,+}++/// User and group ids (stage 17). Linux keeps three of each: real (who+/// you are), effective (what permission checks use) and saved (what a+/// setuid program may switch back to), plus "fs" ids and the+/// supplementary groups. We store them all and report them truthfully,+/// but nothing checks them yet: permissions aren't enforced.+#[derive(Clone, Copy, Debug, PartialEq, Eq)]+pub struct Creds {+    pub ruid: u32,+    pub euid: u32,+    pub suid: u32,+    pub fsuid: u32,+    pub rgid: u32,+    pub egid: u32,+    pub sgid: u32,+    pub fsgid: u32,+    pub groups: [u32; NGROUPS_MAX],+    pub ngroups: usize,+}++/// How many supplementary groups a process can have (Linux allows 65536;+/// 32 is plenty here).+pub const NGROUPS_MAX: usize = 32;++impl Creds {+    pub const ROOT: Creds = Creds { ruid: 0, euid: 0, suid: 0, fsuid: 0, rgid: 0, egid: 0, sgid: 0, fsgid: 0, groups: [0; NGROUPS_MAX], ngroups: 0 }; }  /// A thread name as prctl(PR_GET_NAME) returns it: at most 15 bytes, then NULs.@@ -286,6 +334,13 @@ pub struct Process {     /// descendants' orphans and promises to reap them (see `adopter`).     /// Not inherited.     pub child_subreaper: AtomicBool,+    /// CPU time used (stage 17), see `CpuTimes`.+    pub cpu: CpuTimes,+    /// User and group ids (stage 17): inherited by fork, kept by exec.+    pub creds: Mutex<Creds>,+    /// The nice value (stage 17), -20..19: stored for get/setpriority,+    /// inherited; the scheduler doesn't use it (every thread is equal).+    pub nice: core::sync::atomic::AtomicI32, }  // --- Resource limits (stage 16) ------------------------------------------@@ -439,6 +494,9 @@ pub fn spawn_image(path: &str, elf_image: &[u8], spawn: SpawnArgs) -> Result<Arc         dumpable: AtomicU32::new(1),         no_new_privs: AtomicBool::new(false),         child_subreaper: AtomicBool::new(false),+        cpu: CpuTimes::default(),+        creds: Mutex::new(Creds::ROOT),+        nice: core::sync::atomic::AtomicI32::new(0),     };     serial_println!("[process] started pid {} ({}), entry {:#x}", pid, name, start.rip);     Ok(start_process(process, start, 0, 0, crate::fpu::initial(), signal::AltStack::DISABLED, comm_from(name)))@@ -469,6 +527,7 @@ fn start_task(process: &Arc<Process>, tid: u64, start: TrapFrame, fs_base: u64,          fpu: Mutex::new(fpu),         comm: Mutex::new(comm),         robust_list: AtomicU64::new(0),+        cpu_ticks: AtomicU64::new(0),     });     without_interrupts(|| process.tasks.lock().push(task.clone()));     let l4 = process.mm.lock().l4;@@ -916,6 +975,7 @@ fn process_exit(process: Arc<Process>, status: ExitStatus) {     let files = core::mem::take(&mut process.fds.lock().files);     drop(files);     fs::lock::process_exited(process.pid); // its fcntl record locks+    crate::timer::process_exited(process.pid); // its alarm and POSIX timers (stage 17)     // 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);@@ -1106,6 +1166,13 @@ pub fn wait4(me: &Process, pid: i64, options: u64) -> Result<(u64, i32), Errno>          Found::Event(child, status, exited) => {             if exited {                 reap(&child);+                // Its CPU time (and its reaped children's) now counts as+                // our children's time (times(), getrusage).+                let c = &child.cpu;+                let user = c.user.load(Ordering::Relaxed) + c.child_user.load(Ordering::Relaxed);+                let system = c.system.load(Ordering::Relaxed) + c.child_system.load(Ordering::Relaxed);+                me.cpu.child_user.fetch_add(user, Ordering::Relaxed);+                me.cpu.child_system.fetch_add(system, Ordering::Relaxed);             }             Ok((child.pid, status))         }@@ -1165,6 +1232,9 @@ pub fn fork(parent: &Arc<Process>, frame: &TrapFrame, stack: u64, vfork: bool) -         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),+        cpu: CpuTimes::default(),+        creds: Mutex::new(*parent.creds.lock()),+        nice: core::sync::atomic::AtomicI32::new(parent.nice.load(Ordering::Relaxed)),     };     serial_println!("[process] pid {} forked pid {}", parent.pid, pid);     // The child's FPU/SSE registers start as a copy of ours (we're running,@@ -1256,6 +1326,7 @@ pub fn execve(p: &Arc<Process>, frame: &mut TrapFrame, path: &str, argv: &[Vec<u     *frame = start;     *p.name.lock() = String::from(name);     *p.exe.lock() = String::from(path);+    crate::timer::exec(p.pid); // POSIX timers don't survive exec (the alarm does)     if let Some(me) = thread::current_task() {         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
@@ -533,10 +533,18 @@ fn restart(frame: &mut TrapFrame) { /// (by rewriting `frame`), applies default actions (which may end the /// process right here), and sorts out interrupted syscalls. pub fn deliver(frame: &mut TrapFrame) {+    deliver_outcome(frame);+}++/// `deliver`, also saying what became of an interrupted syscall (for the+/// `--trace` line, stage 17): `Some(true)` = it will run again,+/// `Some(false)` = the program sees EINTR, `None` = nothing was interrupted.+pub fn deliver_outcome(frame: &mut TrapFrame) -> Option<bool> {     if !frame.from_user() {-        return;+        return None;     }-    let Some(t) = thread::current_task() else { return };+    let Some(t) = thread::current_task() else { return None };+    let mut outcome = None;     let p = t.process.clone();     // Writing the signal frame may fault user pages in: interrupts on.     x86_64::instructions::interrupts::enable();@@ -576,8 +584,14 @@ pub fn deliver(frame: &mut TrapFrame) {                 // EINTR, or restart it after the handler (SA_RESTART).                 if frame.is_syscall() {                     match restart_code(frame.rax) {-                        Some(Errno::ERESTARTSYS) if action.flags & SA_RESTART != 0 => restart(frame),-                        Some(_) => frame.rax = -(Errno::EINTR as i64) as u64,+                        Some(Errno::ERESTARTSYS) if action.flags & SA_RESTART != 0 => {+                            restart(frame);+                            outcome = Some(true);+                        }+                        Some(_) => {+                            frame.rax = -(Errno::EINTR as i64) as u64;+                            outcome = Some(false);+                        }                         None => {}                     }                     frame.vector = 0; // dealt with: not a syscall return any more@@ -600,7 +614,7 @@ pub fn deliver(frame: &mut TrapFrame) {                         s.actions[sig as usize - 1] = SigAction::default();                     }                 });-                return; // one handler at a time; the next on its return+                return outcome; // one handler at a time; the next on its return             }         }     }@@ -609,7 +623,9 @@ pub fn deliver(frame: &mut TrapFrame) {     restore_temporary_mask();     if frame.is_syscall() && restart_code(frame.rax).is_some() {         restart(frame);+        outcome = Some(true);     }+    outcome }  /// ppoll / pselect6 / epoll_pwait / (sigsuspend-like) waits: block exactly
@@ -58,7 +58,7 @@ //! |  45 | recvfrom    | fd, buf, len, flags, *sockaddr_in or 0, *addrlen | blocks (SO_RCVTIMEO -> EAGAIN); 0 = TCP peer closed. The address is cut to *addrlen | //! |  49 | bind        | fd, *sockaddr_in, len               | sets the local port (address ignored: we have one) | //! |  50 | listen      | fd, backlog                         | TCP; backlog 1..4 |-//! |  54 | setsockopt  | fd, level, name, *value, len        | only SOL_SOCKET (1) / SO_RCVTIMEO (20) with a `struct timeval`; other options: ENOPROTOOPT |+//! |  54 | setsockopt  | fd, level, name, *value, len        | stage 17 (src/syscall_ext.rs): SO_RCVTIMEO is real; SO_REUSEADDR & co. stored; others ENOPROTOOPT | //! |  56 | clone       | flags, stack, *ptid, *ctid, tls     | fork-like (SIGCHLD [+ CLONE_VFORK]) or a thread: CLONE_VM|FS|FILES|SIGHAND|THREAD|SYSVSEM [+ SETTLS, PARENT_SETTID, CHILD_CLEARTID] | //! |  57 | fork        |                                     | copy-on-write; parent gets the pid, child 0 | //! |  58 | vfork       |                                     | fork + the parent waits until the child execs or exits |@@ -81,7 +81,7 @@ //! |  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 |+//! | 102 | getuid      |                                     | stage 17 (src/syscall_ext.rs): real per-process ids, root (0) by default | //! | 109 | setpgid     | pid, pgid                           | 0 = myself / a new group | //! | 110 | getppid     |                                     | 0 if started by the kernel or orphaned | //! | 111 | getpgrp     |                                     | |@@ -102,7 +102,7 @@ //! | 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 |-//! | 228 | clock_gettime | clock, *timespec                  | REALTIME 0 (CMOS clock at boot + uptime), MONOTONIC 1 (since boot) |+//! | 228 | clock_gettime | clock, *timespec                  | REALTIME 0 (CMOS clock at boot + uptime), MONOTONIC 1 (since boot); stage 17: CPU time 2/3 | //! | 230 | clock_nanosleep | clock, flags, *timespec, *remaining | REALTIME, MONOTONIC or BOOTTIME 7; TIMER_ABSTIME 1 = sleep until that time | //! | 231 | exit_group  | code                                | ends every thread of the process | //! | 232 | epoll_wait  | epfd, *epoll_event, max, timeout_ms | level-triggered; EPOLLONESHOT supported |@@ -112,7 +112,7 @@ //! | 258 | mkdirat     | dirfd, path, mode                   | | //! | 262 | newfstatat  | dirfd, path, *stat, flags           | AT_SYMLINK_NOFOLLOW 0x100, AT_EMPTY_PATH 0x1000 (= fstat(dirfd)) | //! | 263 | unlinkat    | dirfd, path, flags                  | AT_REMOVEDIR 0x200 = rmdir |-//! | 264 | renameat    | olddirfd, old, newdirfd, new        | renameat2 316 too: flags 0 only (else EINVAL) |+//! | 264 | renameat    | olddirfd, old, newdirfd, new        | renameat2 316 too: flags 0 or RENAME_NOREPLACE (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) |@@ -129,11 +129,16 @@ //! | 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) |-//! | 502 | sysinfo     | *SysInfo                            | ours: CPUs, threads, memory, per-CPU busy time (Linux: /proc/stat etc.) |+//! | 502 | rb_sysinfo  | *SysInfo                            | ours: CPUs, threads, memory, per-CPU busy time (Linux: /proc/stat etc.) | //! | 503 | resolve     | name, name_len, *[[u8; 4]], max     | ours: DNS lookup (A records), returns the count. Linux does DNS in libc | //! | 504 | netinfo     | *NetInfo                            | ours: MAC, IP, gateway, DNS, packet counters (Linux: netlink/ioctls) | //! | 505 | trace_me    | on                                  | ours: print my syscalls from now on (survives execve; `run --trace`) | //!+//! Stage 17's calls (link/symlink, chown, statfs/statx, sysinfo 99, times,+//! getrusage, clock_getres, socket names/options, timers, ids, priority,+//! host names, flock, preadv/pwritev, sendfile) live in `syscall_ext.rs`,+//! which has its own table; unknown numbers fall through to it.+//! //! spawn's `argv` is all arguments separated by NUL bytes ("ls\0-l\0/bin"); //! empty means just [program name]. `SpawnOptions` (or a null pointer for //! the defaults) picks the child's stdin/stdout/stderr from the caller's fds@@ -175,7 +180,7 @@ //! OS use, and it's faster.  use crate::errno::Errno;-use crate::fs::file::{File, InodeFile, LinuxStat, Stat};+use crate::fs::file::{File, InodeFile, LinuxStat}; use crate::fs::{self, InodeKind}; use crate::trap::TrapFrame; use crate::{memory, process, signal, thread};@@ -217,7 +222,7 @@ pub const SYS_GETRANDOM: u64 = 318; pub const SYS_CLOCK_GETTIME: u64 = 228; pub const SYS_SPAWN: u64 = 500; pub const SYS_MOUNTS: u64 = 501;-pub const SYS_SYSINFO: u64 = 502;+pub const SYS_RB_SYSINFO: u64 = 502; pub const SYS_RESOLVE: u64 = 503; pub const SYS_NETINFO: u64 = 504; pub const SYS_MMAP: u64 = 9;@@ -516,7 +521,7 @@ pub fn name(nr: u64) -> &'static str {         SYS_GETRANDOM => "getrandom",         SYS_SPAWN => "spawn",         SYS_MOUNTS => "mounts",-        SYS_SYSINFO => "sysinfo",+        SYS_RB_SYSINFO => "rb_sysinfo",         SYS_SOCKET => "socket",         SYS_CONNECT => "connect",         SYS_ACCEPT => "accept",@@ -529,7 +534,7 @@ pub fn name(nr: u64) -> &'static str {         SYS_CLOCK_GETTIME => "clock_gettime",         SYS_RESOLVE => "resolve",         SYS_NETINFO => "netinfo",-        _ => "?",+        _ => crate::syscall_ext::name(nr).unwrap_or("?"),     } } @@ -710,7 +715,7 @@ extern "C" fn syscall_dispatch(frame: &mut TrapFrame) {         SYS_GETDENTS64 => sys_getdents64(a1, a2, a3),         SYS_SPAWN => sys_spawn(a1, a2, a3, a4),         SYS_MOUNTS => sys_mounts(a1, a2),-        SYS_SYSINFO => sys_sysinfo(a1),+        SYS_RB_SYSINFO => sys_sysinfo(a1),         SYS_SOCKET => sys_socket(a1, a2, a3),         SYS_CONNECT => with_socket(a1, |s| s.connect(read_sockaddr(a2)?)).map(|_| 0),         SYS_ACCEPT => sys_accept(a1, a2, a3, 0),@@ -719,11 +724,8 @@ extern "C" fn syscall_dispatch(frame: &mut TrapFrame) {         SYS_RECVFROM => sys_recvfrom(a1, a2, a3, a5, frame.r9),         SYS_BIND => with_socket(a1, |s| s.bind(read_sockaddr(a2)?.1)).map(|_| 0),         SYS_LISTEN => with_socket(a1, |s| s.listen(a2 as usize)).map(|_| 0),-        SYS_SETSOCKOPT => sys_setsockopt(a1, a2, a3, a4, a5),         SYS_CLOCK_GETTIME => sys_clock_gettime(a1, a2),         SYS_CLOCK_NANOSLEEP => sys_clock_nanosleep(a1, a2, a3, a4),-        // One user, root: musl asks at startup (and `id`, `ls -l` ask).-        SYS_GETUID | SYS_GETGID | SYS_GETEUID | SYS_GETEGID => Ok(0),         SYS_RESOLVE => sys_resolve(a1, a2, a3, a4),         SYS_NETINFO => copy_to_user(a1, &crate::net::info()).map(|_| 0),         SYS_GETRANDOM => sys_getrandom(a1, a2),@@ -734,14 +736,14 @@ extern "C" fn syscall_dispatch(frame: &mut TrapFrame) {         SYS_READV => sys_readv(a1, a2, a3),         SYS_WRITEV => sys_writev(a1, a2, a3),         SYS_POLL => crate::poll::sys_poll_ms(a1, a2, a3 as i32 as i64),-        SYS_PPOLL => crate::poll::sys_ppoll(a1, a2, a3, a4),+        SYS_PPOLL => crate::poll::sys_ppoll(a1, a2, a3, a4, a5),         SYS_SELECT => crate::poll::sys_select_tv(a1, a2, a3, a4, a5),         SYS_PSELECT6 => crate::poll::sys_pselect6(a1, a2, a3, a4, a5, frame.r9),         SYS_EPOLL_CREATE => if (a1 as i32) <= 0 { Err(Errno::EINVAL) } else { crate::poll::sys_epoll_create1(0) },         SYS_EPOLL_CREATE1 => crate::poll::sys_epoll_create1(a1),         SYS_EPOLL_CTL => crate::poll::sys_epoll_ctl(a1, a2, a3, a4),-        SYS_EPOLL_WAIT => crate::poll::sys_epoll_pwait(a1, a2, a3 as i32 as i64, a4 as i32 as i64, 0),-        SYS_EPOLL_PWAIT => crate::poll::sys_epoll_pwait(a1, a2, a3 as i32 as i64, a4 as i32 as i64, a5),+        SYS_EPOLL_WAIT => crate::poll::sys_epoll_pwait(a1, a2, a3 as i32 as i64, a4 as i32 as i64, 0, 8),+        SYS_EPOLL_PWAIT => crate::poll::sys_epoll_pwait(a1, a2, a3 as i32 as i64, a4 as i32 as i64, a5, frame.r9),         SYS_FUTEX => current().and_then(|p| crate::futex::sys_futex(&p, a1, a2, a3, a4, a5, frame.r9)),         SYS_GETTID => task().map(|t| t.tid()),         SYS_SET_TID_ADDRESS => task().map(|t| {@@ -778,7 +780,7 @@ extern "C" fn syscall_dispatch(frame: &mut TrapFrame) {         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),+        _ => crate::syscall_ext::dispatch(nr, [a1, a2, a3, a4, a5, frame.r9]).unwrap_or(Err(Errno::ENOSYS)),     };     let ret = match result {         Ok(value) => value as i64,@@ -789,16 +791,27 @@ extern "C" fn syscall_dispatch(frame: &mut TrapFrame) {     if !(nr == SYS_EXECVE && ret == 0) {         frame.rax = ret as u64;     }-    if trace {+    let interrupted = matches!(Errno::from_ret(ret), Some(Errno::ERESTARTSYS | Errno::ERESTARTNOHAND)) && ret < 0;+    if trace && !interrupted {         let pid = current().map(|p| p.pid).unwrap_or(0);         match exec_line {             Some(line) if ret == 0 => crate::kprintln!("[trace {}] {}", pid, line),             _ => crate::kprintln!("[trace {}] {}", pid, trace_line(nr, [a1, a2, a3, a4], ret)),         }     }+    // An interrupted call (-512/-514) is traced *after* signal delivery,+    // with what the program really gets (stage 17): EINTR, or "restarted".+    let line = (trace && interrupted).then(|| trace_line(nr, [a1, a2, a3, a4], -(Errno::EINTR as i64)));     // A signal may have arrived meanwhile (or the call was interrupted by     // one): act on it before returning to the program.-    signal::deliver(frame);+    let outcome = signal::deliver_outcome(frame);+    if let Some(line) = line {+        let pid = current().map(|p| p.pid).unwrap_or(0);+        match outcome {+            Some(true) => crate::kprintln!("[trace {}] {} = ? (interrupted, restarted)", pid, line.rsplit_once(" = ").map(|l| l.0).unwrap_or(&line)),+            _ => crate::kprintln!("[trace {}] {}", pid, line),+        }+    } }  /// One syscall in strace style: `open("/mnt/disk0/readme.txt", 0) = 3`.@@ -869,12 +882,12 @@ pub fn trace_name(nr: u64) -> String {     } } -fn current() -> Result<Arc<process::Process>, Errno> {+pub(crate) fn current() -> Result<Arc<process::Process>, Errno> {     thread::current_process().ok_or(Errno::EINVAL) }  /// The calling thread.-fn task() -> Result<Arc<process::Task>, Errno> {+pub(crate) fn task() -> Result<Arc<process::Task>, Errno> {     thread::current_task().ok_or(Errno::EINVAL) } @@ -946,7 +959,7 @@ fn sys_write(fd: u64, buf: u64, len: u64) -> Result<u64, Errno> { // O_DIRECTORY, and so on.  /// The directory a relative path in an *at() call starts from.-fn at_base(dirfd: u64) -> Result<String, Errno> {+pub(crate) fn at_base(dirfd: u64) -> Result<String, Errno> {     let p = current()?;     if dirfd as i32 as i64 == AT_FDCWD {         return Ok(p.cwd.lock().clone());@@ -959,7 +972,7 @@ fn at_base(dirfd: u64) -> Result<String, Errno> { }  /// A path string (already copied in) made absolute, relative to `dirfd`.-fn path_at(dirfd: u64, raw: &[u8]) -> Result<String, Errno> {+pub(crate) fn path_at(dirfd: u64, raw: &[u8]) -> Result<String, Errno> {     // Our file systems use UTF-8 names (Linux allows any bytes but '/' and NUL).     let path = core::str::from_utf8(raw).map_err(|_| Errno::EINVAL)?;     if path.is_empty() {@@ -972,12 +985,12 @@ fn path_at(dirfd: u64, raw: &[u8]) -> Result<String, Errno> { }  /// Copy in a path argument and make it absolute, relative to `dirfd`.-fn user_path_at(dirfd: u64, ptr: u64) -> Result<String, Errno> {+pub(crate) fn user_path_at(dirfd: u64, ptr: u64) -> Result<String, Errno> {     path_at(dirfd, &user_cstr(ptr, PATH_MAX - 1)?) }  /// Copy in a path argument, relative to the cwd.-fn user_path(ptr: u64) -> Result<String, Errno> {+pub(crate) fn user_path(ptr: u64) -> Result<String, Errno> {     user_path_at(AT_FDCWD as u64, ptr) } @@ -1027,27 +1040,39 @@ fn sys_openat(dirfd: u64, path_ptr: u64, flags: u64, mode: u64) -> Result<u64, E /// a symlink itself, not what it points to) and, with AT_EMPTY_PATH and /// path "", fstat(dirfd). Writes Linux's `struct stat`. fn sys_newfstatat(dirfd: u64, path_ptr: u64, stat_ptr: u64, flags: u64) -> Result<u64, Errno> {+    copy_to_user(stat_ptr, &stat_at(dirfd, path_ptr, flags)?)?;+    Ok(0)+}++/// The `struct stat` newfstatat (and statx, stage 17) report.+pub(crate) fn stat_at(dirfd: u64, path_ptr: u64, flags: u64) -> Result<LinuxStat, Errno> {     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_fstat(dirfd, stat_ptr);+        return fd_stat(dirfd);     }     let path = if raw.is_empty() && flags & AT_EMPTY_PATH != 0 { at_base(dirfd)? } else { path_at(dirfd, &raw)? };     let inode = if flags & AT_SYMLINK_NOFOLLOW != 0 { fs::lookup_nofollow(&path)? } else { fs::lookup(&path)? };-    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)+    Ok(fs::file::linux_stat(&*inode, &path)) }  fn sys_fstat(fd: u64, stat_ptr: u64) -> Result<u64, Errno> {+    copy_to_user(stat_ptr, &fd_stat(fd)?)?;+    Ok(0)+}++/// fstat's `struct stat`: files opened by path report what stat() of the+/// path does. Others (pipes, sockets, consoles): the open file's address+/// as the inode number, unique while it's open.+pub(crate) fn fd_stat(fd: u64) -> Result<LinuxStat, Errno> {     let file = current()?.file(fd)?;-    // Files opened by path: the same inode number stat() gives. Others-    // (pipes, sockets, consoles): the open file's address, unique while open.+    if let (Some(inode), Some(path)) = (file.vfs_inode(), file.path()) {+        return Ok(fs::file::linux_stat(&*inode, &path));+    }     let ino = match file.path() {         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).with_times(file.times()))?;-    Ok(0)+    Ok(LinuxStat::new(&file.stat()?, ino).with_times(file.times())) }  /// access / faccessat(dirfd, path, mode): "does it exist?" There are no@@ -1068,9 +1093,10 @@ fn sys_mkdirat(dirfd: u64, path_ptr: u64, mode: u64) -> Result<u64, Errno> { /// 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) {+pub(crate) 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);+    crate::syscall_ext::set_creator(inode); }  /// unlink / rmdir / unlinkat(dirfd, path, flags): AT_REMOVEDIR = rmdir.@@ -1082,15 +1108,18 @@ fn sys_unlinkat(dirfd: u64, path_ptr: u64, flags: u64) -> Result<u64, Errno> {     Ok(0) } -/// rename / renameat / renameat2 (no flags: RENAME_NOREPLACE & co. are-/// EINVAL). See `fs::rename` for what we can and can't move.+/// rename / renameat / renameat2. Stage 17: RENAME_NOREPLACE (1) works+/// (EEXIST if the new name is taken); RENAME_EXCHANGE (2) and+/// RENAME_WHITEOUT (4) are EINVAL, as on file systems that lack them.+/// See `fs::rename` for what we can and can't move. fn sys_renameat(olddir: u64, old_ptr: u64, newdir: u64, new_ptr: u64, flags: u64) -> Result<u64, Errno> {+    const RENAME_NOREPLACE: u64 = 1;     let old = user_path_at(olddir, old_ptr)?;     let new = user_path_at(newdir, new_ptr)?;-    if flags != 0 {+    if flags & !RENAME_NOREPLACE != 0 {         return Err(Errno::EINVAL);     }-    fs::rename(&old, &new)?;+    fs::rename(&old, &new, flags & RENAME_NOREPLACE != 0)?;     Ok(0) } @@ -1307,13 +1336,14 @@ fn sys_getrandom(buf: u64, len: u64) -> Result<u64, Errno> {  /// Linux's `struct utsname`: six NUL-terminated strings of 65 bytes each. fn sys_uname(buf: u64) -> Result<u64, Errno> {+    let (host, domain) = (crate::syscall_ext::hostname(), crate::syscall_ext::domainname());     let fields = [         "Rustbelt",                     // sysname (`uname -s`)-        "qemu",                         // nodename (hostname)+        host.as_str(),                  // nodename (hostname; sethostname changes it)         env!("CARGO_PKG_VERSION"),      // release (`uname -r`)-        "stage 16: libc batch 1",       // version+        "stage 17: coreutils batch 2",  // version         "x86_64",                       // machine-        "(none)",                       // domainname+        domain.as_str(),                // domainname     ];     let out = user_slice_mut(buf, 6 * 65)?;     out.fill(0);@@ -1389,7 +1419,7 @@ pub struct SockAddrIn {     pub zero: [u8; 8], } -fn read_sockaddr(ptr: u64) -> Result<crate::net::socket::Addr, Errno> {+pub(crate) fn read_sockaddr(ptr: u64) -> Result<crate::net::socket::Addr, Errno> {     let bytes = user_slice(ptr, 16)?;     let family = u16::from_ne_bytes([bytes[0], bytes[1]]);     if family as u64 != AF_INET {@@ -1401,7 +1431,7 @@ fn read_sockaddr(ptr: u64) -> Result<crate::net::socket::Addr, Errno> { /// Write a peer's address like Linux: `*len_ptr` (a u32) says how much /// room there is; we write at most that much and set it to the real size /// (16). No `len_ptr` (only our own older programs do that): all 16 bytes.-fn write_sockaddr(ptr: u64, len_ptr: u64, (addr, port): crate::net::socket::Addr) -> Result<(), Errno> {+pub(crate) fn write_sockaddr(ptr: u64, len_ptr: u64, (addr, port): crate::net::socket::Addr) -> Result<(), Errno> {     if ptr == 0 {         return Ok(()); // caller doesn't want it     }@@ -1421,7 +1451,7 @@ fn write_sockaddr(ptr: u64, len_ptr: u64, (addr, port): crate::net::socket::Addr  /// Run `f` on the socket behind `fd` (ENOTSOCK if it's another kind of /// file). We hold our own Arc to it, so the fd may be closed meanwhile.-fn with_socket<R>(fd: u64, f: impl FnOnce(&crate::net::socket::Socket) -> Result<R, Errno>) -> Result<R, Errno> {+pub(crate) fn with_socket<R>(fd: u64, f: impl FnOnce(&crate::net::socket::Socket) -> Result<R, Errno>) -> Result<R, Errno> {     let file = current()?.file(fd)?;     f(file.as_socket().ok_or(Errno::ENOTSOCK)?) }@@ -1465,23 +1495,6 @@ fn sys_recvfrom(fd: u64, buf: u64, len: u64, addr_ptr: u64, len_ptr: u64) -> Res     Ok(n as u64) } -/// Only SO_RCVTIMEO: `struct timeval {i64 seconds, i64 microseconds}`;-/// zero means "block forever", like Linux.-fn sys_setsockopt(fd: u64, level: u64, name: u64, value: u64, len: u64) -> Result<u64, Errno> {-    if level != SOL_SOCKET || name != SO_RCVTIMEO {-        return Err(Errno::ENOPROTOOPT); // what Linux says for an unknown option-    }-    if len < 16 {-        return Err(Errno::EINVAL);-    }-    let tv = user_slice(value, 16)?;-    let secs = u64::from_ne_bytes(tv[..8].try_into().unwrap());-    let usecs = u64::from_ne_bytes(tv[8..16].try_into().unwrap());-    let total = secs.saturating_mul(1_000_000).saturating_add(usecs);-    with_socket(fd, |s| Ok(s.set_recv_timeout(if total == 0 { None } else { Some(total) })))?;-    Ok(0)-}- /// clock_gettime(clock, *timespec): CLOCK_REALTIME (0, wall-clock time /// since 1970, from the CMOS clock at boot) or the MONOTONIC family (1, /// 4 RAW, 6 COARSE, 7 BOOTTIME: time since boot). CPU-time clocks: EINVAL.@@ -1489,6 +1502,8 @@ fn sys_clock_gettime(clock: u64, ts_ptr: u64) -> Result<u64, Errno> {     let us = match clock {         0 | 5 => crate::time::realtime_us(), // REALTIME, REALTIME_COARSE         1 | 4 | 6 | 7 => crate::time::now_us(),+        // Stage 17: CPU time used so far (process / this thread).+        2 | 3 => crate::syscall_ext::cpu_clock_us(clock)?,         _ => return Err(Errno::EINVAL),     };     copy_to_user(ts_ptr, &[us / 1_000_000, (us % 1_000_000) * 1000])?;@@ -1711,7 +1726,7 @@ fn sys_pwrite(fd: u64, buf: u64, len: u64, offset: u64) -> Result<u64, Errno> { }  /// The `struct iovec {void *base; size_t len;}` array of readv/writev.-fn read_iovecs(ptr: u64, count: u64) -> Result<Vec<(u64, u64)>, Errno> {+pub(crate) fn read_iovecs(ptr: u64, count: u64) -> Result<Vec<(u64, u64)>, Errno> {     if count > 1024 {         return Err(Errno::EINVAL);     }
@@ -0,0 +1,1134 @@+//! Stage 17: the "coreutils / busybox" batch of Linux system calls.+//!+//! syscall.rs grew big, so the calls added in stage 17 live here. The+//! dispatcher there tries its own table first and then asks `dispatch`+//! below; `name` gives `--trace` their names.+//!+//! | nr  | call                 | what it does here                                  |+//! |-----|----------------------|----------------------------------------------------|+//! | 86  | link                 | a second name for a file (ramfs, ext2)             |+//! | 265 | linkat               | same, AT_SYMLINK_FOLLOW / AT_EMPTY_PATH            |+//! | 88  | symlink              | a symbolic link (ramfs, ext2)                      |+//! | 266 | symlinkat            | same, relative to a directory fd                   |+//! | 92  | chown                | change owner/group (stored, not enforced)          |+//! | 93  | fchown               | same, by fd                                        |+//! | 94  | lchown               | same, the symlink itself                           |+//! | 260 | fchownat             | same, relative to a directory fd                   |+//! | 137 | statfs               | sizes and free space of a file system              |+//! | 138 | fstatfs              | same, by fd                                        |+//! | 332 | statx                | the newer, extensible stat                         |+//! | 99  | sysinfo              | Linux's: uptime, memory, process count             |+//! | 98  | getrusage            | CPU time used (self, children, thread)             |+//! | 100 | times                | CPU time in clock ticks (100 per second)           |+//! | 229 | clock_getres         | how fine each clock is                             |+//! | 54  | setsockopt           | SO_RCVTIMEO; SO_REUSEADDR & co. accepted          |+//! | 55  | getsockopt           | SO_ERROR, SO_TYPE, SO_RCVBUF/SNDBUF, ...           |+//! | 51  | getsockname          | our own address and port                           |+//! | 52  | getpeername          | the other end's address and port                   |+//! | 48  | shutdown             | close one direction of a connection                |+//! | 53  | socketpair           | needs AF_UNIX, which we don't have: EAFNOSUPPORT   |+//! | 37  | alarm                | SIGALRM in N seconds                               |+//! | 36  | getitimer            | the ITIMER_REAL timer                              |+//! | 38  | setitimer            | same, set (periodic too)                           |+//! | 222 | timer_create         | POSIX timers (timer.rs)                            |+//! | 223-226 | timer_settime/gettime/getoverrun/delete |                         |+//! | 102/104/107/108 | getuid/getgid/geteuid/getegid | the stored ids           |+//! | 105/106 | setuid/setgid    | change ids (root: any; others: their own)          |+//! | 113/114 | setreuid/setregid |                                                   |+//! | 117/119 | setresuid/setresgid |                                                 |+//! | 118/120 | getresuid/getresgid |                                                 |+//! | 122/123 | setfsuid/setfsgid |                                                   |+//! | 115/116 | getgroups/setgroups | supplementary groups                            |+//! | 140/141 | getpriority/setpriority | the nice value (stored only)                |+//! | 170/171 | sethostname/setdomainname | what uname reports                        |+//! | 73  | flock                | whole-file advisory locks                          |+//! | 295/296 | preadv/pwritev   | readv/writev at an offset                          |+//! | 40  | sendfile             | copy from one fd to another inside the kernel      |++use crate::errno::Errno;+use crate::fs::{self, Inode};+use crate::process::{self, Creds};+use crate::syscall::{+    at_base, copy_to_user, current, path_at, read_iovecs, task, user_cstr, user_path, user_path_at, user_slice,+    user_slice_mut, with_socket, write_sockaddr, AT_EMPTY_PATH, AT_FDCWD, AT_SYMLINK_NOFOLLOW, PATH_MAX,+    SOL_SOCKET, SO_RCVTIMEO,+};+use alloc::string::String;+use alloc::sync::Arc;+use alloc::vec::Vec;+use core::sync::atomic::Ordering;+use spin::Mutex;++pub const SYS_SHUTDOWN: u64 = 48;+pub const SYS_GETSOCKNAME: u64 = 51;+pub const SYS_GETPEERNAME: u64 = 52;+pub const SYS_SOCKETPAIR: u64 = 53;+pub const SYS_SETSOCKOPT: u64 = 54;+pub const SYS_GETSOCKOPT: u64 = 55;+pub const SYS_GETITIMER: u64 = 36;+pub const SYS_ALARM: u64 = 37;+pub const SYS_SETITIMER: u64 = 38;+pub const SYS_SENDFILE: u64 = 40;+pub const SYS_FLOCK: u64 = 73;+pub const SYS_LINK: u64 = 86;+pub const SYS_SYMLINK: u64 = 88;+pub const SYS_CHOWN: u64 = 92;+pub const SYS_FCHOWN: u64 = 93;+pub const SYS_LCHOWN: u64 = 94;+pub const SYS_GETRUSAGE: u64 = 98;+pub const SYS_SYSINFO: u64 = 99;+pub const SYS_TIMES: u64 = 100;+pub const SYS_GETUID: u64 = 102;+pub const SYS_GETGID: u64 = 104;+pub const SYS_SETUID: u64 = 105;+pub const SYS_SETGID: u64 = 106;+pub const SYS_GETEUID: u64 = 107;+pub const SYS_GETEGID: u64 = 108;+pub const SYS_SETREUID: u64 = 113;+pub const SYS_SETREGID: u64 = 114;+pub const SYS_GETGROUPS: u64 = 115;+pub const SYS_SETGROUPS: u64 = 116;+pub const SYS_SETRESUID: u64 = 117;+pub const SYS_GETRESUID: u64 = 118;+pub const SYS_SETRESGID: u64 = 119;+pub const SYS_GETRESGID: u64 = 120;+pub const SYS_SETFSUID: u64 = 122;+pub const SYS_SETFSGID: u64 = 123;+pub const SYS_STATFS: u64 = 137;+pub const SYS_FSTATFS: u64 = 138;+pub const SYS_GETPRIORITY: u64 = 140;+pub const SYS_SETPRIORITY: u64 = 141;+pub const SYS_SETHOSTNAME: u64 = 170;+pub const SYS_SETDOMAINNAME: u64 = 171;+pub const SYS_TIMER_CREATE: u64 = 222;+pub const SYS_TIMER_SETTIME: u64 = 223;+pub const SYS_TIMER_GETTIME: u64 = 224;+pub const SYS_TIMER_GETOVERRUN: u64 = 225;+pub const SYS_TIMER_DELETE: u64 = 226;+pub const SYS_CLOCK_GETRES: u64 = 229;+pub const SYS_FCHOWNAT: u64 = 260;+pub const SYS_LINKAT: u64 = 265;+pub const SYS_SYMLINKAT: u64 = 266;+pub const SYS_PREADV: u64 = 295;+pub const SYS_PWRITEV: u64 = 296;+pub const SYS_STATX: u64 = 332;++/// linkat: link what a symlink points to, not the symlink.+pub const AT_SYMLINK_FOLLOW: u64 = 0x400;+const AT_NO_AUTOMOUNT: u64 = 0x800;+const AT_STATX_SYNC_TYPE: u64 = 0x6000;++/// The stage 17 calls; None = not one of ours (the caller says ENOSYS).+pub fn dispatch(nr: u64, a: [u64; 6]) -> Option<Result<u64, Errno>> {+    let fdcwd = AT_FDCWD as u64;+    Some(match nr {+        // 1. Links.+        SYS_LINK => sys_linkat(fdcwd, a[0], fdcwd, a[1], 0),+        SYS_LINKAT => sys_linkat(a[0], a[1], a[2], a[3], a[4]),+        SYS_SYMLINK => sys_symlinkat(a[0], fdcwd, a[1]),+        SYS_SYMLINKAT => sys_symlinkat(a[0], a[1], a[2]),+        // 2. Sockets options, CPU times.+        SYS_SETSOCKOPT => sys_setsockopt(a[0], a[1], a[2], a[3], a[4]),+        SYS_TIMES => sys_times(a[0]),+        // 3. Owners.+        SYS_CHOWN => sys_fchownat(fdcwd, a[0], a[1], a[2], 0),+        SYS_LCHOWN => sys_fchownat(fdcwd, a[0], a[1], a[2], AT_SYMLINK_NOFOLLOW),+        SYS_FCHOWN => sys_fchown(a[0], a[1], a[2]),+        SYS_FCHOWNAT => sys_fchownat(a[0], a[1], a[2], a[3], a[4]),+        // 4. File systems, statx.+        SYS_STATFS => sys_statfs(a[0], a[1]),+        SYS_FSTATFS => sys_fstatfs(a[0], a[1]),+        SYS_STATX => sys_statx(a[0], a[1], a[2], a[3], a[4]),+        // 5. System information, usage, clocks.+        SYS_SYSINFO => sys_sysinfo(a[0]),+        SYS_GETRUSAGE => sys_getrusage(a[0] as i64, a[1]),+        SYS_CLOCK_GETRES => sys_clock_getres(a[0], a[1]),+        // 6. Sockets.+        SYS_GETSOCKOPT => sys_getsockopt(a[0], a[1], a[2], a[3], a[4]),+        SYS_GETSOCKNAME => with_socket(a[0], |s| Ok(s.local())).and_then(|addr| write_sockaddr(a[1], a[2], addr).map(|_| 0)),+        SYS_GETPEERNAME => with_socket(a[0], |s| s.peer().ok_or(Errno::ENOTCONN)).and_then(|addr| write_sockaddr(a[1], a[2], addr).map(|_| 0)),+        SYS_SHUTDOWN => with_socket(a[0], |s| s.shutdown(a[1])).map(|_| 0),+        SYS_SOCKETPAIR => sys_socketpair(a[0]),+        // 7. Timers.+        SYS_ALARM => sys_alarm(a[0]),+        SYS_GETITIMER => sys_getitimer(a[0], a[1]),+        SYS_SETITIMER => sys_setitimer(a[0], a[1], a[2]),+        SYS_TIMER_CREATE => sys_timer_create(a[0], a[1], a[2]),+        SYS_TIMER_SETTIME => sys_timer_settime(a[0], a[1], a[2], a[3]),+        SYS_TIMER_GETTIME => sys_timer_gettime(a[0], a[1]),+        SYS_TIMER_GETOVERRUN => current().and_then(|p| crate::timer::getoverrun(p.pid, a[0] as i32)),+        SYS_TIMER_DELETE => current().and_then(|p| crate::timer::delete(p.pid, a[0] as i32)).map(|_| 0),+        // 8. Ids, priority, host name.+        SYS_GETUID => creds().map(|c| c.ruid as u64),+        SYS_GETEUID => creds().map(|c| c.euid as u64),+        SYS_GETGID => creds().map(|c| c.rgid as u64),+        SYS_GETEGID => creds().map(|c| c.egid as u64),+        SYS_SETUID => set_id(false, a[0] as u32),+        SYS_SETGID => set_id(true, a[0] as u32),+        SYS_SETREUID => set_re(false, a[0] as u32, a[1] as u32),+        SYS_SETREGID => set_re(true, a[0] as u32, a[1] as u32),+        SYS_SETRESUID => set_res(false, [a[0] as u32, a[1] as u32, a[2] as u32]),+        SYS_SETRESGID => set_res(true, [a[0] as u32, a[1] as u32, a[2] as u32]),+        SYS_GETRESUID => get_res(false, [a[0], a[1], a[2]]),+        SYS_GETRESGID => get_res(true, [a[0], a[1], a[2]]),+        SYS_SETFSUID => set_fs(false, a[0] as u32),+        SYS_SETFSGID => set_fs(true, a[0] as u32),+        SYS_GETGROUPS => sys_getgroups(a[0] as i32 as i64, a[1]),+        SYS_SETGROUPS => sys_setgroups(a[0], a[1]),+        SYS_GETPRIORITY => sys_getpriority(a[0], a[1]),+        SYS_SETPRIORITY => sys_setpriority(a[0], a[1], a[2] as i32),+        SYS_SETHOSTNAME => set_name(&HOSTNAME, a[0], a[1]),+        SYS_SETDOMAINNAME => set_name(&DOMAINNAME, a[0], a[1]),+        // 9. The rest.+        SYS_FLOCK => sys_flock(a[0], a[1]),+        SYS_PREADV => sys_preadv(a[0], a[1], a[2], a[3], false),+        SYS_PWRITEV => sys_preadv(a[0], a[1], a[2], a[3], true),+        SYS_SENDFILE => sys_sendfile(a[0], a[1], a[2], a[3]),+        _ => return None,+    })+}++/// Names for `--trace`.+pub fn name(nr: u64) -> Option<&'static str> {+    Some(match nr {+        SYS_SHUTDOWN => "shutdown",+        SYS_GETSOCKNAME => "getsockname",+        SYS_GETPEERNAME => "getpeername",+        SYS_SOCKETPAIR => "socketpair",+        SYS_GETSOCKOPT => "getsockopt",+        SYS_GETITIMER => "getitimer",+        SYS_ALARM => "alarm",+        SYS_SETITIMER => "setitimer",+        SYS_SENDFILE => "sendfile",+        SYS_FLOCK => "flock",+        SYS_LINK => "link",+        SYS_SYMLINK => "symlink",+        SYS_CHOWN => "chown",+        SYS_FCHOWN => "fchown",+        SYS_LCHOWN => "lchown",+        SYS_GETRUSAGE => "getrusage",+        SYS_SYSINFO => "sysinfo",+        SYS_TIMES => "times",+        SYS_SETUID => "setuid",+        SYS_SETGID => "setgid",+        SYS_SETREUID => "setreuid",+        SYS_SETREGID => "setregid",+        SYS_GETGROUPS => "getgroups",+        SYS_SETGROUPS => "setgroups",+        SYS_SETRESUID => "setresuid",+        SYS_GETRESUID => "getresuid",+        SYS_SETRESGID => "setresgid",+        SYS_GETRESGID => "getresgid",+        SYS_SETFSUID => "setfsuid",+        SYS_SETFSGID => "setfsgid",+        SYS_STATFS => "statfs",+        SYS_FSTATFS => "fstatfs",+        SYS_GETPRIORITY => "getpriority",+        SYS_SETPRIORITY => "setpriority",+        SYS_SETHOSTNAME => "sethostname",+        SYS_SETDOMAINNAME => "setdomainname",+        SYS_TIMER_CREATE => "timer_create",+        SYS_TIMER_SETTIME => "timer_settime",+        SYS_TIMER_GETTIME => "timer_gettime",+        SYS_TIMER_GETOVERRUN => "timer_getoverrun",+        SYS_TIMER_DELETE => "timer_delete",+        SYS_CLOCK_GETRES => "clock_getres",+        SYS_FCHOWNAT => "fchownat",+        SYS_LINKAT => "linkat",+        SYS_SYMLINKAT => "symlinkat",+        SYS_PREADV => "preadv",+        SYS_PWRITEV => "pwritev",+        SYS_STATX => "statx",+        _ => return None,+    })+}++/// Write `bytes` (a C struct we built) to user memory.+fn put(ptr: u64, bytes: &[u8]) -> Result<(), Errno> {+    user_slice_mut(ptr, bytes.len() as u64)?.copy_from_slice(bytes);+    Ok(())+}++/// A little-endian struct builder: `at(offset, value)`.+struct Buf(Vec<u8>);+impl Buf {+    fn new(len: usize) -> Buf {+        Buf(alloc::vec![0; len])+    }+    fn u64(&mut self, off: usize, v: u64) -> &mut Buf {+        self.0[off..off + 8].copy_from_slice(&v.to_le_bytes());+        self+    }+    fn u32(&mut self, off: usize, v: u32) -> &mut Buf {+        self.0[off..off + 4].copy_from_slice(&v.to_le_bytes());+        self+    }+    fn u16(&mut self, off: usize, v: u16) -> &mut Buf {+        self.0[off..off + 2].copy_from_slice(&v.to_le_bytes());+        self+    }+}++fn read_u64(ptr: u64, i: u64) -> Result<u64, Errno> {+    Ok(u64::from_le_bytes(user_slice(ptr + 8 * i, 8)?.try_into().unwrap()))+}++// --- 1. link / symlink ------------------------------------------------------++/// link / linkat(olddirfd, old, newdirfd, new, flags). Like Linux, a+/// symlink is linked itself unless AT_SYMLINK_FOLLOW; AT_EMPTY_PATH with+/// "" links the file open on `olddirfd`. Directories: EPERM. FAT, exFAT+/// (no hard links on disk): EPERM; btrfs (read-only): EROFS.+fn sys_linkat(olddirfd: u64, old_ptr: u64, newdirfd: u64, new_ptr: u64, flags: u64) -> Result<u64, Errno> {+    if flags & !(AT_SYMLINK_FOLLOW | AT_EMPTY_PATH) != 0 {+        return Err(Errno::EINVAL);+    }+    let raw = user_cstr(old_ptr, PATH_MAX - 1)?;+    let new = user_path_at(newdirfd, new_ptr)?;+    if raw.is_empty() && flags & AT_EMPTY_PATH != 0 {+        let file = current()?.file(olddirfd)?;+        let (inode, path) = file.vfs_inode().zip(file.path()).ok_or(Errno::ENOENT)?;+        fs::link_inode(&inode, &path, &new)?;+        return Ok(0);+    }+    let old = path_at(olddirfd, &raw)?;+    fs::link(&old, &new, flags & AT_SYMLINK_FOLLOW != 0)?;+    Ok(0)+}++/// symlink / symlinkat(target, newdirfd, linkpath). The target is stored+/// exactly as given -- it may be relative, or point nowhere yet.+fn sys_symlinkat(target_ptr: u64, newdirfd: u64, path_ptr: u64) -> Result<u64, Errno> {+    let raw = user_cstr(target_ptr, PATH_MAX - 1)?;+    let target = core::str::from_utf8(&raw).map_err(|_| Errno::EINVAL)?;+    let path = user_path_at(newdirfd, path_ptr)?;+    let link = fs::symlink(target, &path)?;+    set_creator(&*link);+    Ok(0)+}++/// A new file, directory or symlink belongs to whoever made it (its+/// fsuid/fsgid). Root's files are owned by 0:0 already, so only other+/// users cost a write; where owners can't be stored (FAT), nothing happens.+pub fn set_creator(inode: &dyn Inode) {+    if let Ok(c) = creds() {+        if c.fsuid != 0 || c.fsgid != 0 {+            let _ = inode.set_owner(Some(c.fsuid), Some(c.fsgid));+        }+    }+}++// --- 2. setsockopt, times(), CPU clocks -----------------------------------++const SO_REUSEADDR: u64 = 2;+const SO_TYPE: u64 = 3;+const SO_ERROR: u64 = 4;+const SO_SNDBUF: u64 = 7;+const SO_RCVBUF: u64 = 8;+const SO_KEEPALIVE: u64 = 9;+const SO_REUSEPORT: u64 = 15;+const SO_SNDTIMEO: u64 = 21;+const SO_ACCEPTCONN: u64 = 30;+const SO_PROTOCOL: u64 = 38;+const SO_DOMAIN: u64 = 39;+const IPPROTO_TCP: u64 = 6;+const TCP_NODELAY: u64 = 1;+/// Where TCP_NODELAY is remembered among the socket's flag bits.+const NODELAY_BIT: u32 = 31;++/// setsockopt. SO_RCVTIMEO really works (recv/accept time out). The rest+/// that servers routinely set are accepted and remembered, but change+/// nothing: SO_REUSEADDR/SO_REUSEPORT (smoltcp lets a port be reused at+/// once anyway), SO_KEEPALIVE (no keepalive probes), SO_SNDBUF/SO_RCVBUF+/// (fixed buffers), SO_SNDTIMEO (sends don't block for long), TCP_NODELAY.+/// Anything else: ENOPROTOOPT, like Linux for an unknown option.+fn sys_setsockopt(fd: u64, level: u64, opt: u64, value: u64, len: u64) -> Result<u64, Errno> {+    let int = || -> Result<u32, Errno> {+        if len < 4 {+            return Err(Errno::EINVAL);+        }+        Ok(u32::from_le_bytes(user_slice(value, 4)?.try_into().unwrap()))+    };+    match (level, opt) {+        (SOL_SOCKET, SO_RCVTIMEO) => {+            if len < 16 {+                return Err(Errno::EINVAL);+            }+            let (secs, usecs) = (read_u64(value, 0)?, read_u64(value, 1)?);+            let total = secs.saturating_mul(1_000_000).saturating_add(usecs);+            with_socket(fd, |s| Ok(s.set_recv_timeout(if total == 0 { None } else { Some(total) })))?;+        }+        (SOL_SOCKET, SO_SNDTIMEO) => {+            if len < 16 {+                return Err(Errno::EINVAL);+            }+            with_socket(fd, |_| Ok(()))?;+        }+        (SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT | SO_KEEPALIVE) => {+            let on = int()? != 0;+            with_socket(fd, |s| Ok(s.set_flag(opt as u32, on)))?;+        }+        (SOL_SOCKET, SO_SNDBUF | SO_RCVBUF) => {+            int()?;+            with_socket(fd, |_| Ok(()))?;+        }+        (IPPROTO_TCP, TCP_NODELAY) => {+            let on = int()? != 0;+            with_socket(fd, |s| {+                if s.kind != crate::net::socket::Kind::Tcp {+                    return Err(Errno::EOPNOTSUPP);+                }+                Ok(s.set_flag(NODELAY_BIT, on))+            })?;+        }+        _ => {+            with_socket(fd, |_| Ok(()))?; // ENOTSOCK / EBADF first, like Linux+            return Err(Errno::ENOPROTOOPT);+        }+    }+    Ok(0)+}++/// getsockopt(fd, level, opt, *value, *len).+fn sys_getsockopt(fd: u64, level: u64, opt: u64, value: u64, len_ptr: u64) -> Result<u64, Errno> {+    use crate::net::socket::Kind;+    let room = u32::from_le_bytes(user_slice(len_ptr, 4)?.try_into().unwrap()) as i32;+    if room < 0 {+        return Err(Errno::EINVAL);+    }+    let answer: Vec<u8> = with_socket(fd, |s| {+        let int = |v: u32| v.to_le_bytes().to_vec();+        Ok(match (level, opt) {+            (SOL_SOCKET, SO_TYPE) => int(if s.kind == Kind::Tcp { 1 } else { 2 }), // SOCK_STREAM / SOCK_DGRAM+            (SOL_SOCKET, SO_ERROR) => int(s.take_error() as u32),+            (SOL_SOCKET, SO_SNDBUF | SO_RCVBUF) => int(s.buffer_size() as u32),+            (SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT | SO_KEEPALIVE) => int(s.flag(opt as u32) as u32),+            (SOL_SOCKET, SO_ACCEPTCONN) => int(s.listening() as u32),+            (SOL_SOCKET, SO_DOMAIN) => int(2), // AF_INET+            (SOL_SOCKET, SO_PROTOCOL) => int(match s.kind {+                Kind::Tcp => 6,+                Kind::Udp => 17,+                Kind::Icmp => 1,+            }),+            (SOL_SOCKET, SO_RCVTIMEO | SO_SNDTIMEO) => {+                let us = if opt == SO_RCVTIMEO { s.recv_timeout_us().unwrap_or(0) } else { 0 };+                let mut b = (us / 1_000_000).to_le_bytes().to_vec();+                b.extend_from_slice(&(us % 1_000_000).to_le_bytes());+                b+            }+            (IPPROTO_TCP, TCP_NODELAY) if s.kind == Kind::Tcp => int(s.flag(NODELAY_BIT) as u32),+            _ => return Err(Errno::ENOPROTOOPT),+        })+    })?;+    let n = answer.len().min(room as usize);+    put(value, &answer[..n])?;+    copy_to_user(len_ptr, &(n as u32))?;+    Ok(0)+}++/// socketpair: Linux makes these with AF_UNIX sockets, which Rustbelt+/// doesn't have yet (deferred). We say so honestly: EAFNOSUPPORT for+/// AF_UNIX, and EOPNOTSUPP for AF_INET -- as Linux does.+fn sys_socketpair(domain: u64) -> Result<u64, Errno> {+    match domain {+        2 => Err(Errno::EOPNOTSUPP),+        _ => Err(Errno::EAFNOSUPPORT),+    }+}++/// The CPU clocks of clock_gettime (2 = this process, 3 = this thread),+/// in microseconds. Counted in 10 ms timer ticks (see process::CpuTimes),+/// so a program that ran for less than a tick reads 0.+pub fn cpu_clock_us(clock: u64) -> Result<u64, Errno> {+    let ticks = match clock {+        2 => {+            let p = current()?;+            p.cpu.user.load(Ordering::Relaxed) + p.cpu.system.load(Ordering::Relaxed)+        }+        3 => task()?.cpu_ticks.load(Ordering::Relaxed),+        _ => return Err(Errno::EINVAL),+    };+    Ok(ticks * process::CPU_TICK_US)+}++/// times(*tms): CPU time in clock ticks (sysconf(_SC_CLK_TCK) = 100 per+/// second, which is exactly our 10 ms timer tick): user, system, and the+/// same for reaped children. Returns ticks since boot.+fn sys_times(buf: u64) -> Result<u64, Errno> {+    let p = current()?;+    if buf != 0 {+        let c = &p.cpu;+        let t = [c.user.load(Ordering::Relaxed), c.system.load(Ordering::Relaxed), c.child_user.load(Ordering::Relaxed), c.child_system.load(Ordering::Relaxed)];+        copy_to_user(buf, &t)?;+    }+    Ok(crate::time::now_us() / process::CPU_TICK_US)+}++/// clock_getres(clock, *res): our clocks count microseconds; the CPU+/// clocks move in 10 ms ticks.+fn sys_clock_getres(clock: u64, res: u64) -> Result<u64, Errno> {+    let ns = match clock {+        0 | 1 | 4 | 5 | 6 | 7 => 1000,+        2 | 3 => process::CPU_TICK_US * 1000,+        _ => return Err(Errno::EINVAL),+    };+    if res != 0 {+        copy_to_user(res, &[0u64, ns])?;+    }+    Ok(0)+}++/// getrusage(who, *rusage): 0 = RUSAGE_SELF, -1 = RUSAGE_CHILDREN (reaped+/// ones), 1 = RUSAGE_THREAD. Fills the two CPU times and ru_maxrss (our+/// resident size now, in KiB); the other 14 counters stay 0.+fn sys_getrusage(who: i64, buf: u64) -> Result<u64, Errno> {+    let p = current()?;+    let c = &p.cpu;+    let (user, system) = match who {+        0 => (c.user.load(Ordering::Relaxed), c.system.load(Ordering::Relaxed)),+        -1 => (c.child_user.load(Ordering::Relaxed), c.child_system.load(Ordering::Relaxed)),+        1 => {+            // Per thread we only count ticks, not user/system: say "user".+            (task()?.cpu_ticks.load(Ordering::Relaxed), 0)+        }+        _ => return Err(Errno::EINVAL),+    };+    let tv = |ticks: u64| {+        let us = ticks * process::CPU_TICK_US;+        (us / 1_000_000, us % 1_000_000)+    };+    let mut b = Buf::new(144);+    let (us, uu) = tv(user);+    let (ss, su) = tv(system);+    b.u64(0, us).u64(8, uu).u64(16, ss).u64(24, su);+    if who != -1 {+        let resident = p.mm.lock().usage().0;+        b.u64(32, resident * 4);+    }+    put(buf, &b.0)?;+    Ok(0)+}++// --- 3. chown family -------------------------------------------------------++/// -1 (as a 32-bit id) = "leave this one alone".+fn id_arg(v: u64) -> Option<u32> {+    let v = v as u32;+    (v != u32::MAX).then_some(v)+}++/// Change an inode's owner. Rustbelt stores owners (ramfs, ext2) but+/// doesn't check permissions, so anyone may chown anything. FAT and exFAT+/// can't store an owner: asking for the owner they already show (root) is+/// fine, anything else is EPERM, like Linux's vfat. btrfs (read-only): EROFS.+fn chown_inode(inode: &dyn Inode, uid: u64, gid: u64) -> Result<u64, Errno> {+    let (uid, gid) = (id_arg(uid), id_arg(gid));+    match inode.set_owner(uid, gid) {+        Err(Errno::EPERM) => {+            let (u, g) = inode.owner();+            if uid.unwrap_or(u) == u && gid.unwrap_or(g) == g {+                Ok(0)+            } else {+                Err(Errno::EPERM)+            }+        }+        other => other.map(|_| 0),+    }+}++fn sys_fchownat(dirfd: u64, path_ptr: u64, uid: u64, gid: 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 {+        if dirfd as i32 as i64 == AT_FDCWD {+            let cwd = at_base(dirfd)?;+            return chown_inode(&*fs::lookup(&cwd)?, uid, gid);+        }+        return sys_fchown(dirfd, uid, gid);+    }+    let path = path_at(dirfd, &raw)?;+    let inode = if flags & AT_SYMLINK_NOFOLLOW != 0 { fs::lookup_nofollow(&path)? } else { fs::lookup(&path)? };+    chown_inode(&*inode, uid, gid)+}++fn sys_fchown(fd: u64, uid: u64, gid: u64) -> Result<u64, Errno> {+    let file = current()?.file(fd)?;+    match file.vfs_inode() {+        Some(inode) => chown_inode(&*inode, uid, gid),+        // Pipes, sockets, terminals: nothing on disk to own. Linux keeps+        // an owner in their in-memory inode; we accept and forget it.+        None => Ok(0),+    }+}++// --- 4. statfs / fstatfs / statx ----------------------------------------++const PIPEFS_MAGIC: u64 = 0x5049_5045;+const SOCKFS_MAGIC: u64 = 0x534F_434B;+const ST_RDONLY: u64 = 1;+const ST_VALID: u64 = 0x20;++/// Linux's `struct statfs` (120 bytes).+fn write_statfs(buf: u64, st: &fs::StatFs, dev: u64) -> Result<u64, Errno> {+    let mut b = Buf::new(120);+    b.u64(0, st.magic)+        .u64(8, st.block_size)+        .u64(16, st.blocks)+        .u64(24, st.free_blocks)+        .u64(32, st.avail_blocks)+        .u64(40, st.files)+        .u64(48, st.free_files)+        .u32(56, dev as u32) // f_fsid: which mount, like st_dev+        .u64(64, st.name_max)+        .u64(72, st.block_size) // f_frsize+        .u64(80, ST_VALID | if st.read_only { ST_RDONLY } else { 0 });+    put(buf, &b.0)?;+    Ok(0)+}++fn statfs_path(path: &str, buf: u64) -> Result<u64, Errno> {+    let (index, fs) = fs::fs_of(path).ok_or(Errno::ENOENT)?;+    write_statfs(buf, &fs.statfs(), index as u64 + 1)+}++fn sys_statfs(path_ptr: u64, buf: u64) -> Result<u64, Errno> {+    let path = user_path(path_ptr)?;+    fs::lookup(&path)?; // ENOENT for a missing path+    statfs_path(&path, buf)+}++fn sys_fstatfs(fd: u64, buf: u64) -> Result<u64, Errno> {+    let file = current()?.file(fd)?;+    if let Some(path) = file.path() {+        return statfs_path(&path, buf);+    }+    let magic = match file.stat()?.kind {+        fs::file::KIND_PIPE => PIPEFS_MAGIC,+        fs::file::KIND_SOCKET => SOCKFS_MAGIC,+        _ => fs::TMPFS_MAGIC, // terminals, epoll: in-memory things+    };+    let st = fs::StatFs { magic, block_size: 4096, name_max: 255, ..Default::default() };+    write_statfs(buf, &st, 0)+}++/// statx(dirfd, path, flags, mask, *statx): the same facts as stat, in+/// the newer layout (256 bytes). We fill all the basic fields (mask+/// STATX_BASIC_STATS = 0x7ff) whatever `mask` asks; no birth time.+fn sys_statx(dirfd: u64, path_ptr: u64, flags: u64, mask: u64, buf: u64) -> Result<u64, Errno> {+    if flags & !(AT_SYMLINK_NOFOLLOW | AT_EMPTY_PATH | AT_NO_AUTOMOUNT | AT_STATX_SYNC_TYPE) != 0+        || flags & AT_STATX_SYNC_TYPE == AT_STATX_SYNC_TYPE+        || mask & 0x8000_0000 != 0+    {+        return Err(Errno::EINVAL);+    }+    let st = crate::syscall::stat_at(dirfd, path_ptr, flags & (AT_SYMLINK_NOFOLLOW | AT_EMPTY_PATH))?;+    let mut b = Buf::new(256);+    b.u32(0, 0x7ff)+        .u32(4, st.st_blksize as u32)+        .u32(16, st.st_nlink as u32)+        .u32(20, st.st_uid)+        .u32(24, st.st_gid)+        .u16(28, st.st_mode as u16)+        .u64(32, st.st_ino)+        .u64(40, st.st_size as u64)+        .u64(48, st.st_blocks as u64);+    for (off, t) in [(64, st.st_atime), (96, st.st_ctime), (112, st.st_mtime)] {+        b.u64(off, t[0] as u64).u32(off + 8, t[1] as u32);+    }+    b.u32(128, (st.st_rdev >> 8) as u32 & 0xfff).u32(132, st.st_rdev as u32 & 0xff);+    b.u32(136, 0).u32(140, st.st_dev as u32); // device: major 0, minor = mount+    put(buf, &b.0)?;+    Ok(0)+}++// --- 5. sysinfo -------------------------------------------------------------++/// sysinfo (99): Linux's `struct sysinfo` (112 bytes), what `free`,+/// `uptime` and `top` read. (Rustbelt's own, richer sysinfo is 502.)+/// The load averages are how busy the CPUs have been since boot, the+/// same number three times: we don't keep 1/5/15-minute averages.+fn sys_sysinfo(buf: u64) -> Result<u64, Errno> {+    let (total, free) = crate::memory::frame_totals();+    let cpus = crate::smp::cpu_count();+    let (mut ticks, mut busy) = (0, 0);+    for cpu in 0..cpus {+        let (t, b) = crate::thread::cpu_ticks(cpu);+        ticks += t;+        busy += b;+    }+    // Load = busy CPUs on average, in 16.16 fixed point.+    let load = if ticks == 0 { 0 } else { busy * cpus as u64 * 65536 / ticks };+    let mut b = Buf::new(112);+    b.u64(0, crate::time::now_us() / 1_000_000)+        .u64(8, load)+        .u64(16, load)+        .u64(24, load)+        .u64(32, total as u64 * 4096)+        .u64(40, free as u64 * 4096)+        .u16(80, process::count().min(u16::MAX as usize) as u16)+        .u32(104, 1); // mem_unit: sizes are in bytes+    put(buf, &b.0)?;+    Ok(0)+}++// --- 7. Timers -------------------------------------------------------------++fn tv_us(ptr: u64) -> Result<(u64, u64), Errno> {+    // struct itimerval { timeval it_interval; timeval it_value; }+    let (is, iu, vs, vu) = (read_u64(ptr, 0)?, read_u64(ptr, 1)?, read_u64(ptr, 2)?, read_u64(ptr, 3)?);+    if iu >= 1_000_000 || vu >= 1_000_000 || (is as i64) < 0 || (vs as i64) < 0 {+        return Err(Errno::EINVAL);+    }+    Ok((vs.saturating_mul(1_000_000).saturating_add(vu), is.saturating_mul(1_000_000).saturating_add(iu)))+}++fn put_itimerval(ptr: u64, (value, interval): (u64, u64)) -> Result<(), Errno> {+    copy_to_user(ptr, &[interval / 1_000_000, interval % 1_000_000, value / 1_000_000, value % 1_000_000])+}++/// alarm(seconds): SIGALRM that many seconds from now (0 cancels).+/// Returns the seconds left on the previous alarm (rounded, never 0 if+/// one was pending).+fn sys_alarm(secs: u64) -> Result<u64, Errno> {+    let pid = current()?.pid;+    let (old, _) = crate::timer::set_itimer(pid, (secs as u32 as u64) * 1_000_000, 0);+    Ok(if old == 0 { 0 } else { ((old + 500_000) / 1_000_000).max(1) })+}++/// ITIMER_REAL (0) only: ITIMER_VIRTUAL and ITIMER_PROF (CPU-time timers)+/// are EINVAL here.+fn sys_getitimer(which: u64, ptr: u64) -> Result<u64, Errno> {+    if which != 0 {+        return Err(Errno::EINVAL);+    }+    put_itimerval(ptr, crate::timer::get_itimer(current()?.pid))?;+    Ok(0)+}++fn sys_setitimer(which: u64, new: u64, old: u64) -> Result<u64, Errno> {+    if which != 0 {+        return Err(Errno::EINVAL);+    }+    let (value, interval) = if new == 0 { (0, 0) } else { tv_us(new)? };+    let prev = crate::timer::set_itimer(current()?.pid, value, interval);+    if old != 0 {+        put_itimerval(old, prev)?;+    }+    Ok(0)+}++/// A timespec as microseconds, rounded up (a timer never fires early).+fn ts_us(ptr: u64, i: u64) -> Result<u64, Errno> {+    let (s, ns) = (read_u64(ptr, 2 * i)?, read_u64(ptr, 2 * i + 1)?);+    if ns >= 1_000_000_000 || (s as i64) < 0 {+        return Err(Errno::EINVAL);+    }+    Ok(s.saturating_mul(1_000_000).saturating_add(ns.div_ceil(1000)))+}++fn put_itimerspec(ptr: u64, (value, interval): (u64, u64)) -> Result<(), Errno> {+    copy_to_user(ptr, &[interval / 1_000_000, interval % 1_000_000 * 1000, value / 1_000_000, value % 1_000_000 * 1000])+}++/// timer_create(clock, *sigevent, *timerid). Clocks: REALTIME, MONOTONIC,+/// BOOTTIME. Notification: SIGEV_SIGNAL, SIGEV_NONE, SIGEV_THREAD_ID.+/// (SIGEV_THREAD is the C library's job: it asks us for SIGEV_THREAD_ID.)+fn sys_timer_create(clock: u64, sev_ptr: u64, id_ptr: u64) -> Result<u64, Errno> {+    use crate::timer::{SigEvent, CLOCK_BOOTTIME, CLOCK_MONOTONIC, CLOCK_REALTIME, SIGEV_NONE, SIGEV_SIGNAL, SIGEV_THREAD_ID};+    if ![CLOCK_REALTIME, CLOCK_MONOTONIC, CLOCK_BOOTTIME].contains(&clock) {+        return Err(Errno::EINVAL);+    }+    let p = current()?;+    let ev = if sev_ptr == 0 {+        SigEvent { notify: SIGEV_SIGNAL, signo: crate::signal::SIGALRM, tid: 0, sigval: None }+    } else {+        // struct sigevent { sigval; int signo; int notify; int tid; ... }+        let raw = user_slice(sev_ptr, 20)?;+        let sigval = u64::from_le_bytes(raw[..8].try_into().unwrap());+        let signo = u32::from_le_bytes(raw[8..12].try_into().unwrap());+        let notify = i32::from_le_bytes(raw[12..16].try_into().unwrap());+        let tid = u32::from_le_bytes(raw[16..20].try_into().unwrap()) as u64;+        match notify {+            SIGEV_NONE => {}+            SIGEV_SIGNAL | SIGEV_THREAD_ID if (1..=64).contains(&signo) => {}+            _ => return Err(Errno::EINVAL),+        }+        if notify == SIGEV_THREAD_ID && !p.tasks().iter().any(|t| t.tid() == tid) {+            return Err(Errno::EINVAL);+        }+        SigEvent { notify, signo, tid, sigval: Some(sigval) }+    };+    user_slice_mut(id_ptr, 4)?; // check before making it+    let id = crate::timer::create(p.pid, clock, ev)?;+    copy_to_user(id_ptr, &id)?;+    Ok(0)+}++/// timer_settime(id, flags, *new, *old). TIMER_ABSTIME (1): the value is a+/// time on the timer's clock.+fn sys_timer_settime(id: u64, flags: u64, new: u64, old: u64) -> Result<u64, Errno> {+    if flags & !1 != 0 {+        return Err(Errno::EINVAL);+    }+    let (interval, value) = (ts_us(new, 0)?, ts_us(new, 1)?);+    let prev = crate::timer::settime(current()?.pid, id as i32, flags & 1 != 0, value, interval)?;+    if old != 0 {+        put_itimerspec(old, prev)?;+    }+    Ok(0)+}++fn sys_timer_gettime(id: u64, ptr: u64) -> Result<u64, Errno> {+    let now = crate::timer::gettime(current()?.pid, id as i32)?;+    put_itimerspec(ptr, now)?;+    Ok(0)+}++// --- 8. Ids, priority, host name -----------------------------------------++fn creds() -> Result<Creds, Errno> {+    Ok(*current()?.creds.lock())+}++/// Change the calling process's creds with `f` (on a copy, kept if Ok).+fn change(f: impl FnOnce(&mut Creds) -> Result<u64, Errno>) -> Result<u64, Errno> {+    let p = current()?;+    let mut creds = p.creds.lock();+    let mut new = *creds;+    let r = f(&mut new)?;+    *creds = new;+    Ok(r)+}++/// The (real, effective, saved, fs) ids of `c`, users or groups.+fn ids(c: &mut Creds, group: bool) -> [&mut u32; 4] {+    if group {+        [&mut c.rgid, &mut c.egid, &mut c.sgid, &mut c.fsgid]+    } else {+        [&mut c.ruid, &mut c.euid, &mut c.suid, &mut c.fsuid]+    }+}++/// setuid / setgid: root (euid 0) sets all of them; anyone else may only+/// switch the effective id to their real or saved one.+fn set_id(group: bool, id: u32) -> Result<u64, Errno> {+    if id == u32::MAX {+        return Err(Errno::EINVAL);+    }+    change(|c| {+        let root = c.euid == 0;+        let [r, e, s, f] = ids(c, group);+        if root {+            (*r, *e, *s, *f) = (id, id, id, id);+        } else if id == *r || id == *s {+            (*e, *f) = (id, id);+        } else {+            return Err(Errno::EPERM);+        }+        Ok(0)+    })+}++/// setreuid / setregid(real, effective), -1 = unchanged.+fn set_re(group: bool, real: u32, eff: u32) -> Result<u64, Errno> {+    change(|c| {+        let root = c.euid == 0;+        let [r, e, s, f] = ids(c, group);+        let (old_r, old_e, old_s) = (*r, *e, *s);+        if real != u32::MAX && !root && real != old_r && real != old_e {+            return Err(Errno::EPERM);+        }+        if eff != u32::MAX && !root && eff != old_r && eff != old_e && eff != old_s {+            return Err(Errno::EPERM);+        }+        if real != u32::MAX {+            *r = real;+        }+        if eff != u32::MAX {+            *e = eff;+        }+        // Linux: the saved id follows the effective one when the real id+        // was set, or the effective one changed to something but the old real.+        if real != u32::MAX || (eff != u32::MAX && eff != old_r) {+            *s = *e;+        }+        *f = *e;+        Ok(0)+    })+}++/// setresuid / setresgid(real, effective, saved), -1 = unchanged. Not+/// root: each new value must be one of the current three.+fn set_res(group: bool, new: [u32; 3]) -> Result<u64, Errno> {+    change(|c| {+        let root = c.euid == 0;+        let [r, e, s, f] = ids(c, group);+        let old = [*r, *e, *s];+        if !root && new.iter().any(|&v| v != u32::MAX && !old.contains(&v)) {+            return Err(Errno::EPERM);+        }+        for (slot, v) in [&mut *r, &mut *e, &mut *s].into_iter().zip(new) {+            if v != u32::MAX {+                *slot = v;+            }+        }+        *f = *e;+        Ok(0)+    })+}++fn get_res(group: bool, ptrs: [u64; 3]) -> Result<u64, Errno> {+    let mut c = creds()?;+    let [r, e, s, _] = ids(&mut c, group);+    for (ptr, v) in ptrs.into_iter().zip([*r, *e, *s]) {+        copy_to_user(ptr, &v)?;+    }+    Ok(0)+}++/// setfsuid / setfsgid: returns the old fs id; changes it if allowed+/// (root, or one of the current ids). Never fails, like Linux.+fn set_fs(group: bool, id: u32) -> Result<u64, Errno> {+    let p = current()?;+    let mut creds = p.creds.lock();+    let root = creds.euid == 0;+    let [r, e, s, f] = ids(&mut creds, group);+    let old = *f;+    if id != u32::MAX && (root || [*r, *e, *s, *f].contains(&id)) {+        *f = id;+    }+    Ok(old as u64)+}++/// getgroups(size, *list): size 0 = just say how many.+fn sys_getgroups(size: i64, list: u64) -> Result<u64, Errno> {+    let c = creds()?;+    if size < 0 {+        return Err(Errno::EINVAL);+    }+    if size == 0 {+        return Ok(c.ngroups as u64);+    }+    if (size as usize) < c.ngroups {+        return Err(Errno::EINVAL);+    }+    for (i, g) in c.groups[..c.ngroups].iter().enumerate() {+        copy_to_user(list + 4 * i as u64, g)?;+    }+    Ok(c.ngroups as u64)+}++/// setgroups(size, *list): root only.+fn sys_setgroups(size: u64, list: u64) -> Result<u64, Errno> {+    if size > process::NGROUPS_MAX as u64 {+        return Err(Errno::EINVAL);+    }+    let raw = user_slice(list, 4 * size)?;+    change(|c| {+        if c.euid != 0 {+            return Err(Errno::EPERM);+        }+        for (i, g) in raw.chunks(4).enumerate() {+            c.groups[i] = u32::from_le_bytes(g.try_into().unwrap());+        }+        c.ngroups = size as usize;+        Ok(0)+    })+}++/// The processes get/setpriority mean: PRIO_PROCESS (0) one pid, PRIO_PGRP+/// (1) a process group, PRIO_USER (2) everything of one real uid. 0 = mine.+fn prio_targets(which: u64, who: u64) -> Result<Vec<Arc<process::Process>>, Errno> {+    let me = current()?;+    let who = who as u32 as u64;+    let list: Vec<_> = match which {+        0 => {+            let pid = if who == 0 { me.pid } else { who };+            process::get(pid).into_iter().collect()+        }+        1 => {+            let pgid = if who == 0 { me.pgid() } else { who };+            process::all().into_iter().filter(|p| p.pgid() == pgid).collect()+        }+        2 => {+            let uid = if who == 0 { me.creds.lock().ruid as u64 } else { who };+            process::all().into_iter().filter(|p| p.creds.lock().ruid as u64 == uid).collect()+        }+        _ => return Err(Errno::EINVAL),+    };+    if list.is_empty() {+        return Err(Errno::ESRCH);+    }+    Ok(list)+}++/// getpriority(which, who). The raw syscall returns 20 - nice (1..40) so+/// that it's never negative; the C library turns it back into the nice value.+fn sys_getpriority(which: u64, who: u64) -> Result<u64, Errno> {+    let best = prio_targets(which, who)?.iter().map(|p| p.nice.load(Ordering::Relaxed)).min().unwrap();+    Ok((20 - best) as u64)+}++/// setpriority(which, who, nice): stored (and inherited), but the+/// scheduler doesn't use it yet. Only root may lower a nice value.+fn sys_setpriority(which: u64, who: u64, nice: i32) -> Result<u64, Errno> {+    let nice = nice.clamp(-20, 19);+    let root = creds()?.euid == 0;+    for p in prio_targets(which, who)? {+        if nice < p.nice.load(Ordering::Relaxed) && !root {+            return Err(Errno::EACCES);+        }+        p.nice.store(nice, Ordering::Relaxed);+    }+    Ok(0)+}++static HOSTNAME: Mutex<Option<String>> = Mutex::new(None);+static DOMAINNAME: Mutex<Option<String>> = Mutex::new(None);++/// The host name `uname` reports ("qemu" until sethostname).+pub fn hostname() -> String {+    HOSTNAME.lock().clone().unwrap_or_else(|| String::from("qemu"))+}++/// The NIS domain name `uname` reports ("(none)" until setdomainname).+pub fn domainname() -> String {+    DOMAINNAME.lock().clone().unwrap_or_else(|| String::from("(none)"))+}++/// sethostname / setdomainname(name, len): at most 64 bytes; root only.+fn set_name(slot: &Mutex<Option<String>>, ptr: u64, len: u64) -> Result<u64, Errno> {+    if len > 64 {+        return Err(Errno::EINVAL);+    }+    if creds()?.euid != 0 {+        return Err(Errno::EPERM);+    }+    let raw = user_slice(ptr, len)?;+    let name = core::str::from_utf8(raw).map_err(|_| Errno::EINVAL)?;+    if name.contains('\0') {+        return Err(Errno::EINVAL);+    }+    *slot.lock() = Some(String::from(name));+    Ok(0)+}++// --- 9. flock, preadv/pwritev, sendfile -------------------------------------++const LOCK_SH: u64 = 1;+const LOCK_EX: u64 = 2;+const LOCK_NB: u64 = 4;+const LOCK_UN: u64 = 8;++/// flock(fd, op): a lock on the whole file, owned by the *open file*+/// (shared by dup'ed and inherited fds; released when its last fd+/// closes). LOCK_SH (shared) / LOCK_EX (exclusive) / LOCK_UN, | LOCK_NB =+/// EAGAIN instead of waiting. Pipes and sockets: nobody else can name+/// them, so there's never a conflict and we just say yes.+fn sys_flock(fd: u64, op: u64) -> Result<u64, Errno> {+    let file = current()?.file(fd)?;+    let cmd = op & !LOCK_NB;+    if ![LOCK_SH, LOCK_EX, LOCK_UN].contains(&cmd) {+        return Err(Errno::EINVAL);+    }+    let (Some(path), Some(inode)) = (file.path(), file.vfs_inode()) else { return Ok(0) };+    // The open file (an InodeFile) is the owner: the Arc's data address.+    let owner = Arc::as_ptr(&file) as *const u8 as usize;+    if cmd == LOCK_UN {+        fs::lock::flock_release(owner);+        return Ok(0);+    }+    // Same file = same key, also through another hard link.+    let key = match inode.ino() {+        Some(ino) => fs::file::ino_hash(alloc::format!("{}:{}", fs::dev_of(&path), ino).as_bytes()),+        None => fs::file::ino_hash(path.as_bytes()),+    };+    drop(file); // don't keep the file open while we sleep+    fs::lock::flock(key, owner, cmd == LOCK_EX, op & LOCK_NB == 0)?;+    Ok(0)+}++/// preadv / pwritev(fd, iov, count, offset): readv/writev at an offset+/// (the file offset doesn't move). Buffer by buffer; stops at a short one.+fn sys_preadv(fd: u64, iov: u64, count: u64, offset: u64, write: bool) -> Result<u64, Errno> {+    if (offset as i64) < 0 {+        return Err(Errno::EINVAL);+    }+    let file = current()?.file(fd)?;+    let iovs = read_iovecs(iov, count)?;+    let mut done = 0u64;+    for (base, len) in iovs {+        let n = if write {+            file.write_at(user_slice(base, len)?, offset + done)+        } else {+            file.read_at(user_slice_mut(base, len)?, offset + done)+        };+        let n = match n {+            Ok(n) => n as u64,+            Err(e) if done == 0 => return Err(e),+            Err(_) => break,+        };+        done += n;+        if n < len {+            break;+        }+    }+    Ok(done)+}++/// sendfile(out_fd, in_fd, *offset, count): copy from a file to any fd+/// (a socket, a pipe, another file) without a trip through user memory.+/// With `offset`, read from there and update it (in_fd's offset stays);+/// without, from in_fd's offset, which moves.+fn sys_sendfile(out_fd: u64, in_fd: u64, off_ptr: u64, count: u64) -> Result<u64, Errno> {+    let p = current()?;+    let (out, input) = (p.file(out_fd)?, p.file(in_fd)?);+    drop(p);+    let mut pos = if off_ptr != 0 {+        let v = read_u64(off_ptr, 0)?;+        if (v as i64) < 0 {+            return Err(Errno::EINVAL);+        }+        v+    } else {+        input.seek(0, 1).map_err(|_| Errno::EINVAL)? // SEEK_CUR+    };+    let count = count.min(0x7fff_f000);+    let mut buf = alloc::vec![0u8; 64 * 1024];+    let mut done = 0u64;+    while done < count {+        let want = (count - done).min(buf.len() as u64) as usize;+        let n = match input.read_at(&mut buf[..want], pos) {+            Ok(n) => n,+            Err(Errno::ESPIPE) => return Err(Errno::EINVAL), // in_fd must be a file+            Err(e) if done == 0 => return Err(e),+            Err(_) => break,+        };+        if n == 0 {+            break; // end of file+        }+        let mut written = 0;+        while written < n {+            match out.write(&buf[written..n]) {+                Ok(0) => break,+                Ok(k) => written += k,+                Err(e) if done == 0 && written == 0 => return Err(e),+                Err(_) => break,+            }+        }+        done += written as u64;+        pos += written as u64;+        if written < n {+            break;+        }+    }+    if off_ptr != 0 {+        copy_to_user(off_ptr, &pos)?;+    } else {+        input.seek(pos as i64, 0)?; // SEEK_SET+    }+    Ok(done)+}+
@@ -0,0 +1,278 @@+//! Timers that send signals (stage 17): alarm, setitimer/getitimer and+//! the POSIX timers (timer_create, timer_settime, ...).+//!+//! A timer is a deadline, maybe with an interval to repeat at, and what to+//! do when it's reached: send a signal (SIGALRM for alarm and setitimer;+//! any signal, maybe to one thread, for timer_create).+//!+//! All timers live in one table. One kernel thread, "timerd", sleeps until+//! the nearest deadline, sends the signals that are due, and goes back to+//! sleep. Why a thread and not the timer interrupt? Sending a signal takes+//! locks and may wake threads; doing that from normal thread context keeps+//! the interrupt handler tiny and follows the locking rules in thread.rs.+//! timerd is started the first time anyone arms a timer.+//!+//! Resolution: timerd sleeps in PIT ticks (~55 ms), so a timer fires up+//! to ~110 ms late, never early. A periodic timer whose signals come late+//! counts the periods it missed as "overruns" (timer_getoverrun), like+//! Linux does when a signal is still pending.+//!+//! Lifetime, as on Linux: fork doesn't copy timers; execve deletes the+//! POSIX timers but keeps the alarm / ITIMER_REAL; exit deletes them all.++use crate::errno::Errno;+use crate::process;+use crate::signal::{self, SigInfo};+use crate::thread::WaitQueue;+use crate::time;+use alloc::vec::Vec;+use core::sync::atomic::{AtomicBool, Ordering};+use spin::Mutex;+use x86_64::instructions::interrupts::without_interrupts;++/// si_code for a POSIX timer's signal.+pub const SI_TIMER: i32 = -2;++// The clocks timer_create understands.+pub const CLOCK_REALTIME: u64 = 0;+pub const CLOCK_MONOTONIC: u64 = 1;+pub const CLOCK_BOOTTIME: u64 = 7;++// sigev_notify: what to do when it fires.+pub const SIGEV_SIGNAL: i32 = 0; // send sigev_signo to the process+pub const SIGEV_NONE: i32 = 1; // nothing (the program polls timer_gettime)+pub const SIGEV_THREAD_ID: i32 = 4; // send sigev_signo to thread sigev_tid++/// At most this many POSIX timers per process: EAGAIN after that.+const MAX_TIMERS: usize = 32;+/// The id we use for a process's ITIMER_REAL (POSIX ids are 0..MAX_TIMERS).+const ITIMER_ID: i32 = -1;++#[derive(Clone, Copy)]+struct Timer {+    pid: u64,+    /// timer_create's id, or ITIMER_ID.+    id: i32,+    clock: u64,+    /// Next expiry, in `time::now_us` microseconds (None = disarmed).+    next: Option<u64>,+    /// Repeat every this many microseconds (0 = once).+    interval: u64,+    signo: u32,+    notify: i32,+    /// SIGEV_THREAD_ID's thread.+    tid: u64,+    sigval: u64,+    /// Expirations missed at the last firing (timer_getoverrun).+    overrun: u64,+}++static TIMERS: Mutex<Vec<Timer>> = Mutex::new(Vec::new());+/// timerd sleeps here; poked when the table changes.+static CHANGED_QUEUE: WaitQueue = WaitQueue::new();+static CHANGED: AtomicBool = AtomicBool::new(false);+static STARTED: AtomicBool = AtomicBool::new(false);++fn with_table<R>(f: impl FnOnce(&mut Vec<Timer>) -> R) -> R {+    without_interrupts(|| f(&mut TIMERS.lock()))+}++/// Tell timerd to look again (starting it the first time).+fn poke() {+    if !STARTED.swap(true, Ordering::AcqRel) {+        crate::thread::spawn("timerd", timerd);+    }+    CHANGED.store(true, Ordering::Release);+    CHANGED_QUEUE.wake_all();+}++/// How far CLOCK_REALTIME is ahead of the monotonic clock (µs), to turn+/// absolute wall-clock deadlines into ours.+fn realtime_offset() -> u64 {+    time::realtime_us().saturating_sub(time::now_us())+}++/// The kernel thread that fires timers (see the top).+fn timerd() {+    loop {+        let now = time::now_us();+        let mut due = Vec::new();+        let next = with_table(|t| {+            for timer in t.iter_mut() {+                let Some(at) = timer.next else { continue };+                if at > now {+                    continue;+                }+                if timer.interval > 0 {+                    let missed = (now - at) / timer.interval;+                    timer.overrun = missed;+                    timer.next = Some(at + (missed + 1) * timer.interval);+                } else {+                    timer.overrun = 0;+                    timer.next = None;+                }+                due.push(*timer);+            }+            t.iter().filter_map(|t| t.next).min()+        });+        for timer in due {+            fire(&timer);+        }+        let changed = || CHANGED.swap(false, Ordering::AcqRel).then_some(());+        match next {+            Some(at) => {+                let deadline = time::deadline_after_us(at.saturating_sub(time::now_us()));+                let _ = CHANGED_QUEUE.wait_until_deadline(deadline, changed);+            }+            None => CHANGED_QUEUE.wait_until(changed),+        }+    }+}++/// Send a due timer's signal.+fn fire(timer: &Timer) {+    if timer.notify == SIGEV_NONE {+        return;+    }+    let Some(p) = process::get(timer.pid) else { return };+    let info = if timer.id == ITIMER_ID {+        SigInfo::from_pid(timer.signo, 0) // SI_KERNEL, like Linux's itimers+    } else {+        let mut info = SigInfo::new(timer.signo, SI_TIMER);+        info.fields[0] = timer.id as u32 as u64 | (timer.overrun.min(i32::MAX as u64)) << 32; // si_tid, si_overrun+        info.fields[1] = timer.sigval; // si_value+        info+    };+    if timer.notify == SIGEV_THREAD_ID {+        if let Some(t) = p.tasks().into_iter().find(|t| t.tid() == timer.tid) {+            signal::send_thread(&t, timer.signo, info);+        }+        return;+    }+    signal::send(&p, timer.signo, info);+}++/// (value, interval) of a timer in microseconds: time left, and the period.+fn read(timer: &Timer, now: u64) -> (u64, u64) {+    (timer.next.map_or(0, |at| at.saturating_sub(now).max(1)), timer.interval)+}++// --- setitimer / getitimer / alarm ---------------------------------------++/// getitimer(ITIMER_REAL): (time left, interval), in microseconds.+pub fn get_itimer(pid: u64) -> (u64, u64) {+    let now = time::now_us();+    with_table(|t| t.iter().find(|x| x.pid == pid && x.id == ITIMER_ID).map_or((0, 0), |x| read(x, now)))+}++/// setitimer(ITIMER_REAL, value, interval) (µs; value 0 disarms). Returns+/// the old (value, interval).+pub fn set_itimer(pid: u64, value: u64, interval: u64) -> (u64, u64) {+    let now = time::now_us();+    let old = with_table(|t| {+        let old = t.iter().position(|x| x.pid == pid && x.id == ITIMER_ID).map(|i| t.remove(i));+        if value > 0 {+            t.push(Timer {+                pid,+                id: ITIMER_ID,+                clock: CLOCK_MONOTONIC,+                next: Some(now + value),+                interval,+                signo: signal::SIGALRM,+                notify: SIGEV_SIGNAL,+                tid: 0,+                sigval: 0,+                overrun: 0,+            });+        }+        old.map_or((0, 0), |x| read(&x, now))+    });+    if value > 0 {+        poke();+    }+    old+}++// --- POSIX timers --------------------------------------------------------++/// What timer_create's `struct sigevent` asked for.+pub struct SigEvent {+    pub notify: i32,+    pub signo: u32,+    pub tid: u64,+    /// None: no sigevent was given, so the value is the timer's id.+    pub sigval: Option<u64>,+}++/// timer_create: a new, disarmed timer. Returns its id.+pub fn create(pid: u64, clock: u64, ev: SigEvent) -> Result<i32, Errno> {+    with_table(|t| {+        let ids: Vec<i32> = t.iter().filter(|x| x.pid == pid && x.id != ITIMER_ID).map(|x| x.id).collect();+        if ids.len() >= MAX_TIMERS {+            return Err(Errno::EAGAIN);+        }+        let id = (0..).find(|i| !ids.contains(i)).unwrap();+        // No sigevent: SIGALRM with the timer id as the value (Linux's default).+        let sigval = ev.sigval.unwrap_or(id as u64);+        t.push(Timer { pid, id, clock, next: None, interval: 0, signo: ev.signo, notify: ev.notify, tid: ev.tid, sigval, overrun: 0 });+        Ok(id)+    })+}++/// timer_settime: arm (value > 0) or disarm. `absolute`: value is a time on+/// the timer's clock, not a delay. Returns the old (value, interval).+pub fn settime(pid: u64, id: i32, absolute: bool, value: u64, interval: u64) -> Result<(u64, u64), Errno> {+    let now = time::now_us();+    let old = with_table(|t| {+        let timer = t.iter_mut().find(|x| x.pid == pid && x.id == id && id != ITIMER_ID).ok_or(Errno::EINVAL)?;+        let old = read(timer, now);+        timer.overrun = 0;+        timer.interval = interval;+        timer.next = match (value, absolute) {+            (0, _) => None,+            (v, false) => Some(now + v),+            (v, true) if timer.clock == CLOCK_REALTIME => Some(v.saturating_sub(realtime_offset()).max(1)),+            (v, true) => Some(v.max(1)), // MONOTONIC/BOOTTIME: our own clock+        };+        Ok(old)+    })?;+    if value > 0 {+        poke();+    }+    Ok(old)+}++/// timer_gettime: (time left, interval) in microseconds.+pub fn gettime(pid: u64, id: i32) -> Result<(u64, u64), Errno> {+    let now = time::now_us();+    with_table(|t| t.iter().find(|x| x.pid == pid && x.id == id && id != ITIMER_ID).map(|x| read(x, now)).ok_or(Errno::EINVAL))+}++/// timer_getoverrun: periods missed at the last expiry.+pub fn getoverrun(pid: u64, id: i32) -> Result<u64, Errno> {+    with_table(|t| t.iter().find(|x| x.pid == pid && x.id == id && id != ITIMER_ID).map(|x| x.overrun.min(i32::MAX as u64)).ok_or(Errno::EINVAL))+}++/// timer_delete.+pub fn delete(pid: u64, id: i32) -> Result<(), Errno> {+    with_table(|t| {+        let i = t.iter().position(|x| x.pid == pid && x.id == id && id != ITIMER_ID).ok_or(Errno::EINVAL)?;+        t.remove(i);+        Ok(())+    })+}++/// execve: POSIX timers go, the alarm stays.+pub fn exec(pid: u64) {+    with_table(|t| t.retain(|x| x.pid != pid || x.id == ITIMER_ID));+}++/// The process ended: all its timers go.+pub fn process_exited(pid: u64) {+    with_table(|t| t.retain(|x| x.pid != pid));+}++/// How many timers exist (tests: nothing left behind).+pub fn count() -> usize {+    with_table(|t| t.len())+}
@@ -0,0 +1,129 @@+//! Stage 17, "coreutils batch 2": links and symlinks, owners, statfs and+//! statx, sysinfo/times/getrusage and the CPU clocks, socket names and+//! options, alarm/setitimer/POSIX timers, user and group ids, priority,+//! the host name, flock, preadv/pwritev and sendfile. The detailed checks+//! live in user/coretest.rs (exit code = failed checks); this file runs+//! each group and checks nothing is left behind -- threads, processes,+//! memory frames, file locks (flock ones too) and timers.+#![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, timer};++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();+    rustbelt::init_net(); // 127.0.0.1 for the socket checks+    test_main();+    rustbelt::hlt_loop();+}++fn run(args: &[&str]) -> ExitStatus {+    let mut spawn = SpawnArgs::kernel("coretest");+    spawn.args.extend(args.iter().map(|a| String::from(*a)));+    let p = process::spawn_path("/mnt/disk0/bin/coretest", 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+/// nothing is left behind.+fn run_without_leaks(group: &str) {+    assert_eq!(run(&[group]), ExitStatus::Exited(0), "coretest {} failed (warm-up)", group);+    let before = counts();+    assert_eq!(run(&[group]), ExitStatus::Exited(0), "coretest {} failed", group);+    assert_eq!(counts(), before, "coretest {} leaked something", group);+    assert_eq!(fs::lock::count(), 0, "coretest {} left file locks behind", group);+    assert_eq!(timer::count(), 0, "coretest {} left timers behind", group);+}++/// link/linkat, symlink/symlinkat on ramfs and ext2 (st_nlink, st_ino,+/// rename over a second link, directories moved); EPERM on FAT/exFAT,+/// EROFS on btrfs, EXDEV across file systems.+#[test_case]+fn links_and_symlinks() {+    run_without_leaks("links");+}++/// chown/fchown/lchown/fchownat: stored on ramfs and ext2; FAT says EPERM+/// unless nothing changes.+#[test_case]+fn owners() {+    run_without_leaks("owners");+}++/// statfs/fstatfs magic numbers and sizes on every file system, statx,+/// st_dev per mount.+#[test_case]+fn statfs_and_statx() {+    run_without_leaks("statfs");+}++/// sysinfo (99) next to our 502, times(), getrusage, CPU-time clocks,+/// clock_getres.+#[test_case]+fn sysinfo_times_and_cpu_clocks() {+    run_without_leaks("info");+}++/// getsockname/getpeername, SO_REUSEADDR & co., SO_TYPE/SO_ERROR,+/// shutdown(SHUT_WR) = end-of-file for the peer, socketpair says no.+#[test_case]+fn socket_names_options_and_shutdown() {+    run_without_leaks("sockets");+}++/// alarm, setitimer/getitimer (one-shot, periodic), POSIX timers+/// (SI_TIMER + si_value, TIMER_ABSTIME, SIGEV_NONE), gone with the process.+#[test_case]+fn alarms_and_timers() {+    run_without_leaks("timers");+}++/// setuid & co. (root drops privileges, the saved id), groups, files owned+/// by their creator, nice values, sethostname -> uname.+#[test_case]+fn ids_priority_and_hostname() {+    run_without_leaks("ids");+}++/// flock (per open file, blocking, released on the last close),+/// preadv/pwritev, sendfile, ppoll/pselect6 mask sizes and masks, and+/// getdents64 while deleting (rm -r).+#[test_case]+fn flock_vectors_sendfile_and_masks() {+    run_without_leaks("misc");+}++/// `--trace` knows the new calls by name.+#[test_case]+fn trace_names_new_syscalls() {+    assert_eq!(syscall::trace_name(86), "link");+    assert_eq!(syscall::trace_name(332), "statx");+    assert_eq!(syscall::trace_name(222), "timer_create");+    assert_eq!(syscall::trace_name(99), "sysinfo");+    assert_eq!(syscall::trace_name(502), "rb_sysinfo");+}++#[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.16.0",+        "Rustbelt 0.17.0",         "B:/ $ exit 3\n",     ] {         assert!(out.contains(expected), "missing {:?} in:\n{}", expected, out);
@@ -0,0 +1,885 @@+//! coretest -- stage 17, "coreutils batch 2": the system calls coreutils+//! and busybox use beyond what libctest (stage 16) covers. Run with one+//! group name; exit code = number of failed checks:+//!   links    link/linkat, symlink/symlinkat on ramfs and ext2; EPERM on+//!            FAT/exFAT, EROFS on btrfs; rename over a second hard link+//!   owners   chown/fchown/lchown/fchownat (stored, not enforced)+//!   statfs   statfs/fstatfs magic numbers and sizes, statx, st_dev+//!   info     sysinfo (99), times, getrusage, CPU clocks, clock_getres+//!   sockets  getsockname/getpeername, get/setsockopt, shutdown, socketpair+//!   timers   alarm, setitimer/getitimer, timer_create & co.+//!   ids      setuid & co., groups, priority, sethostname+//!   misc     flock, preadv/pwritev, sendfile, ppoll/pselect6 masks,+//!            getdents while deleting+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++extern crate alloc;++use alloc::format;+use alloc::vec::Vec;+use core::sync::atomic::{AtomicI64, AtomicU64, Ordering};+use ulib::*;++// Stage 17 syscall numbers (Linux's).+const SYS_SHUTDOWN: u64 = 48;+const SYS_GETSOCKNAME: u64 = 51;+const SYS_GETPEERNAME: u64 = 52;+const SYS_SOCKETPAIR: u64 = 53;+const SYS_GETSOCKOPT: u64 = 55;+const SYS_GETITIMER: u64 = 36;+const SYS_ALARM: u64 = 37;+const SYS_SETITIMER: u64 = 38;+const SYS_SENDFILE: u64 = 40;+const SYS_FLOCK: u64 = 73;+const SYS_LINK: u64 = 86;+const SYS_SYMLINK: u64 = 88;+const SYS_CHOWN: u64 = 92;+const SYS_FCHOWN: u64 = 93;+const SYS_LCHOWN: u64 = 94;+const SYS_GETRUSAGE: u64 = 98;+const SYS_SYSINFO: u64 = 99;+const SYS_TIMES: u64 = 100;+const SYS_GETGID: u64 = 104;+const SYS_SETUID: u64 = 105;+const SYS_SETGID: u64 = 106;+const SYS_GETEUID: u64 = 107;+const SYS_SETREUID: u64 = 113;+const SYS_GETGROUPS: u64 = 115;+const SYS_SETGROUPS: u64 = 116;+const SYS_SETRESUID: u64 = 117;+const SYS_GETRESUID: u64 = 118;+const SYS_GETRESGID: u64 = 120;+const SYS_STATFS: u64 = 137;+const SYS_FSTATFS: u64 = 138;+const SYS_GETPRIORITY: u64 = 140;+const SYS_SETPRIORITY: u64 = 141;+const SYS_SETHOSTNAME: u64 = 170;+const SYS_TIMER_CREATE: u64 = 222;+const SYS_TIMER_SETTIME: u64 = 223;+const SYS_TIMER_GETTIME: u64 = 224;+const SYS_TIMER_GETOVERRUN: u64 = 225;+const SYS_TIMER_DELETE: u64 = 226;+const SYS_CLOCK_GETRES: u64 = 229;+const SYS_FCHOWNAT: u64 = 260;+const SYS_LINKAT: u64 = 265;+const SYS_SYMLINKAT: u64 = 266;+const SYS_PSELECT6: u64 = 270;+const SYS_PREADV: u64 = 295;+const SYS_PWRITEV: u64 = 296;+const SYS_STATX: u64 = 332;++const AT_FDCWD: i64 = -100;+const AT_SYMLINK_NOFOLLOW: u64 = 0x100;+const AT_SYMLINK_FOLLOW: u64 = 0x400;+const AT_EMPTY_PATH: u64 = 0x1000;+const O_RDWR: u64 = 2;+const EROFS: i64 = 30;+const EMLINK_OR_EPERM: [i64; 2] = [EPERM, 31];+const EACCES: i64 = 13;+const ENOTCONN: i64 = 107;+const ECONNREFUSED: i64 = 111;+const ENOPROTOOPT: i64 = 92;+const EAFNOSUPPORT: i64 = 97;+const EPIPE: i64 = 32;+const SIGALRM: i32 = 14;++fn cpath(s: &str) -> Vec<u8> {+    let mut v = Vec::from(s.as_bytes());+    v.push(0);+    v+}++fn p2(nr: u64, a: &str, b: &str) -> i64 {+    let (a, b) = (cpath(a), cpath(b));+    syscall3(nr, a.as_ptr() as u64, b.as_ptr() as u64, 0)+}++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 read_file(path: &str) -> Vec<u8> {+    let fd = open(path, O_RDONLY);+    let mut out = Vec::new();+    if fd < 0 {+        return out;+    }+    let mut buf = [0u8; 512];+    loop {+        let n = read(fd, &mut buf);+        if n <= 0 {+            break;+        }+        out.extend_from_slice(&buf[..n as usize]);+    }+    close(fd);+    out+}++fn st(path: &str) -> Stat {+    let mut s = Stat::default();+    if stat(path, &mut s) < 0 {+        s.ino = 0;+        s.nlink = 0;+    }+    s+}++fn lst(path: &str) -> Stat {+    let mut s = Stat::default();+    lstat(path, &mut s);+    s+}++/// Burn CPU in user mode for `ms` (looking at the clock only now and then,+/// so it's nearly all user time).+fn spin_ms(ms: u64) {+    let end = now_us() + ms * 1000;+    let mut x = 0u64;+    while now_us() < end {+        for _ in 0..200_000 {+            x = core::hint::black_box(x.wrapping_mul(31).wrapping_add(7));+        }+    }+}++fn main() -> i64 {+    let group = arg(1).unwrap_or("");+    let mut c = Checks::new("coretest");+    match group {+        "links" => links(&mut c),+        "owners" => owners(&mut c),+        "statfs" => statfs(&mut c),+        "info" => info(&mut c),+        "sockets" => sockets(&mut c),+        "timers" => timers(&mut c),+        "ids" => ids(&mut c),+        "misc" => misc(&mut c),+        _ => c.check(false, "usage: coretest links|owners|statfs|info|sockets|timers|ids|misc"),+    }+    c.done()+}++// --- links ---------------------------------------------------------------------++fn links_on(c: &mut Checks, dir: &str) {+    let (a, b, l, d) = (format!("{}/a.txt", dir), format!("{}/b.txt", dir), format!("{}/l", dir), format!("{}/d", dir));+    unlink(&a);+    unlink(&b);+    unlink(&l);+    rmdir(&d);+    write_file(&a, b"hello");+    c.check(p2(SYS_LINK, &a, &b) == 0, &format!("{}: link a.txt b.txt", dir));+    let (sa, sb) = (st(&a), st(&b));+    c.check(sa.ino == sb.ino && sa.ino != 0, &format!("{}: both names, one inode number", dir));+    c.check(sa.nlink == 2 && sb.nlink == 2, &format!("{}: st_nlink is 2", dir));+    let fd = open(&b, O_WRONLY);+    lseek(fd, 0, 2);+    write(fd, b" world");+    close(fd);+    c.check(read_file(&a) == b"hello world", &format!("{}: a write through one name shows through the other", dir));+    c.check(p2(SYS_LINK, &a, &b) == -EEXIST, &format!("{}: link onto an existing name: EEXIST", dir));+    // rename onto another hard link of the same file: nothing happens (POSIX).+    c.check(rename(&a, &b) == 0 && st(&a).nlink == 2 && st(&b).nlink == 2, &format!("{}: rename a.txt over its own link b.txt: both stay", dir));+    c.check(unlink(&a) == 0 && st(&b).nlink == 1 && read_file(&b) == b"hello world", &format!("{}: unlink one name, the data stays", dir));+    // symlinks+    c.check(p2(SYS_SYMLINK, "b.txt", &l) == 0, &format!("{}: symlink b.txt l", dir));+    let mut buf = [0u8; 64];+    let n = readlink(&l, &mut buf);+    c.check(n == 5 && &buf[..5] == b"b.txt", &format!("{}: readlink gives the target as written", dir));+    c.check(lst(&l).is_symlink() && st(&l).is_file() && st(&l).ino == st(&b).ino, &format!("{}: lstat sees the link, stat the file", dir));+    c.check(read_file(&l) == b"hello world", &format!("{}: open follows the symlink", dir));+    c.check(p2(SYS_SYMLINK, "x", &l) == -EEXIST, &format!("{}: symlink onto an existing name: EEXIST", dir));+    // linkat: the symlink itself, or (AT_SYMLINK_FOLLOW) what it points to.+    let (l2, l3) = (format!("{}/l2", dir), format!("{}/l3", dir));+    let (cl, cl2, cl3) = (cpath(&l), cpath(&l2), cpath(&l3));+    let r = syscall5(SYS_LINKAT, AT_FDCWD as u64, cl.as_ptr() as u64, AT_FDCWD as u64, cl2.as_ptr() as u64, 0);+    c.check(r == 0 && lst(&l2).is_symlink(), &format!("{}: linkat without FOLLOW links the symlink itself", dir));+    let r = syscall5(SYS_LINKAT, AT_FDCWD as u64, cl.as_ptr() as u64, AT_FDCWD as u64, cl3.as_ptr() as u64, AT_SYMLINK_FOLLOW);+    c.check(r == 0 && lst(&l3).is_file() && st(&b).nlink == 2, &format!("{}: linkat AT_SYMLINK_FOLLOW links the file", dir));+    // symlinkat relative to a directory fd; a dangling one is fine.+    mkdir(&d);+    let dfd = open(&d, O_RDONLY | O_DIRECTORY);+    let (t, name) = (cpath("../nowhere"), cpath("dangling"));+    c.check(syscall3(SYS_SYMLINKAT, t.as_ptr() as u64, dfd as u64, name.as_ptr() as u64) == 0, &format!("{}: symlinkat(dirfd) makes a dangling link", dir));+    c.check(lst(&format!("{}/dangling", d)).is_symlink() && open(&format!("{}/dangling", d), O_RDONLY) == -ENOENT, &format!("{}: ...which lstat sees and open can't follow", dir));+    close(dfd);+    c.check(p2(SYS_LINK, &d, &format!("{}/dlink", dir)) == -EPERM, &format!("{}: hard link to a directory: EPERM", dir));+    // A directory moved into another one (native rename).+    let e = format!("{}/e", dir);+    rmdir(&e);+    mkdir(&e);+    c.check(rename(&d, &format!("{}/d", e)) == 0 && lst(&format!("{}/d/dangling", e)).is_symlink(), &format!("{}: rename moves a full directory", dir));+    // A long target (ext2 keeps short ones in the inode, long ones in a block).+    let long: alloc::string::String = core::iter::repeat('x').take(100).collect();+    let ll = format!("{}/long", dir);+    unlink(&ll);+    c.check(p2(SYS_SYMLINK, &long, &ll) == 0 && readlink(&ll, &mut [0u8; 200]) == 100, &format!("{}: a 100-byte symlink target", dir));+    // clean up+    for f in [&b, &l, &l2, &l3, &ll, &format!("{}/d/dangling", e)] {+        unlink(f);+    }+    rmdir(&format!("{}/d", e));+    rmdir(&e);+    c.check(access(&b) == -ENOENT && access(&e) == -ENOENT, &format!("{}: all cleaned up", dir));+}++fn links(c: &mut Checks) {+    mkdir("/tmp/lk");+    links_on(c, "/tmp/lk");+    rmdir("/tmp/lk");+    mkdir("/mnt/disk1/lk");+    links_on(c, "/mnt/disk1/lk");+    rmdir("/mnt/disk1/lk");+    // Across file systems: EXDEV.+    write_file("/tmp/x.txt", b"x");+    c.check(p2(SYS_LINK, "/tmp/x.txt", "/mnt/disk1/x.txt") == -EXDEV, "link across file systems: EXDEV");+    unlink("/tmp/x.txt");+    // FAT and exFAT have no links on disk; btrfs is mounted read-only.+    write_file("/mnt/disk0/lk.txt", b"x");+    c.check(EMLINK_OR_EPERM.contains(&-p2(SYS_LINK, "/mnt/disk0/lk.txt", "/mnt/disk0/lk2.txt")), "FAT: link is EPERM");+    c.check(p2(SYS_SYMLINK, "lk.txt", "/mnt/disk0/lk3") == -EPERM, "FAT: symlink is EPERM");+    unlink("/mnt/disk0/lk.txt");+    write_file("/mnt/disk2/lk.txt", b"x");+    c.check(p2(SYS_SYMLINK, "lk.txt", "/mnt/disk2/lk3") == -EPERM, "exFAT: symlink is EPERM");+    unlink("/mnt/disk2/lk.txt");+    c.check(p2(SYS_LINK, "/mnt/disk3/hello.txt", "/mnt/disk3/h2") == -EROFS, "btrfs: link is EROFS");+    c.check(p2(SYS_SYMLINK, "hello.txt", "/mnt/disk3/h3") == -EROFS, "btrfs: symlink is EROFS");+    let s = st("/mnt/disk3/hello.txt");+    c.check(s.nlink >= 1 && s.ino != 0, "btrfs: stat shows the real link count and inode");+}++// --- owners --------------------------------------------------------------------++fn chown(nr: u64, path: &str, uid: u32, gid: u32) -> i64 {+    let p = cpath(path);+    syscall3(nr, p.as_ptr() as u64, uid as u64, gid as u64)+}++fn owners(c: &mut Checks) {+    for dir in ["/tmp", "/mnt/disk1"] {+        let (f, l) = (format!("{}/own.txt", dir), format!("{}/own.lnk", dir));+        unlink(&f);+        unlink(&l);+        write_file(&f, b"x");+        p2(SYS_SYMLINK, "own.txt", &l);+        c.check(chown(SYS_CHOWN, &f, 1000, 100) == 0, &format!("{}: chown 1000:100", dir));+        let s = st(&f);+        c.check(s.uid == 1000 && s.gid == 100, &format!("{}: stat shows 1000:100", dir));+        c.check(chown(SYS_CHOWN, &f, u32::MAX, 50) == 0 && st(&f).uid == 1000 && st(&f).gid == 50, &format!("{}: -1 leaves the uid alone", dir));+        c.check(chown(SYS_LCHOWN, &l, 7, 7) == 0 && lst(&l).uid == 7 && st(&f).uid == 1000, &format!("{}: lchown changes the link, not the file", dir));+        c.check(chown(SYS_CHOWN, &l, 8, 8) == 0 && st(&f).uid == 8 && lst(&l).uid == 7, &format!("{}: chown through a link changes the file", dir));+        let fd = open(&f, O_RDONLY);+        c.check(syscall3(SYS_FCHOWN, fd as u64, 9, 9) == 0 && st(&f).uid == 9, &format!("{}: fchown", dir));+        let empty = cpath("");+        let r = syscall5(SYS_FCHOWNAT, fd as u64, empty.as_ptr() as u64, 0, 0, AT_EMPTY_PATH);+        c.check(r == 0 && st(&f).uid == 0, &format!("{}: fchownat(fd, \"\", AT_EMPTY_PATH)", dir));+        close(fd);+        let name = cpath("own.lnk");+        let dfd = open(dir, O_RDONLY | O_DIRECTORY);+        let r = syscall5(SYS_FCHOWNAT, dfd as u64, name.as_ptr() as u64, 3, 3, AT_SYMLINK_NOFOLLOW);+        c.check(r == 0 && lst(&l).uid == 3 && st(&f).uid == 0, &format!("{}: fchownat(dirfd, AT_SYMLINK_NOFOLLOW)", dir));+        close(dfd);+        unlink(&f);+        unlink(&l);+    }+    // Directories too.+    mkdir("/tmp/ownd");+    c.check(chown(SYS_CHOWN, "/tmp/ownd", 5, 6) == 0 && st("/tmp/ownd").uid == 5, "chown a directory");+    rmdir("/tmp/ownd");+    // FAT: no owners on disk. Root:root (what it shows) is fine, anything else EPERM.+    write_file("/mnt/disk0/own.txt", b"x");+    c.check(chown(SYS_CHOWN, "/mnt/disk0/own.txt", 0, 0) == 0, "FAT: chown to 0:0 (no change) succeeds");+    c.check(chown(SYS_CHOWN, "/mnt/disk0/own.txt", 1000, 1000) == -EPERM, "FAT: chown to 1000 is EPERM");+    unlink("/mnt/disk0/own.txt");+    write_file("/mnt/disk2/own.txt", b"x");+    c.check(chown(SYS_CHOWN, "/mnt/disk2/own.txt", 1000, u32::MAX) == -EPERM, "exFAT: chown to 1000 is EPERM");+    unlink("/mnt/disk2/own.txt");+    c.check(chown(SYS_CHOWN, "/mnt/disk3/hello.txt", 1, 1) == -EROFS, "btrfs: chown is EROFS");+    c.check(chown(SYS_CHOWN, "/tmp/missing", 1, 1) == -ENOENT, "chown of a missing file: ENOENT");+    // Pipes: nothing to own, accepted.+    let (r, w) = pipe().unwrap();+    c.check(syscall3(SYS_FCHOWN, r as u64, 1, 1) == 0, "fchown on a pipe: accepted");+    close(r);+    close(w);+}++// --- statfs / statx --------------------------------------------------------------++fn statfs_of(path: &str) -> (i64, [u64; 15]) {+    let mut buf = [0u64; 15];+    let p = cpath(path);+    (syscall3(SYS_STATFS, p.as_ptr() as u64, buf.as_mut_ptr() as u64, 0), buf)+}++fn statfs(c: &mut Checks) {+    let cases: [(&str, u64, &str); 6] = [+        ("/", 0x8584_58f6, "ramfs"),+        ("/tmp", 0x8584_58f6, "ramfs"),+        ("/mnt/disk0", 0x4d44, "FAT (MSDOS_SUPER_MAGIC)"),+        ("/mnt/disk1/", 0xEF53, "ext2"),+        ("/mnt/disk2", 0x2011_BAB0, "exFAT"),+        ("/mnt/disk3/hello.txt", 0x9123_683E, "btrfs"),+    ];+    for (path, magic, what) in cases {+        let (r, b) = statfs_of(path);+        c.check(r == 0 && b[0] == magic, &format!("statfs {}: f_type = {}", path, what));+        if path.starts_with("/mnt") {+            c.check(b[1] >= 512 && b[2] > 0 && b[3] <= b[2] && b[4] <= b[3], &format!("statfs {}: sizes make sense (bsize {}, {} blocks, {} free)", path, b[1], b[2], b[3]));+            c.check(b[8] >= 255, &format!("statfs {}: f_namelen {}", path, b[8]));+        }+    }+    let (_, b) = statfs_of("/mnt/disk3");+    c.check(b[10] & 1 == 1, "btrfs: ST_RDONLY in f_flags");+    let (_, b) = statfs_of("/mnt/disk1");+    c.check(b[10] & 1 == 0 && b[5] > 0 && b[6] <= b[5], "ext2: writable, counts inodes");+    // Writing a file uses up free blocks on ext2.+    let (_, before) = statfs_of("/mnt/disk1");+    write_file("/mnt/disk1/sf.bin", &[7u8; 64 * 1024]);+    let (_, after) = statfs_of("/mnt/disk1");+    c.check(after[3] < before[3], "ext2: a 64 KiB file lowers f_bfree");+    unlink("/mnt/disk1/sf.bin");+    c.check(statfs_of("/nope").0 == -ENOENT, "statfs of a missing path: ENOENT");+    let fd = open("/mnt/disk0", O_RDONLY | O_DIRECTORY);+    let mut b = [0u64; 15];+    c.check(syscall3(SYS_FSTATFS, fd as u64, b.as_mut_ptr() as u64, 0) == 0 && b[0] == 0x4d44, "fstatfs on a FAT directory fd");+    close(fd);+    let (r, w) = pipe().unwrap();+    c.check(syscall3(SYS_FSTATFS, r as u64, b.as_mut_ptr() as u64, 0) == 0 && b[0] == 0x5049_5045, "fstatfs on a pipe: PIPEFS_MAGIC");+    close(r);+    close(w);+    // st_dev tells file systems apart.+    c.check(st("/tmp").dev != st("/mnt/disk1").dev && st("/mnt/disk1/").dev == st("/mnt/disk1/lost+found").dev, "st_dev: one per file system");+    // statx+    write_file("/tmp/sx.txt", b"twelve bytes");+    let mut sx = [0u8; 256];+    let p = cpath("/tmp/sx.txt");+    let r = syscall5(SYS_STATX, AT_FDCWD as u64, p.as_ptr() as u64, 0, 0x7ff, sx.as_mut_ptr() as u64);+    let u32at = |b: &[u8], o: usize| u32::from_le_bytes(b[o..o + 4].try_into().unwrap());+    let u64at = |b: &[u8], o: usize| u64::from_le_bytes(b[o..o + 8].try_into().unwrap());+    let s = st("/tmp/sx.txt");+    c.check(r == 0 && u32at(&sx, 0) & 0x7ff == 0x7ff, "statx: basic fields in stx_mask");+    c.check(u64at(&sx, 40) == 12 && u64at(&sx, 32) == s.ino && u32at(&sx, 16) == 1, "statx: size, inode number, nlink match stat");+    c.check(u32::from(u16::from_le_bytes([sx[28], sx[29]])) == s.mode && u64at(&sx, 112) as i64 == s.mtime[0], "statx: mode and mtime match stat");+    c.check(u32at(&sx, 140) as u64 == s.dev, "statx: device minor = st_dev");+    let fd = open("/tmp/sx.txt", O_RDONLY);+    let empty = cpath("");+    let r = syscall5(SYS_STATX, fd as u64, empty.as_ptr() as u64, AT_EMPTY_PATH, 0x7ff, sx.as_mut_ptr() as u64);+    c.check(r == 0 && u64at(&sx, 40) == 12, "statx(fd, \"\", AT_EMPTY_PATH)");+    close(fd);+    let r = syscall5(SYS_STATX, AT_FDCWD as u64, p.as_ptr() as u64, 0x4, 0x7ff, sx.as_mut_ptr() as u64);+    c.check(r == -EINVAL, "statx with an unknown flag: EINVAL");+    p2(SYS_SYMLINK, "sx.txt", "/tmp/sx.lnk");+    let pl = cpath("/tmp/sx.lnk");+    syscall5(SYS_STATX, AT_FDCWD as u64, pl.as_ptr() as u64, AT_SYMLINK_NOFOLLOW, 0x7ff, sx.as_mut_ptr() as u64);+    c.check(u32::from(u16::from_le_bytes([sx[28], sx[29]])) & S_IFMT == S_IFLNK, "statx AT_SYMLINK_NOFOLLOW: the link itself");+    unlink("/tmp/sx.lnk");+    unlink("/tmp/sx.txt");+}++// --- info: sysinfo, times, getrusage, clocks ---------------------------------------++fn clock_ns(clock: u64) -> (i64, u64) {+    let mut ts = [0u64; 2];+    let r = syscall3(SYS_CLOCK_GETTIME, clock, ts.as_mut_ptr() as u64, 0);+    (r, ts[0] * 1_000_000_000 + ts[1])+}++fn info(c: &mut Checks) {+    let mut si = [0u64; 14];+    c.check(syscall3(SYS_SYSINFO, si.as_mut_ptr() as u64, 0, 0) == 0, "sysinfo (99): 0");+    let procs = si[10] & 0xffff;+    let mem_unit = si[13] & 0xffff_ffff;+    c.check(si[0] > 0 && si[4] > si[5] && si[5] > 0, "sysinfo: uptime, totalram > freeram > 0");+    c.check(procs >= 1 && mem_unit == 1, "sysinfo: procs >= 1, mem_unit 1");+    c.check(sysinfo().cpus == 4, "Rustbelt's own sysinfo (502) still works");+    // CPU time: spin 300 ms, it shows up everywhere.+    let mut tms = [0u64; 4];+    let t0 = syscall3(SYS_TIMES, tms.as_mut_ptr() as u64, 0, 0);+    let (_, p0) = clock_ns(2);+    spin_ms(300);+    let t1 = syscall3(SYS_TIMES, tms.as_mut_ptr() as u64, 0, 0);+    c.check(t1 - t0 >= 25 && t1 - t0 <= 60, &format!("times: returns ticks (100/s): {} after 300 ms", t1 - t0));+    c.check(tms[0] + tms[1] >= 15, &format!("times: utime+stime {} ticks after spinning 300 ms", tms[0] + tms[1]));+    c.check(tms[0] >= tms[1], "times: spinning is mostly user time");+    let (r, p1) = clock_ns(2);+    c.check(r == 0 && p1 - p0 >= 150_000_000, "CLOCK_PROCESS_CPUTIME_ID advanced >= 150 ms");+    let (r, t) = clock_ns(3);+    c.check(r == 0 && t >= 150_000_000, "CLOCK_THREAD_CPUTIME_ID too");+    let mut ru = [0u64; 18];+    c.check(syscall3(SYS_GETRUSAGE, 0, ru.as_mut_ptr() as u64, 0) == 0, "getrusage(RUSAGE_SELF)");+    c.check(ru[0] * 1_000_000 + ru[1] >= 150_000 && ru[4] > 0, "getrusage: ru_utime >= 150 ms, ru_maxrss > 0");+    // Children's times count once they're reaped.+    let pid = fork();+    if pid == 0 {+        spin_ms(200);+        exit(0);+    }+    wait4(pid, 0).ok();+    syscall3(SYS_TIMES, tms.as_mut_ptr() as u64, 0, 0);+    c.check(tms[2] + tms[3] >= 10, &format!("times: the reaped child's {} ticks are in cutime+cstime", tms[2] + tms[3]));+    syscall3(SYS_GETRUSAGE, (-1i64) as u64, ru.as_mut_ptr() as u64, 0);+    c.check(ru[0] * 1_000_000 + ru[1] + ru[2] * 1_000_000 + ru[3] >= 100_000, "getrusage(RUSAGE_CHILDREN) has it too");+    c.check(syscall3(SYS_GETRUSAGE, 5, ru.as_mut_ptr() as u64, 0) == -EINVAL, "getrusage(5): EINVAL");+    let mut res = [0u64; 2];+    c.check(syscall3(SYS_CLOCK_GETRES, 1, res.as_mut_ptr() as u64, 0) == 0 && res == [0, 1000], "clock_getres(MONOTONIC): 1 µs");+    c.check(syscall3(SYS_CLOCK_GETRES, 2, res.as_mut_ptr() as u64, 0) == 0 && res == [0, 10_000_000], "clock_getres(PROCESS_CPUTIME): 10 ms");+    c.check(syscall3(SYS_CLOCK_GETRES, 0, 0, 0) == 0, "clock_getres with a NULL pointer: 0");+    c.check(syscall3(SYS_CLOCK_GETRES, 42, res.as_mut_ptr() as u64, 0) == -EINVAL, "clock_getres(42): EINVAL");+}++// --- sockets -----------------------------------------------------------------------++fn getsockopt_int(fd: i64, level: u64, opt: u64) -> (i64, u32) {+    let mut v = 0u32;+    let mut len = 4u32;+    let r = syscall5(SYS_GETSOCKOPT, fd as u64, level, opt, &mut v as *mut u32 as u64, &mut len as *mut u32 as u64);+    (r, v)+}++fn setsockopt_int(fd: i64, level: u64, opt: u64, v: u32) -> i64 {+    syscall5(SYS_SETSOCKOPT, fd as u64, level, opt, &v as *const u32 as u64, 4)+}++fn sockname(nr: u64, fd: i64) -> (i64, SockAddrIn) {+    let mut sa = SockAddrIn::default();+    let mut len = 16u32;+    let r = syscall3(nr, fd as u64, &mut sa as *mut _ as u64, &mut len as *mut u32 as u64);+    (r, sa)+}++fn sockets(c: &mut Checks) {+    sigaction_raw(13, 1, 0, 0); // SIGPIPE: SIG_IGN, so a write after shutdown is just EPIPE+    let srv = socket(SOCK_STREAM, 0);+    c.check(setsockopt_int(srv, 1, 2, 1) == 0, "setsockopt SO_REUSEADDR: 0 (was ENOPROTOOPT)");+    c.check(getsockopt_int(srv, 1, 2) == (0, 1), "getsockopt SO_REUSEADDR reads it back");+    c.check(setsockopt_int(srv, 1, 15, 1) == 0 && setsockopt_int(srv, 1, 9, 1) == 0, "SO_REUSEPORT, SO_KEEPALIVE: 0");+    c.check(setsockopt_int(srv, 6, 1, 1) == 0, "TCP_NODELAY: 0");+    c.check(setsockopt_int(srv, 1, 99, 1) == -ENOPROTOOPT, "an unknown option: ENOPROTOOPT");+    c.check(bind(srv, 7091) == 0 && listen(srv, 4) == 0, "bind 7091 + listen");+    let (r, sa) = sockname(SYS_GETSOCKNAME, srv);+    c.check(r == 0 && sa.port() == 7091 && sa.family == 2, "getsockname on the listener: port 7091");+    c.check(getsockopt_int(srv, 1, 30) == (0, 1), "SO_ACCEPTCONN: listening");+    c.check(sockname(SYS_GETPEERNAME, srv).0 == -ENOTCONN, "getpeername on a listener: ENOTCONN");+    let cl = socket(SOCK_STREAM, 0);+    c.check(syscall3(SYS_SHUTDOWN, cl as u64, 1, 0) == -ENOTCONN, "shutdown before connecting: ENOTCONN");+    c.check(connect(cl, Ip([127, 0, 0, 1]), 7091) == 0, "connect to 127.0.0.1:7091");+    let (conn, _) = accept(srv);+    c.check(conn >= 0, "accept");+    let (r, me) = sockname(SYS_GETSOCKNAME, cl);+    let (r2, peer) = sockname(SYS_GETPEERNAME, conn);+    c.check(r == 0 && r2 == 0 && me.addr == [127, 0, 0, 1] && me.port() == peer.port() && me.port() != 0, "client's getsockname = server's getpeername");+    let (_, p) = sockname(SYS_GETPEERNAME, cl);+    c.check(p.port() == 7091 && p.addr == [127, 0, 0, 1], "client's getpeername: 127.0.0.1:7091");+    c.check(getsockopt_int(cl, 1, 3) == (0, 1), "SO_TYPE: SOCK_STREAM");+    c.check(getsockopt_int(cl, 1, 4) == (0, 0), "SO_ERROR: 0");+    let (r, rcv) = getsockopt_int(cl, 1, 8);+    c.check(r == 0 && rcv > 0, "SO_RCVBUF > 0");+    // shutdown(SHUT_WR): the other side reads end-of-file, but can still answer.+    write(cl, b"ping");+    c.check(syscall3(SYS_SHUTDOWN, cl as u64, 1, 0) == 0, "shutdown(SHUT_WR)");+    let mut buf = [0u8; 16];+    let mut got = Vec::new();+    loop {+        let n = read(conn, &mut buf);+        if n <= 0 {+            c.check(n == 0, "server reads end-of-file after the data");+            break;+        }+        got.extend_from_slice(&buf[..n as usize]);+    }+    c.check(got == b"ping", "server got \"ping\"");+    c.check(write(cl, b"x") == -EPIPE, "client write after SHUT_WR: EPIPE");+    write(conn, b"pong");+    let n = read(cl, &mut buf);+    c.check(n == 4 && &buf[..4] == b"pong", "client still reads the answer");+    close(conn);+    close(cl);+    close(srv);+    // UDP+    let u = socket(SOCK_DGRAM, 0);+    c.check(getsockopt_int(u, 1, 3) == (0, 2), "UDP: SO_TYPE is SOCK_DGRAM");+    bind(u, 7092);+    c.check(sockname(SYS_GETSOCKNAME, u).1.port() == 7092, "UDP getsockname: 7092");+    close(u);+    // Nonblocking connect to a closed port: SO_ERROR says why.+    let nb = socket(SOCK_STREAM, 0);+    set_nonblocking(nb, true);+    let r = connect(nb, Ip([127, 0, 0, 1]), 7093);+    c.check(r == -EINPROGRESS || r == -ECONNREFUSED, "nonblocking connect to a closed port: EINPROGRESS");+    if r == -EINPROGRESS {+        let mut fds = [PollFd { fd: nb as i32, events: POLLOUT, revents: 0 }];+        poll(&mut fds, 1000);+        c.check(getsockopt_int(nb, 1, 4) == (0, ECONNREFUSED as u32), "SO_ERROR: ECONNREFUSED");+        c.check(getsockopt_int(nb, 1, 4) == (0, 0), "SO_ERROR is cleared once read");+    }+    close(nb);+    let mut sv = [0i32; 2];+    c.check(syscall5(SYS_SOCKETPAIR, 1, 1, 0, sv.as_mut_ptr() as u64, 0) == -EAFNOSUPPORT, "socketpair(AF_UNIX): EAFNOSUPPORT (no AF_UNIX yet)");+    c.check(syscall3(SYS_SHUTDOWN, 0, 1, 0) == -88, "shutdown on a non-socket: ENOTSOCK");+}++// --- timers --------------------------------------------------------------------------++static ALRM: AtomicI64 = AtomicI64::new(0);+static TCODE: AtomicI64 = AtomicI64::new(0);+static TVAL: AtomicU64 = AtomicU64::new(0);++extern "C" fn on_alarm(_sig: i32) {+    ALRM.fetch_add(1, Ordering::SeqCst);+}++extern "C" fn on_timer(_sig: i32, info: *const SigInfo, _ctx: *mut u8) {+    unsafe {+        TCODE.store((*info).code as i64, Ordering::SeqCst);+        TVAL.store(*((info as *const u8).add(24) as *const u64), Ordering::SeqCst);+    }+    ALRM.fetch_add(1, Ordering::SeqCst);+}++fn wait_for(n: i64, ms: u64) -> bool {+    let end = now_us() + ms * 1000;+    while ALRM.load(Ordering::SeqCst) < n && now_us() < end {+        sleep_ms(5);+    }+    ALRM.load(Ordering::SeqCst) >= n+}++/// Sleep the whole `ms`: a signal cuts nanosleep short (EINTR), so go back+/// to sleep until the time is really up.+fn sleep_through(ms: u64) {+    let end = now_us() + ms * 1000;+    loop {+        let now = now_us();+        if now >= end {+            return;+        }+        nanosleep_us(end - now);+    }+}++fn itimer(value_us: u64, interval_us: u64) -> [u64; 4] {+    let new = [interval_us / 1_000_000, interval_us % 1_000_000, value_us / 1_000_000, value_us % 1_000_000];+    let mut old = [0u64; 4];+    syscall3(SYS_SETITIMER, 0, new.as_ptr() as u64, old.as_mut_ptr() as u64);+    old+}++fn timers(c: &mut Checks) {+    signal(SIGALRM, on_alarm);+    ALRM.store(0, Ordering::SeqCst);+    c.check(syscall3(SYS_ALARM, 5, 0, 0) == 0, "alarm(5): no alarm before");+    c.check(syscall3(SYS_ALARM, 0, 0, 0) == 5, "alarm(0) cancels it: 5 seconds were left");+    // One-shot setitimer.+    let t0 = now_us();+    itimer(100_000, 0);+    let mut cur = [0u64; 4];+    syscall3(SYS_GETITIMER, 0, cur.as_mut_ptr() as u64, 0);+    c.check(cur[2] == 0 && cur[3] > 0 && cur[3] <= 100_000, "getitimer: time left <= 100 ms");+    c.check(wait_for(1, 1000), "setitimer 100 ms: SIGALRM arrives");+    let dt = now_us() - t0;+    c.check(dt >= 100_000 && dt < 400_000, &format!("...after {} ms (never early)", dt / 1000));+    sleep_through(150);+    c.check(ALRM.load(Ordering::SeqCst) == 1, "one-shot: only once");+    // pause() wakes up for it.+    syscall3(SYS_ALARM, 1, 0, 0);+    let t0 = now_us();+    pause();+    c.check(ALRM.load(Ordering::SeqCst) == 2 && now_us() - t0 >= 900_000, "alarm(1) + pause: woken after a second");+    // Periodic.+    ALRM.store(0, Ordering::SeqCst);+    itimer(60_000, 60_000);+    sleep_through(500);+    let old = itimer(0, 0);+    let n = ALRM.load(Ordering::SeqCst);+    c.check((3..=9).contains(&n), &format!("periodic 60 ms timer: {} signals in 500 ms", n));+    c.check(old[0] == 0 && old[1] == 60_000, "setitimer returns the old interval");+    sleep_through(150);+    c.check(ALRM.load(Ordering::SeqCst) == n, "disarmed: no more signals");+    c.check(syscall3(SYS_SETITIMER, 1, cur.as_ptr() as u64, 0) == -EINVAL, "ITIMER_VIRTUAL: EINVAL (not supported)");+    // POSIX timers.+    sigaction_info(SIGUSR1, on_timer, 0);+    ALRM.store(0, Ordering::SeqCst);+    let mut sev = [0u64; 8];+    sev[0] = 42; // sigval+    sev[1] = SIGUSR1 as u64; // signo, notify = SIGEV_SIGNAL (0)+    let mut id = -1i32;+    let r = syscall3(SYS_TIMER_CREATE, 1, sev.as_ptr() as u64, &mut id as *mut i32 as u64);+    c.check(r == 0 && id >= 0, "timer_create(CLOCK_MONOTONIC, SIGUSR1)");+    let spec = [0u64, 0, 0, 80_000_000]; // interval 0, value 80 ms+    c.check(syscall6(SYS_TIMER_SETTIME, id as u64, 0, spec.as_ptr() as u64, 0, 0, 0) == 0, "timer_settime 80 ms");+    let mut now = [0u64; 4];+    syscall3(SYS_TIMER_GETTIME, id as u64, now.as_mut_ptr() as u64, 0);+    c.check(now[2] == 0 && now[3] > 0 && now[3] <= 80_000_000, "timer_gettime: time left <= 80 ms");+    c.check(wait_for(1, 1000), "the timer's signal arrives");+    c.check(TCODE.load(Ordering::SeqCst) == -2 && TVAL.load(Ordering::SeqCst) == 42, "si_code SI_TIMER, si_value 42");+    c.check(syscall3(SYS_TIMER_GETOVERRUN, id as u64, 0, 0) == 0, "timer_getoverrun: 0");+    // Absolute time on CLOCK_MONOTONIC, periodic.+    ALRM.store(0, Ordering::SeqCst);+    let (s, ns) = clock_gettime(1);+    let at = s * 1_000_000_000 + ns + 50_000_000;+    let spec = [0u64, 50_000_000, at / 1_000_000_000, at % 1_000_000_000];+    c.check(syscall6(SYS_TIMER_SETTIME, id as u64, 1, spec.as_ptr() as u64, 0, 0, 0) == 0, "timer_settime TIMER_ABSTIME, every 50 ms");+    c.check(wait_for(3, 1000), "...fires repeatedly");+    let zero = [0u64; 4];+    let mut old = [0u64; 4];+    syscall6(SYS_TIMER_SETTIME, id as u64, 0, zero.as_ptr() as u64, old.as_mut_ptr() as u64, 0, 0);+    c.check(old[1] == 50_000_000, "disarming returns the old interval");+    c.check(syscall3(SYS_TIMER_DELETE, id as u64, 0, 0) == 0, "timer_delete");+    c.check(syscall3(SYS_TIMER_DELETE, id as u64, 0, 0) == -EINVAL, "timer_delete again: EINVAL");+    // SIGEV_NONE: nothing is sent, the time just runs down.+    sev[1] = 1u64 << 32; // notify = SIGEV_NONE+    syscall3(SYS_TIMER_CREATE, 0, sev.as_ptr() as u64, &mut id as *mut i32 as u64);+    let spec = [0u64, 0, 10, 0];+    syscall6(SYS_TIMER_SETTIME, id as u64, 0, spec.as_ptr() as u64, 0, 0, 0);+    sleep_through(50);+    syscall3(SYS_TIMER_GETTIME, id as u64, now.as_mut_ptr() as u64, 0);+    c.check(now[2] == 9 || (now[2] < 10 && now[2] >= 9), "SIGEV_NONE on CLOCK_REALTIME: the time runs down");+    syscall3(SYS_TIMER_DELETE, id as u64, 0, 0);+    c.check(syscall3(SYS_TIMER_CREATE, 2, 0, &mut id as *mut i32 as u64) == -EINVAL, "timer_create on a CPU clock: EINVAL");+    sev[1] = (99u64 << 32) | SIGUSR1 as u64;+    c.check(syscall3(SYS_TIMER_CREATE, 1, sev.as_ptr() as u64, &mut id as *mut i32 as u64) == -EINVAL, "an unknown sigev_notify: EINVAL");+    // A child's timers don't survive it.+    let pid = fork();+    if pid == 0 {+        syscall3(SYS_ALARM, 100, 0, 0);+        let mut id = 0i32;+        syscall3(SYS_TIMER_CREATE, 1, 0, &mut id as *mut i32 as u64);+        exit(0);+    }+    wait4(pid, 0).ok();+    c.check(syscall3(SYS_ALARM, 0, 0, 0) == 0, "the child's alarm isn't ours");+}++// --- ids, priority, hostname --------------------------------------------------------------++fn res(nr: u64) -> [u32; 3] {+    let mut v = [0u32; 3];+    syscall3(nr, &mut v[0] as *mut u32 as u64, &mut v[1] as *mut u32 as u64, &mut v[2] as *mut u32 as u64);+    v+}++fn in_child(f: impl FnOnce() -> i64) -> i64 {+    let pid = fork();+    if pid == 0 {+        exit(f());+    }+    match wait4(pid, 0) {+        Ok((_, s)) => wexitstatus(s) as i64,+        Err(e) => e,+    }+}++fn ids(c: &mut Checks) {+    c.check(syscall3(SYS_GETUID, 0, 0, 0) == 0 && syscall3(SYS_GETEUID, 0, 0, 0) == 0 && syscall3(SYS_GETGID, 0, 0, 0) == 0, "we start as root");+    c.check(res(SYS_GETRESUID) == [0, 0, 0], "getresuid: 0 0 0");+    // Root drops to 1000 for good.+    let r = in_child(|| {+        let mut fails = 0;+        let groups = [5u32, 6];+        fails += (syscall3(SYS_SETGROUPS, 2, groups.as_ptr() as u64, 0) != 0) as i64;+        let mut got = [0u32; 8];+        fails += (syscall3(SYS_GETGROUPS, 0, 0, 0) != 2) as i64;+        fails += (syscall3(SYS_GETGROUPS, 8, got.as_mut_ptr() as u64, 0) != 2 || got[..2] != groups) as i64;+        fails += (syscall3(SYS_GETGROUPS, 1, got.as_mut_ptr() as u64, 0) != -EINVAL) as i64;+        fails += (syscall3(SYS_SETGID, 100, 0, 0) != 0 || res(SYS_GETRESGID) != [100, 100, 100]) as i64;+        fails += (syscall3(SYS_SETUID, 1000, 0, 0) != 0 || res(SYS_GETRESUID) != [1000, 1000, 1000]) as i64;+        fails += (syscall3(SYS_SETUID, 0, 0, 0) != -EPERM) as i64;+        fails += (syscall3(SYS_SETGROUPS, 0, 0, 0) != -EPERM) as i64;+        let name = b"evil";+        fails += (syscall3(SYS_SETHOSTNAME, name.as_ptr() as u64, 4, 0) != -EPERM) as i64;+        // A file it makes is its own.+        unlink("/tmp/u1000.txt");+        write_file("/tmp/u1000.txt", b"x");+        let s = st("/tmp/u1000.txt");+        fails += (s.uid != 1000 || s.gid != 100) as i64;+        fails += (syscall3(SYS_SETPRIORITY, 0, 0, 5) != 0) as i64;+        fails += (syscall3(SYS_SETPRIORITY, 0, 0, 0) != -EACCES) as i64; // only root raises priority+        fails+    });+    c.check(r == 0, "child: setgroups/getgroups, setgid, setuid(1000) for good, then EPERM; its files are 1000:100; nice only up");+    c.check(st("/tmp/u1000.txt").uid == 1000, "the file stays 1000's");+    unlink("/tmp/u1000.txt");+    // setresuid keeps root in the saved id: it can come back.+    let r = in_child(|| {+        let mut fails = 0;+        fails += (syscall3(SYS_SETRESUID, 1000, 1000, 0) != 0) as i64;+        fails += (syscall3(SYS_GETEUID, 0, 0, 0) != 1000) as i64;+        fails += (syscall3(SYS_SETRESUID, 2000, u64::MAX, u64::MAX) != -EPERM) as i64;+        fails += (syscall3(SYS_SETUID, 0, 0, 0) != 0 || res(SYS_GETRESUID) != [1000, 0, 0]) as i64;+        // setreuid(-1, 1000): the effective id goes back to the real one, so+        // (Linux's rule) the saved id stays 0...+        fails += (syscall3(SYS_SETREUID, u32::MAX as u64, 1000, 0) != 0 || res(SYS_GETRESUID) != [1000, 1000, 0]) as i64;+        // ...while setting the real id copies the effective one into it.+        fails += (syscall3(SYS_SETUID, 0, 0, 0) != 0 || syscall3(SYS_SETREUID, 1000, 1000, 0) != 0 || res(SYS_GETRESUID) != [1000, 1000, 1000]) as i64;+        fails += (syscall3(SYS_SETUID, 0, 0, 0) != -EPERM) as i64; // no way back now+        fails+    });+    c.check(r == 0, "child: setresuid(1000,1000,0), back to euid 0 via the saved id, setreuid");+    c.check(res(SYS_GETRESUID) == [0, 0, 0], "the parent is still root");+    // Priority.+    c.check(syscall3(SYS_GETPRIORITY, 0, 0, 0) == 20, "getpriority: 20 (nice 0; the raw call returns 20 - nice)");+    c.check(syscall3(SYS_SETPRIORITY, 0, 0, 7) == 0 && syscall3(SYS_GETPRIORITY, 0, 0, 0) == 13, "setpriority nice 7 -> 13");+    c.check(in_child(|| syscall3(SYS_GETPRIORITY, 0, 0, 0)) == 13, "a child inherits it");+    c.check(syscall3(SYS_SETPRIORITY, 0, 0, (-25i64) as u64) == 0 && syscall3(SYS_GETPRIORITY, 0, 0, 0) == 40, "nice -25 is clamped to -20 (root may)");+    syscall3(SYS_SETPRIORITY, 0, 0, 0);+    c.check(syscall3(SYS_GETPRIORITY, 0, 99999, 0) == -ESRCH, "getpriority of no such process: ESRCH");+    c.check(syscall3(SYS_GETPRIORITY, 7, 0, 0) == -EINVAL, "getpriority with a bad `which`: EINVAL");+    // Host name.+    let name = b"rustbelt-test";+    c.check(syscall3(SYS_SETHOSTNAME, name.as_ptr() as u64, name.len() as u64, 0) == 0, "sethostname rustbelt-test");+    let mut u = [0u8; 6 * 65];+    c.check(uname(&mut u)[1] == "rustbelt-test", "uname shows it");+    c.check(syscall3(SYS_SETHOSTNAME, name.as_ptr() as u64, 65, 0) == -EINVAL, "a 65-byte name: EINVAL");+    let q = b"qemu";+    syscall3(SYS_SETHOSTNAME, q.as_ptr() as u64, 4, 0);+    c.check(uname(&mut u)[1] == "qemu", "back to qemu");+}++// --- misc: flock, preadv/pwritev, sendfile, sigmasks, getdents ------------------------------++static USR1: AtomicI64 = AtomicI64::new(0);+extern "C" fn on_usr1(_sig: i32) {+    USR1.fetch_add(1, Ordering::SeqCst);+}++fn misc(c: &mut Checks) {+    // flock: locks belong to the open file (not the process, not the fd).+    write_file("/tmp/fl.txt", b"x");+    let a = open("/tmp/fl.txt", O_RDWR);+    let b = open("/tmp/fl.txt", O_RDWR);+    c.check(syscall3(SYS_FLOCK, a as u64, 2, 0) == 0, "flock LOCK_EX");+    c.check(syscall3(SYS_FLOCK, b as u64, 2 | 4, 0) == -EAGAIN, "another open file: LOCK_EX|LOCK_NB is EAGAIN");+    c.check(syscall3(SYS_FLOCK, b as u64, 1 | 4, 0) == -EAGAIN, "...and LOCK_SH|LOCK_NB too");+    let a2 = dup(a);+    c.check(syscall3(SYS_FLOCK, a2 as u64, 2 | 4, 0) == 0, "a dup of the lock holder may lock again");+    c.check(syscall3(SYS_FLOCK, a as u64, 1, 0) == 0 && syscall3(SYS_FLOCK, b as u64, 1 | 4, 0) == 0, "converted to shared: others may share it");+    c.check(syscall3(SYS_FLOCK, b as u64, 8, 0) == 0 && syscall3(SYS_FLOCK, a as u64, 2, 0) == 0, "LOCK_UN, then LOCK_EX again");+    // A child blocks until the holder lets go.+    let pid = fork();+    if pid == 0 {+        // Our copies of a, a2 and b share the parent's open files: close+        // them, or the lock could never go away (like on Linux).+        close(a);+        close(a2);+        close(b);+        let fd = open("/tmp/fl.txt", O_RDWR);+        let t0 = now_us();+        let r = syscall3(SYS_FLOCK, fd as u64, 2, 0);+        exit(if r == 0 && now_us() - t0 >= 80_000 { 0 } else { 1 });+    }+    sleep_ms(150);+    close(a); // a2 still holds it+    sleep_ms(30);+    c.check(try_wait(pid) == -EAGAIN, "closing one of two fds keeps the lock");+    close(a2); // last fd: released+    let r = wait4(pid, 0);+    c.check(matches!(r, Ok((_, s)) if wexitstatus(s) == 0), "a blocked flock waits, and gets it when the last fd closes");+    c.check(syscall3(SYS_FLOCK, b as u64, 2 | 4, 0) == 0, "the child's lock went with it");+    c.check(syscall3(SYS_FLOCK, b as u64, 3, 0) == -EINVAL, "flock with a bad op: EINVAL");+    close(b);+    unlink("/tmp/fl.txt");+    // preadv / pwritev+    write_file("/tmp/pv.txt", b"0123456789");+    let fd = open("/tmp/pv.txt", O_RDWR);+    let (mut x, mut y) = ([0u8; 3], [0u8; 4]);+    let iov = [x.as_mut_ptr() as u64, 3, y.as_mut_ptr() as u64, 4];+    c.check(syscall5(SYS_PREADV, fd as u64, iov.as_ptr() as u64, 2, 2, 0) == 7 && &x == b"234" && &y == b"5678", "preadv at offset 2 into two buffers");+    c.check(lseek(fd, 0, 1) == 0, "...without moving the file offset");+    let (p, q) = (b"ab", b"cd");+    let iov = [p.as_ptr() as u64, 2, q.as_ptr() as u64, 2];+    c.check(syscall5(SYS_PWRITEV, fd as u64, iov.as_ptr() as u64, 2, 8, 0) == 4, "pwritev at 8 (past the end)");+    close(fd);+    c.check(read_file("/tmp/pv.txt") == b"01234567abcd", "the file grew to 0123456789 -> 01234567abcd");+    // sendfile+    let src = open("/tmp/pv.txt", O_RDONLY);+    let dst = open("/tmp/sf.txt", O_WRONLY | O_CREAT | O_TRUNC);+    c.check(syscall5(SYS_SENDFILE, dst as u64, src as u64, 0, 100, 0) == 12, "sendfile file -> file: 12 bytes");+    c.check(lseek(src, 0, 1) == 12, "...moving the input offset");+    let mut off = 4u64;+    let (r, w) = pipe().unwrap();+    c.check(syscall5(SYS_SENDFILE, w as u64, src as u64, &mut off as *mut u64 as u64, 3, 0) == 3 && off == 7, "sendfile at an offset into a pipe updates *offset");+    let mut buf = [0u8; 8];+    c.check(read(r, &mut buf) == 3 && &buf[..3] == b"456", "the pipe got 456");+    c.check(syscall5(SYS_SENDFILE, dst as u64, r as u64, 0, 3, 0) == -EINVAL, "sendfile from a pipe: EINVAL");+    close(r);+    close(w);+    close(src);+    close(dst);+    c.check(read_file("/tmp/sf.txt") == b"01234567abcd", "the copy matches");+    unlink("/tmp/sf.txt");+    unlink("/tmp/pv.txt");+    // ppoll / pselect6: the mask size must be 8.+    let mask = 0u64;+    let ts = [0u64, 0];+    let mut fds = [PollFd { fd: 0, events: POLLIN, revents: 0 }];+    c.check(syscall5(SYS_PPOLL, fds.as_mut_ptr() as u64, 1, ts.as_ptr() as u64, &mask as *const u64 as u64, 4) == -EINVAL, "ppoll with sigsetsize 4: EINVAL");+    let arg = [&mask as *const u64 as u64, 16];+    c.check(syscall6(SYS_PSELECT6, 0, 0, 0, 0, ts.as_ptr() as u64, arg.as_ptr() as u64) == -EINVAL, "pselect6 with sigsetsize 16: EINVAL");+    let arg = [&mask as *const u64 as u64, 8];+    c.check(syscall6(SYS_PSELECT6, 0, 0, 0, 0, ts.as_ptr() as u64, arg.as_ptr() as u64) == 0, "pselect6 with sigsetsize 8: fine");+    // ppoll's mask: a blocked, pending signal is let in for the wait only.+    signal(SIGUSR1, on_usr1);+    USR1.store(0, Ordering::SeqCst);+    let old = sigprocmask(SIG_BLOCK, sigbit(SIGUSR1)) as u64;+    kill(getpid(), SIGUSR1);+    let (r, w) = pipe().unwrap();+    let mut fds = [PollFd { fd: r as i32, events: POLLIN, revents: 0 }];+    let ts = [1u64, 0];+    let open_mask = old & !sigbit(SIGUSR1);+    let rr = syscall5(SYS_PPOLL, fds.as_mut_ptr() as u64, 1, ts.as_ptr() as u64, &open_mask as *const u64 as u64, 8);+    c.check(rr == -EINTR && USR1.load(Ordering::SeqCst) == 1, "ppoll with SIGUSR1 unblocked: EINTR, handler ran");+    c.check(sigprocmask(SIG_BLOCK, 0) as u64 & sigbit(SIGUSR1) != 0, "the old mask (SIGUSR1 blocked) is back");+    sigprocmask(SIG_SETMASK, old);+    close(r);+    close(w);+    // getdents64 while deleting (rm -r): nothing is skipped.+    for dir in ["/tmp/gd", "/mnt/disk1/gd"] {+        mkdir(dir);+        for i in 0..40 {+            write_file(&format!("{}/f{}", dir, i), b"x");+        }+        let fd = open(dir, O_RDONLY | O_DIRECTORY);+        let mut buf = [0u8; 256]; // small: many getdents calls+        let mut seen = 0;+        let mut ino_ok = true;+        loop {+            let n = getdents64(fd, &mut buf);+            if n <= 0 {+                break;+            }+            let mut names = Vec::new();+            let mut pos = 0usize;+            while pos < n as usize {+                let ino = u64::from_le_bytes(buf[pos..pos + 8].try_into().unwrap());+                let reclen = u16::from_le_bytes([buf[pos + 16], buf[pos + 17]]) as usize;+                let raw = &buf[pos + 19..pos + reclen];+                let name = core::str::from_utf8(&raw[..raw.iter().position(|&b| b == 0).unwrap_or(raw.len())]).unwrap_or("?");+                if name != "." && name != ".." {+                    names.push((alloc::string::String::from(name), ino));+                }+                pos += reclen;+            }+            for (name, ino) in names {+                let path = format!("{}/{}", dir, name);+                ino_ok &= st(&path).ino == ino;+                unlink(&path);+                seen += 1;+            }+        }+        close(fd);+        c.check(seen == 40 && rmdir(dir) == 0, &format!("{}: deleting while reading: all 40 seen, directory empty", dir));+        c.check(ino_ok, &format!("{}: d_ino matches st_ino", dir));+    }+}
@@ -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 16"), "uname: Rustbelt, stage 16");+    c.check(u[0] == "Rustbelt" && u[3].starts_with("stage 17"), "uname: Rustbelt, stage 17");      let (secs, _) = clock_gettime(0);     c.check(secs > 1_600_000_000, "CLOCK_REALTIME is a real date (after 2020)");
@@ -83,7 +83,7 @@ pub const AT_EMPTY_PATH: u64 = 0x1000; /// Longest path the kernel takes, counting the NUL. pub const PATH_MAX: usize = 4096; pub const SYS_MOUNTS: u64 = 501;-pub const SYS_SYSINFO: u64 = 502;+pub const SYS_RB_SYSINFO: u64 = 502; // ours (Linux's sysinfo is 99) pub const SYS_SOCKET: u64 = 41; pub const SYS_CONNECT: u64 = 42; pub const SYS_ACCEPT: u64 = 43;@@ -686,7 +686,7 @@ pub struct SysInfo { /// Ask the kernel for CPU, thread, process and memory counts. pub fn sysinfo() -> SysInfo {     let mut info = SysInfo::default();-    syscall3(SYS_SYSINFO, &mut info as *mut SysInfo as u64, 0, 0);+    syscall3(SYS_RB_SYSINFO, &mut info as *mut SysInfo as u64, 0, 0);     info } /// Current directory into `buf`; returns its length (or -errno). The