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