RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

authorTyler Fowler <tyler@localhost>2026-10-30 09:46:08 -0700
commit02590b467a004b14ab80c26c0860b4a39fcfaee9
parent2a105612c9f6eeb692b8d5862fb2b27ae68a3ee9
treebrowse files at this commit
Stage 14: processes (0.14.0)

fork with copy-on-write (refcounted frames, CoW page faults, CR0.WP),
vfork and fork-style clone; execve with Linux's argv/envp/auxv stack
(AT_PHDR, AT_PAGESZ, AT_ENTRY, AT_RANDOM from virtio-rng, AT_EXECFN...);
mmap/munmap/mprotect/brk with VMAs and demand paging (anonymous private
and shared, file-backed private); signals: rt_sigaction, rt_sigprocmask,
rt_sigreturn with Linux rt_sigframes, kill, pause, default actions incl.
stop/continue, SIGSEGV on bad accesses, SIGCHLD, SIGPIPE, EINTR and
SA_RESTART; wait4 with Linux status encoding; getppid, exit_group, dup,
dup2, dup3, pipe2, fcntl, O_CLOEXEC; process groups and a tty foreground
group (Ctrl+C -> SIGINT, Ctrl+Z -> SIGTSTP). Syscalls now return via
iretq through a common TrapFrame.

The shell uses fork + exec with job control: Ctrl+C, kill, fg, bg.
New user programs forktest, cowtest, mmaptest, sigtest and
tests/process.rs (11 tests). README: learner chapter on fork/exec,
copy-on-write, memory maps and signals; roadmap updated. New boot.png.
MCargo.lock+1 -1
MCargo.toml+1 -1
MREADME.md+725 -174
Mboot.png+0 -0
Mbuild.rs+1 -0
Mdisk/scripts/selftest.sh+11 -0
Mscripts/run-headless.sh+12 -5
Mscripts/screenshot.sh+23 -12
Msrc/dev.rs+1 -1
Msrc/elf.rs+76 -27
Msrc/errno.rs+10 -2
Msrc/fs/file.rs+8 -0
Msrc/fs/pipe.rs+3 -3
Msrc/interrupts.rs+80 -58
Msrc/lib.rs+5 -2
Msrc/memory.rs+181 -17
Asrc/mm.rs+465 -0
Msrc/net/socket.rs+4 -2
Msrc/process.rs+577 -203
Asrc/signal.rs+715 -0
Msrc/syscall.rs+464 -81
Msrc/task/keyboard.rs+3 -1
Msrc/thread.rs+75 -34
Asrc/trap.rs+189 -0
Msrc/tty.rs+35 -2
Msrc/virtio/console.rs+6 -5
Mtests/guard_pages.rs+2 -2
Atests/process.rs+158 -0
Mtests/shell.rs+1 -1
Mtests/user_mode.rs+1 -1
Auser/cowtest.rs+107 -0
Auser/forktest.rs+186 -0
Auser/mmaptest.rs+130 -0
Muser/sh.rs+255 -32
Auser/sigtest.rs+189 -0
Muser/ulib.rs+479 -33

36 files changed, 5179 insertions, 700 deletions

@@ -261,7 +261,7 @@ dependencies = [  [[package]] name = "rustbelt"-version = "0.13.0"+version = "0.14.0" dependencies = [  "bootloader",  "conquer-once",
@@ -1,6 +1,6 @@ [package] name = "rustbelt"-version = "0.13.0"+version = "0.14.0" edition = "2024"  [dependencies]
@@ -32,10 +32,18 @@ Under the shell:   (wait queues) without using any CPU while they wait. - **User programs.** Small Rust ELF binaries run in **ring 3** as processes,   each with its own address space. Their memory is freed when they exit. They-  talk to the kernel through about 39 `syscall`s, numbered like Linux's:-  `read`, `write`, `open`, `stat`, `getdents64`, `pipe`, `uname`, `socket`,-  `connect`, `getrandom`... A few are our own: `spawn`, `wait`, `mounts`, `sysinfo`,-  `resolve` and `netinfo`.+  talk to the kernel through 63 `syscall`s, numbered like Linux's:+  `read`, `write`, `open`, `stat`, `getdents64`, `pipe2`, `uname`, `socket`,+  `connect`, `getrandom`... A few are our own: `spawn`, `mounts`, `sysinfo`,+  `resolve`, `netinfo` and `trace_me`.+- **The Unix process model.** `fork` with **copy-on-write**, `execve` with+  Linux's `argv`/`envp`/auxiliary-vector stack layout, `wait4`, **`mmap`**+  (anonymous, shared, and file-backed private mappings), `mprotect`,+  `munmap` and `brk` with **demand paging**, and **signals** with real+  signal frames: handlers, masks, `sigreturn`, SIGSEGV on bad accesses,+  SIGCHLD, SIGPIPE, and **Ctrl+C** to the foreground process group. The+  shell runs programs with fork + exec and has job control (`kill`, `fg`,+  `bg`, Ctrl+Z). - **Files.** Files live behind a Linux-style **VFS**: a ramfs at `/` and   `/tmp`, a **FAT32** partition mounted at `/mnt/disk0`, an **ext2**   disk at `/mnt/disk1`, an **exFAT** disk at `/mnt/disk2` and a **btrfs**@@ -68,14 +76,14 @@ that drives the shell with a script. Most code follows the blog_os sources (MIT / Apache-2.0) closely, with extra comments. Everything else is new: the serial mirroring, uptime clock, demo features and scripts, and everything past chapter 12. That covers preemptive-threads, user mode, syscalls, ELF loading, the VFS, PCI, virtio-blk,+threads, user mode, syscalls, fork/exec/mmap/signals, ELF loading, the VFS, PCI, virtio-blk, partitions, FAT32, the shell, the ext2, exFAT and btrfs drivers, multi-core (SMP) support, the virtio-net driver and socket layer, and the virtio-rng, virtio-console and virtio-gpu drivers with the framebuffer console. The 8x16 font is [Spleen](https://github.com/fcambus/spleen) by Frederic Cambus (BSD 2-clause, license in `src/font.rs`). -![Rustbelt 0.13.0 (stage 13: virtual drivers) on the 1024x768 virtio-gpu framebuffer: `tile` drew a gradient wallpaper and four tiled windows with gaps and borders; the text console now lives in the left window with the cyan focus border, showing ls -l /dev (fb0, hvc0, null, random, urandom), random bytes, dice rolls and a ping](boot.png)+![Rustbelt 0.14.0 (stage 14: processes) 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)"), ends a background job with `kill %1` (exit code 143), and runs cowtest and sigtest, which both report "all checks passed"](boot.png)  ## New to Rust and C? Start here @@ -137,7 +145,8 @@ asking the kernel for something. *SMP* means several CPUs sharing memory; the first one to boot is the *BSP*, the others *APs*.  **Try things.** `cargo run`, then in the shell: `help`, `ver`, `sysinfo`,-`spin 200 > A:/tmp/spin.txt &` followed by `sysinfo` (watch one CPU go to 100%), `overflow`+`spin 200 > A:/tmp/spin.txt &` followed by `sysinfo` (watch one CPU go to 100%), `spin 1000`+and then Ctrl+C, `sigtest`, `overflow` (a stack overflow, caught), `tile` (the console moves into a window; `tile -r` moves it back), `random -d 6 5`. Change something small -- the message in `user/overflow.rs`, the bar width in `user/sysinfo.rs` -- and run@@ -278,12 +287,13 @@ Cargo features (in `Cargo.toml`): Expected serial output of `./scripts/run-headless.sh`. Boot is quick now that the demos are behind a feature; most of the output is the shell running `disk/scripts/selftest.sh`. Each command is echoed after its prompt. The-`[process] started/exited` lines are the kernel's own log. They go to serial+`[process] forked/exec/exited` lines are the kernel's own log (every program+is started with fork + exec since stage 14; the `...` lines are cut here). They go to serial only, so the VGA screen shows just the shell session (and kernel messages such as a killed process).  ```-Rustbelt 0.13.0 -- a hobby x86_64 kernel in Rust+Rustbelt 0.14.0 -- a hobby x86_64 kernel in Rust [smp] 4 CPUs online [pci] 00:00.0 8086:1237 host bridge ... (12 PCI devices: the network card, the GPU, the rng, the console, 4 disks)@@ -318,11 +328,11 @@ Rustbelt 0.13.0 -- a hobby x86_64 kernel in Rust [virtio] fbcon: text console on the framebuffer, 128x48 characters [net] eth0: virtio-net, MAC 52:54:00:12:34:56, IRQ 10 [net] DHCP lease: 10.0.2.15/24, gateway 10.0.2.2, DNS 10.0.2.3-[process] started pid 1 (sh), entry 0x8000407500+[process] started pid 1 (sh), entry 0x800040ada5 A:/ $ # Rustbelt shell self-test: `cargo run --features headless` runs this with A:/ $ # `sh /mnt/disk0/scripts/selftest.sh` (B:/scripts/selftest.sh) and scripts/run-headless.sh checks the output. A:/ $ ver-Rustbelt 0.13.0 (stage 13: virtual drivers) x86_64+Rustbelt 0.14.0 (stage 14: processes) x86_64 A:/ $ drives A:  /            ramfs B:  /mnt/disk0   fat32@@ -333,68 +343,86 @@ A:/ $ pwd / A:/ $ cd B: B:/ $ ls-[process] started pid 2 (ls), entry 0x8000401880+[process] pid 1 forked pid 2+[process] pid 2 exec /mnt/disk0/bin/ls bin/        notes/      readme.txt  scripts/ [process] pid 2 (ls) exited with code 0 B:/ $ ls -l notes-[process] started pid 3 (ls), entry 0x8000401880+[process] pid 1 forked pid 3+[process] pid 3 exec /mnt/disk0/bin/ls        125  todo.txt [process] pid 3 (ls) exited with code 0 B:/ $ type B:/readme.txt-[process] started pid 4 (cat), entry 0x8000400260+[process] pid 1 forked pid 4+[process] pid 4 exec /mnt/disk0/bin/cat Hello from the FAT32 disk! This file lives on a virtio-blk disk, in a GPT partition, in a FAT32 file system -- read through the kernel's VFS. In the Rustbelt shell this disk is drive B: -- try `cd B:` and `ls`. [process] pid 4 (cat) exited with code 0 B:/ $ cat b:notes/todo.txt | grep -i EXT | wc -l-[process] started pid 5 (cat), entry 0x8000400260-[process] started pid 6 (grep), entry 0x8000400610-[process] started pid 7 (wc), entry 0x8000400260+[process] pid 1 forked pid 5+[process] pid 1 forked pid 6+[process] pid 5 exec /mnt/disk0/bin/cat+[process] pid 1 forked pid 7 [process] pid 5 (cat) exited with code 0+[process] pid 6 exec /mnt/disk0/bin/grep+[process] pid 7 exec /mnt/disk0/bin/wc [process] pid 6 (grep) exited with code 0 1 [process] pid 7 (wc) exited with code 0 B:/ $ dir bin | wc -l-[process] started pid 8 (ls), entry 0x8000401880-[process] started pid 9 (wc), entry 0x8000400260+[process] pid 1 forked pid 8+[process] pid 1 forked pid 9+[process] pid 8 exec /mnt/disk0/bin/ls+[process] pid 9 exec /mnt/disk0/bin/wc [process] pid 8 (ls) exited with code 0-25+29 [process] pid 9 (wc) exited with code 0 B:/ $ echo first line > A:/tmp/out.txt B:/ $ echo second line >> /tmp/out.txt B:/ $ cat < /tmp/out.txt-[process] started pid 10 (cat), entry 0x8000400260+[process] pid 1 forked pid 10+[process] pid 10 exec /mnt/disk0/bin/cat first line second line [process] pid 10 (cat) exited with code 0 B:/ $ md /tmp/work-[process] started pid 11 (mkdir), entry 0x8000400100+[process] pid 1 forked pid 11+[process] pid 11 exec /mnt/disk0/bin/mkdir [process] pid 11 (mkdir) exited with code 0 B:/ $ copy B:/notes/todo.txt /tmp/work-[process] started pid 12 (cp), entry 0x80004006b0+[process] pid 1 forked pid 12+[process] pid 12 exec /mnt/disk0/bin/cp [process] pid 12 (cp) exited with code 0 B:/ $ touch /tmp/work/empty.txt-[process] started pid 13 (touch), entry 0x8000400100+[process] pid 1 forked pid 13+[process] pid 13 exec /mnt/disk0/bin/touch [process] pid 13 (touch) exited with code 0 B:/ $ ls -l A:/tmp/work-[process] started pid 14 (ls), entry 0x8000401880+[process] pid 1 forked pid 14+[process] pid 14 exec /mnt/disk0/bin/ls          0  empty.txt        125  todo.txt [process] pid 14 (ls) exited with code 0 B:/ $ del /tmp/work/todo.txt /tmp/work/empty.txt /tmp/work-[process] started pid 15 (rm), entry 0x8000400100+[process] pid 1 forked pid 15+[process] pid 15 exec /mnt/disk0/bin/rm [process] pid 15 (rm) exited with code 0 B:/ $ grep "B:" readme.txt-[process] started pid 16 (grep), entry 0x8000400610+[process] pid 1 forked pid 16+[process] pid 16 exec /mnt/disk0/bin/grep In the Rustbelt shell this disk is drive B: -- try `cd B:` and `ls`. [process] pid 16 (grep) exited with code 0 B:/ $ alias ll="ls -l" B:/ $ ll A:/tmp-[process] started pid 17 (ls), entry 0x8000401880+[process] pid 1 forked pid 17+[process] pid 17 exec /mnt/disk0/bin/ls         23  out.txt [process] pid 17 (ls) exited with code 0 B:/ $ run --trace cat B:/notes/todo.txt-[process] started pid 18 (cat), entry 0x8000400260+[process] pid 1 forked pid 18+[process] pid 18 exec /mnt/disk0/bin/cat+[trace 18] execve("/mnt/disk0/bin/cat", ...) = 0 [trace 18] open("/mnt/disk0/notes/todo.txt", 0o0) = 3 [trace 18] read(3, "Done: the shell (cd, ls, cat, run, start"..., 512) = 125 Done: the shell (cd, ls, cat, run, start, pipes, redirection, drive letters).@@ -405,47 +433,57 @@ Next: more file systems -- ext2, exFAT, btrfs. [trace 18] exit(0) [process] pid 18 (cat) exited with code 0 B:/ $ start sleep 1-[process] started pid 19 (sleep), entry 0x8000400000+[process] pid 1 forked pid 19 [pid 19] sleep started in the background B:/ $ jobs-[pid 19] running  start sleep 1+[1] [pid 19] [process] pid 19 exec /mnt/disk0/bin/sleep+running  start sleep 1 B:/ $ wait [process] pid 19 (sleep) exited with code 0 [done] pid 19 (start sleep 1), exit code 0 B:/ $ cd C: C:/ $ ls-[process] started pid 20 (ls), entry 0x8000401880+[process] pid 1 forked pid 20+[process] pid 20 exec /mnt/disk0/bin/ls big.txt      docs/        hello-link@  hello.txt    lost+found/  readme.txt sparse.bin [process] pid 20 (ls) exited with code 0 C:/ $ cat hello-link-[process] started pid 21 (cat), entry 0x8000400260+[process] pid 1 forked pid 21+[process] pid 21 exec /mnt/disk0/bin/cat Hello from the ext2 disk! This file lives on a second virtio disk, in an ext2 file system that Rustbelt reads and writes with its own driver. [process] pid 21 (cat) exited with code 0 C:/ $ grep "line 12345" big.txt-[process] started pid 22 (grep), entry 0x8000400610+[process] pid 1 forked pid 22+[process] pid 22 exec /mnt/disk0/bin/grep big.txt line 12345 [process] pid 22 (grep) exited with code 0 C:/ $ cp B:/notes/todo.txt C:/todo.txt-[process] started pid 23 (cp), entry 0x80004006b0+[process] pid 1 forked pid 23+[process] pid 23 exec /mnt/disk0/bin/cp [process] pid 23 (cp) exited with code 0 C:/ $ cat C:/todo.txt | grep -i ext2-[process] started pid 24 (cat), entry 0x8000400260-[process] started pid 25 (grep), entry 0x8000400610+[process] pid 1 forked pid 24+[process] pid 1 forked pid 25+[process] pid 24 exec /mnt/disk0/bin/cat [process] pid 24 (cat) exited with code 0+[process] pid 25 exec /mnt/disk0/bin/grep Next: more file systems -- ext2, exFAT, btrfs. [process] pid 25 (grep) exited with code 0 C:/ $ md C:/made-here-[process] started pid 26 (mkdir), entry 0x8000400100+[process] pid 1 forked pid 26+[process] pid 26 exec /mnt/disk0/bin/mkdir [process] pid 26 (mkdir) exited with code 0 C:/ $ echo written by Rustbelt > C:/made-here/note.txt C:/ $ type C:/made-here/note.txt-[process] started pid 27 (cat), entry 0x8000400260+[process] pid 1 forked pid 27+[process] pid 27 exec /mnt/disk0/bin/cat written by Rustbelt [process] pid 27 (cat) exited with code 0 C:/ $ ls -l C:/-[process] started pid 28 (ls), entry 0x8000401880+[process] pid 1 forked pid 28+[process] pid 28 exec /mnt/disk0/bin/ls -rw-r--r--     304000  big.txt drwxr-xr-x      <DIR>  docs/ lrwxrwxrwx             hello-link -> hello.txt@@ -458,7 +496,8 @@ drwxr-xr-x      <DIR>  made-here/ [process] pid 28 (ls) exited with code 0 C:/ $ cd D: D:/ $ ls-[process] started pid 29 (ls), entry 0x8000401880+[process] pid 1 forked pid 29+[process] pid 29 exec /mnt/disk0/bin/ls Long File Name With Spaces.txt MixedCase.TXT Photos/@@ -469,33 +508,41 @@ readme.txt Ünïcödé café.txt [process] pid 29 (ls) exited with code 0 D:/ $ cat "long file name with spaces.txt"-[process] started pid 30 (cat), entry 0x8000400260+[process] pid 1 forked pid 30+[process] pid 30 exec /mnt/disk0/bin/cat This file has a long name with spaces in it. exFAT stores names of up to 255 UTF-16 characters, 15 per "File Name" directory entry. [process] pid 30 (cat) exited with code 0 D:/ $ cp B:/notes/todo.txt D:/-[process] started pid 31 (cp), entry 0x80004006b0+[process] pid 1 forked pid 31+[process] pid 31 exec /mnt/disk0/bin/cp [process] pid 31 (cp) exited with code 0 D:/ $ cp C:/hello.txt "Copied From Ext2.txt"-[process] started pid 32 (cp), entry 0x80004006b0+[process] pid 1 forked pid 32+[process] pid 32 exec /mnt/disk0/bin/cp [process] pid 32 (cp) exited with code 0 D:/ $ cp mixedcase.txt C:/from-exfat.txt-[process] started pid 33 (cp), entry 0x80004006b0+[process] pid 1 forked pid 33+[process] pid 33 exec /mnt/disk0/bin/cp [process] pid 33 (cp) exited with code 0 D:/ $ cat C:/from-exfat.txt-[process] started pid 34 (cat), entry 0x8000400260+[process] pid 1 forked pid 34+[process] pid 34 exec /mnt/disk0/bin/cat MixedCase.TXT: exFAT keeps the case you wrote but ignores it when looking names up. [process] pid 34 (cat) exited with code 0 D:/ $ md "New Folder"-[process] started pid 35 (mkdir), entry 0x8000400100+[process] pid 1 forked pid 35+[process] pid 35 exec /mnt/disk0/bin/mkdir [process] pid 35 (mkdir) exited with code 0 D:/ $ echo hi from exFAT > "New Folder/Note.txt" D:/ $ type "new folder/NOTE.TXT"-[process] started pid 36 (cat), entry 0x8000400260+[process] pid 1 forked pid 36+[process] pid 36 exec /mnt/disk0/bin/cat hi from exFAT [process] pid 36 (cat) exited with code 0 D:/ $ ls -l D:/-[process] started pid 37 (ls), entry 0x8000401880+[process] pid 1 forked pid 37+[process] pid 37 exec /mnt/disk0/bin/ls        142  Copied From Ext2.txt        132  Long File Name With Spaces.txt         84  MixedCase.TXT@@ -510,7 +557,8 @@ D:/ $ ls -l D:/ [process] pid 37 (ls) exited with code 0 D:/ $ cd E: E:/ $ ls -l-[process] started pid 38 (ls), entry 0x8000401880+[process] pid 1 forked pid 38+[process] pid 38 exec /mnt/disk0/bin/ls drwxr-xr-x      <DIR>  Music/ drwxr-xr-x      <DIR>  archive/ -rw-r--r--    2160000  big.txt@@ -524,161 +572,281 @@ drwxr-xr-x      <DIR>  projects/ -rw-r--r--    1048576  zeros.bin [process] pid 38 (ls) exited with code 0 E:/ $ cat hello.txt-[process] started pid 39 (cat), entry 0x8000400260+[process] pid 1 forked pid 39+[process] pid 39 exec /mnt/disk0/bin/cat Hello from btrfs! (an inline extent: these bytes live inside the B-tree leaf) [process] pid 39 (cat) exited with code 0 E:/ $ cat hello-link-[process] started pid 40 (cat), entry 0x8000400260+[process] pid 1 forked pid 40+[process] pid 40 exec /mnt/disk0/bin/cat Hello from btrfs! (an inline extent: these bytes live inside the B-tree leaf) [process] pid 40 (cat) exited with code 0 E:/ $ cat docs/btrfs/cow.txt-[process] started pid 41 (cat), entry 0x8000400260+[process] pid 1 forked pid 41+[process] pid 41 exec /mnt/disk0/bin/cat Copy-on-write: btrfs never overwrites a block in place. To change a leaf it writes a new copy somewhere free, then a new copy of its parent that points to it, and so on up to the root. The superblock is the last thing written; until then the old tree is still complete on disk. [process] pid 41 (cat) exited with code 0 E:/ $ wc big.txt-[process] started pid 42 (wc), entry 0x8000400260+[process] pid 1 forked pid 42+[process] pid 42 exec /mnt/disk0/bin/wc  120000  360000 2160000 big.txt [process] pid 42 (wc) exited with code 0 E:/ $ grep "line 119999" big.txt-[process] started pid 43 (grep), entry 0x8000400610+[process] pid 1 forked pid 43+[process] pid 43 exec /mnt/disk0/bin/grep btrfs line 119999 [process] pid 43 (grep) exited with code 0 E:/ $ cat projects/rustbelt/stage10.txt-[process] started pid 44 (cat), entry 0x8000400260+[process] pid 1 forked pid 44+[process] pid 44 exec /mnt/disk0/bin/cat Stage 10: a read-only btrfs driver. Next: core polish (SMP, memory cleanup, guard pages, a proper scheduler), then networking. [process] pid 44 (cat) exited with code 0 E:/ $ cp docs/btrfs/cow.txt D:/cow.txt-[process] started pid 45 (cp), entry 0x80004006b0+[process] pid 1 forked pid 45+[process] pid 45 exec /mnt/disk0/bin/cp [process] pid 45 (cp) exited with code 0 E:/ $ type D:/COW.TXT | wc -l-[process] started pid 46 (cat), entry 0x8000400260-[process] started pid 47 (wc), entry 0x8000400260+[process] pid 1 forked pid 46+[process] pid 1 forked pid 47+[process] pid 46 exec /mnt/disk0/bin/cat [process] pid 46 (cat) exited with code 0+[process] pid 47 exec /mnt/disk0/bin/wc 4 [process] pid 47 (wc) exited with code 0 E:/ $ cp docs/btrfs/chunks.txt C:/chunks.txt-[process] started pid 48 (cp), entry 0x80004006b0+[process] pid 1 forked pid 48+[process] pid 48 exec /mnt/disk0/bin/cp [process] pid 48 (cp) exited with code 0 E:/ $ cat C:/chunks.txt | wc -l-[process] started pid 49 (cat), entry 0x8000400260-[process] started pid 50 (wc), entry 0x8000400260+[process] pid 1 forked pid 49+[process] pid 1 forked pid 50+[process] pid 49 exec /mnt/disk0/bin/cat [process] pid 49 (cat) exited with code 0+[process] pid 50 exec /mnt/disk0/bin/wc 3 [process] pid 50 (wc) exited with code 0 E:/ $ echo hi > new.txt sh: new.txt: Read-only file system E:/ $ rm hello.txt-[process] started pid 51 (rm), entry 0x8000400100+[process] pid 1 forked pid 51+[process] pid 51 exec /mnt/disk0/bin/rm rm: hello.txt: Read-only file system [process] pid 51 (rm) exited with code 1 [exit code 1] E:/ $ mkdir newdir-[process] started pid 52 (mkdir), entry 0x8000400100+[process] pid 1 forked pid 52+[process] pid 52 exec /mnt/disk0/bin/mkdir mkdir: newdir: Read-only file system [process] pid 52 (mkdir) exited with code 1 [exit code 1] E:/ $ run evil-[process] started pid 53 (evil), entry 0x80004000f0+[process] pid 1 forked pid 53+[process] pid 53 exec /mnt/disk0/bin/evil [evil] pid 53: reading kernel memory at 0x200000...-[kernel] user page fault: address 0x200000, error PageFaultErrorCode(PROTECTION_VIOLATION | USER_MODE)-[kernel] page fault in user mode at rip 0x800040017e-[process] pid 53 (evil) killed: page fault-[exit code 139]+[kernel] user page fault: address 0x200000, rip 0x8000400558, PageFaultErrorCode(PROTECTION_VIOLATION | USER_MODE), Unmapped+[process] pid 53 (evil) killed: page fault (SIGSEGV)+[exit code 139] (killed by SIGSEGV) E:/ $ # Stage 11: a stack overflow hits the guard zone and is killed cleanly; E:/ $ # then two CPU hogs in the background, and sysinfo shows the load per CPU. E:/ $ run overflow-[process] started pid 54 (overflow), entry 0x8000400130+[process] pid 1 forked pid 54+[process] pid 54 exec /mnt/disk0/bin/overflow [overflow] pid 54: recursing until the stack runs out...-[kernel] user page fault: address 0xffffffae88, error PageFaultErrorCode(CAUSED_BY_WRITE | USER_MODE)-[kernel] stack overflow in user mode at rip 0x8000400021-[process] pid 54 (overflow) killed: stack overflow-[exit code 139]+[kernel] user page fault: address 0xffffefed38, rip 0x80004000e1, PageFaultErrorCode(CAUSED_BY_WRITE | USER_MODE), Unmapped+[process] pid 54 (overflow) killed: stack overflow (SIGSEGV)+[exit code 139] (killed by SIGSEGV) E:/ $ start spin-[process] started pid 55 (spin), entry 0x8000400400+[process] pid 1 forked pid 55 [pid 55] spin started in the background-[spin] pid 55: spinning without syscalls... E:/ $ start spin-[process] started pid 56 (spin), entry 0x8000400400+[process] pid 55 exec /mnt/disk0/bin/spin+[process] pid 1 forked pid 56+[spin] pid 55: spinning without syscalls... [pid 56] spin started in the background-[spin] pid 56: spinning without syscalls...+[process] pid 56 exec /mnt/disk0/bin/spin E:/ $ sysinfo-[process] started pid 57 (sysinfo), entry 0x8000400420+[spin] pid 56: spinning without syscalls...+[process] pid 1 forked pid 57+[process] pid 57 exec /mnt/disk0/bin/sysinfo [spin] round 1 done (count = 30000000) [spin] round 1 done (count = 30000000) [spin] round 2 done (count = 60000000) [spin] round 2 done (count = 60000000)-4 CPUs online (this program ran on CPU 1), up 11 s-10 threads, 4 processes, 1296 KiB of memory frames in use-[spin] round 3 done (count = 90000000)-[process] pid 55 (spin) exited with code 0+4 CPUs online (this program ran on CPU 2), up 15 s+11 threads, 4 processes, 4572 KiB of memory frames in use CPU load over the last quarter second:-  CPU0 [####################] 100%+  CPU0 [....................]   0%   CPU1 [....................]   0%+[spin] round 3 done (count = 90000000)+[process] pid 55 (spin) exited with code 0   CPU2 [####################] 100%-  CPU3 [....................]   0%+[spin] round 3 done (count = 90000000)+[process] pid 56 (spin) exited with code 0+  CPU3 [####################] 100% [process] pid 57 (sysinfo) exited with code 0 [done] pid 55 (start spin), exit code 0+[done] pid 56 (start spin), exit code 0 E:/ $ wait-... E:/ $ # Stage 12: networking. DHCP gave us an address; ping QEMU's gateway; fetch E:/ $ # from the test web server QEMU runs for 10.0.2.100 (guestfwd), and save a E:/ $ # 108 KB file to D:; look up a name (only works if the host has DNS); then E:/ $ # serve one request with httpd (run-headless.sh curls it through hostfwd). E:/ $ ifconfig-[process] started pid 58 (ifconfig), entry 0x8000400490-eth0: virtio-net  MAC 52:54:00:12:34:56  IRQ 11+[process] pid 1 forked pid 58+[process] pid 58 exec /mnt/disk0/bin/ifconfig+eth0: virtio-net  MAC 52:54:00:12:34:56  IRQ 10       inet 10.0.2.15/24  gateway 10.0.2.2  DNS 10.0.2.3  (DHCP)-      RX 10 packets (1 KiB)  TX 9 packets (0 KiB)-...+      RX 15 packets (1 KiB)  TX 14 packets (1 KiB)+[process] pid 58 (ifconfig) exited with code 0 E:/ $ ping -c 3 -i 200 10.0.2.2+[process] pid 1 forked pid 59+[process] pid 59 exec /mnt/disk0/bin/ping PING 10.0.2.2 (10.0.2.2): 56 data bytes-64 bytes from 10.0.2.2: icmp_seq=1 time=8.34 ms-64 bytes from 10.0.2.2: icmp_seq=2 time=2.48 ms-64 bytes from 10.0.2.2: icmp_seq=3 time=2.27 ms+64 bytes from 10.0.2.2: icmp_seq=1 time=8.97 ms+64 bytes from 10.0.2.2: icmp_seq=2 time=2.15 ms+64 bytes from 10.0.2.2: icmp_seq=3 time=2.19 ms --- 10.0.2.2 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss-rtt min/avg/max = 2.27/4.36/8.34 ms-...+rtt min/avg/max = 2.15/4.43/8.97 ms+[process] pid 59 (ping) exited with code 0 E:/ $ fetch http://10.0.2.100/+[process] pid 1 forked pid 60+[process] pid 60 exec /mnt/disk0/bin/fetch Hello from the host, via QEMU guestfwd!-...+[process] pid 60 (fetch) exited with code 0 E:/ $ wget -o D:/seq.txt http://10.0.2.100/big-fetch: HTTP/1.0 200 OK, saved 108894 bytes to /mnt/disk2/seq.txt in 108 ms (984 KiB/s)-...+[process] pid 1 forked pid 61+[process] pid 61 exec /mnt/disk0/bin/fetch+fetch: HTTP/1.0 200 OK, saved 108894 bytes to /mnt/disk2/seq.txt in 126 ms (843 KiB/s)+[process] pid 61 (fetch) exited with code 0 E:/ $ cat D:/seq.txt | wc -l+[process] pid 1 forked pid 62+[process] pid 1 forked pid 63+[process] pid 62 exec /mnt/disk0/bin/cat+[process] pid 63 exec /mnt/disk0/bin/wc+[process] pid 62 (cat) exited with code 0 20000-...+[process] pid 63 (wc) exited with code 0 E:/ $ host example.com+[process] pid 1 forked pid 64+[process] pid 64 exec /mnt/disk0/bin/host example.com has address 198.18.0.1-...+[process] pid 64 (host) exited with code 0 E:/ $ httpd -n 1 -t 20+[process] pid 1 forked pid 65+[process] pid 65 exec /mnt/disk0/bin/httpd [httpd] listening on port 80-[httpd] 10.0.2.2:38550 GET / -> 200 (134 bytes)+[httpd] 10.0.2.2:48206 GET / -> 200 (133 bytes) [httpd] served 1 request, bye-...+[process] pid 65 (httpd) exited with code 0+E:/ $ # Stage 13: virtual drivers. The devices in /dev; random bytes from+E:/ $ # virtio-rng; a shell on the second console (getty -1 runs the commands+E:/ $ # QEMU feeds into hvc0 from a file; its output goes to another file that+E:/ $ # run-headless.sh checks); the tiling demo on the virtio-gpu framebuffer. E:/ $ ls -l /dev+[process] pid 1 forked pid 66+[process] pid 66 exec /mnt/disk0/bin/ls crw-rw-rw-      <DEV>  fb0 crw-rw-rw-      <DEV>  hvc0 crw-rw-rw-      <DEV>  null crw-rw-rw-      <DEV>  random crw-rw-rw-      <DEV>  urandom+[process] pid 66 (ls) exited with code 0 E:/ $ random -n 8-7f1178e9b80be627+[process] pid 1 forked pid 67+[process] pid 67 exec /mnt/disk0/bin/random+a83abb283794a4d6+[process] pid 67 (random) exited with code 0 E:/ $ random -d 6 5-4 4 6 1 2+[process] pid 1 forked pid 68+[process] pid 68 exec /mnt/disk0/bin/random+3 3 6 6 5+[process] pid 68 (random) exited with code 0 E:/ $ getty -1 /dev/hvc0-[process] started pid 69 (getty), entry 0x8000400460-[process] started pid 70 (sh), entry 0x8000407500-...                                  (that shell's output goes to hvc0, see below)+[process] pid 1 forked pid 69+[process] pid 69 exec /mnt/disk0/bin/getty+[process] started pid 70 (sh), entry 0x800040ada5+[process] pid 70 forked pid 71+[process] pid 71 exec /mnt/disk0/bin/random+[process] pid 71 (random) exited with code 0+[process] pid 70 forked pid 72+[process] pid 72 exec /mnt/disk0/bin/ls+[process] pid 72 (ls) exited with code 0+[process] pid 70 (sh) exited with code 0 [process] pid 69 (getty) exited with code 0 E:/ $ tile+[process] pid 1 forked pid 73+[process] pid 73 exec /mnt/disk0/bin/tile tile: 4 windows on 1024x768, console 61x46+[process] pid 73 (tile) exited with code 0 E:/ $ tile -r+[process] pid 1 forked pid 74+[process] pid 74 exec /mnt/disk0/bin/tile console: full screen, 128x48 characters+[process] pid 74 (tile) exited with code 0+E:/ $ # Stage 14: processes. Every program above was started with fork + execve;+E:/ $ # these check the process model itself (each prints "all checks passed").+E:/ $ # Then a background job ended with the kill builtin (SIGTERM: 128+15).+E:/ $ forktest+[process] pid 1 forked pid 75+[process] pid 75 exec /mnt/disk0/bin/forktest+[process] pid 75 forked pid 76+[process] pid 76 (forktest) exited with code 7+[forktest] ok: fork returns the child's pid in the parent+[forktest] ok: child saw getppid() == parent; wait4 gives its exit code 7+...+[forktest] all checks passed+[process] pid 75 (forktest) exited with code 0+E:/ $ cowtest+[process] pid 1 forked pid 92+[process] pid 92 exec /mnt/disk0/bin/cowtest+[process] pid 92 forked pid 93+[process] pid 93 (cowtest) exited with code 0+[cowtest] ok: the child starts with the parent's data and can change its copy+[cowtest] ok: the child's writes to global data stay in the child+...+[cowtest] all checks passed+[process] pid 92 (cowtest) exited with code 0+E:/ $ mmaptest+[process] pid 1 forked pid 98+[process] pid 98 exec /mnt/disk0/bin/mmaptest+[mmaptest] ok: mmap 3 pages of anonymous memory+[mmaptest] ok: it starts zeroed+[mmaptest] ok: it keeps what we write+[mmaptest] ok: mmap 64 MiB: no memory used yet (pages come on first touch)+...+[mmaptest] all checks passed+[process] pid 98 (mmaptest) exited with code 0+E:/ $ sigtest+[process] pid 1 forked pid 99+[process] pid 99 exec /mnt/disk0/bin/sigtest+[sigtest] ok: SIG_IGN: an ignored signal does nothing+[sigtest] ok: rt_sigaction installs a handler+[sigtest] ok: kill(getpid(), SIGUSR1) runs the handler once, with sig = 10+[sigtest] ok: after rt_sigreturn the interrupted code computes the same result+...+[sigtest] all checks passed+[process] pid 99 (sigtest) exited with code 0+E:/ $ start spin 1000+[process] pid 1 forked pid 110+[pid 110] spin started in the background+E:/ $ kill %1+[process] pid 110 exec /mnt/disk0/bin/spin+[process] pid 110 (spin) killed by SIGTERM+E:/ $ wait+[done] pid 110 (start spin 1000), exit code 143+E:/ $ kill -l+ 1) SIGHUP      2) SIGINT      3) SIGQUIT     4) SIGILL      5) SIGTRAP   + 6) SIGABRT     7) SIGBUS      8) SIGFPE      9) SIGKILL    10) SIGUSR1   +11) SIGSEGV    12) SIGUSR2    13) SIGPIPE    14) SIGALRM    15) SIGTERM   +16) SIGSTKFLT  17) SIGCHLD    18) SIGCONT    19) SIGSTOP    20) SIGTSTP   +21) SIGTTIN    22) SIGTTOU    23) SIGURG     24) SIGXCPU    25) SIGXFSZ   +26) SIGVTALRM  27) SIGPROF    28) SIGWINCH   29) SIGIO      30) SIGPWR    +31) SIGSYS      E:/ $ nosuchprogram sh: nosuchprogram: command not found (try `help`) E:/ $ cd Q:@@ -691,8 +859,8 @@ A:/tmp $ exit 0 [ok] shell self-test finished: Exited(0) [memory] 1015 frames in use before the shell, 1015 after it and all its children exited [run-headless] frames in use returned to the baseline-[run-headless] host curl got httpd's page: Hello from Rustbelt! Rustbelt 0.13.0 (stage 13: virtual drivers)-[run-headless] all 60+12 expected shell and hvc0 lines found+[run-headless] host curl got httpd's page: Hello from Rustbelt! Rustbelt 0.14.0 (stage 14: processes)+[run-headless] all 67+12 expected shell and hvc0 lines found [run-headless] kernel exited OK ``` @@ -703,11 +871,12 @@ commands in `scripts/hvc0-session.txt`: ``` ... E:/ $ getty -1 /dev/hvc0-[process] started pid 69 (getty), entry 0x8000400460+[process] pid 1 forked pid 69+[process] pid 69 exec /mnt/disk0/bin/getty  Rustbelt getty: shell on this terminal (type `exit` to end it) E:/ $ ver-Rustbelt 0.13.0 (stage 13: virtual drivers) x86_64+Rustbelt 0.14.0 (stage 14: processes) x86_64 E:/ $ echo hi over hvc0 hi over hvc0 E:/ $ random -d 20 3@@ -731,7 +900,8 @@ without DNS that line is `host: example.com: ...` and the run still passes.)  The run ends with the same number of frames in use before and after the-shell, even though 74 processes have come and gone; `run-headless.sh`+shell, even though about 110 processes have come and gone (forked,+exec'd, sharing pages copy-on-write); `run-headless.sh` fails if they differ. Exited processes give back their page tables, code, user stacks and kernel stacks. (Of the 1015 at boot, 768 are the 3 MiB framebuffer; most of the rest are the virtio queues and buffers of the@@ -784,7 +954,7 @@ heap_allocation::simple_allocation...               [ok] | `tests/stack_overflow.rs` | stack overflow lands in the double-fault handler on the IST stack (`harness = false`) | | `tests/executor.rs` | `SimpleExecutor` runs an async fn to completion; the waker-based `Executor` interleaves two tasks that `yield_now().await` (a1 b1 a2 b2 a3 b3) | | `tests/shell.rs` | **the shell**: tokenizer (operators without spaces, quotes, comments), parser (pipelines, `<` `>` `>>`, `&`, error cases), drive-letter assignment from a mount table, `B:/x` -> `/mnt/disk0/x` translation and back for the prompt; then the real `/bin/sh` runs a script (cd with a drive letter, ls, a pipe, `>`/`>>`/`<`, `ver`, `exit 3`) with its stdout in a pipe and the output is checked; background jobs (`start`, `jobs`, `wait`); 20 processes started and reaped leave `frames_in_use` unchanged |-| `tests/user_mode.rs` | programs loaded from the FAT disk: `hello` exits 0, `demo` passes all its checks (exit 0), `evil` is killed with code 139, `spin` is preempted; spawn with arguments + trace + exit code 7, then reaped; a child's stdout into a pipe; spawn errors (`ENOENT`, `ENOEXEC`, `EISDIR`, `ECHILD`); `wait` with `WNOHANG` returns `EAGAIN` while the child runs; ELF loader rejects garbage; user pointers give `EFAULT` (length 0 is fine); `sleep` blocks for the right number of ticks; a `WaitQueue` wakes its waiter |+| `tests/user_mode.rs` | programs loaded from the FAT disk: `hello` exits 0, `demo` passes all its checks (exit 0), `evil` is killed by SIGSEGV (code 139), `spin` is preempted; spawn with arguments + trace + exit code 7, then reaped; a child's stdout into a pipe; spawn errors (`ENOENT`, `ENOEXEC`, `EISDIR`, `ECHILD`); `wait` with `WNOHANG` returns `EAGAIN` while the child runs; ELF loader rejects garbage; user pointers give `EFAULT` (length 0 is fine); `sleep` blocks for the right number of ticks; a `WaitQueue` wakes its waiter | | `tests/vfs.rs` | the root holds `dev`, `etc`, `hello.txt`, `mnt`, `tmp`; read `/hello.txt` to EOF, path normalisation (`.`, `..`, relative to a cwd), `ENOENT`/`ENOTDIR`/`EINVAL`/`EISDIR`/`EEXIST`, `/tmp` is a separate ramfs, longest-prefix mounts (4 with `/dev`), sparse writes zero-fill, listing, remove (`ENOTEMPTY`, `EBUSY` on mount points), seek/stat/truncate, `getdents64` record layout, pipes pass bytes and report EOF / `EPIPE` | | `tests/block.rs` | PCI scan finds the virtio disk; sector 1 of the image is the GPT header; multi-sector reads equal single-sector reads; write + read back; out-of-range and partial-sector requests fail; the GPT partition is registered as `vda1` (with a FAT32 boot sector); MBR parsing on a RAM disk | | `tests/fat.rs` | the FAT32 volume is mounted at `/mnt/disk0` with the expected files (`bin`, `notes`, `readme.txt`, `scripts`); read `readme.txt`; every program in `bin/` is a valid ELF; create/write/read/truncate/remove a 10 KB file; directories with long names, `ENOTEMPTY`; the `BlockIo` adapter handles unaligned reads and writes |@@ -795,7 +965,8 @@ heap_allocation::simple_allocation...               [ok] | `tests/smp.rs` | **multi-core and the scheduler**: all 4 CPUs are online and take timer ticks; 4 busy threads run on 2+ CPUs *at the same time* (per-CPU busy ticks); 3 `spin` processes run on 2+ CPUs; 4 threads on 4 CPUs add 200,000 to a locked counter without losing one; `sleep_ticks(10)` lasts 10-12 ticks; sleepers wake in deadline order (3, 6, 9); while 4 threads sleep the CPUs are idle (<10% busy, so no busy-polling); two threads ping-pong 1,000 times through a `WaitQueue`; **leak checks**: 200 threads and 40 processes spawned and exited leave `frames_in_use`, the thread count, the process count and the number of kernel stacks exactly where they were | | `tests/net.rs` | **networking**, all against things QEMU itself provides, so it's deterministic and works offline: DHCP gives 10.0.2.15/24, gateway 10.0.2.2, DNS 10.0.2.3 and the MAC is QEMU's default; 3 ICMP echoes to the gateway come back with their payload; a UDP **TFTP** read of `readme.txt` from slirp's TFTP server (`tftp=disk` in `test-args`); a 64 KiB TCP round trip through the `cat` echo server (guestfwd 10.0.2.100:7); an HTTP GET of 108,894 bytes through the socket API; `ECONNREFUSED` from a closed host port; a UDP `recv` with a 300 ms timeout returns `EAGAIN` after 250-1000 ms; the `ping` and `fetch` programs (saving to a file, and exit 1 on a 404); 20 TCP + 20 UDP sockets opened and closed leave the socket count and `frames_in_use` unchanged; DNS for `example.com`, **skipped** (not failed) if the host has no DNS | | `tests/virtio.rs` | **the virtual drivers**: 2 x 4 KiB from virtio-rng differ, aren't constant, have 15384-17384 one bits out of 32768 and contain all 256 byte values; the `random` program (hex, dice, a usage error); hvc0 receives exactly the bytes QEMU feeds in from `tests/hvc0-input.txt`, and 8 KiB written to it go out with nothing dropped; virtio-gpu answers `GET_DISPLAY_INFO`, refuses a flush of a resource that doesn't exist with `ERR_INVALID_RESOURCE_ID`, full and clipped flushes work, a pattern written into the framebuffer reads back; the text console is on the framebuffer (128x48) and printing lights up pixels; `/dev` lists `fb0 hvc0 null random urandom`; `tile` draws the expected colours at known pixels, moves the console to 61x46 and `tile -r` back, with no frames leaked. `scripts/gpu-check.sh` checks what QEMU actually displays |-| `tests/guard_pages.rs` | the guard page sits right below each kernel stack, and the user stack's 64 KiB guard zone has the right bounds; a kernel thread that recurses forever is killed with "kernel stack overflow" three times in a row (the rest of the kernel carries on, and its stack is freed); the user `overflow` program ends with `Killed("stack overflow")` and leaves no frames behind |+| `tests/guard_pages.rs` | the guard page sits right below each kernel stack, and the user stack's 64 KiB guard zone has the right bounds; a kernel thread that recurses forever is killed with "kernel stack overflow" three times in a row (the rest of the kernel carries on, and its stack is freed); the user `overflow` program ends with `Killed(SIGSEGV, "stack overflow")` and leaves no frames behind |+| `tests/process.rs` | **the process model** (stage 14), mostly by running the user test programs: `cowtest` (globals, heap and stack stay separate after fork in both directions, three generations, `MAP_SHARED` vs `MAP_PRIVATE`); `forktest` (fork/wait4/getppid, 10 children reaped with `wait4(-1)`, `ECHILD`, execve errors, vfork + exec, `pipe2(O_CLOEXEC)`, fork + dup2 + exec through a pipe, a child dying of SIGSEGV); **exec argv/envp/auxv** (`forktest args "two words" ""` with `FOO=bar EMPTY=`: argc, every argv and envp string, `AT_PAGESZ`, `AT_ENTRY` = `_start`, `AT_PHDR` covering the entry, `AT_RANDOM`, `AT_EXECFN`, stack position); `mmaptest` (zeroed anonymous memory, 64 MiB that costs nothing until touched, SIGSEGV handlers that see `si_addr` and `SEGV_ACCERR`/`SEGV_MAPERR` and fix the fault so the access is retried, `PROT_NONE`, file-backed private mappings, `brk`, `EINVAL`s, memory back to the start); an unhandled fault is `Killed(SIGSEGV)` with status 11 and exit code 139; `sigtest` (handler round trip through sigreturn with registers intact, masks and pending signals, SIGKILL can't be caught, default actions, SIGCHLD with `si_pid`/`si_status`, SIGPIPE, stop/continue seen by `wait4(WUNTRACED/WCONTINUED)`, `EINTR` vs `SA_RESTART`, `kill` errors, signal 0, `kill(-pgid)`); wait-status encoding; a **fork bomb** stops at `EAGAIN`, is killed and reaped, and leaves frames and the process count unchanged; **20 fork/exec/exit loops leak no frames** and leave no shared frames; **Ctrl+C** (`tty::input_char('\x03')`) kills `spin` in the foreground group with SIGINT (exit code 130), and does nothing when no group is in the foreground |  How it works: `#![feature(custom_test_frameworks)]` collects every `#[test_case]` function and passes them to `rustbelt::test_runner` (in `src/lib.rs`), which@@ -841,17 +1012,23 @@ src/smp.rs                multi-core bring-up: AP trampoline (real mode -> long                            INIT/SIPI, per-CPU data (swapgs), CPU numbering, waking idle CPUs src/apic.rs               local APIC: enable, timer (calibrated against the PIT), EOI, IPIs src/acpi.rs               finds the ACPI RSDP/RSDT/MADT to list the CPUs-src/syscall.rs            syscall/sysret MSR setup, asm entry stub, dispatch + all syscalls (table at the top)-src/process.rs            Process (pid, parent, cwd, args, fd table, exit status), spawn from a path or-                          ELF bytes, iretq into ring 3, exit/kill, wait (reaping)+src/syscall.rs            syscall MSR setup, asm entry stub, dispatch + all syscalls (table at the top)+src/trap.rs               TrapFrame (all user registers) + the common iretq return to user mode+src/process.rs            Process (pid, parent, pgid, cwd, env, fd table + close-on-exec, exit status),+                          spawn, fork, execve (argv/envp/auxv stack), exit/kill, wait4 (reaping), groups+src/mm.rs                 a process's memory map: regions (VMAs), demand paging, copy-on-write faults,+                          mmap/munmap/mprotect/brk, fork of an address space+src/signal.rs             signals: actions, masks, pending sets, default actions, rt_sigframe setup and+                          sigreturn, SIGCHLD/stop/continue, delivery on the way back to user mode src/pci.rs                PCI bus scan through config ports 0xCF8/0xCFC, device names src/block/mod.rs          BlockDevice trait, device registry (vda, vda1...), RamDisk (for tests) src/block/virtio_blk.rs   legacy virtio-blk driver: one virtqueue, polled, bounce buffer src/block/partition.rs    MBR + GPT parsing, Partition = a BlockDevice window onto its disk-src/elf.rs                minimal ELF64 loader (PT_LOAD segments -> user pages)+src/elf.rs                minimal ELF64 loader (PT_LOAD segments -> regions; program headers for AT_PHDR) src/errno.rs              Linux errno numbers src/tty.rs                console line discipline (cooked: echo + line editing; raw: every key, arrows-                          as escape sequences) + Console (the File behind fds 0/1/2)+                          as escape sequences; Ctrl+C/Ctrl+Z signal the foreground process group)+                          + Console (the File behind fds 0/1/2) src/fs/mod.rs             VFS: Inode + FileSystem traits, mount table, path normalize/lookup/create/remove,                           symlink following (ELOOP after 8), init (boot files),                           mount_disks (ext2, exFAT, btrfs or FAT partitions/disks -> /mnt/diskN)@@ -882,10 +1059,12 @@ disk3/                    files copied onto the exFAT disk (long, mixed-case, Un disk4/                    files copied onto the btrfs disks (hello.txt, docs/, Music/, projects/ and                           archive/ become subvolumes); build.rs adds big.txt, random.bin, zeros.bin,                           empty.txt and symlinks-user/ulib.rs              tiny user-space "libc": syscall wrappers, println!/eprintln!, a 64 KiB heap-                          (Vec/String), line reading, strerror, _start, panic handler+user/ulib.rs              tiny user-space "libc": syscall wrappers (fork, execve, wait4 + W* macros, mmap,+                          signals with a restorer), println!/eprintln!, a brk-based heap (Vec/String),+                          line reading, strerror, _start reading argv/envp/auxv, panic handler user/link.ld              linker script for user programs (base 0x80_0040_0000)-user/sh.rs                the shell: line editor, builtins, aliases, PATH, pipes, redirection, jobs+user/sh.rs                the shell: line editor, builtins, aliases, PATH, pipes, redirection, fork + exec,+                          job control (process groups, Ctrl+C, kill, fg, bg) user/shparse.rs           the shell's parser + drive letters (also compiled into tests/shell.rs) user/{ls,cat,wc,grep,mkdir,rm,touch,cp,sleep}.rs   the shell's tools user/{hello,evil,spin,demo,child}.rs  test/demo programs (all programs go to /mnt/disk0/bin)@@ -897,6 +1076,8 @@ src/net/virtio_net.rs     legacy virtio-net driver: RX/TX virtqueues, MAC, smolt src/net/socket.rs         BSD-style sockets (TCP, UDP, ICMP) on smoltcp, blocking on a wait queue user/{ifconfig,ping,host,fetch,httpd}.rs  the network programs user/{random,getty,tile}.rs  stage 13: getrandom, a shell on hvc0, the framebuffer tiling demo+user/{forktest,cowtest,mmaptest,sigtest}.rs  stage 14: the process-model checks (each prints ok/FAILED+                          lines and "all checks passed"; tests/process.rs runs them) scripts/hvc0-session.txt  the commands run-headless.sh feeds into hvc0 (for `getty -1 /dev/hvc0`) scripts/gpu-check.sh      types `tile`, dumps the virtio-gpu screen (PPM), compares pixels scripts/http-reply.sh     the host side of `fetch http://10.0.2.100/`: QEMU runs it per connection@@ -1210,10 +1391,10 @@ overflow is caught at the first byte. - **Stack probes.** A function with a big local array could jump *over* a   4 KiB guard page. `"stack-probes": {"kind": "inline"}` in the target JSON   makes the compiler touch each page of a big frame in order, so it can't.-- **User stacks** (16 KiB) get a 64 KiB never-mapped zone underneath, and-  the ELF loader refuses programs whose segments reach into it. A fault-  there kills the program with "stack overflow" (exit code 139). Try-  `overflow` in the shell.+- **User stacks** (1 MiB since stage 14, pages appearing as it grows) get a+  64 KiB never-mapped zone underneath, and the ELF loader refuses programs+  whose segments reach into it. A fault there is a SIGSEGV, reported as+  "stack overflow" (exit code 139). Try `overflow` in the shell.  Kernel stacks are reused: a freed stack's slot goes on a free list and its page tables stay, so they never need freeing. **Why no TLB shootdown?** When@@ -1295,11 +1476,16 @@ One call, step by step:    scheduler), push user RSP, `swapgs` back, push `rcx`, `r11` and the args. 4. Shuffle into the C ABI and call `dispatch(nr, a1..a5)` in Rust with    interrupts re-enabled (so a long syscall, e.g. a blocking `read`, can be preempted).-5. `dispatch` validates user pointers (`user_slice`: range must be inside the-   user half *and* mapped with `USER_ACCESSIBLE` in the current page table),+5. `dispatch` validates user pointers (`user_slice`: the range must be inside+   the user half *and* inside the process's memory regions; pages not there+   yet are faulted in, copy-on-write pages copied before a write),    runs the call, and returns a value or `-errno` in `rax`.-6. `cli`, pop everything back, restore user RSP, `sysretq` (RIP = rcx,-   RFLAGS = r11, CPL = 3).+6. Since stage 14 the entry saves *all* registers as a `TrapFrame`+   (`src/trap.rs`) and returns through the same exit as interrupts: deliver+   any pending signal (which may rewrite the frame to enter a handler), pop+   everything back and `iretq`. Stage 13 used `cli` + `sysretq` (RIP = rcx,+   RFLAGS = r11), which is faster but can't restore `rcx` and `r11`, and a+   return from a signal handler needs every register exactly as it was.  With several CPUs, the "current kernel stack" and "saved user RSP" can't be globals (two CPUs can be in a syscall at once), so they're per-CPU, reached@@ -1311,16 +1497,26 @@ Numbers are Linux's wherever Linux has the same call, so man pages and | nr | name | args | notes | |---|---|---|---| | 0 | `read` | fd, buf, len | console: blocks until Enter; pipe: blocks until data or EOF; socket: like `recv` |-| 1 | `write` | fd, buf, len | console = VGA + serial; pipe: `EPIPE` once the reader is gone; socket: like `send` |-| 2 | `open` | path, path_len, flags | `O_CREAT` 0o100, `O_TRUNC` 0o1000, `O_APPEND` 0o2000 (every write goes to the end: `>>`); directories can be opened (for `getdents64`) |+| 1 | `write` | fd, buf, len | console = screen + serial; pipe: `EPIPE` **and SIGPIPE** once the reader is gone; socket: like `send` |+| 2 | `open` | path, path_len, flags | `O_CREAT` 0o100, `O_TRUNC` 0o1000, `O_APPEND` 0o2000 (every write goes to the end: `>>`); `O_CLOEXEC` 0o2000000 (closed by `execve`); directories can be opened (for `getdents64`) | | 3 | `close` | fd | | | 4 | `stat` | path, path_len, *Stat | follows symlinks | | 5 | `fstat` | fd, *Stat | | | 8 | `lseek` | fd, offset, whence | `SEEK_SET/CUR/END` = 0/1/2; `ESPIPE` on console/pipes |-| 16 | `ioctl` | fd, request, arg | console: `TTY_SET_RAW` (1): raw mode on (arg 1) / off (0); returns the old mode. `/dev/fb0`: `FB_GET_INFO` (0x4600) writes `[width, height, bytes per row, 32]` to `*arg`; `FB_FLUSH` (0x46f0) shows `*[x, y, w, h]` (arg 0: the rows this fd wrote); `FB_FILL` (0x46f1) fills `*[x, y, w, h, 0xRRGGBB]`; `FB_SET_CONSOLE` (0x46f2) moves the text console into `*[x, y, w, h]` (arg 0: full screen) and returns `cols << 16 \| rows`. Others: `ENOTTY` |-| 22 | `pipe` | *[u32; 2] | [read end, write end] |+| 9 | `mmap` | addr, len, prot, flags, fd, offset | MAP_PRIVATE/SHARED, MAP_ANONYMOUS, MAP_FIXED; files: private only |+| 10 | `mprotect` | addr, len, prot | PROT_READ 1, PROT_WRITE 2, PROT_EXEC 4 |+| 11 | `munmap` | addr, len |  |+| 12 | `brk` | addr | returns the (new) end of the heap; brk(0) = current |+| 13 | `rt_sigaction` | sig, *new, *old, 8 | kernel `struct sigaction`; handlers need SA_RESTORER |+| 14 | `rt_sigprocmask` | how, *set, *old, 8 | SIG_BLOCK 0 / SIG_UNBLOCK 1 / SIG_SETMASK 2 |+| 15 | `rt_sigreturn` |  | only from a signal handler's restorer |+| 16 | `ioctl` | fd, request, arg | console: `TTY_SET_RAW` (1): raw mode on (arg 1) / off (0); returns the old mode. `/dev/fb0`: `FB_GET_INFO` (0x4600) writes `[width, height, bytes per row, 32]` to `*arg`; `FB_FLUSH` (0x46f0) shows `*[x, y, w, h]` (arg 0: the rows this fd wrote); `FB_FILL` (0x46f1) fills `*[x, y, w, h, 0xRRGGBB]`; `FB_SET_CONSOLE` (0x46f2) moves the text console into `*[x, y, w, h]` (arg 0: full screen) and returns `cols << 16 \| rows`. Console, stage 14: `TIOCGPGRP`/`TIOCSPGRP` (0x540f/0x5410) get/set the foreground process group (Ctrl+C goes to it), `TIOCGWINSZ` (0x5413) the size in characters. Others: `ENOTTY` |+| 22 | `pipe` | *[u32; 2] | [read end, write end]; `pipe2` (293) also takes `O_CLOEXEC` | | 24 | `sched_yield` | | |-| 35 | `sleep_ms` | ms | **ours**: Linux nr 35 is `nanosleep(timespec)` |+| 32 | `dup` | fd | lowest free fd |+| 33 | `dup2` | old, new | closes `new` first if open |+| 34 | `pause` |  | sleeps until a signal; returns EINTR |+| 35 | `sleep_ms` | ms | **ours**: Linux nr 35 is `nanosleep(timespec)`. A signal ends it early with `EINTR` | | 39 | `getpid` | | | | 41 | `socket` | domain, type, protocol | `AF_INET` (2) only; `SOCK_STREAM` (1) = TCP, `SOCK_DGRAM` (2) = UDP, or `SOCK_DGRAM` + `IPPROTO_ICMP` (1) = ping socket (like Linux's unprivileged ping sockets). Returns an fd; `close` closes it | | 42 | `connect` | fd, *sockaddr_in, len | TCP: blocks for the handshake (10 s, `ETIMEDOUT`; `ECONNREFUSED` on a reset). UDP: sets the default peer |@@ -1330,23 +1526,37 @@ Numbers are Linux's wherever Linux has the same call, so man pages and | 49 | `bind` | fd, *sockaddr_in, len | local port (address must be 0 or ours) | | 50 | `listen` | fd, backlog | TCP; up to 4 connections can be waiting | | 54 | `setsockopt` | fd, level, name, *val, len | only `SOL_SOCKET`/`SO_RCVTIMEO` (a `timeval`) |-| 60 | `exit` | code | |-| 61 | `wait` | pid, flags | **simpler than Linux `wait4`**: blocks, returns the exit code (139 = killed, like a shell shows SIGSEGV), reaps the child (freeing its memory); `ECHILD` if not your child; flags `WNOHANG` (1): don't block, `EAGAIN` while it's still running |-| 63 | `uname` | *utsname | Linux's struct: 6 NUL-padded 65-byte fields: "Rustbelt", "qemu", "0.13.0", "stage 13: virtual drivers", "x86_64", "(none)" |+| 56 | `clone` | flags, stack, ... | only fork-like flags: SIGCHLD + optional CLONE_VFORK (else ENOSYS) |+| 57 | `fork` |  | copy-on-write; parent gets the pid, child 0 |+| 58 | `vfork` |  | fork + the parent waits until the child execs or exits |+| 59 | `execve` | path, argv, envp (C strings) | Linux's stack layout: argc, argv, envp, auxv |+| 60 | `exit` | code | ends the process (one thread each, so the same as `exit_group`, 231) |+| 61 | `wait4` | pid, *status, options, *rusage | **Linux's `wait4` since stage 14** (stage 13 had a simpler `wait`): pid > 0 one child, -1 any, 0 / < -1 a process group; `*status` gets the packed status (`WIFEXITED`, `WEXITSTATUS`, `WIFSIGNALED`, `WTERMSIG`, `WIFSTOPPED`...); options `WNOHANG` 1 (returns 0 if nobody is done), `WUNTRACED` 2, `WCONTINUED` 8; reaps the child; `ECHILD` if none; rusage must be 0 |+| 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.14.0", "stage 14: processes", "x86_64", "(none)" |+| 72 | `fcntl` | fd, cmd, arg | F_DUPFD 0, F_GETFD 1, F_SETFD 2, F_GETFL 3, F_SETFL 4, F_DUPFD_CLOEXEC 1030 | | 79 | `getcwd` | buf, len | returns the length (no NUL); `ERANGE` if too small |-| 80 | `chdir` | path, path_len | per-process cwd, inherited by spawned children |+| 80 | `chdir` | path, path_len | per-process cwd, inherited by forked and spawned children | | 83 | `mkdir` | path, path_len | | | 84 | `rmdir` | path, path_len | empty directories only (`ENOTEMPTY`); `EBUSY` on a mount point | | 87 | `unlink` | path, path_len | files and symlinks (the link, not its target); directories give `EISDIR` | | 89 | `readlink` | path, path_len, buf, len | a symlink's target (no NUL); returns its length, cut to `len` like Linux; `EINVAL` if not a symlink |+| 109 | `setpgid` | pid, pgid | 0 = myself / a new group |+| 110 | `getppid` |  | 0 if started by the kernel or orphaned |+| 111 | `getpgrp` |  |  |+| 121 | `getpgid` | pid |  | | 217 | `getdents64` | fd, buf, len | real Linux `struct linux_dirent64` records (ino, off, reclen, type, name\0) | | 228 | `clock_gettime` | clock, *timespec | microsecond resolution (TSC calibrated against the PIT); any clock id counts from boot |+| 231 | `exit_group` | code | = exit (one thread per process) |+| 292 | `dup3` | old, new, flags | flags: O_CLOEXEC |+| 293 | `pipe2` | *[u32; 2], flags | flags: O_CLOEXEC | | 318 | `getrandom` | buf, len, flags | random bytes from virtio-rng (the host's `/dev/urandom`), up to 64 KiB per call; never blocks, flags ignored. Without virtio-rng: a TSC-seeded xorshift (not secure) |-| 500 | `spawn` | path, path_len, argv, argv_len, *SpawnOptions | **ours**: start a program, returns its pid |+| 500 | `spawn` | path, path_len, argv, argv_len, *SpawnOptions | **ours**: start a program, returns its pid (fork + exec in one step, like `posix_spawn`; the kernel's first shell and `getty` use it, the shell uses fork + exec) | | 501 | `mounts` | buf, len | **ours** (Linux reads `/proc/mounts`): the mount table as text, one `path fstype` line per mount, in mount order; returns the length, `ERANGE` if too small. The shell assigns drive letters from it | | 502 | `sysinfo` | *SysInfo | **ours**: CPU count, the CPU you're on, thread/process/frame counts, uptime ticks, and per-CPU timer ticks and busy ticks (sample twice for the load) | | 503 | `resolve` | name, name_len, *addrs, max | **ours** (Linux does DNS in libc, in user space): IPv4 addresses for a name via the DHCP-given DNS server; returns how many; `ENOENT` / `ETIMEDOUT` | | 504 | `netinfo` | *NetInfo | **ours** (Linux has `ioctl`s and netlink): MAC, IRQ, address/prefix, gateway, DNS, packet and byte counters |+| 505 | `trace_me` | on | ours: print my syscalls from now on (survives execve; `run --trace`) |  Deviations from Linux worth knowing: @@ -1358,21 +1568,23 @@ Deviations from Linux worth knowing:   6 = socket, 7 = device (`/dev/*`; `getdents64` says `DT_CHR`); `mode`   is the permission bits (`0o644`), or 0 on file systems without them   (ramfs, FAT). No inode numbers, owners or times yet.-- **spawn instead of fork + exec.** `fork` copies the whole address space (or-  needs copy-on-write page tables to be fast) and `exec` immediately throws the-  copy away. `spawn` does the useful part in one step, like Windows-  `CreateProcess` or POSIX `posix_spawn`. What Unix shells do between fork and-  exec -- pointing fds 0/1/2 at a pipe or a file -- is spawn's `SpawnOptions`:-  `{ stdin, stdout, stderr: i64 (one of *my* fds, -1 = same as mine), flags }`.-  `flags = SPAWN_TRACE` (1) prints every syscall the child makes, as-  `[trace pid] open("notes/todo.txt", 0o0) = 3` in strace style (strings-  shown, errors as `-2 ENOENT`), which is what the shell's `run --trace`-  uses. fork + exec could still be added later.-- **argv** is passed as one buffer with NUL-separated arguments-  (`"child\0with\0three args"`; empty = just the program name). The kernel-  copies the strings onto the new process's stack and starts `_start` with-  `rdi = argc`, `rsi = pointer to argc [ptr, len] pairs` (Rust-friendly-  slices instead of C's `char **argv`). `ulib` wraps that as `argc()`/`arg(i)`.+- **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+  signal frame have Linux's layout: a C library can use them unchanged (see+  "Processes" below for how they work).+- **spawn as well as fork + exec.** `spawn` (500) does both steps at once,+  like Windows `CreateProcess` or POSIX `posix_spawn`. What Unix shells do+  between fork and exec -- pointing fds 0/1/2 at a pipe or a file -- is+  spawn's `SpawnOptions`: `{ stdin, stdout, stderr: i64 (one of *my* fds,+  -1 = same as mine), flags }`. `flags = SPAWN_TRACE` (1) prints every+  syscall the child makes, as `[trace pid] open("notes/todo.txt", 0o0) = 3`+  in strace style (strings shown, errors as `-2 ENOENT`). The shell now uses+  fork + exec, and `trace_me` (505) to get the same tracing for `run --trace`.+  spawn's argv is one buffer with NUL-separated arguments+  (`"child\0with\0three args"`); either way the program starts with+  Linux's `argc`/`argv`/`envp`/auxv stack, which `ulib` reads as+  `arg(i)`/`getenv`/`getauxval`.  Errors are negative errno values like Linux (`-2` ENOENT, `-9` EBADF, `-14` EFAULT, `-38` ENOSYS...). Unknown numbers return `-ENOSYS`.@@ -1387,8 +1599,9 @@ switch), and L4 slot 1 is the process's own: ``` 0x0000_0000_0020_0000   kernel code (bootloader 0.9 puts it here)        shared, ring 0 only 0x0000_0080_0000_0000   USER_START (L4 slot 1)                           per process-0x0000_0080_0040_0000     program (.text R-X, .rodata R--, .data/.bss RW-)-0x0000_00ff_ffff_b000     user stack (16 KiB, RW-, grows down)+0x0000_0080_0040_0000     program (.text R-X, .rodata R--, .data/.bss RW-), then the brk heap+                          ... mmap regions, handed out downwards (stage 14, see below)+0x0000_00ff_ffef_f000     user stack (1 MiB, RW-, grows down; a 64 KiB guard zone under it) 0x0000_0100_0000_0000   USER_END 0x0000_0100_0020_0000   boot stack (L4 slot 2)                           shared 0x0000_0180_0000_0000   all physical memory (phys_mem_offset, slot 3)    shared@@ -1410,8 +1623,10 @@ thread starts in the kernel, then `iretq`s into ring 3 with a hand-built frame (SS=user data, RSP=user stack, RFLAGS=IF, CS=user code, RIP=ELF entry).  When ring-3 code faults (page fault with `USER_MODE`, general protection fault-or invalid opcode with CS RPL 3), the handler calls `process::exit_current`-instead of panicking: the process is marked `Killed`, its thread exits, the+or invalid opcode with CS RPL 3), the kernel doesn't panic. A page fault may+just be demand paging or copy-on-write (see stage 14); otherwise the process+gets a signal (SIGSEGV, SIGILL, SIGFPE...). If it has no handler for it, it is+killed (`Killed(SIGSEGV, ...)`, exit code 139), its thread exits and the scheduler picks someone else. A fault in ring 0 still panics -- that's a kernel bug.  ### ELF loading and building user programs (`src/elf.rs`, `build.rs`, `user/`)@@ -1433,9 +1648,10 @@ through the VFS (see "Disks" below). `user/ulib.rs` is a tiny libc-equivalent included by each program: the `syscall` inline asm, wrappers, `print!/println!/eprintln!`, `_start` (calls `main`, then `exit` with its return value) and a panic handler (`exit(101)`).-Since stage 7 it also has a **heap**: a 64 KiB array in `.bss` with a-power-of-two size-class allocator (free list per size) as the-`#[global_allocator]`. A program that says `extern crate alloc;` can use+Since stage 7 it also has a **heap** with a power-of-two size-class+allocator (free list per size) as the `#[global_allocator]`; since stage 14+its memory comes from `brk` (up to 1 MiB blocks) instead of a fixed 64 KiB+array. A program that says `extern crate alloc;` can use `Vec`, `String` and `format!`; the `x86_64-unknown-none` target ships a precompiled `alloc` too. @@ -1653,7 +1869,9 @@ B:/ $ | `drives` | the drive letters and what they map to | | `alias [name=value]` | list aliases or define one: `alias ll="ls -l"` | | `history` | the commands typed so far |-| `jobs` / `wait [pid]` | list background jobs / wait for them (all, or one pid) |+| `jobs` / `wait [pid]` | list background jobs (`[1] [pid 5] running  ...`) / wait for them (all, or one pid) |+| `kill [-SIG] pid\|%job...` / `kill -l` | send a signal (default SIGTERM; `-9`, `-KILL`, `-INT`...); `%1` = job 1, a whole process group; `-l` lists the signals |+| `fg [%job]` / `bg [%job]` | continue a stopped job (Ctrl+Z) in the foreground / background | | `cls`, `clear` | clear the screen | | `help` | the command list | | `exit [code]` | leave the shell (with the last command's exit code, or `code`) |@@ -1683,11 +1901,18 @@ Programs in `B:/bin` (`user/*.rs`, each 30-150 lines): | `getty [-1] device` | run a shell on another terminal, e.g. `getty /dev/hvc0 &`; restarts it when it exits (`-1`: once) | | `tile [1-6]` / `tile -r` | draw a tiling-window-manager mock-up on `/dev/fb0` and move the console into the first window; `-r` gives the console the whole screen back | | `hello`, `demo`, `evil`, `spin`, `child` | the stage 5/6 test programs: `run demo` runs all the stage 6 syscall checks |+| `forktest`, `cowtest`, `mmaptest`, `sigtest` | the stage 14 checks of fork/exec/wait4, copy-on-write, mmap and signals; each ends with "all checks passed" (`forktest bomb` is a bounded fork bomb, `mmaptest crash` dies of SIGSEGV) |  Built-in aliases are the DOS names: `dir` = `ls`, `type` = `cat`, `del` = `rm`, `md` = `mkdir`, `copy` = `cp`; and for networking, `ip` = `ifconfig`, `nslookup` = `host`, `wget` = `fetch`. +**Ctrl+C** interrupts the program in the foreground (SIGINT to its process+group; the shell shows `[exit code 130] (interrupted: SIGINT)`), and on an+empty or half-typed line it just starts a new one. **Ctrl+Z** stops it+(`fg` continues it). Since stage 14 the shell starts every program with+`fork` + `execve`, in its own process group (see "Processes" below).+ **Line editing**: Backspace, and Up/Down to browse the history. Left/Right and Tab aren't supported yet. **Scripts**: `sh file` runs the commands in a file and echoes each one after its prompt. If stdin isn't the console@@ -2755,6 +2980,282 @@ bar, and the console window's black background with text on it. - Every new user program's heap is still 64 KiB, so `tile` writes the   wallpaper in 32 KiB pieces. +## Processes: fork, exec, memory maps and signals (stage 14)++Until stage 13 the kernel had one way to start a program, `spawn`, and one+way to stop it: exit, or be killed for a bad memory access. Stage 14 adds+the classic Unix process model, with Linux's own syscall numbers and+structs, so a C library such as [musl](https://musl.libc.org/) could later+run on it unchanged:++- **`fork`** (57), **`vfork`** (58) and fork-like **`clone`** (56): copy a+  running process, using copy-on-write.+- **`execve`** (59): replace a process's program with another. It sets up+  `argv`, `envp` and the auxiliary vector exactly like Linux.+- **`mmap`/`munmap`/`mprotect`** (9/11/10) and **`brk`** (12): memory+  regions created on request and filled lazily (demand paging).+- **Signals**: `rt_sigaction`, `rt_sigprocmask`, `rt_sigreturn`, `kill`,+  `pause`, with real signal frames on the user stack and default actions.+  A bad memory access is now a SIGSEGV that a program can catch. Children+  that exit send SIGCHLD, a broken pipe sends SIGPIPE, and Ctrl+C sends+  SIGINT.+- **`wait4`** (61) with Linux's status encoding, `getppid`, `exit_group`,+  `dup`/`dup2`/`dup3`, `pipe2`, `fcntl`, close-on-exec, and process groups+  plus a terminal "foreground group" for job control.++The shell now starts every program with fork + exec. Ctrl+C interrupts the+foreground program. `kill`, `fg` and `bg` are builtins, and Ctrl+Z stops a+program.++```+A:/ $ spin 1000 > /tmp/hog.txt+^C+[exit code 130] (interrupted: SIGINT)+A:/ $ start spin 1000 > /tmp/bg.txt+[pid 5] spin started in the background+A:/ $ jobs+[1] [pid 5] running  start spin 1000 > /tmp/bg.txt+A:/ $ kill %1+A:/ $ wait+[done] pid 5 (start spin 1000 > /tmp/bg.txt), exit code 143+A:/ $ run overflow+[overflow] pid 6: recursing until the stack runs out...+[process] pid 6 (overflow) killed: stack overflow (SIGSEGV)+[exit code 139] (killed by SIGSEGV)+A:/ $ kill -l+ 1) SIGHUP      2) SIGINT      3) SIGQUIT     4) SIGILL      5) SIGTRAP+...+```++(130 = 128 + 2, 143 = 128 + 15 (SIGTERM), 139 = 128 + 11 (SIGSEGV): a shell+reports "killed by signal N" as exit code 128 + N.)++### fork and exec, and why Unix splits them++A **process** is a running program: its memory (the *address space*), its+registers, its open files, its current directory. Unix creates processes+in two steps:++1. **`fork()`** makes an almost exact copy of the calling process. Both+   copies return from the same `fork()` call: the parent gets the child's+   pid, the child gets 0. That return value is the only difference.+2. **`execve(path, argv, envp)`** throws away the calling process's+   program and memory and loads a new program in their place. The pid, the+   open files, the current directory and the parent stay the same.++A shell running `ls -l > out.txt` does:++```+pid = fork()+if pid == 0:                       # the child (still running the shell's code)+    fd = open("out.txt", O_CREAT|O_TRUNC|O_WRONLY)+    dup2(fd, 1)                    # make stdout point at the file+    close(fd)+    execve("/bin/ls", ["ls", "-l"], environ)   # becomes ls; never returns on success+    exit(127)                      # only reached if execve failed+else:                              # the parent: the shell+    wait4(pid, &status, 0, 0)      # sleep until ls is done+```++**Why two steps?** The gap between `fork` and `exec` is the point. The child+is still running *the shell's code*, so it can set up the new program's+world with the ordinary system calls: redirect fds with `dup2`, change+directory, join a process group, reset signal handlers. `execve` then keeps+all of that. Without the split, every one of those options would need a+parameter of one big "create process" call. Windows' `CreateProcess` has+ten parameters, and our old `spawn` had its `SpawnOptions` struct for+exactly this reason. The cost is that `fork` seems to copy a whole address+space only for `exec` to throw it away a moment later, which is why the next+section matters. (`posix_spawn` and `vfork` exist for the same reason.+Rustbelt keeps `spawn` (500) as a convenience. The kernel itself uses it to+start the first shell, and `getty` uses it.)++**What `execve` hands the new program.** Linux starts a program with+everything on its stack, and Rustbelt now does exactly the same:++```+            (higher addresses)+            "PATH=/bin:..."  "ls"  "-l"   16 random bytes   ... the strings themselves+            AT_NULL 0                                         auxiliary vector:+            AT_RANDOM -> the 16 bytes                         (key, value) pairs that tell+            AT_ENTRY, AT_PHDR, AT_PHNUM, AT_PAGESZ, ...       the C library about the program+            NULL+            envp[0] -> "PATH=/bin:..."                        environment pointers+            NULL+            argv[1] -> "-l"+            argv[0] -> "ls"+  rsp ->    argc = 2+```++The **auxiliary vector** is how the kernel tells the C library things only+the kernel knows: where the program's headers are (`AT_PHDR`, so musl can+find its thread-local storage template), the page size (`AT_PAGESZ`), the+entry point (`AT_ENTRY`), 16 random bytes for stack canaries and hash seeds+(`AT_RANDOM`, from virtio-rng), the path it was run as (`AT_EXECFN`), and+the user and group ids. `user/ulib.rs`'s `_start` now reads this layout+(`arg(i)`, `getenv`, `getauxval`), and `forktest args` checks every field.++### Copy-on-write++Copying all of a process's memory on `fork` would be slow and usually+wasted (an `exec` follows). Instead, `fork` copies only the **page+tables**. The child's table points at the *same* physical frames as the+parent's, and every writable private page is made **read-only in both**:++```+  parent page table          physical frames          child page table+  0x...1000  R  ----------->  [frame 42, refs 2] <-----------  0x...1000  R+  0x...2000  R  ----------->  [frame 77, refs 2] <-----------  0x...2000  R+```++Reading works as before. The first **write** to such a page causes a page+fault. The fault handler (`mm::Mm::handle_fault`) looks at the region the+address belongs to: it is writable, so the fault is "copy-on-write", not a+bug. If the frame's **reference count** is above 1, the handler copies the+page into a fresh frame, maps the copy writable for the writer and drops+one reference. If the count is 1, the other process has already made its+own copy (or exited), so the writer gets write access back with no copy at+all. Code pages are never written, so they stay shared forever.++The reference counts live in `memory.rs` (a map from frame to count, kept+only for frames that are shared). A frame goes back to the allocator when+its last reference goes away. The leak tests fork, exec and exit hundreds+of times and check that the number of frames in use and the number of+shared frames return to where they started.++One subtle consequence: the *kernel* also writes into user pages (a+`read()` copying file data into a buffer, for example). If that buffer is+a copy-on-write page, the kernel would write into the frame the parent+still uses. Since stage 14 the CPU's `CR0.WP` bit is on, so even ring 0+faults on read-only pages, and every syscall first runs the buffer through+the same fault logic (`mm::user_access_ok`). The pages are copied before+the kernel touches them.++### Virtual memory maps: mmap, brk and demand paging++Every process sees the same addresses (0x80_0000_0000 and up), but each has+its own page tables, so the same address leads to different physical frames+in different processes. That is **virtual memory**. Stage 14 describes a+process's memory as a list of **regions** (Linux calls them VMAs) instead+of a list of mapped pages:++```+0x0000_0080_0040_0000   program: .text R-X, .rodata R--, .data/.bss RW-     (loaded at exec)+                        heap: from the end of the program up to brk       (grows with brk)+                        ...  free  ...+                        mmap regions, handed out downwards from MMAP_TOP   (mmap)+0x0000_00ff_ffee_f000   64 KiB guard zone (never mapped: overflow -> SIGSEGV)+0x0000_00ff_ffef_f000   user stack, 1 MiB, pages appear as it grows+0x0000_00ff_ffff_f000   one unmapped page, then USER_END+```++A region is a **promise**, not memory. `mmap(64 MiB)` just records the+range. The first touch of a page in it causes a page fault, and the handler+finds the region, allocates a zeroed frame (or reads the file's bytes into+it), maps it, and lets the instruction run again. This is **demand+paging**, and `mmaptest` checks it: a 64 MiB mapping costs no memory until+pages are touched, then about one frame per touched page. Programs never+notice any of this.++What's supported:++- `MAP_PRIVATE | MAP_ANONYMOUS`: plain memory (what `malloc` uses for big+  blocks). It is copy-on-write across fork.+- `MAP_SHARED | MAP_ANONYMOUS`: memory that **stays shared** after fork.+  Parent and child see each other's writes (`cowtest` checks this). Its+  frames are allocated at mmap time.+- `MAP_PRIVATE` of a file: the file's bytes are read in page by page on+  first touch, with zeros past the end of the file. Writes (after+  `mprotect`) go to a private copy and never reach the file. `MAP_SHARED`+  of a file returns `ENODEV`, an honest error rather than silently lost+  writes.+- `MAP_FIXED`, `munmap` of any page range (it can split a region),+  `mprotect` to `PROT_NONE`/`READ`/`WRITE`/`EXEC`, and `brk` to grow and+  shrink the heap. `user/ulib.rs`'s allocator now gets its memory from+  `brk`, so programs aren't stuck with stage 13's fixed 64 KiB heap.++An access outside every region, or one the region forbids (writing a+read-only page, reading `PROT_NONE`), is a real error. The process gets+**SIGSEGV**, with `si_addr` set to the faulting address and `si_code`+`SEGV_MAPERR` (nothing mapped there) or `SEGV_ACCERR` (not allowed).++### Signals++A **signal** is a small number delivered to a process to say "something+happened": SIGINT (2) when you press Ctrl+C, SIGSEGV (11) for a bad memory+access, SIGCHLD (17) when a child exits, SIGPIPE (13) when it writes to a+pipe nobody reads, SIGTERM (15) for a polite "please exit", SIGKILL (9) for+a final one. For each signal a process chooses (`rt_sigaction`) between:++- the **default action**: terminate (most), terminate "with a core dump"+  (SIGQUIT, SIGSEGV... We write no core files, so `WCOREDUMP` stays 0, as on+  Linux with core dumps turned off), ignore (SIGCHLD, SIGURG, SIGWINCH), stop (SIGSTOP,+  SIGTSTP from Ctrl+Z) or continue (SIGCONT);+- **ignore** it (`SIG_IGN`);+- a **handler**: a function in the program that runs when the signal+  arrives, interrupting whatever the program was doing.++A process can also **block** signals for a while (`rt_sigprocmask`). They+then stay pending until it unblocks them. SIGKILL and SIGSTOP can't be+caught, blocked or ignored, so there is always a way to stop any process.++**How a handler runs.** Sending a signal only sets a bit in the target's+`pending` set and wakes it if it is sleeping. The target acts on the+signal on its way back to user mode: after a syscall, a page fault or a+timer interrupt. To run a handler, the kernel pushes a **signal frame**+onto the user stack: all the user registers at the moment of interruption,+the old signal mask, and a `siginfo` (which signal, who sent it, which+address faulted). Then it edits the saved registers so that the return to+user mode lands in the handler, with a return address pointing at a tiny+"restorer" function (`mov rax, 15; syscall`). When the handler returns,+the restorer calls **`rt_sigreturn`**, which copies the saved registers+back from the frame, and the program carries on exactly where it was.+`sigtest` checks this by interrupting a calculation and comparing the+result. The frame is Linux's `struct rt_sigframe`, so musl's handlers will+work unchanged.++This is also why syscalls now return to user mode with `iretq` through a+common `TrapFrame` (`src/trap.rs`) rather than `sysretq`. Returning into a+handler, or back from one, has to restore *every* register, including the+`rcx` and `r11` that `sysretq` overwrites.++**Interrupted syscalls.** A process sleeping in the kernel (reading the+keyboard or a pipe, `sleep`, `wait4`, `pause`) is woken when a signal+arrives. The call then fails with `EINTR`, or is restarted transparently if+the handler was installed with `SA_RESTART`. `sigtest` checks both.++**Process groups and Ctrl+C.** A pipeline like `cat x | grep y` is two+processes but one **job**. The shell puts them in their own **process+group** (`setpgid`) and tells the terminal that this group is in the+**foreground** (`ioctl(TIOCSPGRP)`). When you press Ctrl+C, the console+(`src/tty.rs`) sends SIGINT to the whole foreground group. The shell itself+ignores SIGINT, so only the job dies. `kill -TERM %1` sends a signal to a+background job's group, Ctrl+Z (SIGTSTP) stops the foreground job, and `fg`+and `bg` continue it with SIGCONT.++**wait4 and status codes.** A parent learns how a child ended from+`wait4`, which returns a packed status word decoded with Linux's macros:+`WIFEXITED`/`WEXITSTATUS` (exited with a code), `WIFSIGNALED`/`WTERMSIG`/+`WCOREDUMP` (killed by a signal), `WIFSTOPPED`/`WSTOPSIG` (with `WUNTRACED`)+and `WIFCONTINUED` (with `WCONTINUED`). The parent also gets SIGCHLD each+time a child exits, stops or continues.++### What it does and doesn't do++- One thread per process: no `CLONE_VM`/`CLONE_THREAD`, no futexes yet+  (they are next on the roadmap, because libc threads need them). `clone`+  accepts only fork-style flags (`SIGCHLD`, optionally `CLONE_VFORK`).+- `vfork` is fork + "the parent waits until the child execs or exits". It+  is correct for every legal use of vfork, just not faster than fork.+- At most 64 processes (`MAX_PROCESSES`). A fork bomb gets `EAGAIN` at the+  limit, and the test kills it and checks that nothing leaked.+- No `MAP_SHARED` file mappings (no page cache), no swap, no+  `sigaltstack`, no real-time signal queueing (a pending signal is one bit,+  as for Linux's standard signals), no users or permissions (uid 0).+- Ctrl+C and the foreground group work on the main console. hvc0's+  `getty` shell runs programs without a foreground group, so there Ctrl+C+  is just a character. Proper ttys and ptys are on the roadmap.+ ## Roadmap  Done: all 12 chapters of the 2nd edition, plus preemptive kernel threads,@@ -2798,8 +3299,10 @@ framebuffer console). - [x] btrfs, hand-written, read-only: chunk tree (single/DUP), root/FS/csum trees, crc32c everywhere, zlib/lzo/zstd, holes, subvolumes, checked against `btrfs check` and the host's files — [btrfs on-disk format](https://btrfs.readthedocs.io/en/latest/dev/On-disk-format.html) - [x] Networking (stage 12): virtio-net (legacy, interrupt-driven), smoltcp for ARP/IPv4/ICMP/UDP/TCP/DHCP/DNS, BSD-style socket syscalls with blocking on wait queues, `ifconfig`/`ping`/`host`/`fetch`/`httpd` — [OSDev: Virtio](https://wiki.osdev.org/Virtio), [smoltcp](https://docs.rs/smoltcp), [OSDev: Networking](https://wiki.osdev.org/Category:Networking), [Beej's Guide to Network Programming](https://beej.us/guide/bgnet/) - [x] Virtual drivers (stage 13): a modern (1.0) virtio transport shared by virtio-rng (`getrandom`, `/dev/random`, smoltcp's seed), virtio-console (`/dev/hvc0`, `getty`) and virtio-gpu 2D (a 1024x768 framebuffer, a bitmap-font console, `/dev/fb0`, `tile`) — [virtio 1.2 spec](https://docs.oasis-open.org/virtio/virtio/v1.2/virtio-v1.2.html), [OSDev: Virtio](https://wiki.osdev.org/Virtio), [Linux virtio-gpu driver](https://github.com/torvalds/linux/tree/master/drivers/gpu/drm/virtio)-- [ ] `fork`, `exec`, `mmap`, signals, then a musl port (next)-- [ ] A tiling compositor on the framebuffer (later)+- [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)+- [ ] Kernel-side groundwork for libc: `poll`/`select`, non-blocking sockets, loopback, futexes and threads, ttys and ptys (next)+- [ ] The OS split and a musl port (after that)+- [ ] A Hyprland-style tiling compositor on the framebuffer (later)  ### Filesystem roadmap (Tyler's plan) @@ -2852,18 +3355,38 @@ Next, in this order:    moving virtio-blk and virtio-net to the modern transport, resizing the    display, a hardware cursor, virtio-input (keyboard/mouse events for the    compositor), 3D (virgl, see above).-4. **Running Linux software** (next): `fork`, `exec`, `mmap` and signals,-   then a [musl](https://musl.libc.org/) port, so ordinary C programs-   recompiled against Rustbelt's syscalls (numbered like Linux's already,-   sockets and `getrandom` included) can run. `mmap` of `/dev/fb0` comes-   with it, so programs can draw without a syscall per rectangle.-5. **The compositor** (after that): see below; `tile` is its first sketch.+4. ~~**The process model**~~ — done (stage 14): `fork` (copy-on-write),+   `execve` (with the auxiliary vector a libc needs), `mmap` and friends+   with demand paging, signals, `wait4`, process groups and job control.+   Left over for later: `MAP_SHARED` file mappings (needs a page cache),+   `sigaltstack`, queued real-time signals, users and permissions.+5. **The rest of the kernel side of a libc** (next), roughly in this order:+   - `poll`/`select` (and later `epoll`), so one program can wait on+     several fds: the keyboard, a pipe and a socket at once;+   - **non-blocking sockets** (`O_NONBLOCK`, `EAGAIN`, `connect` in+     progress) and a **loopback** interface (127.0.0.1), so programs can+     talk to each other over TCP on one machine;+   - **futexes and threads**: `clone(CLONE_VM | CLONE_THREAD ...)`,+     `set_tid_address`, `arch_prctl(ARCH_SET_FS)` for thread-local storage,+     and `futex` wait/wake, which is what libc mutexes and `pthread_join`+     are built on;+   - **ttys and ptys**: a proper terminal layer (termios, sessions,+     controlling terminals, `/dev/tty`, pseudo-terminals), so hvc0 gets job+     control and a terminal emulator becomes possible;+   - smaller things musl expects: `nanosleep`, `readv`/`writev`, `openat`+     with C strings, a bigger `stat`, `getuid` and friends.+6. **The OS split and a musl port** (after that): user space moves into its+   own tree (libc, toolchain, programs), and [musl](https://musl.libc.org/)+   is ported, so ordinary C programs recompiled against Rustbelt's+   syscalls (numbered like Linux's already) can run. `mmap` of `/dev/fb0`+   comes with it, so programs can draw without a syscall per rectangle.+7. **The compositor** (after that): see below; `tile` is its first sketch.  **Long term: a graphical desktop.** The goal is a **Hyprland-style tiling compositor**: windows tile automatically, with keyboard-driven layouts and smooth animations. It will draw **natively on the virtio-gpu framebuffer** first, with Rustbelt's own programs as clients, so it needs nothing beyond-step 3 plus `mmap` and an input device. **Wayland client support** comes later, once step 4 can run+step 3 plus `mmap` and an input device. **Wayland client support** comes later, once step 6 can run ordinary Linux programs: the compositor then speaks the Wayland protocol, so existing Wayland apps can open windows on it. @@ -2875,8 +3398,8 @@ is the planned future hardware target. The split into a separate Other ideas, any time:  - **Shell extras**: Left/Right cursor movement, Tab completion of file-  names, `$?` and environment variables, Ctrl+C to kill the foreground job-  (that needs signals or a kill syscall).+  names, `$?`, `export` and `$VAR` expansion (the environment itself is+  there since stage 14). - **More programs**: `more`/`less` (needs raw mode and screen size), an editor.  Small exercises along the way: a timer-based@@ -2983,6 +3506,34 @@ 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 14:**+  - *The kernel wrote straight through copy-on-write.* Ring 0 ignores+    read-only page flags unless `CR0.WP` is set, so a `read()` into a+    forked child's buffer changed the parent's memory too. `memory::init`+    now sets `CR0.WP`, and every syscall runs user buffers through the+    fault logic first (`mm::user_access_ok`).+  - *`sysretq` can't return from a signal handler.* It loads RIP and RFLAGS+    from `rcx` and `r11`, so those two registers can never be restored.+    Syscalls now return through `iretq` like interrupts (`src/trap.rs`).+  - *`siginfo` field offsets.* For SIGCHLD, Linux puts `si_pid` at offset+    16, `si_uid` at 20 and `si_status` at 24. `ulib`'s struct had the+    status one `i32` too far, so `sigtest` saw 0 instead of the exit code.+  - *Leaked file descriptors across exec.* The shell's script file was open+    as fd 3 when it forked, so every program inherited it and `run --trace`+    showed `open(...) = 4` instead of 3. The shell now opens scripts, pipes+    and redirections with `O_CLOEXEC`, as real shells do.+  - *Tracing `execve`.* The trace line was built after the call, when the+    path string lived in an address space that no longer existed+    (`<bad pointer>`). It's now formatted before the call.+  - *`kill %1` without job control.* In a script (no terminal), jobs aren't+    in their own process group, so `kill -pgid` found nobody (`ESRCH`); it+    now signals the job's pid there.+  - *A fork bomb must leave nothing behind.* `MAX_PROCESSES` (64) makes the+    65th fork fail with `EAGAIN`. The test counts frames and processes+    before and after killing the bomb, and after a warm-up run, so lazily+    allocated kernel structures don't look like leaks.+  - *`AT_EXECFN` was just `forktest`* when started by `spawn`, which only+    passed the basename along. `spawn_image` now gets the full path. - **Pitfalls hit in stage 13:**   - *`memory allocation of 65536 bytes failed` in `tile`.* User programs     have a fixed 64 KiB heap (`user/ulib.rs`), so a 64 KiB row buffer can

Binary file changed.

@@ -75,6 +75,7 @@ const PROGRAMS: &[&str] = &[     "sysinfo", "overflow", // stage 11: CPU load, and a stack overflow (guard pages)     "ifconfig", "ping", "host", "fetch", "httpd", // stage 12: networking     "random", "getty", "tile", // stage 13: virtual drivers+    "forktest", "cowtest", "mmaptest", "sigtest", // stage 14: processes ];  const DISK_SECTORS: u64 = 64 * 1024 * 1024 / 512;
@@ -89,6 +89,17 @@ random -d 6 5 getty -1 /dev/hvc0 tile tile -r+# Stage 14: processes. Every program above was started with fork + execve;+# these check the process model itself (each prints "all checks passed").+# Then a background job ended with the kill builtin (SIGTERM: 128+15).+forktest+cowtest+mmaptest+sigtest+start spin 1000+kill %1+wait+kill -l nosuchprogram cd Q: cd A:/tmp
@@ -85,7 +85,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.13.0 (stage 13: virtual drivers) x86_64" # ver+        "Rustbelt 0.14.0 (stage 14: processes) x86_64" # ver         "[smp] 4 CPUs online"                          # smp.rs started 3 more cores         "B:  /mnt/disk0   fat32"                       # drives         "C:  /mnt/disk1   ext2"@@ -142,6 +142,13 @@ if [ -z "${1:-}" ] && [ "$code" = 33 ]; then         "crw-rw-rw-      <DEV>  hvc0"         "tile: 4 windows on 1024x768, console 61x46"   # the tiling demo         "console: full screen, 128x48 characters"      # tile -r+        "[forktest] all checks passed"                 # stage 14: processes+        "[forktest args] all checks passed"            # exec'd with argv/envp/auxv+        "[cowtest] all checks passed"+        "[mmaptest] all checks passed"+        "[sigtest] all checks passed"+        "(start spin 1000), exit code 143"             # kill %1 (SIGTERM)+        "15) SIGTERM"                                  # kill -l         "sh: nosuchprogram: command not found"         "cd: there is no drive Q:"         "A:/tmp $ pwd"@@ -150,11 +157,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` -> 25,+    # Pipe results: `... | grep -i EXT | wc -l` -> 1, `dir bin | wc -l` -> 29,     # `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 25 3 4 20000; do+    for n in 1 29 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@@ -165,7 +172,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.13.0" "$CURL_OUT" 2>/dev/null; then+    if grep -qF "Hello from Rustbelt! Rustbelt 0.14.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@@ -174,7 +181,7 @@ if [ -z "${1:-}" ] && [ "$code" = 33 ]; then     grep -qxE -- "[0-9a-f]{16}" "$LOG" || { echo "[run-headless] MISSING: random -n 8 output"; missing=1; }     grep -qxE -- "[1-6]( [1-6]){4}" "$LOG" || { echo "[run-headless] MISSING: random -d 6 5 output"; missing=1; }     # The shell on hvc0 ran the host's commands and answered over hvc0.-    for line in "Rustbelt getty: shell on this terminal" "Rustbelt 0.13.0 (stage 13" \+    for line in "Rustbelt getty: shell on this terminal" "Rustbelt 0.14.0 (stage 14" \                 "$ 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
@@ -8,7 +8,8 @@ # # Usage: ./scripts/screenshot.sh [out.png|out.ppm] [session.txt] [expected text...] # session.txt: one command per line, typed key by key followed by Enter.-# In it, {BS} is Backspace and {UP} is the up arrow.+# In it, {BS} is Backspace, {UP} the up arrow and {^C} Ctrl+C (a line that+# is just {^C} isn't followed by Enter). # Exits 1 if any expected text doesn't show up. set -euo pipefail cd "$(dirname "$0")/.."@@ -24,19 +25,26 @@ shift $(( $# > 2 ? 2 : $# )) if [ -z "$SESSION" ]; then     # The default session (what boot.png shows): the tiling demo moves the     # text console into the left window of the virtio-gpu framebuffer, then-    # a few commands run in it: the devices, random numbers from virtio-rng,-    # and a ping. "vx{BS}er" types a typo and fixes it.+    # stage 14's process model at work: a CPU hog (output to a file) stopped+    # with Ctrl+C, another one in the background ended with `kill`, two test+    # programs. "vx{BS}er" types a typo and fixes it.     SESSION=$(mktemp /tmp/session.XXXXXX)     cat > "$SESSION" <<'TXT' tile vx{BS}er-ls -l /dev-random-random -d 6 5-ping -c 2 -i 200 10.0.2.2+spin 1000 > /tmp/hog.txt+{^C}+wc -l /tmp/hog.txt+start spin 1000 > /tmp/bg.txt+jobs+kill %1+wait+cowtest | grep passed+sigtest | grep passed TXT-    set -- "Rustbelt 0.13.0 (stage 13: virtual drivers) x86_64" "tile: 4 windows on 1024x768, console 61x46" \-        "<DEV>  fb0" "2 packets transmitted, 2 received, 0% packet loss"+    set -- "Rustbelt 0.14.0 (stage 14: processes) x86_64" "tile: 4 windows on 1024x768, console 61x46" \+        "^C" "(interrupted: SIGINT)" "(start spin 1000 > /tmp/bg.txt), exit code 143" \+        "[cowtest] all checks passed" "[sigtest] all checks passed" fi SOCK=$(mktemp -u /tmp/qemu-mon.XXXXXX) SERIAL_LOG=$(mktemp /tmp/serial.XXXXXX)@@ -62,17 +70,20 @@ sock, out, session = sys.argv[1:] names = {' ': 'spc', ',': 'comma', '.': 'dot', '/': 'slash', '-': 'minus', '=': 'equal',          ':': 'shift-semicolon', '|': 'shift-backslash', '>': 'shift-dot', '<': 'shift-comma',          '"': 'shift-apostrophe', "'": 'apostrophe', '_': 'shift-minus', '!': 'shift-1',-         '#': 'shift-3', '&': 'shift-7', '{BS}': 'backspace', '{UP}': 'up', '\n': 'ret'}+         '#': 'shift-3', '&': 'shift-7', '%': 'shift-5', '{BS}': 'backspace', '{UP}': 'up',+         '{^C}': 'ctrl-c', '\n': 'ret'} def key(c):     if c in names: return names[c]     if c.isupper(): return 'shift-' + c.lower()     return c s = socket.socket(socket.AF_UNIX); s.connect(sock) for line in open(session):-    for c in re.findall(r'\{[A-Z]+\}|.|\n', line, re.S):+    if line.strip() == '{^C}':+        line = '{^C}'   # Ctrl+C alone, no Enter after it+    for c in re.findall(r'\{\^?[A-Z]+\}|.|\n', line, re.S):         s.sendall(f"sendkey {key(c)}\n".encode()); time.sleep(0.08)     time.sleep(0.8)   # let the command run-time.sleep(0.5)+time.sleep(2.5)   # let the last command finish fmt = 'ppm' if out.endswith('.ppm') else 'png' s.sendall(f"screendump {out} -f {fmt}\n".encode()); time.sleep(1) s.sendall(b"quit\n"); time.sleep(0.5)
@@ -134,7 +134,7 @@ struct Hvc;  impl File for Hvc {     fn read(&self, buf: &mut [u8]) -> Result<usize, Errno> {-        Ok(console::read(buf))+        console::read(buf)     }     fn write(&self, buf: &[u8]) -> Result<usize, Errno> {         console::write(buf);
@@ -1,4 +1,5 @@-//! A tiny ELF64 loader: just enough to load a statically linked user program.+//! A tiny ELF64 loader: just enough to load a statically linked user program+//! (execve and spawn use it). //! //! An ELF file starts with a header saying what it is (64-bit, little endian, //! x86_64, executable) and where the *program headers* are. Each program@@ -11,7 +12,9 @@ //! are bounds-checked. Reference: `man 5 elf`, https://wiki.osdev.org/ELF  use crate::memory::{self, USER_END, USER_START};-use x86_64::structures::paging::{Page, PageTableFlags, PhysFrame, Size4KiB};+use crate::mm::{Kind, Mm, Vma, PROT_EXEC, PROT_READ, PROT_WRITE};+use alloc::vec::Vec;+use x86_64::structures::paging::{Page, PageTableFlags, Size4KiB}; use x86_64::VirtAddr;  #[derive(Debug, PartialEq, Eq)]@@ -100,54 +103,93 @@ fn parse(data: &[u8]) -> Result<(u64, impl Iterator<Item = Result<Segment, ElfEr     Ok((entry, segments)) } -/// Load the ELF image `data` into address space `l4`. Returns the entry point.-pub fn load(data: &[u8], l4: PhysFrame) -> Result<u64, ElfError> {+/// What `load` tells execve about the program (for the auxiliary vector).+pub struct LoadInfo {+    pub entry: u64,+    /// End of the highest segment: the heap (brk) starts on the next page.+    pub end: u64,+    /// Where the program headers are in the loaded image, if a segment+    /// includes them (AT_PHDR). Otherwise execve copies them to the stack.+    pub phdr: Option<u64>,+    /// File offset, entry size and count of the program headers.+    pub phoff: u64,+    pub phent: u64,+    pub phnum: u64,+}++/// Parse and check everything (headers, segment bounds, permissions) without+/// mapping anything: execve calls this *before* it destroys the old program,+/// so a bad file can still be reported as an error.+fn plan(data: &[u8]) -> Result<(LoadInfo, Vec<Segment>), ElfError> {     let (entry, segments) = parse(data)?;     if !(USER_START..USER_END).contains(&entry) {         return Err(ElfError::OutsideUserSpace);     }-    let nx_enabled = memory::nx_enabled();+    let segments = segments.collect::<Result<Vec<Segment>, ElfError>>()?;+    let (phoff, phent, phnum) = (read_u64(data, 32)?, read_u16(data, 54)? as u64, read_u16(data, 56)? as u64);+    let mut info = LoadInfo { entry, end: USER_START, phdr: None, phoff, phent, phnum };     // The previous segment's last page and its flags.-    let mut previous: Option<(Page<Size4KiB>, PageTableFlags)> = None;--    for segment in segments {-        let seg = segment?;+    let mut previous: Option<(u64, u32)> = None;+    for seg in &segments {         let file_end = seg.offset.checked_add(seg.filesz).ok_or(ElfError::BadProgramHeader)?;         let mem_end = seg.vaddr.checked_add(seg.memsz).ok_or(ElfError::BadProgramHeader)?;         if seg.filesz > seg.memsz || file_end > data.len() as u64 {             return Err(ElfError::BadProgramHeader);         }         // Segments must stay below the user stack and its guard zone.-        if seg.vaddr < USER_START || mem_end > crate::process::USER_STACK_ZONE_START {+        if seg.vaddr < USER_START || mem_end > crate::mm::USER_STACK_ZONE_START {             return Err(ElfError::OutsideUserSpace);         }         if seg.memsz == 0 {             continue;         }+        // Segments come sorted by address. If this one starts on the page+        // where the previous one ended, both would get one page with one+        // set of permissions: e.g. .bss on a read-only .rodata page, and the+        // program dies on its first write. Refuse such files up front.+        let perms = seg.flags & (PF_W | PF_X);+        if previous.is_some_and(|(last_page, f)| last_page >= seg.vaddr & !0xfff && f != perms) {+            return Err(ElfError::SharedPage);+        }+        previous = Some(((mem_end - 1) & !0xfff, perms));+        info.end = info.end.max(mem_end);+        // Are the program headers inside this segment's file bytes?+        let ph_end = phoff + phent * phnum;+        if seg.offset <= phoff && ph_end <= file_end {+            info.phdr = Some(seg.vaddr + (phoff - seg.offset));+        }+    }+    Ok((info, segments))+} +/// Load the ELF image `data` into the (empty) address space `mm`: map and+/// fill every segment (eagerly -- programs are small), and record each as+/// a region.+pub fn load(data: &[u8], mm: &mut Mm) -> Result<LoadInfo, ElfError> {+    let (info, segments) = plan(data)?;+    let nx_enabled = memory::nx_enabled();+    for seg in segments.iter().filter(|s| s.memsz > 0) {+        let mem_end = seg.vaddr + seg.memsz;         let mut flags = PageTableFlags::empty();+        let mut prot = PROT_READ;         if seg.flags & PF_W != 0 {             flags |= PageTableFlags::WRITABLE;+            prot |= PROT_WRITE;         }         if seg.flags & PF_X == 0 && nx_enabled {             flags |= PageTableFlags::NO_EXECUTE; // data can't be run as code         }+        if seg.flags & PF_X != 0 {+            prot |= PROT_EXEC;+        }          // Map every page the segment touches, then copy its file bytes in.         // We write through the kernel's physical-memory window (the new-        // address space isn't active, so its user addresses mean nothing here).+        // address space may not be the active one).         let first = Page::<Size4KiB>::containing_address(VirtAddr::new(seg.vaddr));         let last = Page::<Size4KiB>::containing_address(VirtAddr::new(mem_end - 1));-        // Segments come sorted by address. If this one starts on the page-        // where the previous one ended, map_user_page would keep the old-        // page's permissions: e.g. .bss on a read-only .rodata page, and the-        // program dies on its first write. Refuse such files up front.-        if previous.is_some_and(|(page, f)| page >= first && f != flags) {-            return Err(ElfError::SharedPage);-        }-        previous = Some((last, flags));         for page in Page::range_inclusive(first, last) {-            let frame = memory::map_user_page(l4, page, flags).ok_or(ElfError::OutOfMemory)?;+            let frame = memory::map_user_page(mm.l4, page, flags).ok_or(ElfError::OutOfMemory)?;              // Which part of this page is covered by file bytes?             let page_start = page.start_address().as_u64();@@ -163,15 +205,22 @@ pub fn load(data: &[u8], l4: PhysFrame) -> Result<u64, ElfError> {             }             // Bytes past filesz (.bss) stay zero: map_user_page zeroes new pages.         }+        // The region (a page shared with the previous segment -- same+        // permissions, checked above -- already belongs to that one).+        let mut start = first.start_address().as_u64();+        let end = last.start_address().as_u64() + 4096;+        if let Some(prev) = mm.vmas().last() {+            start = start.max(prev.end);+        }+        if start < end {+            mm.insert(Vma::new(start, end, prot, Kind::Program));+        }     }-    Ok(entry)+    Ok(info) } -/// Just validate the headers (used by tests).+/// Just validate the file (execve before the point of no return, tests).+/// Returns the entry point. pub fn check(data: &[u8]) -> Result<u64, ElfError> {-    let (entry, segments) = parse(data)?;-    for s in segments {-        s?;-    }-    Ok(entry)+    plan(data).map(|(info, _)| info.entry) }
@@ -4,7 +4,10 @@ #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(i64)] pub enum Errno {+    EPERM = 1,         // operation not permitted (e.g. setpgid on a stranger)     ENOENT = 2,        // no such file or directory+    ESRCH = 3,         // no such process (kill)+    EINTR = 4,         // interrupted by a signal     EIO = 5,           // input/output error (disk trouble)     E2BIG = 7,         // argument list too long     ENOEXEC = 8,       // not a valid executable@@ -46,14 +49,19 @@ pub enum Errno {     ENOTCONN = 107,       // not connected     ETIMEDOUT = 110,      // connect/DNS timed out     ECONNREFUSED = 111,   // nobody listens on that port (got a RST)+    // Kernel-internal (never seen by programs): "a signal interrupted this+    // call". On the way back to user space signal.rs turns them into+    // EINTR or restarts the call (Linux uses the same numbers).+    ERESTARTSYS = 512,    // restart if no handler ran, or it has SA_RESTART+    ERESTARTNOHAND = 514, // restart only if no handler ran (pause, sleep) }  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; 41] = [-            ENOENT, EIO, E2BIG, ENOEXEC, EBADF, ECHILD, EAGAIN, ENOMEM, EFAULT, EBUSY, EEXIST, ENODEV,+        const ALL: [Errno; 46] = [+            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,
@@ -71,6 +71,10 @@ pub trait File: Send + Sync {     fn as_framebuffer(&self) -> Option<&crate::dev::Framebuffer> {         None     }+    /// mmap of a file needs the regular file's inode behind the fd.+    fn inode(&self) -> Option<Arc<dyn Inode>> {+        None+    } }  /// A file or directory opened through the VFS: inode + current position.@@ -130,6 +134,10 @@ impl File for InodeFile {         Ok(Stat::of(&*self.inode))     } +    fn inode(&self) -> Option<Arc<dyn Inode>> {+        (self.inode.kind() == InodeKind::File).then(|| 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).
@@ -45,8 +45,8 @@ impl File for PipeReader {         if buf.is_empty() {             return Ok(0);         }-        // Block until there's data or no writer is left.-        Ok(self.0.readers_wait.wait_until(|| {+        // Block until there's data or no writer is left (or a signal).+        self.0.readers_wait.wait_until_intr(|| {             let mut state = self.0.state.lock();             if state.buffer.is_empty() {                 return if state.write_closed { Some(0) } else { None };@@ -56,7 +56,7 @@ impl File for PipeReader {                 *dst = src;             }             Some(n)-        }))+        })     }     fn write(&self, _buf: &[u8]) -> Result<usize, Errno> {         Err(Errno::EBADF) // wrong end
@@ -10,9 +10,13 @@ //! All CPUs share this one IDT. Device IRQs (PIT, keyboard) only ever //! arrive on CPU 0; APIC timer ticks and IPIs arrive on every CPU. //!-//! Handlers use the special `extern "x86-interrupt"` calling convention: the-//! compiler saves/restores all registers and returns with `iretq`.+//! Most handlers use the special `extern "x86-interrupt"` calling convention:+//! the compiler saves/restores all registers and returns with `iretq`. The+//! ones that can interrupt a user program and must be able to send it a+//! signal (faults, the APIC timer) use trap.rs's stubs instead. +use crate::signal::{self, SigInfo};+use crate::trap::{self, TrapFrame}; use crate::{gdt, println, serial_println}; use core::sync::atomic::{AtomicU64, Ordering}; use lazy_static::lazy_static;@@ -58,10 +62,17 @@ lazy_static! {     static ref IDT: InterruptDescriptorTable = {         let mut idt = InterruptDescriptorTable::new();         idt.breakpoint.set_handler_fn(breakpoint_handler);-        idt.page_fault.set_handler_fn(page_fault_handler);-        idt.general_protection_fault.set_handler_fn(general_protection_fault_handler);-        idt.invalid_opcode.set_handler_fn(invalid_opcode_handler);         unsafe {+            // Exceptions a program can cause, and the APIC timer, go through+            // trap.rs's stubs: they save all registers in a TrapFrame, so a+            // signal can be delivered on the way back to user mode.+            let addr = |f: unsafe extern "C" fn()| x86_64::VirtAddr::new(f as usize as u64);+            idt.divide_error.set_handler_addr(addr(trap::trap_divide_error));+            idt.invalid_opcode.set_handler_addr(addr(trap::trap_invalid_opcode));+            idt.general_protection_fault.set_handler_addr(addr(trap::trap_general_protection));+            idt.page_fault.set_handler_addr(addr(trap::trap_page_fault));+            idt[InterruptIndex::ApicTimer.as_u8()].set_handler_addr(addr(trap::trap_apic_timer));+             // Run the double-fault handler on its own known-good stack (IST).             idt.double_fault                 .set_handler_fn(double_fault_handler)@@ -77,7 +88,6 @@ lazy_static! {         idt[PIC_1_OFFSET + 9].set_handler_fn(pci_irq9_handler);         idt[PIC_1_OFFSET + 10].set_handler_fn(pci_irq10_handler);         idt[PIC_1_OFFSET + 11].set_handler_fn(pci_irq11_handler);-        idt[InterruptIndex::ApicTimer.as_u8()].set_handler_fn(apic_timer_handler);         idt[InterruptIndex::Reschedule.as_u8()].set_handler_fn(reschedule_handler);         idt[InterruptIndex::ApicSpurious.as_u8()].set_handler_fn(spurious_handler);         idt@@ -99,55 +109,74 @@ extern "x86-interrupt" fn breakpoint_handler(stack_frame: InterruptStackFrame) {     serial_println!("EXCEPTION: BREAKPOINT\n{:#?}", stack_frame); } -/// Did the exception happen in user mode (ring 3)? Then it's the process's-/// fault, not the kernel's: kill the process and keep the kernel running.-/// The CPU puts the interrupted code's CS in the frame; its low 2 bits are-/// the privilege level the code ran at.-fn kill_if_user(stack_frame: &InterruptStackFrame, what: &'static str) {-    use x86_64::PrivilegeLevel;-    if stack_frame.code_segment.rpl() == PrivilegeLevel::Ring3 {-        serial_println!(-            "[kernel] {} in user mode at rip {:#x}",-            what,-            stack_frame.instruction_pointer.as_u64()-        );-        crate::process::exit_current(crate::process::ExitStatus::Killed(what)); // never returns+/// Rust side of trap.rs's stubs: CPU exceptions a program can cause, and+/// the APIC timer. On the way out, if we're returning to user mode, act on+/// pending signals (that may run a handler, or end the process).+pub extern "C" fn trap_handler(frame: &mut TrapFrame) {+    match frame.vector {+        0x40 => {+            // Every CPU's APIC timer, ~100 times a second: time's up for the+            // running thread, let the next one have a go (see thread.rs).+            crate::apic::eoi(); // before switching: the next thread may be new+            crate::thread::timer_tick();+        }+        14 => page_fault(frame),+        0 => exception(frame, signal::SIGFPE, 1 /* FPE_INTDIV */, frame.rip, "divide error"),+        6 => exception(frame, signal::SIGILL, 1 /* ILL_ILLOPC */, frame.rip, "invalid opcode"),+        13 => exception(frame, signal::SIGSEGV, signal::SI_KERNEL, 0, "general protection fault"),+        v => panic!("unexpected trap {}", v),     }+    signal::deliver(frame); } -/// General protection fault: e.g. a privileged instruction (`hlt`, `cli`)-/// in ring 3, or a bad segment selector.-extern "x86-interrupt" fn general_protection_fault_handler(-    stack_frame: InterruptStackFrame,-    error_code: u64,-) {-    kill_if_user(&stack_frame, "general protection fault");-    panic!("EXCEPTION: GENERAL PROTECTION FAULT (error {:#x})\n{:#?}", error_code, stack_frame);-}--/// Invalid opcode (`ud2`, or garbage executed as code).-extern "x86-interrupt" fn invalid_opcode_handler(stack_frame: InterruptStackFrame) {-    kill_if_user(&stack_frame, "invalid opcode");-    panic!("EXCEPTION: INVALID OPCODE\n{:#?}", stack_frame);+/// An exception in user mode is the program's fault, not the kernel's: it+/// gets a signal (which kills it, unless it has a handler). In the kernel+/// it's a bug: stop.+fn exception(frame: &TrapFrame, sig: u32, code: i32, addr: u64, what: &'static str) {+    if !frame.from_user() {+        panic!("EXCEPTION: {} (error {:#x})\n{:#x?}", what, frame.error, frame);+    }+    serial_println!("[kernel] {} in user mode at rip {:#x}", what, frame.rip);+    signal::force(sig, SigInfo::fault(sig, code, addr), what); } -/// Page fault: accessing unmapped/protected memory. CR2 holds the bad address,-/// the error code says why (not present? write? user mode?). A real OS would-/// load the page from disk / swap here; we kill a faulting user process, and-/// stop if the kernel itself faulted.-extern "x86-interrupt" fn page_fault_handler(-    stack_frame: InterruptStackFrame,-    error_code: PageFaultErrorCode,-) {+/// Page fault: CR2 holds the address, the error code says why (page not+/// present? write? user mode? instruction fetch?). In user mode this is+/// usually *not* an error: demand paging and copy-on-write (mm.rs) fix the+/// page up and the instruction runs again. Only if the address isn't in+/// any region (or the region forbids the access) does the program get+/// SIGSEGV. The kernel itself never faults on user memory (syscalls check+/// buffers first), so a kernel-mode page fault is a bug.+fn page_fault(frame: &mut TrapFrame) {+    use crate::mm::Fault;     use x86_64::registers::control::Cr2;     let addr = Cr2::read_raw();-    if error_code.contains(PageFaultErrorCode::USER_MODE) {-        serial_println!("[kernel] user page fault: address {:#x}, error {:?}", addr, error_code);-        // Just below the user stack is a guard zone that's never mapped.-        if crate::process::in_user_stack_guard(addr) {-            kill_if_user(&stack_frame, "stack overflow");-        }-        kill_if_user(&stack_frame, "page fault");+    let error = PageFaultErrorCode::from_bits_truncate(frame.error);+    if frame.from_user() {+        // We came from ring 3, so we hold no locks: interrupts may come in+        // (reading a file-backed page waits for the disk).+        x86_64::instructions::interrupts::enable();+        let write = error.contains(PageFaultErrorCode::CAUSED_BY_WRITE);+        let exec = error.contains(PageFaultErrorCode::INSTRUCTION_FETCH);+        let p = crate::thread::current_process().expect("user fault without a process");+        let result = if (crate::memory::USER_START..crate::memory::USER_END).contains(&addr) {+            p.mm.lock().handle_fault(addr, write, exec)+        } else {+            Err(Fault::Unmapped) // a kernel address+        };+        drop(p);+        let Err(fault) = result else { return }; // fixed: retry the instruction+        serial_println!("[kernel] user page fault: address {:#x}, rip {:#x}, {:?}, {:?}", addr, frame.rip, error, fault);+        let (sig, code, why) = match fault {+            // Just below the user stack is a guard zone that's never mapped.+            Fault::Unmapped if crate::process::in_user_stack_guard(addr) => (signal::SIGSEGV, signal::SEGV_MAPERR, "stack overflow"),+            Fault::Unmapped => (signal::SIGSEGV, signal::SEGV_MAPERR, "page fault"),+            Fault::Protection => (signal::SIGSEGV, signal::SEGV_ACCERR, "page fault"),+            Fault::Io => (signal::SIGBUS, 2 /* BUS_ADRERR */, "I/O error"),+            Fault::OutOfMemory => (signal::SIGKILL, signal::SI_KERNEL, "out of memory"),+        };+        signal::force(sig, SigInfo::fault(sig, code, addr), why);+        return;     }     // The kernel touched a kernel stack's guard page: that thread overflowed.     if crate::stack::is_guard_page(addr) {@@ -155,10 +184,10 @@ extern "x86-interrupt" fn page_fault_handler(     }     println!("EXCEPTION: PAGE FAULT at {:?}", Cr2::read());     serial_println!(-        "EXCEPTION: PAGE FAULT\nAccessed address: {:?}\nError code: {:?}\n{:#?}",+        "EXCEPTION: PAGE FAULT\nAccessed address: {:?}\nError code: {:?}\n{:#x?}",         Cr2::read(),-        error_code,-        stack_frame+        error,+        frame     );      // In the deliberate page-fault demo, reaching here means success.@@ -242,13 +271,6 @@ extern "x86-interrupt" fn timer_interrupt_handler(_stack_frame: InterruptStackFr     } } -/// Every CPU's APIC timer, ~100 times a second: time's up for the running-/// thread, let the next one have a go (see thread.rs).-extern "x86-interrupt" fn apic_timer_handler(_stack_frame: InterruptStackFrame) {-    crate::apic::eoi(); // before switching, as above-    crate::thread::timer_tick();-}- /// Another CPU queued work and saw we were idle. extern "x86-interrupt" fn reschedule_handler(_stack_frame: InterruptStackFrame) {     crate::apic::eoi();
@@ -34,14 +34,17 @@ pub mod fs; // VFS: Inode/FileSystem/File traits, mount table, ramfs pub mod gdt; // GDT (kernel + user segments) + TSS (RSP0, double-fault stack) pub mod interrupts; // IDT, exception handlers, PIC, timer + keyboard pub mod net; // networking: virtio-net driver + smoltcp TCP/IP, sockets-pub mod memory; // paging, frame allocator, per-process address spaces+pub mod memory; // paging, frame allocator, page tables, shared-frame counts+pub mod mm; // a process's memory map: regions, demand paging, copy-on-write pub mod pci; // PCI bus enumeration (config space via ports 0xCF8/0xCFC)-pub mod process; // processes: address space + thread + fd table, ring 3 entry+pub mod process; // processes: fork, execve, exit, wait4, process groups pub mod serial; // COM1 serial port+pub mod signal; // signals: kill, handlers, signal frames, default actions pub mod smp; // multi-core: start the other CPUs, per-CPU data pub mod stack; // kernel stacks with guard pages pub mod syscall; // syscall/sysret entry + system call implementations pub mod task; // cooperative multitasking: async tasks + executor+pub mod trap; // TrapFrame: saved user registers, the common return to ring 3 pub mod time; // microsecond clock (TSC calibrated against the PIT) pub mod thread; // threads + the multi-core scheduler (sleep/wake, wait queues) pub mod tty; // console: keyboard line discipline + fd 0/1/2
@@ -17,6 +17,7 @@ //! swaps only the user part. (Linux does the same with the upper half; our //! bootloader put the kernel in the *lower* half, so we use a free slot.) +use alloc::collections::BTreeMap; use bootloader::bootinfo::{MemoryMap, MemoryRegionType}; use core::sync::atomic::{AtomicU64, Ordering}; use spin::Mutex;@@ -50,6 +51,14 @@ static FRAME_ALLOCATOR: Mutex<Option<BootInfoFrameAllocator>> = Mutex::new(None) pub unsafe fn init(physical_memory_offset: VirtAddr) -> OffsetPageTable<'static> {     PHYS_OFFSET.store(physical_memory_offset.as_u64(), Ordering::Relaxed);     KERNEL_L4.store(Cr3::read().0.start_address().as_u64(), Ordering::Relaxed);+    // CR0.WP: read-only pages are read-only for the kernel too. Without it+    // a kernel write into a copy-on-write page would silently change the+    // frame that parent and child share. (The bootloader sets it already;+    // this makes sure. The other CPUs copy our CR0 in smp.rs.)+    unsafe {+        use x86_64::registers::control::{Cr0, Cr0Flags};+        Cr0::update(|flags| flags.insert(Cr0Flags::WRITE_PROTECT));+    }     unsafe {         let level_4_table = active_level_4_table(physical_memory_offset);         OffsetPageTable::new(level_4_table, physical_memory_offset)@@ -246,7 +255,7 @@ pub fn alloc_contiguous_frames(count: usize) -> Option<PhysAddr> {  /// A frame filled with zeros (new page tables and user pages must not leak /// old data).-fn alloc_zeroed_frame() -> Option<PhysFrame> {+pub fn alloc_zeroed_frame() -> Option<PhysFrame> {     let frame = alloc_frame()?;     let ptr: *mut u8 = phys_to_virt(frame.start_address()).as_mut_ptr();     unsafe { core::ptr::write_bytes(ptr, 0, 4096) };@@ -280,28 +289,37 @@ pub fn new_address_space() -> Option<PhysFrame> {     Some(frame) } -/// Free everything a process's address space owns: every user page's frame,-/// the page tables of the user slot (levels 3, 2, 1) and the level-4 table.-/// The kernel entries are shared with every other address space, so they're-/// left alone.+/// Free everything a process's address space owns: every user page's frame+/// (or just our share of it, see `release_frame`), the page tables of the+/// user slot (levels 3, 2, 1) and the level-4 table. The kernel entries+/// are shared with every other address space, so they're left alone. /// /// Unsafe: `l4` must not be in use by any thread (not loaded in CR3, and the /// thread that ran in it must have finished). pub unsafe fn free_address_space(l4: PhysFrame) {-    // A page table, reached through the physical-memory map.-    fn table(frame: PhysFrame) -> &'static PageTable {-        unsafe { &*phys_to_virt(frame.start_address()).as_ptr() }-    }+    clear_user_space(l4);+    free_frame(l4);+}++/// A page table, reached through the physical-memory map.+fn table_mut(frame: PhysFrame) -> &'static mut PageTable {+    unsafe { &mut *phys_to_virt(frame.start_address()).as_mut_ptr() }+}++/// Remove the whole user part of address space `l4` (execve does this to+/// the running process: the caller must flush the TLB afterwards). The+/// level-4 table itself stays.+pub fn clear_user_space(l4: PhysFrame) {     // The used entries' frames (user space only has 4 KiB pages, no huge ones).-    fn used(t: &'static PageTable) -> impl Iterator<Item = PhysFrame> {+    fn used(t: &'static mut PageTable) -> impl Iterator<Item = PhysFrame> {         t.iter().filter(|e| !e.is_unused()).filter_map(|e| e.frame().ok())     }-    let user_l4_entry = &table(l4)[USER_L4_INDEX];+    let user_l4_entry = &mut table_mut(l4)[USER_L4_INDEX];     if let Ok(l3) = user_l4_entry.frame() {-        for l2 in used(table(l3)) {-            for l1 in used(table(l2)) {-                for page in used(table(l1)) {-                    free_frame(page);+        for l2 in used(table_mut(l3)) {+            for l1 in used(table_mut(l2)) {+                for page in used(table_mut(l1)) {+                    release_frame(page);                 }                 free_frame(l1);             }@@ -309,7 +327,153 @@ pub unsafe fn free_address_space(l4: PhysFrame) {         }         free_frame(l3);     }-    free_frame(l4);+    user_l4_entry.set_unused();+}++// ---------------------------------------------------------------------------+// Shared frames (copy-on-write and shared memory)+// ---------------------------------------------------------------------------+//+// After fork, parent and child map the *same* frames until one of them+// writes (copy-on-write); MAP_SHARED memory is mapped by several processes+// for good. A frame may only be freed when the last of them lets go, so+// shared frames carry a reference count. Most frames are never shared, so+// instead of a counter for every frame of RAM (Linux's `struct page`) we+// keep a small map with only the shared ones: no entry = one owner.++/// Physical address -> number of owners (always >= 2). Locked with+/// interrupts off (frames are released in the scheduler, see thread.rs).+static SHARED_FRAMES: Mutex<BTreeMap<u64, u32>> = Mutex::new(BTreeMap::new());++/// One more address space maps `frame`.+pub fn share_frame(frame: PhysFrame) {+    without_interrupts(|| {+        let mut shared = SHARED_FRAMES.lock();+        let count = shared.entry(frame.start_address().as_u64()).or_insert(1);+        *count += 1;+    })+}++/// How many address spaces map `frame` (1 if it isn't shared).+pub fn frame_refcount(frame: PhysFrame) -> u32 {+    without_interrupts(|| SHARED_FRAMES.lock().get(&frame.start_address().as_u64()).copied().unwrap_or(1))+}++/// An address space stops using `frame`: free it if that was the last one.+pub fn release_frame(frame: PhysFrame) {+    let last = without_interrupts(|| {+        let mut shared = SHARED_FRAMES.lock();+        let addr = frame.start_address().as_u64();+        match shared.get_mut(&addr) {+            Some(count) => {+                *count -= 1;+                if *count == 1 {+                    shared.remove(&addr); // one owner left: no entry needed+                }+                false+            }+            None => true,+        }+    });+    if last {+        free_frame(frame);+    }+}++/// Number of frames with more than one owner right now (for tests).+pub fn shared_frame_count() -> usize {+    without_interrupts(|| SHARED_FRAMES.lock().len())+}++// ---------------------------------------------------------------------------+// User page-table entries, one page at a time (mm.rs decides what goes where)+// ---------------------------------------------------------------------------++/// Flags for the L3/L2/L1 tables above user pages: always present, writable+/// and user-accessible. The *leaf* entry alone decides what's allowed (the+/// CPU combines all levels, so the upper ones must allow everything).+const USER_TABLE_FLAGS: PageTableFlags =+    PageTableFlags::PRESENT.union(PageTableFlags::WRITABLE).union(PageTableFlags::USER_ACCESSIBLE);++fn user_page(addr: u64) -> Page<Size4KiB> {+    assert!((USER_START..USER_END).contains(&addr), "not a user address");+    Page::containing_address(VirtAddr::new(addr))+}++/// The frame and flags mapped at user address `addr` in `l4`, if any.+pub fn user_pte(l4: PhysFrame, addr: u64) -> Option<(PhysFrame, PageTableFlags)> {+    let mapper = unsafe { mapper_for(l4) };+    match mapper.translate(VirtAddr::new(addr & !0xfff)) {+        TranslateResult::Mapped { frame, flags, .. } => {+            Some((PhysFrame::containing_address(frame.start_address()), flags))+        }+        _ => None,+    }+}++/// Map `frame` at user address `addr` (must be unmapped) with `flags`.+pub fn map_user_frame(l4: PhysFrame, addr: u64, frame: PhysFrame, flags: PageTableFlags) -> Option<()> {+    let mut mapper = unsafe { mapper_for(l4) };+    let page = user_page(addr);+    unsafe { mapper.map_to_with_table_flags(page, frame, flags, USER_TABLE_FLAGS, &mut GlobalFrameAllocator).ok()?.ignore() };+    Some(())+}++/// Change the flags of the page at `addr` (must be mapped).+pub fn set_user_pte_flags(l4: PhysFrame, addr: u64, flags: PageTableFlags) {+    let mut mapper = unsafe { mapper_for(l4) };+    if let Ok(flush) = unsafe { mapper.update_flags(user_page(addr), flags) } {+        flush.ignore(); // the caller flushes (flush_user_page)+    }+}++/// Point the (mapped) page at `addr` to another frame. Returns the old one.+pub fn replace_user_frame(l4: PhysFrame, addr: u64, frame: PhysFrame, flags: PageTableFlags) -> Option<PhysFrame> {+    let old = unmap_user_page(l4, addr)?;+    map_user_frame(l4, addr, frame, flags)?;+    Some(old)+}++/// Remove the mapping at `addr`; returns its frame (the caller releases it).+/// The page tables stay (they're freed with the address space).+pub fn unmap_user_page(l4: PhysFrame, addr: u64) -> Option<PhysFrame> {+    let mut mapper = unsafe { mapper_for(l4) };+    let (frame, flush) = mapper.unmap(user_page(addr)).ok()?;+    flush.ignore();+    Some(frame)+}++/// Drop this CPU's cached translation of user page `addr` (call after+/// changing a mapping of the *active* address space; for an inactive one+/// it's harmless). Other CPUs can't have it cached: a process runs on one+/// CPU at a time and switching CR3 flushes user mappings.+pub fn flush_user_page(addr: u64) {+    x86_64::instructions::tlb::flush(VirtAddr::new(addr & !0xfff));+}++/// Copy the user part of address space `src` into the (empty) `dst`, for+/// fork. Both end up mapping the same frames (each frame's reference count+/// goes up). `flags(addr, old)` decides the new flags of each page, in+/// *both* address spaces -- fork uses it to make private pages read-only,+/// so the first write to them faults and gets its own copy.+/// On failure (out of memory) `dst` may be partly filled: free it.+pub fn clone_user_space(src: PhysFrame, dst: PhysFrame, mut flags: impl FnMut(u64, PageTableFlags) -> PageTableFlags) -> Option<()> {+    let Ok(l3) = table_mut(src)[USER_L4_INDEX].frame() else { return Some(()) };+    for (i3, e3) in table_mut(l3).iter().enumerate().filter(|(_, e)| !e.is_unused()) {+        let l2 = e3.frame().ok()?;+        for (i2, e2) in table_mut(l2).iter().enumerate().filter(|(_, e)| !e.is_unused()) {+            let l1 = e2.frame().ok()?;+            for (i1, e1) in table_mut(l1).iter_mut().enumerate().filter(|(_, e)| !e.is_unused()) {+                let addr = USER_START + ((i3 as u64) << 30) + ((i2 as u64) << 21) + ((i1 as u64) << 12);+                let frame = e1.frame().ok()?;+                let new_flags = flags(addr, e1.flags());+                e1.set_flags(new_flags);+                map_user_frame(dst, addr, frame, new_flags)?;+                share_frame(frame);+            }+        }+    }+    Some(()) }  /// Make sure `page` (in user space) is mapped in address space `l4` with at@@ -337,7 +501,7 @@ pub fn map_user_page(l4: PhysFrame, page: Page, flags: PageTableFlags) -> Option     // USER_ACCESSIBLE, those parent entries get it too (the CPU checks the     // user bit at every level). The TLB doesn't matter: this address space     // may not even be active, and the mapping is new. Hence `ignore`.-    unsafe { mapper.map_to(page, frame, flags, &mut GlobalFrameAllocator).ok()?.ignore() };+    unsafe { mapper.map_to_with_table_flags(page, frame, flags, USER_TABLE_FLAGS, &mut GlobalFrameAllocator).ok()?.ignore() };     Some(frame) } 
@@ -0,0 +1,465 @@+//! A process's memory map: which address ranges exist and what they are.+//!+//! Stage 13 mapped every page of a program up front. Now an address space+//! is described by *regions* (Linux: VMAs, "virtual memory areas"), and+//! pages appear only when touched:+//!+//!   program   the ELF segments (code, rodata, data+bss), loaded eagerly+//!   heap      from the end of the program up to `brk` (grows with brk())+//!   mmap      what mmap() created: anonymous memory, or a file's bytes+//!   stack     1 MiB under USER_STACK_TOP, with a 64 KiB guard zone below+//!+//! **Demand paging.** A region is just a promise. The first access to one+//! of its pages finds no page-table entry, the CPU raises a page fault, and+//! `handle_fault` sees the address is inside a region: it allocates a+//! zeroed frame (or reads the file's bytes into it), maps it, and the+//! instruction runs again -- the program never notices. An access outside+//! every region (or one the region forbids, like a write to read-only+//! memory) is a real error: the process gets SIGSEGV. So mmap(64 MiB)+//! costs nothing until the program uses it.+//!+//! **Copy-on-write.** fork() doesn't copy memory: the child gets new page+//! tables pointing at the *same* frames, and every private writable page is+//! made read-only in both. Reads just work. The first write faults;+//! `handle_fault` sees "write to a writable region, page is read-only":+//! if somebody else still maps the frame (reference count > 1) it copies+//! the page into a fresh frame for the writer, otherwise the writer is the+//! last owner and simply gets write access back. Pages that are never+//! written (all the code, usually) stay shared forever.+//!+//! **Shared mappings** (MAP_SHARED | MAP_ANONYMOUS) stay shared across fork:+//! no copy-on-write, both processes see each other's writes. We allocate+//! their frames right away at mmap time (a page first touched *after* a+//! fork would otherwise get two different frames, one per process).+//!+//! **Kernel access to user memory.** Syscalls touch user buffers directly+//! (e.g. `read` copies into one). Before that, `user_access_ok` runs the+//! same fault handling for every page of the buffer, so the pages exist+//! and are writable (copies made) before the kernel uses them: the kernel+//! itself never page-faults on user memory.+//!+//! Locking: `Process::mm` is a plain spinlock, taken only by the process's+//! own thread (syscalls, its page faults, fork, exec) -- never by interrupt+//! handlers, and with interrupts on.++use crate::errno::Errno;+use crate::fs::Inode;+use crate::memory::{self, USER_END, USER_START};+use alloc::{sync::Arc, vec::Vec};+use x86_64::structures::paging::{PageTableFlags as Flags, PhysFrame};++pub const PAGE_SIZE: u64 = 4096;++/// The user stack: 1 MiB (pages appear as it grows), ending one unmapped+/// page below USER_END.+pub const USER_STACK_TOP: u64 = USER_END - 0x1000;+pub const USER_STACK_SIZE: u64 = 1024 * 1024;+/// Below the stack: a 64 KiB guard zone that is never mapped, so a runaway+/// recursion hits it and gets SIGSEGV ("stack overflow") instead of+/// silently growing into other memory. Big, so that one large stack frame+/// can't jump over it.+pub const USER_STACK_GUARD: u64 = 64 * 1024;+/// Lowest address of stack + guard: program segments must end below this.+pub const USER_STACK_ZONE_START: u64 = USER_STACK_TOP - USER_STACK_SIZE - USER_STACK_GUARD;+/// mmap hands out addresses downwards from here (the heap grows up towards it).+pub const MMAP_TOP: u64 = USER_STACK_ZONE_START;++/// Is user address `addr` in the guard zone under the user stack?+pub fn in_user_stack_guard(addr: u64) -> bool {+    (USER_STACK_ZONE_START..USER_STACK_TOP - USER_STACK_SIZE).contains(&addr)+}++// mmap/mprotect protection bits (Linux's values).+pub const PROT_NONE: u64 = 0;+pub const PROT_READ: u64 = 1;+pub const PROT_WRITE: u64 = 2;+pub const PROT_EXEC: u64 = 4;++#[derive(Clone, Copy, Debug, PartialEq, Eq)]+pub enum Kind {+    Program,+    Heap,+    Mmap,+    Stack,+}++/// One region: `start..end` (page aligned) with the same permissions and+/// the same kind of contents.+#[derive(Clone)]+pub struct Vma {+    pub start: u64,+    pub end: u64,+    pub prot: u64,+    /// MAP_SHARED: writes are seen by every process mapping it.+    pub shared: bool,+    /// File-backed: (inode, file offset of `start`). Pages are read from+    /// the file on first touch; bytes past the end of the file are zero.+    pub file: Option<(Arc<dyn Inode>, u64)>,+    pub kind: Kind,+}++impl Vma {+    pub fn new(start: u64, end: u64, prot: u64, kind: Kind) -> Vma {+        Vma { start, end, prot, shared: false, file: None, kind }+    }++    /// May a program access this region this way? (x86 can't express+    /// write-only, so any access needs at least one of the bits.)+    fn allows(&self, write: bool, exec: bool) -> bool {+        self.prot != PROT_NONE && (!write || self.prot & PROT_WRITE != 0) && (!exec || self.prot & PROT_EXEC != 0)+    }++    /// Page-table flags for one of its pages. `writable`: may this page be+    /// writable right now? (Not if it's a private page still shared with+    /// another process: copy-on-write.) PROT_NONE pages stay present but+    /// lose USER_ACCESSIBLE, so the frame isn't lost and any user access+    /// faults.+    fn pte_flags(&self, writable: bool) -> Flags {+        let mut flags = Flags::PRESENT;+        if self.prot != PROT_NONE {+            flags |= Flags::USER_ACCESSIBLE;+        }+        if writable && self.prot & PROT_WRITE != 0 {+            flags |= Flags::WRITABLE;+        }+        if self.prot & PROT_EXEC == 0 && memory::nx_enabled() {+            flags |= Flags::NO_EXECUTE;+        }+        flags+    }++    /// Cut off and return the part from `at` on (`at` inside the region).+    fn split_off(&mut self, at: u64) -> Vma {+        let mut tail = self.clone();+        tail.start = at;+        if let Some((_, offset)) = &mut tail.file {+            *offset += at - self.start;+        }+        self.end = at;+        tail+    }+}++/// Why a page fault couldn't be fixed.+#[derive(Debug, Clone, Copy, PartialEq, Eq)]+pub enum Fault {+    /// No region there: SIGSEGV (SEGV_MAPERR).+    Unmapped,+    /// The region doesn't allow this access: SIGSEGV (SEGV_ACCERR).+    Protection,+    /// No free frame left (Linux would wake the OOM killer).+    OutOfMemory,+    /// Reading the file failed: SIGBUS.+    Io,+}++/// A process's address space: its page tables and its regions.+pub struct Mm {+    /// Level-4 page table. Never changes (execve empties and reuses it).+    pub l4: PhysFrame,+    /// Regions, sorted by address, never overlapping.+    vmas: Vec<Vma>,+    /// The heap: brk_start..brk (brk_start is page aligned).+    pub brk_start: u64,+    pub brk: u64,+}++fn page_up(x: u64) -> u64 {+    x.next_multiple_of(PAGE_SIZE)+}++impl Mm {+    /// A new, empty address space.+    pub fn new() -> Option<Mm> {+        Some(Mm { l4: memory::new_address_space()?, vmas: Vec::new(), brk_start: 0, brk: 0 })+    }++    pub fn vmas(&self) -> &[Vma] {+        &self.vmas+    }++    /// The region containing `addr`.+    fn find(&self, addr: u64) -> Option<&Vma> {+        self.vmas.iter().find(|v| (v.start..v.end).contains(&addr))+    }++    /// Does any region overlap `start..end`?+    fn overlaps(&self, start: u64, end: u64) -> bool {+        self.vmas.iter().any(|v| v.start < end && start < v.end)+    }++    /// Add a region (the range must be free).+    pub fn insert(&mut self, vma: Vma) {+        debug_assert!(!self.overlaps(vma.start, vma.end));+        let i = self.vmas.iter().position(|v| v.start > vma.start).unwrap_or(self.vmas.len());+        self.vmas.insert(i, vma);+    }++    /// Split regions so that none crosses `start` or `end`.+    fn split_at(&mut self, start: u64, end: u64) {+        let mut i = 0;+        while i < self.vmas.len() {+            for at in [start, end] {+                let v = &mut self.vmas[i];+                if v.start < at && at < v.end {+                    let tail = v.split_off(at);+                    self.vmas.insert(i + 1, tail);+                }+            }+            i += 1;+        }+    }++    /// Remove everything in `start..end` (page aligned): regions and pages.+    pub fn unmap(&mut self, start: u64, end: u64) {+        self.split_at(start, end);+        self.vmas.retain(|v| v.end <= start || v.start >= end);+        for addr in (start..end).step_by(PAGE_SIZE as usize) {+            if let Some(frame) = memory::unmap_user_page(self.l4, addr) {+                memory::release_frame(frame);+                memory::flush_user_page(addr);+            }+        }+    }++    /// mprotect: new permissions for `start..end`, which must be fully+    /// covered by regions (else ENOMEM, like Linux).+    pub fn protect(&mut self, start: u64, end: u64, prot: u64) -> Result<(), Errno> {+        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); // a hole+            }+            covered = v.end;+        }+        if covered < end {+            return Err(Errno::ENOMEM);+        }+        self.split_at(start, end);+        for v in self.vmas.iter_mut().filter(|v| v.start >= start && v.end <= end) {+            v.prot = prot;+            // Pages already there get the new flags. Private pages start out+            // read-only: the next write (if allowed) makes them writable+            // again, copying first if the frame is shared.+            for addr in (v.start..v.end).step_by(PAGE_SIZE as usize) {+                if memory::user_pte(self.l4, addr).is_some() {+                    memory::set_user_pte_flags(self.l4, addr, v.pte_flags(v.shared));+                    memory::flush_user_page(addr);+                }+            }+        }+        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> {+        let floor = page_up(self.brk).max(USER_START);+        let mut top = MMAP_TOP;+        for v in self.vmas.iter().rev().filter(|v| v.start < MMAP_TOP) {+            if v.end <= top && top - v.end >= len {+                return Some(top - len); // the gap above this region fits+            }+            top = top.min(v.start);+        }+        (top >= floor + len).then(|| top - len)+    }++    /// Is `start..end` inside user space and free (for MAP_FIXED / brk)?+    pub fn range_ok(start: u64, end: u64) -> bool {+        start >= USER_START && end <= MMAP_TOP && start < end+    }++    /// Fill the pages of `start..end` right away (shared anonymous memory).+    pub fn populate(&mut self, start: u64, end: u64) -> Result<(), Fault> {+        for addr in (start..end).step_by(PAGE_SIZE as usize) {+            self.handle_fault(addr, false, false)?;+        }+        Ok(())+    }++    /// brk(addr): move the end of the heap. Returns the new end (the old+    /// one if the request can't be met -- that's how Linux reports errors).+    pub fn set_brk(&mut self, new: u64) -> u64 {+        if new < self.brk_start {+            return self.brk; // brk(0) asks where the heap ends+        }+        let (old_end, new_end) = (page_up(self.brk), page_up(new));+        if new_end > old_end {+            if !Mm::range_ok(old_end, new_end) || self.overlaps(old_end, new_end) {+                return self.brk; // would run into a mapping+            }+            match self.vmas.iter_mut().find(|v| v.kind == Kind::Heap && v.end == old_end) {+                Some(heap) => heap.end = new_end,+                None => self.insert(Vma::new(old_end, new_end, PROT_READ | PROT_WRITE, Kind::Heap)),+            }+        } else if new_end < old_end {+            self.unmap(new_end, old_end);+        }+        self.brk = new;+        new+    }++    /// The page fault handler's real work (also used before the kernel+    /// touches user memory): make the page at `addr` present and allow the+    /// access, if its region allows it.+    pub fn handle_fault(&mut self, addr: u64, write: bool, exec: bool) -> Result<(), Fault> {+        let vma = self.find(addr).ok_or(Fault::Unmapped)?;+        if !vma.allows(write, exec) {+            return Err(Fault::Protection);+        }+        let page = addr & !(PAGE_SIZE - 1);+        match memory::user_pte(self.l4, page) {+            // Not there yet: demand paging.+            None => {+                let frame = memory::alloc_zeroed_frame().ok_or(Fault::OutOfMemory)?;+                if let Some((inode, offset)) = &vma.file {+                    let dst = memory::phys_to_virt(frame.start_address()).as_mut_ptr::<u8>();+                    let buf = unsafe { core::slice::from_raw_parts_mut(dst, PAGE_SIZE as usize) };+                    // A short read (end of file) leaves the rest zero.+                    if inode.read_at((offset + page - vma.start) as usize, buf).is_err() {+                        memory::free_frame(frame);+                        return Err(Fault::Io);+                    }+                }+                let flags = vma.pte_flags(true);+                if memory::map_user_frame(self.l4, page, frame, flags).is_none() {+                    memory::free_frame(frame);+                    return Err(Fault::OutOfMemory);+                }+            }+            // There, but read-only, and this is a write: copy-on-write.+            Some((frame, flags)) if write && !flags.contains(Flags::WRITABLE) => {+                if vma.shared || memory::frame_refcount(frame) == 1 {+                    // We're the only owner (the other one copied or exited):+                    // no copy needed, just allow writing again.+                    memory::set_user_pte_flags(self.l4, page, vma.pte_flags(true));+                } else {+                    let copy = memory::alloc_frame().ok_or(Fault::OutOfMemory)?;+                    unsafe {+                        core::ptr::copy_nonoverlapping(+                            memory::phys_to_virt(frame.start_address()).as_ptr::<u8>(),+                            memory::phys_to_virt(copy.start_address()).as_mut_ptr::<u8>(),+                            PAGE_SIZE as usize,+                        );+                    }+                    let flags = vma.pte_flags(true);+                    memory::replace_user_frame(self.l4, page, copy, flags).ok_or(Fault::OutOfMemory)?;+                    memory::release_frame(frame); // our share of the original+                }+            }+            // There and allowed by the region, but the PTE says no: after an+            // mprotect that widened the permissions. Refresh the flags.+            Some((frame, _)) => {+                let writable = vma.shared || memory::frame_refcount(frame) == 1;+                memory::set_user_pte_flags(self.l4, page, vma.pte_flags(writable && write));+            }+        }+        memory::flush_user_page(page);+        Ok(())+    }++    /// Make sure the program could access `addr..addr+len` (faulting pages+    /// in as needed), so the kernel can safely use the memory directly.+    pub fn prepare(&mut self, addr: u64, len: u64, write: bool) -> bool {+        let Some(end) = addr.checked_add(len) else { return false };+        if addr < USER_START || end > USER_END {+            return false;+        }+        let mut page = addr & !(PAGE_SIZE - 1);+        while page < end {+            let ok = match memory::user_pte(self.l4, page) {+                Some((_, f)) => f.contains(Flags::USER_ACCESSIBLE) && (!write || f.contains(Flags::WRITABLE)),+                None => false,+            };+            if !ok && self.handle_fault(page, write, false).is_err() {+                return false;+            }+            page += PAGE_SIZE;+        }+        true+    }++    /// Copy `data` to user address `addr` of this address space (which+    /// need not be the active one: we write through the physical-memory+    /// window). Used to build a new program's stack.+    pub fn write_bytes(&mut self, mut addr: u64, mut data: &[u8]) -> Result<(), Fault> {+        while !data.is_empty() {+            self.handle_fault(addr, true, false)?;+            let (frame, _) = memory::user_pte(self.l4, addr).ok_or(Fault::Unmapped)?;+            let offset = addr % PAGE_SIZE;+            let n = data.len().min((PAGE_SIZE - offset) as usize);+            let dst = memory::phys_to_virt(frame.start_address()) + offset;+            unsafe { core::ptr::copy_nonoverlapping(data.as_ptr(), dst.as_mut_ptr::<u8>(), n) };+            addr += n as u64;+            data = &data[n..];+        }+        Ok(())+    }++    /// fork: a copy of this address space sharing every frame. Private+    /// writable pages become read-only in *both* (copy-on-write); shared+    /// ones stay as they are. Our own TLB must forget the old writable+    /// entries, so the caller (the running process) flushes it.+    pub fn fork(&self) -> Option<Mm> {+        let mut child = Mm::new()?;+        child.vmas = self.vmas.clone();+        child.brk_start = self.brk_start;+        child.brk = self.brk;+        let vmas = &self.vmas;+        let ok = memory::clone_user_space(self.l4, child.l4, |addr, flags| {+            let shared = vmas.iter().any(|v| v.shared && (v.start..v.end).contains(&addr));+            if shared { flags } else { flags - Flags::WRITABLE }+        });+        flush_all();+        ok.map(|_| child) // on failure, dropping `child` frees what it got+    }++    /// execve: throw the whole user part away (regions, pages, tables).+    pub fn clear(&mut self) {+        memory::clear_user_space(self.l4);+        self.vmas.clear();+        self.brk_start = 0;+        self.brk = 0;+        flush_all();+    }++    /// Pages mapped right now (resident) and the total size of all regions.+    pub fn usage(&self) -> (u64, u64) {+        let mut resident = 0;+        let mut total = 0;+        for v in &self.vmas {+            total += v.end - v.start;+            for addr in (v.start..v.end).step_by(PAGE_SIZE as usize) {+                resident += memory::user_pte(self.l4, addr).is_some() as u64;+            }+        }+        (resident, total / PAGE_SIZE)+    }+}++impl Drop for Mm {+    /// The process is gone (its thread finished, nobody runs on these+    /// page tables): give every frame back.+    fn drop(&mut self) {+        unsafe { memory::free_address_space(self.l4) };+    }+}++/// Forget all of this CPU's user translations (reload CR3).+fn flush_all() {+    use x86_64::registers::control::Cr3;+    let (frame, flags) = Cr3::read();+    unsafe { Cr3::write(frame, flags) };+}++/// Syscalls: may the *current* process access `len` bytes at `addr`+/// (faulting the pages in, breaking copy-on-write for writes)? Never trust+/// a pointer from user space: without this check a program could make the+/// kernel read or overwrite kernel memory for it.+pub fn user_access_ok(addr: u64, len: u64, write: bool) -> bool {+    match crate::thread::current_process() {+        Some(p) => p.mm.lock().prepare(addr, len, write),+        None => false, // kernel threads have no user memory+    }+}
@@ -122,11 +122,13 @@ impl Socket {         mut f: impl FnMut(&mut State, &mut Net) -> Option<Result<R, Errno>>,     ) -> Result<R, Errno> {         let deadline = timeout_us.map_or(u64::MAX, crate::time::deadline_after_us);+        // Interruptible: Ctrl+C must be able to stop a program waiting+        // for the network.         super::CHANGED-            .wait_until_deadline(deadline, || match self.locked(&mut f) {+            .wait_until_deadline_intr(deadline, || match self.locked(&mut f) {                 Ok(r) => r,                 Err(e) => Some(Err(e)),-            })+            })?             .unwrap_or(Err(Errno::EAGAIN))     } 
@@ -1,130 +1,208 @@-//! Processes = a thread + its own address space + a file descriptor table-//! (+ arguments, a current directory and a parent).+//! Processes: a thread + an address space (`Mm`) + a file descriptor table+//! (+ arguments, environment, a current directory, a parent, a process+//! group and signal state). //!-//! Starting one (`spawn_image`):-//!   1. new level-4 page table sharing the kernel mappings (memory.rs)-//!   2. load the ELF program's segments into its user part (elf.rs)-//!   3. map a user stack just below `USER_END`-//!   4. fd table: 0, 1, 2 = whatever the parent chose (console by default,-//!      or e.g. a pipe -- that's how the shell will do `a | b` and `> file`)-//!   5. put it in the process table and spawn a thread with that CR3. The-//!      thread starts in the kernel (`user_thread_entry`), copies the-//!      arguments onto the user stack and "returns" into ring 3 with `iretq`.+//! **How Unix makes processes: fork + exec.** There's no "start program X"+//! call. Instead:+//!   * `fork()` duplicates the calling process. Both copies return from the+//!     same fork call: the parent gets the child's pid, the child gets 0.+//!     The child has a copy of everything: memory, open files, cwd, signal+//!     handlers.+//!   * `execve(path, argv, envp)` replaces the *running* program with+//!     another one: new code, new memory, same process (pid, open files,+//!     cwd, parent).+//! Why split it? Because between the two the child is still running the+//! *parent's* code, and can set itself up with all the normal syscalls:+//! `dup2` a pipe onto stdout, `chdir`, `setpgid`, reset signal handlers,+//! close files. No "spawn options" structure has to anticipate every+//! possible setup: a shell does `cmd > file` as fork, open, dup2, exec.+//! Copying all memory just to throw it away at exec would be slow; that's+//! what copy-on-write (mm.rs) is for: fork only copies page tables. //!-//! Why `spawn(path, argv)` instead of Unix `fork()` + `exec()`? fork copies-//! the whole address space (or needs copy-on-write page tables to be fast),-//! and then exec throws the copy away again. spawn ("create a new process-//! running this program") is one simple step, and it's what Windows-//! (CreateProcess) and POSIX's own `posix_spawn` offer. Redirection, which-//! Unix shells do between fork and exec, is done with spawn's stdio options.+//! `spawn` (our syscall 500 from stage 9, and `spawn_path` for the kernel)+//! stays as a convenience: new process + exec in one step, like+//! `posix_spawn`. //!-//! Exit and wait: a process's exit status is stored in it and its `exited`-//! wait queue is woken; the parent's `wait(pid)` blocks on that queue, waits-//! until the thread is completely gone, then removes the child from the-//! process table ("reaps" it -- until then it's what Unix calls a zombie).+//! **Exit and wait.** A process's exit status is stored in it, its parent+//! gets SIGCHLD and is woken; `wait4` returns the status and removes the+//! 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. //!-//! Memory: a process's frames and page tables are freed when the last-//! `Arc<Process>` goes away (`impl Drop`). That's always after its thread is-//! gone: the thread holds an Arc until the scheduler frees it, which happens-//! only once the CPU has switched to another thread and address space. So-//! no CPU can still be using the page tables we free.+//! **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+//! and tells the terminal which group is in the *foreground*; Ctrl+C then+//! sends SIGINT to that whole group -- every program of the pipeline, but+//! not the shell (tty.rs). //!-//! Orphans: when a parent exits first, its running children become orphans-//! (parent = ORPHAN); nobody will wait for them, so they remove themselves-//! from the table when they exit. Unix hands them to `init` instead.-//!-//! Not done yet: signals/kill, more than one thread per process.+//! **Memory lifetime.** A process's frames and page tables are freed when+//! the last `Arc<Process>` goes away (dropping its `Mm`). That's always+//! after its thread is gone: the thread holds an Arc until the scheduler+//! frees it, which happens only once the CPU has switched to another+//! thread and address space. So no CPU can still be using the page tables.  use crate::elf::{self, ElfError}; use crate::errno::Errno; use crate::fs::{self, file::File};-use crate::memory::{self, USER_END};+use crate::mm::Mm;+use crate::signal::{self, SigInfo, SignalState}; use crate::thread::{self, WaitQueue};+use crate::trap::TrapFrame; use crate::{serial_println, tty::Console}; use alloc::{collections::BTreeMap, string::String, sync::Arc, vec, vec::Vec};-use core::sync::atomic::{AtomicU32, AtomicU64, AtomicUsize, Ordering};+use core::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering}; use spin::Mutex;-use x86_64::VirtAddr; use x86_64::instructions::interrupts::without_interrupts;-use x86_64::structures::paging::{Page, PageTableFlags, PhysFrame, Size4KiB};--/// The user stack: 16 KiB, ending one (unmapped) page below USER_END.-pub const USER_STACK_TOP: u64 = USER_END - 0x1000;-pub const USER_STACK_SIZE: u64 = 16 * 1024;-/// Below the stack: a 64 KiB guard zone that is never mapped, so a runaway-/// recursion hits it and gets killed with "stack overflow" (instead of-/// scribbling over the program's data). Big, so that one large stack frame-/// can't jump over it.-pub const USER_STACK_GUARD: u64 = 64 * 1024;-/// Lowest address of stack + guard: program segments must end below this.-pub const USER_STACK_ZONE_START: u64 = USER_STACK_TOP - USER_STACK_SIZE - USER_STACK_GUARD;++pub use crate::mm::{in_user_stack_guard, USER_STACK_GUARD, USER_STACK_SIZE, USER_STACK_TOP, USER_STACK_ZONE_START};+ /// `parent` of a process whose parent exited first. pub const ORPHAN: u64 = u64::MAX;--/// Is user address `addr` in the guard zone under the user stack?-pub fn in_user_stack_guard(addr: u64) -> bool {-    (USER_STACK_ZONE_START..USER_STACK_TOP - USER_STACK_SIZE).contains(&addr)-} /// Max open files per process.-const MAX_FDS: usize = 16;-/// Max total size of a program's arguments (they go on its 16 KiB stack).-pub const MAX_ARGS_SIZE: usize = 2048;-/// Exit code `wait` reports for a process the kernel killed (like a shell-/// shows 128 + SIGSEGV(11) for a segfault).-pub const KILLED_EXIT_CODE: i64 = 139;+pub const MAX_FDS: usize = 64;+/// Max total size of a program's arguments + environment strings.+pub const MAX_ARGS_SIZE: usize = 64 * 1024;+/// Max processes at once: fork() fails with EAGAIN beyond that, which is+/// what stops a fork bomb from eating all memory.+pub const MAX_PROCESSES: usize = 64;+/// Environment of processes the kernel starts.+pub const DEFAULT_ENV: [&str; 3] = ["PATH=/bin:/mnt/disk0/bin", "HOME=/", "TERM=linux"];  #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ExitStatus {     /// Called exit(code).     Exited(i64),-    /// Killed by the kernel (e.g. "page fault").-    Killed(&'static str),+    /// Killed by signal number .0; .1 says why (for messages): the signal+    /// name, or e.g. "page fault" / "stack overflow" for SIGSEGV.+    Killed(u32, &'static str), }  impl ExitStatus {-    /// The number `wait` returns.+    /// The number a shell shows: the exit code, or 128 + the signal number+    /// (so 139 = 128 + SIGSEGV, 130 = 128 + SIGINT).     pub fn code(self) -> i64 {         match self {             ExitStatus::Exited(code) => code,-            ExitStatus::Killed(_) => KILLED_EXIT_CODE,+            ExitStatus::Killed(sig, _) => 128 + sig as i64,+        }+    }++    /// The `int status` of wait4 (decoded by the W* macros, see ulib.rs):+    /// exit code in bits 8-15, or the killing signal in bits 0-6.+    pub fn wait_status(self) -> i32 {+        match self {+            ExitStatus::Exited(code) => ((code & 0xff) << 8) as i32,+            ExitStatus::Killed(sig, _) => sig as i32,         }     } } +/// Open files: fd number -> file, plus "close on exec" flags.+#[derive(Clone)]+pub struct FdTable {+    files: Vec<Option<Arc<dyn File>>>,+    /// Bit n: close fd n when the process execs (FD_CLOEXEC / O_CLOEXEC).+    cloexec: u64,+}++impl FdTable {+    fn new(stdio: [Arc<dyn File>; 3]) -> FdTable {+        FdTable { files: stdio.into_iter().map(Some).collect(), cloexec: 0 }+    }++    fn get(&self, fd: u64) -> Result<Arc<dyn File>, Errno> {+        self.files.get(fd as usize).and_then(|f| f.clone()).ok_or(Errno::EBADF)+    }++    /// 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())) {+            Some(fd) => fd,+            None => return Err(Errno::EMFILE),+        };+        self.set(fd, file, cloexec);+        Ok(fd as u64)+    }++    /// Put `file` at `fd` (whatever was there is returned, to be closed).+    fn set(&mut self, fd: usize, file: Arc<dyn File>, cloexec: bool) -> Option<Arc<dyn File>> {+        if self.files.len() <= fd {+            self.files.resize(fd + 1, None);+        }+        self.set_cloexec(fd, cloexec);+        self.files[fd].replace(file)+    }++    fn set_cloexec(&mut self, fd: usize, on: bool) {+        if on { self.cloexec |= 1 << fd } else { self.cloexec &= !(1 << fd) }+    }++    /// execve: close the close-on-exec ones. Returns them (close them+    /// after unlocking).+    fn exec(&mut self) -> Vec<Arc<dyn File>> {+        let mut closed = Vec::new();+        for (fd, slot) in self.files.iter_mut().enumerate() {+            if self.cloexec & (1 << fd) != 0 {+                closed.extend(slot.take());+            }+        }+        self.cloexec = 0;+        closed+    }+}+ pub struct Process {     pub pid: u64,     /// Parent pid; 0 = started by the kernel itself, ORPHAN = parent exited.     pub parent: AtomicU64,-    pub name: String,-    /// Print every syscall this process makes (`spawn` flag; `run --trace`).-    pub trace: bool,+    /// Process group (job control; see the top of this file).+    pgid: AtomicU64,+    /// Program name (changes with execve).+    name: Mutex<String>,+    /// Print every syscall this process makes (`run --trace`; survives exec).+    pub trace: AtomicBool,     /// Current directory (always a normalized absolute path).     pub cwd: Mutex<String>,-    entry: u64,-    /// Level-4 page table of its address space (freed in `drop`).-    l4: PhysFrame,+    /// The address space.+    pub mm: Mutex<Mm>,     /// Its thread's id (set right after the thread is created).     tid: AtomicUsize,     /// CPUs its thread has run on (bit per CPU; kept up by the scheduler).     pub ran_on: AtomicU32,-    args: Vec<String>,-    fds: Mutex<Vec<Option<Arc<dyn File>>>>,+    /// Registers its thread starts user mode with (taken by the thread).+    start: Mutex<Option<TrapFrame>>,+    /// The environment it was started with (`spawn` passes it on).+    env: Mutex<Vec<String>>,+    fds: Mutex<FdTable>,     /// Locked with interrupts off only (it's read in a wait_until condition).     status: Mutex<Option<ExitStatus>>,     exited: WaitQueue,+    /// Its own wait4 sleeps here; children wake it when they change state.+    pub child_wait: WaitQueue,+    /// Signal handlers, mask, pending signals. Locked with interrupts off.+    pub signals: Mutex<SignalState>,+    /// vfork: the parent waits until this child execs or exits.+    vfork_done: AtomicBool, }  static NEXT_PID: AtomicU64 = AtomicU64::new(1); /// All processes that haven't been reaped yet, by pid. Locked with-/// interrupts off only (exit_current takes it with interrupts off).+/// interrupts off only (exit_current takes it with interrupts off). Lock+/// order: this table before any process's `signals`. static PROCESSES: Mutex<BTreeMap<u64, Arc<Process>>> = Mutex::new(BTreeMap::new());+/// vfork parents wait here.+static VFORK_WAIT: WaitQueue = WaitQueue::new();  /// Everything needed to start a process besides the program itself. pub struct SpawnArgs {     pub args: Vec<String>, // argv; args[0] is the program name by convention+    pub env: Vec<String>,     pub stdio: [Arc<dyn File>; 3],     pub cwd: String,     pub parent: u64,+    /// Process group: 0 = a new group of its own.+    pub pgid: u64,     pub trace: bool, } @@ -134,22 +212,30 @@ impl SpawnArgs {         let console: Arc<dyn File> = Arc::new(Console);         SpawnArgs {             args: vec![String::from(name)],+            env: DEFAULT_ENV.iter().map(|s| String::from(*s)).collect(),             stdio: [console.clone(), console.clone(), console],             cwd: String::from("/"),             parent: 0,+            pgid: 0,             trace: false,         }     } } +fn elf_errno(e: ElfError) -> Errno {+    match e {+        ElfError::OutOfMemory => Errno::ENOMEM,+        _ => Errno::ENOEXEC, // "Exec format error"+    }+}+ /// Load the program at `path` (absolute) from the VFS and start it. pub fn spawn_path(path: &str, spawn: SpawnArgs) -> Result<Arc<Process>, Errno> {     let image = fs::read_all(path)?; // ENOENT, EISDIR, EIO, ...-    let name = path.rsplit('/').next().unwrap_or(path);-    spawn_image(name, &image, spawn).map_err(|e| match e {-        ElfError::OutOfMemory => Errno::ENOMEM,-        _ => Errno::ENOEXEC, // "Exec format error"-    })+    if count() >= MAX_PROCESSES {+        return Err(Errno::EAGAIN);+    }+    spawn_image(path, &image, spawn).map_err(elf_errno) }  /// Start ELF program `elf_image` from the kernel (console stdio, no args).@@ -157,145 +243,207 @@ pub fn spawn(name: &str, elf_image: &[u8]) -> Result<Arc<Process>, ElfError> {     spawn_image(name, elf_image, SpawnArgs::kernel(name)) } -/// Load `elf_image` into a new address space and start it.-pub fn spawn_image(name: &str, elf_image: &[u8], spawn: SpawnArgs) -> Result<Arc<Process>, ElfError> {-    let l4 = memory::new_address_space().ok_or(ElfError::OutOfMemory)?;-    match load_into(elf_image, l4) {-        Ok(entry) => Ok(start_process(name, l4, entry, spawn)),-        Err(e) => {-            // Nobody runs in this address space yet: give its memory back.-            unsafe { memory::free_address_space(l4) };-            Err(e)-        }-    }-}--/// Load the program and map its stack; returns the entry point.-fn load_into(elf_image: &[u8], l4: PhysFrame) -> Result<u64, ElfError> {-    let entry = elf::load(elf_image, l4)?;-    let mut stack_flags = PageTableFlags::WRITABLE;-    if memory::nx_enabled() {-        stack_flags |= PageTableFlags::NO_EXECUTE;-    }-    let first = Page::<Size4KiB>::containing_address(VirtAddr::new(USER_STACK_TOP - USER_STACK_SIZE));-    let last = Page::<Size4KiB>::containing_address(VirtAddr::new(USER_STACK_TOP - 1));-    for page in Page::range_inclusive(first, last) {-        memory::map_user_page(l4, page, stack_flags).ok_or(ElfError::OutOfMemory)?;-    }-    Ok(entry)-}--/// Create the `Process` and its thread.-fn start_process(name: &str, l4: PhysFrame, entry: u64, spawn: SpawnArgs) -> Arc<Process> {+/// Load `elf_image` into a new address space and start it. `path` is+/// where it came from (AT_EXECFN); the process is named after its last part.+pub fn spawn_image(path: &str, elf_image: &[u8], spawn: SpawnArgs) -> Result<Arc<Process>, ElfError> {+    let name = path.rsplit('/').next().unwrap_or(path);+    let mut mm = Mm::new().ok_or(ElfError::OutOfMemory)?;+    // (On error, dropping `mm` gives its memory back.)+    let args: Vec<&[u8]> = spawn.args.iter().map(|a| a.as_bytes()).collect();+    let env: Vec<&[u8]> = spawn.env.iter().map(|a| a.as_bytes()).collect();+    let start = load_program(&mut mm, elf_image, &args, &env, path)?;     let [stdin, stdout, stderr] = spawn.stdio;-    let process = Arc::new(Process {-        pid: NEXT_PID.fetch_add(1, Ordering::Relaxed),+    let pid = NEXT_PID.fetch_add(1, Ordering::Relaxed);+    let process = Process {+        pid,         parent: AtomicU64::new(spawn.parent),-        name: String::from(name),-        trace: spawn.trace,+        pgid: AtomicU64::new(if spawn.pgid == 0 { pid } else { spawn.pgid }),+        name: Mutex::new(String::from(name)),+        trace: AtomicBool::new(spawn.trace),         cwd: Mutex::new(spawn.cwd),-        entry,-        l4,+        mm: Mutex::new(mm),         tid: AtomicUsize::new(usize::MAX),         ran_on: AtomicU32::new(0),-        args: spawn.args,-        fds: Mutex::new(vec![Some(stdin), Some(stdout), Some(stderr)]),+        start: Mutex::new(Some(start)),+        env: Mutex::new(spawn.env),+        fds: Mutex::new(FdTable::new([stdin, stdout, stderr])),         status: Mutex::new(None),         exited: WaitQueue::new(),-    });+        child_wait: WaitQueue::new(),+        signals: Mutex::new(SignalState::new()),+        vfork_done: AtomicBool::new(false),+    };+    serial_println!("[process] started pid {} ({}), entry {:#x}", pid, name, start.rip);+    Ok(start_thread(process))+}++/// Put the process in the table and give it a thread.+fn start_thread(process: Process) -> Arc<Process> {+    let process = Arc::new(process);+    let l4 = process.mm.lock().l4;     without_interrupts(|| PROCESSES.lock().insert(process.pid, process.clone()));-    serial_println!("[process] started pid {} ({}), entry {:#x}", process.pid, name, entry);-    let tid = thread::spawn_with(name, user_thread_entry, l4, Some(process.clone()));+    let name = process.name();+    let tid = thread::spawn_with(&name, user_thread_entry, l4, Some(process.clone()));     process.tid.store(tid, Ordering::Release);     process }  /// First code of every process thread (still ring 0, but already running-/// with the process's CR3 and kernel stack).+/// with the process's CR3 and kernel stack): enter user mode with the+/// prepared registers. fn user_thread_entry() {     let process = thread::current_process().expect("process thread without process");-    // Copy the arguments onto the top of the user stack (we can write there-    // directly: this process's address space is active). Layout, top down:-    //   the argument strings' bytes (not NUL-terminated)-    //   argv: [ptr, len] pairs, one per argument   <- rsi-    //   (stack continues below)-    // and rdi = argc. Rust's `&str` is ptr+len, so pairs are handier than-    // C's NUL-terminated `char **argv`.-    let mut sp = USER_STACK_TOP;-    let mut pairs: Vec<[u64; 2]> = Vec::with_capacity(process.args.len());-    for arg in &process.args {-        sp -= arg.len() as u64;-        unsafe { core::ptr::copy_nonoverlapping(arg.as_ptr(), sp as *mut u8, arg.len()) };-        pairs.push([sp, arg.len() as u64]);-    }-    sp &= !0xf; // 16-byte align-    sp -= 16 * pairs.len() as u64;-    let argv = sp;-    unsafe { core::ptr::copy_nonoverlapping(pairs.as_ptr(), argv as *mut [u64; 2], pairs.len()) };-    // Functions expect rsp % 16 == 8 on entry (as if a return address had-    // just been pushed), so start 8 bytes below the 16-aligned spot.-    let (entry, argc) = (process.entry, pairs.len() as u64);-    drop(pairs);-    drop(process); // enter_user_mode never returns: don't leak the Arc-    unsafe { enter_user_mode(entry, sp - 8, argc, argv) }+    let frame = process.start.lock().take().expect("no start registers");+    drop(process); // return_to_user never returns: don't leak the Arc+    crate::trap::return_to_user(frame) } -/// Jump to ring 3. `iretq` pops RIP, CS, RFLAGS, RSP, SS -- we push a fake-/// interrupt frame with user selectors (RPL 3), so "returning" from it drops-/// the privilege level. RFLAGS = 0x202: interrupts enabled (+ the always-1 bit).-/// rdi/rsi carry `_start(argc, argv)`'s arguments.-unsafe fn enter_user_mode(rip: u64, rsp: u64, argc: u64, argv: u64) -> ! {-    let s = crate::gdt::selectors();-    unsafe {-        core::arch::asm!(-            "push {ss}",-            "push {rsp}",-            "push {rflags}",-            "push {cs}",-            "push {rip}",-            // Don't leak kernel values to user space in registers.-            "xor eax, eax", "xor ebx, ebx", "xor ecx, ecx", "xor edx, edx",-            "xor ebp, ebp",-            "xor r8d, r8d", "xor r9d, r9d", "xor r10d, r10d", "xor r11d, r11d",-            "xor r12d, r12d", "xor r13d, r13d", "xor r14d, r14d", "xor r15d, r15d",-            "iretq",-            ss = in(reg) s.user_data.0 as u64,-            rsp = in(reg) rsp,-            rflags = in(reg) 0x202u64,-            cs = in(reg) s.user_code.0 as u64,-            rip = in(reg) rip,-            in("rdi") argc,-            in("rsi") argv,-            options(noreturn)-        );+/// Random bytes for AT_RANDOM (virtio-rng; the TSC if there's none).+fn random_bytes(buf: &mut [u8]) {+    if !crate::virtio::rng::fill(buf) {+        let mut x = unsafe { core::arch::x86_64::_rdtsc() } | 1;+        for b in buf {+            x ^= x << 13;+            x ^= x >> 7;+            x ^= x << 17;+            *b = x as u8;+        }     } } +// Auxiliary vector keys (Linux's AT_* values, `man getauxval`).+const AT_NULL: u64 = 0;+const AT_PHDR: u64 = 3;+const AT_PHENT: u64 = 4;+const AT_PHNUM: u64 = 5;+const AT_PAGESZ: u64 = 6;+const AT_BASE: u64 = 7;+const AT_FLAGS: u64 = 8;+const AT_ENTRY: u64 = 9;+const AT_UID: u64 = 11;+const AT_EUID: u64 = 12;+const AT_GID: u64 = 13;+const AT_EGID: u64 = 14;+const AT_PLATFORM: u64 = 15;+const AT_HWCAP: u64 = 16;+const AT_CLKTCK: u64 = 17;+const AT_SECURE: u64 = 23;+const AT_RANDOM: u64 = 25;+const AT_EXECFN: u64 = 31;++/// Load a program into the empty address space `mm` and build its initial+/// stack the way Linux does, so a C library's `_start` finds everything+/// where it expects it. Returns the registers to start with.+///+/// The stack, from USER_STACK_TOP downwards:+///+///   strings: execfn, environment, arguments, "x86_64", 16 random bytes,+///            (a copy of the program headers if no segment contains them)+///   (padding to 16 bytes)+///   auxv:  pairs of [key, value] ..., [AT_NULL, 0]+///   envp:  pointers to the environment strings ..., NULL+///   argv:  pointers to the argument strings ..., NULL+///   argc                                          <- rsp (16-byte aligned)+pub fn load_program(mm: &mut Mm, image: &[u8], argv: &[&[u8]], envp: &[&[u8]], execfn: &str) -> Result<TrapFrame, ElfError> {+    use crate::mm::{Kind, Vma, PROT_READ, PROT_WRITE};+    let info = elf::load(image, mm)?;+    // The heap starts on the page after the program; the stack is a region+    // that fills in as it's used.+    mm.brk_start = info.end.next_multiple_of(4096);+    mm.brk = mm.brk_start;+    mm.insert(Vma::new(USER_STACK_TOP - USER_STACK_SIZE, USER_STACK_TOP, PROT_READ | PROT_WRITE, Kind::Stack));++    let mut sp = USER_STACK_TOP;+    let mut push = |mm: &mut Mm, bytes: &[u8], nul: bool| -> Result<u64, ElfError> {+        sp -= bytes.len() as u64 + nul as u64;+        mm.write_bytes(sp, bytes).map_err(|_| ElfError::OutOfMemory)?;+        if nul {+            mm.write_bytes(sp + bytes.len() as u64, &[0]).map_err(|_| ElfError::OutOfMemory)?;+        }+        Ok(sp)+    };+    let execfn_ptr = push(mm, execfn.as_bytes(), true)?;+    let env_ptrs: Vec<u64> = envp.iter().map(|e| push(mm, e, true)).collect::<Result<_, _>>()?;+    let arg_ptrs: Vec<u64> = argv.iter().map(|a| push(mm, a, true)).collect::<Result<_, _>>()?;+    let platform = push(mm, b"x86_64", true)?;+    let mut random = [0u8; 16];+    random_bytes(&mut random);+    let random_ptr = push(mm, &random, false)?;+    let phdr = match info.phdr {+        Some(addr) => addr,+        None => {+            let start = info.phoff as usize;+            push(mm, &image[start..start + (info.phent * info.phnum) as usize], false)?+        }+    };++    let mut words: Vec<u64> = vec![arg_ptrs.len() as u64];+    words.extend(&arg_ptrs);+    words.push(0);+    words.extend(&env_ptrs);+    words.push(0);+    #[rustfmt::skip]+    words.extend([+        AT_PHDR, phdr, AT_PHENT, info.phent, AT_PHNUM, info.phnum,+        AT_PAGESZ, 4096, AT_BASE, 0, AT_FLAGS, 0, AT_ENTRY, info.entry,+        AT_UID, 0, AT_EUID, 0, AT_GID, 0, AT_EGID, 0,+        AT_PLATFORM, platform, AT_HWCAP, 0, AT_CLKTCK, 100, AT_SECURE, 0,+        AT_RANDOM, random_ptr, AT_EXECFN, execfn_ptr, AT_NULL, 0,+    ]);+    let rsp = (sp - 8 * words.len() as u64) & !0xf;+    let bytes: Vec<u8> = words.iter().flat_map(|w| w.to_le_bytes()).collect();+    mm.write_bytes(rsp, &bytes).map_err(|_| ElfError::OutOfMemory)?;+    // The ABI also says: rdx = function for atexit (0 = none), others zero.+    Ok(TrapFrame::new_user(info.entry, rsp))+}+ impl Process {     /// The open file behind `fd`.     pub fn file(&self, fd: u64) -> Result<Arc<dyn File>, Errno> {-        let fds = self.fds.lock();-        fds.get(fd as usize).and_then(|f| f.clone()).ok_or(Errno::EBADF)+        self.fds.lock().get(fd)     }      /// Put `file` into the lowest free fd slot (like Unix).     pub fn add_file(&self, file: Arc<dyn File>) -> Result<u64, Errno> {-        let mut fds = self.fds.lock();-        if let Some(i) = fds.iter().position(|f| f.is_none()) {-            fds[i] = Some(file);-            return Ok(i as u64);-        }-        if fds.len() >= MAX_FDS {-            return Err(Errno::EMFILE);+        self.fds.lock().add(file, 0, false)+    }++    /// open(O_CLOEXEC), pipe2, fcntl(F_DUPFD...): lowest free fd >= `min`.+    pub fn add_file_with(&self, file: Arc<dyn File>, min: usize, cloexec: bool) -> Result<u64, Errno> {+        self.fds.lock().add(file, min, cloexec)+    }++    /// dup2/dup3: make `new` refer to the same file as `old`.+    pub fn dup_to(&self, old: u64, new: u64, cloexec: bool) -> Result<u64, Errno> {+        if new as usize >= MAX_FDS {+            return Err(Errno::EBADF);         }-        fds.push(Some(file));-        Ok(fds.len() as u64 - 1)+        let replaced = {+            let mut fds = self.fds.lock();+            let file = fds.get(old)?;+            fds.set(new as usize, file, cloexec)+        };+        drop(replaced); // closing the old file may block: after unlocking+        Ok(new)+    }++    pub fn cloexec(&self, fd: u64) -> Result<bool, Errno> {+        let fds = self.fds.lock();+        fds.get(fd)?;+        Ok(fds.cloexec & (1 << fd) != 0)+    }++    pub fn set_cloexec(&self, fd: u64, on: bool) -> Result<(), Errno> {+        let mut fds = self.fds.lock();+        fds.get(fd)?;+        fds.set_cloexec(fd as usize, on);+        Ok(())     }      pub fn close(&self, fd: u64) -> Result<(), Errno> {         let file = {             let mut fds = self.fds.lock();-            match fds.get_mut(fd as usize) {+            match fds.files.get_mut(fd as usize) {                 Some(slot @ Some(_)) => slot.take(),                 _ => return Err(Errno::EBADF),             }@@ -313,13 +461,33 @@ impl Process {         let cwd = self.cwd.lock().clone();         fs::normalize(&cwd, path)     }-} -impl Drop for Process {-    /// The last reference is gone, so its thread is gone too: give the-    /// address space's frames and page tables back.-    fn drop(&mut self) {-        unsafe { memory::free_address_space(self.l4) };+    pub fn name(&self) -> String {+        self.name.lock().clone()+    }++    pub fn parent(&self) -> u64 {+        self.parent.load(Ordering::Relaxed)+    }++    pub fn pgid(&self) -> u64 {+        self.pgid.load(Ordering::Relaxed)+    }++    pub fn tracing(&self) -> bool {+        self.trace.load(Ordering::Relaxed)+    }++    pub fn env(&self) -> Vec<String> {+        self.env.lock().clone()+    }++    /// Wake its thread if it's blocked (it then checks for signals).+    pub fn wake(&self) {+        let tid = self.tid.load(Ordering::Acquire);+        if tid != usize::MAX {+            thread::wake(tid);+        }     } } @@ -333,20 +501,38 @@ pub fn count() -> usize {     without_interrupts(|| PROCESSES.lock().len()) } -/// End the current process (exit syscall, or killed by an exception).+/// Every process in the table.+pub fn all() -> Vec<Arc<Process>> {+    without_interrupts(|| PROCESSES.lock().values().cloned().collect())+}++/// The (not exited) members of process group `pgid`.+pub fn group_members(pgid: u64) -> Vec<Arc<Process>> {+    without_interrupts(|| {+        PROCESSES.lock().values().filter(|p| p.pgid() == pgid && p.status().is_none()).cloned().collect()+    })+}++/// End the current process (exit syscall, or killed by a signal). pub fn exit_current(status: ExitStatus) -> ! {     if let Some(process) = thread::current_process() {         // Close all files (a pipe's reader sees EOF once its writers are gone).-        let files = core::mem::take(&mut *process.fds.lock());+        let files = core::mem::take(&mut process.fds.lock().files);         drop(files);         crate::tty::process_exited(process.pid);-        // Serial only: on screen the shell reports exit codes itself.+        let name = process.name();         match status {+            // Serial only: on screen the shell reports exit codes itself.             ExitStatus::Exited(code) => {-                serial_println!("[process] pid {} ({}) exited with code {}", process.pid, process.name, code)+                serial_println!("[process] pid {} ({}) exited with code {}", process.pid, name, code)+            }+            // A crash (SIGSEGV...) is worth a line on screen; a signal+            // somebody sent on purpose (Ctrl+C, kill) isn't.+            ExitStatus::Killed(sig, why) if why != signal::name(sig) => {+                crate::kprintln!("[process] pid {} ({}) killed: {} ({})", process.pid, name, why, signal::name(sig))             }-            ExitStatus::Killed(why) => {-                crate::kprintln!("[process] pid {} ({}) killed: {}", process.pid, process.name, why)+            ExitStatus::Killed(sig, _) => {+                serial_println!("[process] pid {} ({}) killed by {}", process.pid, name, signal::name(sig))             }         }         // No preemption from here on: we're about to disappear anyway.@@ -359,8 +545,7 @@ pub fn exit_current(status: ExitStatus) -> ! {             let mut table = PROCESSES.lock();             *process.status.lock() = Some(status);             let mut removed = Vec::new();-            let children: Vec<Arc<Process>> =-                table.values().filter(|c| c.parent.load(Ordering::Relaxed) == process.pid).cloned().collect();+            let children: Vec<Arc<Process>> = table.values().filter(|c| c.parent() == process.pid).cloned().collect();             for child in children {                 if child.status().is_some() {                     removed.extend(table.remove(&child.pid)); // exited: reap it@@ -368,21 +553,39 @@ pub fn exit_current(status: ExitStatus) -> ! {                     child.parent.store(ORPHAN, Ordering::Relaxed); // still running                 }             }-            if process.parent.load(Ordering::Relaxed) == ORPHAN {-                removed.extend(table.remove(&process.pid)); // nobody will wait for us+            // Nobody will wait for us if we're an orphan, or if our parent+            // said so by ignoring SIGCHLD explicitly (POSIX): reap ourselves.+            let parent_ignores = table.get(&process.parent()).is_some_and(|p| {+                let action = p.signals.lock().action(signal::SIGCHLD);+                action.handler == signal::SIG_IGN || action.flags & signal::SA_NOCLDWAIT != 0+            });+            if process.parent() == ORPHAN || parent_ignores {+                removed.extend(table.remove(&process.pid));             }             removed         };         drop(removed); // (frees their memory if they're completely gone)-        process.exited.wake_all(); // the parent may be blocked in wait()+        process.exited.wake_all(); // the kernel may be blocked in wait()+        process.vfork_done.store(true, Ordering::Release);+        VFORK_WAIT.wake_all();+        let code = match status {+            ExitStatus::Exited(_) => signal::CLD_EXITED,+            ExitStatus::Killed(sig, _) if signal::default_action(sig) == signal::DefaultAction::Core => signal::CLD_DUMPED,+            ExitStatus::Killed(..) => signal::CLD_KILLED,+        };+        let si_status = match status {+            ExitStatus::Exited(c) => c as i32,+            ExitStatus::Killed(sig, _) => sig as i32,+        };+        signal::notify_parent(&process, code, si_status);         drop(process);     }     thread::exit_current() }  /// Block (the calling thread) until `process` has ended and its thread is-/// gone, then remove it from the process table. Used by the kernel and by-/// the `wait` syscall. Once the caller drops its own `Arc`, all of the+/// 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 /// process's memory is free again. pub fn wait(process: &Process) -> ExitStatus {     let status = process.exited.wait_until(|| *process.status.lock());@@ -406,12 +609,15 @@ fn reap(process: &Process) {     drop(removed); } -/// `wait` flag: don't block; `EAGAIN` if the child is still running.+// wait4 options. pub const WNOHANG: u64 = 1;+pub const WUNTRACED: u64 = 2;+pub const WCONTINUED: u64 = 8; -/// The `wait(pid, flags)` syscall: only a process's parent may wait for it.+/// Kernel-side wait for one child (the tests use it): EAGAIN with WNOHANG+/// while it runs. pub fn wait_child(parent: u64, pid: u64, flags: u64) -> Result<ExitStatus, Errno> {-    let child = get(pid).filter(|c| c.parent.load(Ordering::Relaxed) == parent).ok_or(Errno::ECHILD)?;+    let child = get(pid).filter(|c| c.parent() == parent).ok_or(Errno::ECHILD)?;     if flags & WNOHANG != 0 {         let status = child.status().ok_or(Errno::EAGAIN)?;         reap(&child);@@ -419,3 +625,171 @@ pub fn wait_child(parent: u64, pid: u64, flags: u64) -> Result<ExitStatus, Errno     }     Ok(wait(&child)) }++/// What wait4 found.+enum Found {+    Event(Arc<Process>, i32, bool), // child, status word, exited (reap it)+    Nothing,                        // WNOHANG and no news+}++/// wait4(pid, *status, options): wait for a child to exit (or, with+/// WUNTRACED / WCONTINUED, to stop / continue). `pid` > 0: that child;+/// -1: any child; 0: any child in my process group; < -1: any child in+/// group -pid. Returns (pid, status), or pid 0 with WNOHANG and no news.+pub fn wait4(me: &Process, pid: i64, options: u64) -> Result<(u64, i32), Errno> {+    if options & !(WNOHANG | WUNTRACED | WCONTINUED) != 0 {+        return Err(Errno::EINVAL);+    }+    let matches = |c: &Process| {+        c.parent() == me.pid+            && match pid {+                -1 => true,+                0 => c.pgid() == me.pgid(),+                p if p < -1 => c.pgid() == p.unsigned_abs(),+                p => c.pid == p as u64,+            }+    };+    let found = me.child_wait.wait_until_intr(|| {+        let table = PROCESSES.lock();+        let mut any = false;+        for c in table.values().filter(|c| matches(c)) {+            any = true;+            if let Some(status) = *c.status.lock() {+                return Some(Ok(Found::Event(c.clone(), status.wait_status(), true)));+            }+            let mut s = c.signals.lock();+            match s.report {+                Some(signal::Report::Stopped(sig)) if options & WUNTRACED != 0 => {+                    s.report = None;+                    return Some(Ok(Found::Event(c.clone(), 0x7f | (sig as i32) << 8, false)));+                }+                Some(signal::Report::Continued) if options & WCONTINUED != 0 => {+                    s.report = None;+                    return Some(Ok(Found::Event(c.clone(), 0xffff, false)));+                }+                _ => {}+            }+        }+        if !any {+            Some(Err(Errno::ECHILD))+        } else if options & WNOHANG != 0 {+            Some(Ok(Found::Nothing))+        } else {+            None // sleep until a child changes state (or a signal)+        }+    })??;+    match found {+        Found::Nothing => Ok((0, 0)),+        Found::Event(child, status, exited) => {+            if exited {+                reap(&child);+            }+            Ok((child.pid, status))+        }+    }+}++/// fork (and vfork / basic clone): a new process, a copy of `parent`, that+/// starts by returning from the same syscall with rax = 0. `stack`: a new+/// stack pointer for the child (clone), or 0 = the same.+pub fn fork(parent: &Arc<Process>, frame: &TrapFrame, stack: u64, vfork: bool) -> Result<u64, Errno> {+    if count() >= MAX_PROCESSES {+        return Err(Errno::EAGAIN); // fork bombs stop here+    }+    // The heart of it: copy the page tables, sharing the frames+    // copy-on-write (mm.rs).+    let mm = parent.mm.lock().fork().ok_or(Errno::ENOMEM)?;+    let mut start = *frame;+    start.rax = 0; // "you're the child"+    if stack != 0 {+        start.rsp = stack;+    }+    let pid = NEXT_PID.fetch_add(1, Ordering::Relaxed);+    let signals = without_interrupts(|| parent.signals.lock().fork());+    let child = Process {+        pid,+        parent: AtomicU64::new(parent.pid),+        pgid: AtomicU64::new(parent.pgid()),+        name: Mutex::new(parent.name()),+        trace: AtomicBool::new(parent.tracing()),+        cwd: Mutex::new(parent.cwd.lock().clone()),+        mm: Mutex::new(mm),+        tid: AtomicUsize::new(usize::MAX),+        ran_on: AtomicU32::new(0),+        start: Mutex::new(Some(start)),+        env: Mutex::new(parent.env()),+        fds: Mutex::new(parent.fds.lock().clone()), // same open files (Arc clones)+        status: Mutex::new(None),+        exited: WaitQueue::new(),+        child_wait: WaitQueue::new(),+        signals: Mutex::new(signals),+        vfork_done: AtomicBool::new(false),+    };+    serial_println!("[process] pid {} forked pid {}", parent.pid, pid);+    let child = start_thread(child);+    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).+        VFORK_WAIT.wait_until(|| child.vfork_done.load(Ordering::Acquire).then_some(()));+    }+    Ok(pid)+}++/// execve, after the arguments have been copied in: replace the current+/// program. `frame` gets the new program's registers.+pub fn execve(p: &Process, frame: &mut TrapFrame, path: &str, argv: &[Vec<u8>], envp: &[Vec<u8>]) -> Result<(), Errno> {+    let size: usize = argv.iter().chain(envp).map(|s| s.len() + 1 + 8).sum();+    if size > MAX_ARGS_SIZE {+        return Err(Errno::E2BIG);+    }+    let image = fs::read_all(path)?;+    // Check the whole file *before* the point of no return: once the old+    // program's memory is gone, there's nobody left to report an error to.+    elf::check(&image).map_err(elf_errno)?;+    let name = path.rsplit('/').next().unwrap_or(path);+    let args: Vec<&[u8]> = argv.iter().map(|a| a.as_slice()).collect();+    let env: Vec<&[u8]> = envp.iter().map(|a| a.as_slice()).collect();+    // --- point of no return ---+    let loaded = {+        let mut mm = p.mm.lock();+        mm.clear();+        load_program(&mut mm, &image, &args, &env, path)+    };+    let Ok(start) = loaded else {+        exit_current(ExitStatus::Killed(signal::SIGSEGV, "exec failed (out of memory)"));+    };+    *frame = start;+    *p.name.lock() = String::from(name);+    *p.env.lock() = envp.iter().map(|e| String::from_utf8_lossy(e).into_owned()).collect();+    without_interrupts(|| p.signals.lock().exec());+    let closed = p.fds.lock().exec();+    drop(closed);+    p.vfork_done.store(true, Ordering::Release);+    VFORK_WAIT.wake_all();+    serial_println!("[process] pid {} exec {}", p.pid, path);+    Ok(())+}++/// setpgid(pid, pgid): move process `pid` (0 = me; myself or a child of+/// mine) into group `pgid` (0 = a new group named after it). The group+/// must exist unless it's the new one.+pub fn setpgid(me: &Process, pid: u64, pgid: u64) -> Result<(), Errno> {+    let pid = if pid == 0 { me.pid } else { pid };+    let target = get(pid).ok_or(Errno::ESRCH)?;+    if target.pid != me.pid && target.parent() != me.pid {+        return Err(Errno::ESRCH);+    }+    let pgid = if pgid == 0 { pid } else { pgid };+    // (Exited-but-not-reaped members count: a pipeline's first program may+    // already be done when the shell puts the second one in its group.)+    if pgid != pid && !without_interrupts(|| PROCESSES.lock().values().any(|p| p.pgid() == pgid)) {+        return Err(Errno::EPERM);+    }+    target.pgid.store(pgid, Ordering::Relaxed);+    Ok(())+}++/// Send `sig` from the kernel to group `pgid` (Ctrl+C, Ctrl+Z).+pub fn signal_group(pgid: u64, sig: u32) {+    let _ = signal::send_group(pgid, sig, SigInfo::from_pid(sig, 0));+}
@@ -0,0 +1,715 @@+//! Signals: asynchronous notifications to a process.+//!+//! A signal is a small number (SIGINT = 2, SIGSEGV = 11, ...) sent to a+//! process by the kernel (a bad memory access, Ctrl+C, a child exited, a+//! write to a pipe nobody reads) or by another process (`kill`). For each+//! signal a process chooses what happens (`rt_sigaction`):+//!   * the default action -- terminate, terminate "with core dump" (we don't+//!     write core files, so the same), ignore, stop or continue;+//!   * ignore it;+//!   * or run a *handler*: a function of the program, called in the middle+//!     of whatever it was doing, like a hardware interrupt but in user space.+//! It can also *block* signals for a while (`rt_sigprocmask`): they stay+//! pending until unblocked. SIGKILL and SIGSTOP can't be caught, blocked or+//! ignored -- that's how you get rid of any process.+//!+//! **When is a signal acted on?** Sending only sets a bit in the target's+//! `pending` set (and wakes it if it sleeps). The target acts on it the+//! next time it returns from the kernel to user mode: after a syscall, a+//! page fault, or a timer interrupt (`deliver`, called from trap.rs's+//! return paths). A process blocked in a syscall (reading the keyboard,+//! waiting for a child) is woken up; the syscall gives up with EINTR, or is+//! transparently restarted if the handler was installed with SA_RESTART.+//!+//! **Running a handler** (`setup_frame`): we push a *signal frame* on the+//! user's stack holding all its registers (`ucontext`), the blocked mask+//! and a `siginfo` (who sent it, which address faulted...), then change the+//! saved registers so that "returning" to user mode lands in the handler:+//!+//!     rip = handler, rdi = signal number, rsi = &siginfo, rdx = &ucontext+//!     rsp = the frame, whose first word is the return address: `restorer`+//!+//! The restorer is a two-instruction function in the C library (`mov rax,+//! 15; syscall` = rt_sigreturn). When the handler returns, it runs, and+//! rt_sigreturn copies the saved registers back from the frame: the program+//! continues exactly where it was interrupted. The frame layout is Linux's+//! `struct rt_sigframe`, so real libcs (musl) will work unchanged.+//!+//! **Stop and continue** (job control): SIGSTOP / SIGTSTP (Ctrl+Z) stop a+//! process -- its thread just sleeps -- until SIGCONT. The parent learns of+//! it from `wait4(WUNTRACED)` and SIGCHLD.++use crate::errno::Errno;+use crate::process::{self, ExitStatus, Process};+use crate::thread::{self, WaitQueue};+use crate::trap::TrapFrame;+use alloc::sync::Arc;+use alloc::vec::Vec;+use x86_64::instructions::interrupts::without_interrupts;++pub const SIGHUP: u32 = 1;+pub const SIGINT: u32 = 2;+pub const SIGQUIT: u32 = 3;+pub const SIGILL: u32 = 4;+pub const SIGTRAP: u32 = 5;+pub const SIGABRT: u32 = 6;+pub const SIGBUS: u32 = 7;+pub const SIGFPE: u32 = 8;+pub const SIGKILL: u32 = 9;+pub const SIGUSR1: u32 = 10;+pub const SIGSEGV: u32 = 11;+pub const SIGUSR2: u32 = 12;+pub const SIGPIPE: u32 = 13;+pub const SIGALRM: u32 = 14;+pub const SIGTERM: u32 = 15;+pub const SIGCHLD: u32 = 17;+pub const SIGCONT: u32 = 18;+pub const SIGSTOP: u32 = 19;+pub const SIGTSTP: u32 = 20;+pub const SIGTTIN: u32 = 21;+pub const SIGTTOU: u32 = 22;+pub const SIGURG: u32 = 23;+pub const SIGXCPU: u32 = 24;+pub const SIGXFSZ: u32 = 25;+pub const SIGWINCH: u32 = 28;+pub const SIGSYS: u32 = 31;+/// Signals 1..=64 (32..64 are the "real-time" ones: no default meaning).+pub const NSIG: u32 = 64;++/// Names for `kill -l`, messages and the shell.+pub fn name(sig: u32) -> &'static str {+    const NAMES: [&str; 32] = [+        "SIG0", "SIGHUP", "SIGINT", "SIGQUIT", "SIGILL", "SIGTRAP", "SIGABRT", "SIGBUS", "SIGFPE", "SIGKILL", "SIGUSR1",+        "SIGSEGV", "SIGUSR2", "SIGPIPE", "SIGALRM", "SIGTERM", "SIGSTKFLT", "SIGCHLD", "SIGCONT", "SIGSTOP", "SIGTSTP",+        "SIGTTIN", "SIGTTOU", "SIGURG", "SIGXCPU", "SIGXFSZ", "SIGVTALRM", "SIGPROF", "SIGWINCH", "SIGIO", "SIGPWR",+        "SIGSYS",+    ];+    NAMES.get(sig as usize).copied().unwrap_or("SIGRT")+}++/// What happens to a signal nobody installed a handler for.+#[derive(Debug, Clone, Copy, PartialEq, Eq)]+pub enum DefaultAction {+    Terminate,+    /// Terminate + write a core file (we don't write one).+    Core,+    Ignore,+    Stop,+    Continue,+}++pub fn default_action(sig: u32) -> DefaultAction {+    use DefaultAction::*;+    match sig {+        SIGCHLD | SIGURG | SIGWINCH => Ignore,+        SIGCONT => Continue,+        SIGSTOP | SIGTSTP | SIGTTIN | SIGTTOU => Stop,+        SIGQUIT | SIGILL | SIGTRAP | SIGABRT | SIGBUS | SIGFPE | SIGSEGV | SIGXCPU | SIGXFSZ | SIGSYS => Core,+        _ => Terminate,+    }+}++// sigaction handler values and flags (Linux's).+pub const SIG_DFL: u64 = 0;+pub const SIG_IGN: u64 = 1;+pub const SA_NOCLDSTOP: u64 = 0x1;+pub const SA_NOCLDWAIT: u64 = 0x2;+pub const SA_SIGINFO: u64 = 0x4;+pub const SA_RESTORER: u64 = 0x0400_0000;+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;+// rt_sigprocmask `how`.+pub const SIG_BLOCK: u64 = 0;+pub const SIG_UNBLOCK: u64 = 1;+pub const SIG_SETMASK: u64 = 2;+// siginfo codes.+pub const SI_USER: i32 = 0;+pub const SI_KERNEL: i32 = 0x80;+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;+pub const CLD_KILLED: i32 = 2;+pub const CLD_DUMPED: i32 = 3;+pub const CLD_STOPPED: i32 = 5;+pub const CLD_CONTINUED: i32 = 6;++/// The set bit for signal `sig` in a 64-bit signal set (signal 1 = bit 0).+pub const fn bit(sig: u32) -> u64 {+    1 << (sig - 1)+}+/// Never blockable.+const UNBLOCKABLE: u64 = bit(SIGKILL) | bit(SIGSTOP);++/// The kernel's `struct sigaction` (what rt_sigaction reads and writes;+/// the field order differs from the libc one).+#[repr(C)]+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]+pub struct SigAction {+    pub handler: u64,+    pub flags: u64,+    pub restorer: u64,+    pub mask: u64,+}++/// Linux's `siginfo_t`: 128 bytes. Which fields follow the first three+/// depends on the signal: kill() -> sender pid + uid; SIGCHLD -> child pid,+/// uid, exit status; SIGSEGV -> faulting address.+#[repr(C)]+#[derive(Debug, Clone, Copy)]+pub struct SigInfo {+    pub signo: i32,+    pub errno: i32,+    pub code: i32,+    _pad: i32,+    /// si_pid (i32) + si_uid (u32), or si_addr; then si_status (i32).+    pub fields: [u64; 14],+}++impl SigInfo {+    pub fn new(sig: u32, code: i32) -> SigInfo {+        SigInfo { signo: sig as i32, errno: 0, code, _pad: 0, fields: [0; 14] }+    }+    /// Sent by process `pid` with kill().+    pub fn from_pid(sig: u32, pid: u64) -> SigInfo {+        let mut info = SigInfo::new(sig, if pid == 0 { SI_KERNEL } else { SI_USER });+        info.fields[0] = pid & 0xffff_ffff; // si_pid, si_uid = 0 (root)+        info+    }+    /// A fault at `addr` (SIGSEGV, SIGBUS, SIGFPE, SIGILL).+    pub fn fault(sig: u32, code: i32, addr: u64) -> SigInfo {+        let mut info = SigInfo::new(sig, code);+        info.fields[0] = addr;+        info+    }+    /// SIGCHLD: child `pid` exited / was killed / stopped / continued.+    pub fn child(code: i32, pid: u64, status: i32) -> SigInfo {+        let mut info = SigInfo::new(SIGCHLD, code);+        info.fields[0] = pid & 0xffff_ffff;+        info.fields[1] = status as u32 as u64;+        info+    }+}++/// What the parent's wait4(WUNTRACED / WCONTINUED) can report besides exits.+#[derive(Debug, Clone, Copy, PartialEq, Eq)]+pub enum Report {+    Stopped(u32),+    Continued,+}++/// A process's signal state. Locked with interrupts off (`Process::signals`).+pub struct SignalState {+    pub pending: u64,+    pub blocked: u64,+    actions: [SigAction; NSIG as usize],+    /// siginfo of each pending signal (standard signals don't queue: one+    /// pending instance each, like Linux).+    info: Vec<(u32, SigInfo)>,+    /// Why the kernel forced this signal (a fault), for the kill message.+    reason: Option<&'static str>,+    pub stopped: bool,+    /// A stop/continue the parent hasn't collected with wait4 yet.+    pub report: Option<Report>,+}++impl SignalState {+    pub fn new() -> SignalState {+        SignalState {+            pending: 0,+            blocked: 0,+            actions: [SigAction::default(); NSIG as usize],+            info: Vec::new(),+            reason: None,+            stopped: false,+            report: None,+        }+    }++    /// fork: the child inherits the handlers and the blocked mask, but no+    /// pending signals.+    pub fn fork(&self) -> SignalState {+        SignalState { actions: self.actions, blocked: self.blocked, ..SignalState::new() }+    }++    /// execve: the program with the handlers is gone, so caught signals go+    /// back to the default. Ignored ones stay ignored (that's how `nohup`+    /// works), and so do the mask and pending signals.+    pub fn exec(&mut self) {+        for action in self.actions.iter_mut() {+            if action.handler != SIG_IGN {+                *action = SigAction::default();+            }+        }+    }++    pub fn action(&self, sig: u32) -> SigAction {+        self.actions[sig as usize - 1]+    }++    /// Would this signal be thrown away right now?+    fn ignored(&self, sig: u32) -> bool {+        let handler = self.action(sig).handler;+        handler == SIG_IGN+            || (handler == SIG_DFL && matches!(default_action(sig), DefaultAction::Ignore | DefaultAction::Continue))+    }++    /// Pending and not blocked: should interrupt a wait.+    fn deliverable(&self) -> u64 {+        self.pending & !(self.blocked & !UNBLOCKABLE)+    }++    /// Take the lowest deliverable signal.+    fn dequeue(&mut self) -> Option<(u32, SigInfo)> {+        let set = self.deliverable();+        if set == 0 {+            return None;+        }+        // SIGKILL first, otherwise the lowest number.+        let sig = if set & bit(SIGKILL) != 0 { SIGKILL } else { set.trailing_zeros() + 1 };+        self.pending &= !bit(sig);+        let info = match self.info.iter().position(|(s, _)| *s == sig) {+            Some(i) => self.info.swap_remove(i).1,+            None => SigInfo::new(sig, SI_KERNEL),+        };+        Some((sig, info))+    }++    /// Mark `sig` pending with this siginfo.+    fn raise(&mut self, sig: u32, info: SigInfo) {+        self.pending |= bit(sig);+        self.info.retain(|(s, _)| *s != sig);+        self.info.push((sig, info));+    }+}++/// Thread of a stopped process waits here for SIGCONT / SIGKILL.+static STOPPED: WaitQueue = WaitQueue::new();++/// Send signal `sig` to `p`. Returns at once; `p` acts on it on its way+/// back to user mode.+pub fn send(p: &Arc<Process>, sig: u32, info: SigInfo) {+    if p.status().is_some() || !(1..=NSIG).contains(&sig) {+        return; // already exited (a zombie): nothing to do+    }+    let continued = without_interrupts(|| {+        let mut s = p.signals.lock();+        let mut continued = false;+        if sig == SIGCONT || sig == SIGKILL {+            // Continue (even if SIGCONT itself is ignored or blocked), and+            // forget pending stops.+            s.pending &= !(bit(SIGSTOP) | bit(SIGTSTP) | bit(SIGTTIN) | bit(SIGTTOU));+            if s.stopped {+                s.stopped = false;+                s.report = Some(Report::Continued);+                continued = true;+            }+        }+        if default_action(sig) == DefaultAction::Stop {+            s.pending &= !bit(SIGCONT); // a stop cancels a pending continue+        }+        // Ignored signals are dropped right away -- unless blocked: the+        // program may unblock it after installing a handler.+        if sig == SIGKILL || !s.ignored(sig) || s.blocked & bit(sig) != 0 {+            s.raise(sig, info);+        }+        continued+    });+    if continued {+        STOPPED.wake_all();+        notify_parent(p, CLD_CONTINUED, SIGCONT as i32);+    }+    // If it's blocked in a syscall, wake it so it notices (see+    // thread::WaitQueue::wait_until_intr). If it's running, it'll notice at+    // its next return to user mode -- at the latest at the next timer tick.+    p.wake();+    STOPPED.wake_all(); // a stopped one needs to see SIGKILL+}++/// Send to every process in process group `pgid`. ESRCH if it's empty.+pub fn send_group(pgid: u64, sig: u32, info: SigInfo) -> Result<(), Errno> {+    let members = process::group_members(pgid);+    if members.is_empty() {+        return Err(Errno::ESRCH);+    }+    for p in &members {+        send(p, sig, info);+    }+    Ok(())+}++/// The current process caused a fault (SIGSEGV, SIGFPE, SIGILL, SIGBUS).+/// It can't be ignored or blocked -- the program can't just continue at+/// the faulting instruction -- so if it is, the default action (death)+/// applies, like Linux's force_sig.+pub fn force(sig: u32, info: SigInfo, reason: &'static str) {+    let Some(p) = thread::current_process() else { return };+    without_interrupts(|| {+        let mut s = p.signals.lock();+        if s.action(sig).handler == SIG_IGN || s.blocked & bit(sig) != 0 {+            s.actions[sig as usize - 1] = SigAction::default();+            s.blocked &= !bit(sig);+        }+        s.raise(sig, info);+        s.reason = Some(reason);+    });+}++/// Does the current process have a signal that should interrupt a+/// blocking syscall? (Called from thread.rs with interrupts off.)+pub fn interrupted() -> bool {+    match thread::current_process() {+        Some(p) => without_interrupts(|| p.signals.lock().deliverable() != 0),+        None => false, // kernel threads don't get signals+    }+}++/// Tell `child`'s parent that it stopped/continued/exited: wake its wait4+/// and send it SIGCHLD.+pub fn notify_parent(child: &Process, code: i32, status: i32) {+    let Some(parent) = process::get(child.parent()) else { return };+    parent.child_wait.wake_all();+    let nocldstop = without_interrupts(|| parent.signals.lock().action(SIGCHLD).flags & SA_NOCLDSTOP != 0);+    if nocldstop && (code == CLD_STOPPED || code == CLD_CONTINUED) {+        return;+    }+    send(&parent, SIGCHLD, SigInfo::child(code, child.pid, status));+}++/// The system-call results that mean "interrupted by a signal".+fn restart_code(rax: u64) -> Option<Errno> {+    match Errno::from_ret(rax as i64) {+        Some(e @ (Errno::ERESTARTSYS | Errno::ERESTARTNOHAND)) => Some(e),+        _ => None,+    }+}++/// Make the interrupted syscall run again when we return to user mode:+/// restore its number and back up rip over the 2-byte `syscall` instruction.+fn restart(frame: &mut TrapFrame) {+    frame.rax = frame.error; // the syscall number (see trap.rs)+    frame.rip -= 2;+}++/// On the way back to user mode: act on pending signals. Runs handlers+/// (by rewriting `frame`), applies default actions (which may end the+/// process right here), and sorts out interrupted syscalls.+pub fn deliver(frame: &mut TrapFrame) {+    if !frame.from_user() {+        return;+    }+    let Some(p) = thread::current_process() else { return };+    // Writing the signal frame may fault user pages in: interrupts on.+    x86_64::instructions::interrupts::enable();+    loop {+        let next = without_interrupts(|| {+            let mut s = p.signals.lock();+            let (sig, info) = s.dequeue()?;+            Some((sig, info, s.action(sig), s.reason.take()))+        });+        let Some((sig, info, action, reason)) = next else { break };+        match action.handler {+            SIG_IGN if sig != SIGKILL && sig != SIGSTOP => continue,+            SIG_IGN | SIG_DFL => match default_action(sig) {+                DefaultAction::Ignore | DefaultAction::Continue => continue,+                DefaultAction::Stop if sig == SIGKILL => unreachable!(),+                DefaultAction::Stop => stop(&p, sig),+                DefaultAction::Terminate | DefaultAction::Core => {+                    let why = reason.unwrap_or(name(sig));+                    drop(p);+                    process::exit_current(ExitStatus::Killed(sig, why));+                }+            },+            _ => {+                // A handler. First the interrupted syscall: give up with+                // EINTR, or restart it after the handler (SA_RESTART).+                if frame.is_syscall() {+                    match restart_code(frame.rax) {+                        Some(Errno::ERESTARTSYS) if action.flags & SA_RESTART != 0 => restart(frame),+                        Some(_) => frame.rax = -(Errno::EINTR as i64) as u64,+                        None => {}+                    }+                    frame.vector = 0; // dealt with: not a syscall return any more+                }+                if setup_frame(&p, frame, sig, &info, &action).is_err() {+                    // Couldn't write the frame (stack overflowed or bad+                    // restorer): like Linux, the process dies of SIGSEGV.+                    drop(p);+                    process::exit_current(ExitStatus::Killed(SIGSEGV, "bad signal frame"));+                }+                without_interrupts(|| {+                    let mut s = p.signals.lock();+                    s.blocked |= action.mask & !UNBLOCKABLE;+                    if action.flags & SA_NODEFER == 0 {+                        s.blocked |= bit(sig); // no nesting of the same signal+                    }+                    if action.flags & SA_RESETHAND != 0 {+                        s.actions[sig as usize - 1] = SigAction::default();+                    }+                });+                return; // one handler at a time; the next on its return+            }+        }+    }+    // No handler ran: an interrupted syscall simply runs again.+    if frame.is_syscall() && restart_code(frame.rax).is_some() {+        restart(frame);+    }+}++/// Stop the current process until SIGCONT (or SIGKILL).+fn stop(p: &Arc<Process>, sig: u32) {+    without_interrupts(|| {+        let mut s = p.signals.lock();+        s.stopped = true;+        s.report = Some(Report::Stopped(sig));+    });+    crate::serial_println!("[signal] pid {} stopped by {}", p.pid, name(sig));+    notify_parent(p, CLD_STOPPED, sig as i32);+    STOPPED.wait_until(|| {+        let s = p.signals.lock();+        (!s.stopped || s.pending & bit(SIGKILL) != 0).then_some(())+    });+}++// ---------------------------------------------------------------------------+// The signal frame (Linux's struct rt_sigframe, x86_64)+// ---------------------------------------------------------------------------++/// The saved registers (`struct sigcontext`; this order is Linux's ABI).+#[repr(C)]+#[derive(Clone, Copy, Default)]+struct SigContext {+    r8: u64,+    r9: u64,+    r10: u64,+    r11: u64,+    r12: u64,+    r13: u64,+    r14: u64,+    r15: u64,+    rdi: u64,+    rsi: u64,+    rbp: u64,+    rbx: u64,+    rdx: u64,+    rax: u64,+    rcx: u64,+    rsp: u64,+    rip: u64,+    eflags: u64,+    cs_gs_fs_ss: u64, // four 16-bit selectors+    err: u64,+    trapno: u64,+    oldmask: u64,+    cr2: u64,+    fpstate: u64, // pointer to saved FPU/SSE state; 0 = none (see below)+    reserved: [u64; 8],+}++#[repr(C)]+#[derive(Clone, Copy, Default)]+struct UContext {+    flags: u64,+    link: u64,+    stack: [u64; 3], // stack_t: ss_sp, ss_flags (+pad), ss_size+    mcontext: SigContext,+    sigmask: u64,+}++#[repr(C)]+#[derive(Clone, Copy)]+struct RtSigFrame {+    /// Return address of the handler: the restorer (-> rt_sigreturn).+    pretcode: u64,+    uc: UContext,+    info: SigInfo,+}++/// User-settable RFLAGS bits (carry, parity, aux, zero, sign, trap,+/// direction, overflow, alignment check, resume). The rest (interrupt+/// enable, I/O privilege level...) only the kernel decides.+const USER_RFLAGS: u64 = 0x0004_0DD5 | (1 << 16);++/// Push a signal frame on the user stack and point `frame` at the handler.+///+/// Note: we don't save the FPU/SSE registers (`fpstate` = 0). Our kernel+/// doesn't use them and our programs' handlers don't touch floats, so they+/// survive anyway; a full libc needs XSAVE state here (roadmap).+fn setup_frame(p: &Process, frame: &mut TrapFrame, sig: u32, info: &SigInfo, action: &SigAction) -> Result<(), ()> {+    if action.flags & SA_RESTORER == 0 || action.restorer == 0 {+        return Err(()); // nowhere to return to (x86_64 requires a restorer)+    }+    let size = core::mem::size_of::<RtSigFrame>() as u64;+    // Below the 128-byte "red zone" (leaf functions may use it without+    // moving rsp), aligned so the handler starts with rsp % 16 == 8, as+    // if it had been called.+    let sp = ((frame.rsp.wrapping_sub(128).wrapping_sub(size)) & !0xf).wrapping_sub(8);+    let blocked = without_interrupts(|| p.signals.lock().blocked);+    let s = crate::gdt::selectors();+    let mcontext = SigContext {+        r8: frame.r8,+        r9: frame.r9,+        r10: frame.r10,+        r11: frame.r11,+        r12: frame.r12,+        r13: frame.r13,+        r14: frame.r14,+        r15: frame.r15,+        rdi: frame.rdi,+        rsi: frame.rsi,+        rbp: frame.rbp,+        rbx: frame.rbx,+        rdx: frame.rdx,+        rax: frame.rax,+        rcx: frame.rcx,+        rsp: frame.rsp,+        rip: frame.rip,+        eflags: frame.rflags,+        cs_gs_fs_ss: s.user_code.0 as u64 | (s.user_data.0 as u64) << 48,+        err: if frame.vector == 14 { frame.error } else { 0 },+        trapno: frame.vector,+        oldmask: blocked,+        cr2: if sig == SIGSEGV || sig == SIGBUS { info.fields[0] } else { 0 },+        ..Default::default()+    };+    let rt = RtSigFrame {+        pretcode: action.restorer,+        uc: UContext { mcontext, sigmask: blocked, ..Default::default() },+        info: *info,+    };+    if !crate::mm::user_access_ok(sp, size, true) {+        return Err(());+    }+    unsafe { core::ptr::write_unaligned(sp as *mut RtSigFrame, rt) };+    frame.rsp = sp;+    frame.rip = action.handler;+    frame.rdi = sig as u64;+    frame.rsi = sp + core::mem::offset_of!(RtSigFrame, info) as u64;+    frame.rdx = sp + core::mem::offset_of!(RtSigFrame, uc) as u64;+    frame.rax = 0;+    frame.rflags &= !(1 << 8 | 1 << 10); // no single-step, direction flag clear+    Ok(())+}++/// rt_sigreturn (15): the handler returned into the restorer. The frame+/// starts 8 bytes below rsp (`ret` popped `pretcode`). Put back the saved+/// registers and mask -- after checking them: the program may have changed+/// the frame (that's allowed and used), but must not get kernel privileges+/// or a kernel rip out of it.+pub fn sys_rt_sigreturn(frame: &mut TrapFrame) {+    let sp = frame.rsp.wrapping_sub(8);+    let size = core::mem::size_of::<RtSigFrame>() as u64;+    if !crate::mm::user_access_ok(sp, size, false) {+        process::exit_current(ExitStatus::Killed(SIGSEGV, "bad sigreturn frame"));+    }+    let rt: RtSigFrame = unsafe { core::ptr::read_unaligned(sp as *const RtSigFrame) };+    let m = rt.uc.mcontext;+    let user = crate::memory::USER_START..crate::memory::USER_END;+    if !user.contains(&m.rip) || !(user.start..=user.end).contains(&m.rsp) {+        process::exit_current(ExitStatus::Killed(SIGSEGV, "bad sigreturn frame"));+    }+    let s = crate::gdt::selectors();+    *frame = TrapFrame {+        r8: m.r8,+        r9: m.r9,+        r10: m.r10,+        r11: m.r11,+        r12: m.r12,+        r13: m.r13,+        r14: m.r14,+        r15: m.r15,+        rdi: m.rdi,+        rsi: m.rsi,+        rbp: m.rbp,+        rbx: m.rbx,+        rdx: m.rdx,+        rax: m.rax,+        rcx: m.rcx,+        rsp: m.rsp,+        rip: m.rip,+        rflags: 0x202 | (m.eflags & USER_RFLAGS),+        cs: s.user_code.0 as u64,+        ss: s.user_data.0 as u64,+        vector: 0, // not a syscall return: rax is the restored value+        error: 0,+    };+    if let Some(p) = thread::current_process() {+        without_interrupts(|| p.signals.lock().blocked = rt.uc.sigmask & !UNBLOCKABLE);+    }+}++// ---------------------------------------------------------------------------+// Syscalls+// ---------------------------------------------------------------------------++fn check_sig(sig: u64) -> Result<u32, Errno> {+    if (1..=NSIG as u64).contains(&sig) { Ok(sig as u32) } else { Err(Errno::EINVAL) }+}++/// rt_sigaction(sig, *new, *old, sigsetsize = 8).+pub fn sys_rt_sigaction(p: &Process, sig: u64, new: Option<SigAction>, set_size: u64) -> Result<SigAction, Errno> {+    let sig = check_sig(sig)?;+    if set_size != 8 || (new.is_some() && (sig == SIGKILL || sig == SIGSTOP)) {+        return Err(Errno::EINVAL);+    }+    Ok(without_interrupts(|| {+        let mut s = p.signals.lock();+        let old = s.action(sig);+        if let Some(mut new) = new {+            new.mask &= !UNBLOCKABLE;+            s.actions[sig as usize - 1] = new;+            // Setting a signal to "ignored" discards it if pending (POSIX).+            if s.ignored(sig) {+                s.pending &= !bit(sig);+            }+        }+        old+    }))+}++/// rt_sigprocmask(how, *set, *old, sigsetsize = 8). Returns the old mask.+pub fn sys_rt_sigprocmask(p: &Process, how: u64, set: Option<u64>, set_size: u64) -> Result<u64, Errno> {+    if set_size != 8 {+        return Err(Errno::EINVAL);+    }+    without_interrupts(|| {+        let mut s = p.signals.lock();+        let old = s.blocked;+        if let Some(set) = set {+            s.blocked = match how {+                SIG_BLOCK => old | set,+                SIG_UNBLOCK => old & !set,+                SIG_SETMASK => set,+                _ => return Err(Errno::EINVAL),+            } & !UNBLOCKABLE;+        }+        Ok(old)+    })+}++/// kill(pid, sig): pid > 0 one process, 0 = my process group, -1 = every+/// process but me, < -1 = process group -pid. sig 0 = just check it exists.+pub fn sys_kill(me: &Process, pid: i64, sig: u64) -> Result<u64, Errno> {+    if sig > NSIG as u64 {+        return Err(Errno::EINVAL);+    }+    let sig = sig as u32;+    let targets: Vec<Arc<Process>> = match pid {+        0 => process::group_members(me.pgid()),+        -1 => process::all().into_iter().filter(|p| p.pid != me.pid).collect(),+        p if p < -1 => process::group_members(p.unsigned_abs()),+        p => process::get(p as u64).into_iter().collect(),+    };+    let targets: Vec<_> = targets.into_iter().filter(|p| p.status().is_none()).collect();+    if targets.is_empty() {+        return Err(Errno::ESRCH);+    }+    if sig != 0 {+        for t in &targets {+            send(t, sig, SigInfo::from_pid(sig, me.pid));+        }+    }+    Ok(0)+}
@@ -1,37 +1,41 @@-//! System calls via the `syscall` / `sysret` instructions.+//! System calls via the `syscall` instruction. //!-//! ABI (like Linux x86_64): number in rax, arguments in rdi, rsi, rdx (r10,-//! r8 for 4th/5th), result in rax; negative results are `-errno`. The-//! `syscall` instruction itself clobbers rcx (return address) and r11 (saved+//! ABI (like Linux x86_64): number in rax, arguments in rdi, rsi, rdx, r10,+//! r8, r9; result in rax; negative results are `-errno`. The `syscall`+//! instruction itself clobbers rcx (return address) and r11 (saved //! RFLAGS); every other register is preserved for the caller. //! //! Numbers are Linux's where Linux has the same call, so `strace` output and-//! man pages mostly apply. Paths are passed as pointer + length (Linux uses-//! NUL-terminated C strings); relative paths start at the process's cwd.+//! man pages mostly apply. The process calls added in stage 14 (fork,+//! execve, wait4, mmap, signals, dup, pipe2, fcntl, process groups) follow+//! Linux exactly -- C strings, structs, flags -- so a C library can use+//! them as they are. The older file calls still take paths as pointer ++//! length (Linux: NUL-terminated); relative paths start at the cwd. //! //! | nr  | name        | arguments                           | note | //! |-----|-------------|-------------------------------------|------| //! |   0 | read        | fd, buf, len                        | blocks: console until Enter, pipe until data/EOF |-//! |   1 | write       | fd, buf, len                        | |-//! |   2 | open        | path, path_len, flags               | O_CREAT 0o100, O_TRUNC 0o1000; directories too (for getdents) |+//! |   1 | write       | fd, buf, len                        | to a pipe without readers: EPIPE + SIGPIPE |+//! |   2 | open        | path, path_len, flags               | O_CREAT 0o100, O_TRUNC 0o1000, O_APPEND, O_CLOEXEC 0o2000000 | //! |   3 | close       | fd                                  | | //! |   4 | stat        | path, path_len, *Stat               | our own small `Stat {kind, size, mode}`; follows symlinks | //! |   5 | fstat       | fd, *Stat                           | | //! |   8 | lseek       | fd, offset, whence                  | SEEK_SET/CUR/END = 0/1/2 |-//! |  16 | ioctl       | fd, request, arg                    | console: TTY_SET_RAW (ours); /dev/fb0: FB_GET_INFO, FB_FLUSH, FB_FILL, FB_SET_CONSOLE (below) |+//! |   9 | mmap        | addr, len, prot, flags, fd, offset  | MAP_PRIVATE/SHARED, MAP_ANONYMOUS, MAP_FIXED; files: private only |+//! |  10 | mprotect    | addr, len, prot                     | PROT_READ 1, PROT_WRITE 2, PROT_EXEC 4 |+//! |  11 | munmap      | addr, len                           | |+//! |  12 | brk         | addr                                | returns the (new) end of the heap; brk(0) = current |+//! |  13 | rt_sigaction | sig, *new, *old, 8                 | kernel `struct sigaction`; handlers need SA_RESTORER |+//! |  14 | rt_sigprocmask | how, *set, *old, 8               | SIG_BLOCK 0 / SIG_UNBLOCK 1 / SIG_SETMASK 2 |+//! |  15 | rt_sigreturn |                                    | only from a signal handler's restorer |+//! |  16 | ioctl       | fd, request, arg                    | console: TTY_SET_RAW (ours), TIOCGPGRP, TIOCSPGRP, TIOCGWINSZ; /dev/fb0: FB_* (below) | //! |  22 | pipe        | *[u32; 2]                           | [read end, write end] | //! |  24 | sched_yield |                                     | |-//! |  35 | sleep_ms    | milliseconds                        | Linux's nr 35 is nanosleep(timespec); ours is simpler |+//! |  32 | dup         | fd                                  | lowest free fd |+//! |  33 | dup2        | old, new                            | closes `new` first if open |+//! |  34 | pause       |                                     | sleeps until a signal; returns EINTR |+//! |  35 | sleep_ms    | milliseconds                        | Linux's nr 35 is nanosleep(timespec); ours is simpler. EINTR on a signal | //! |  39 | getpid      |                                     | |-//! |  60 | exit        | code                                | |-//! |  61 | wait        | pid, flags                          | returns the exit code (139 if killed); WNOHANG = 1 -> EAGAIN if running. Linux wait4 is fancier |-//! |  63 | uname       | *utsname                            | Linux's `struct utsname` (6 x 65-byte strings) |-//! |  79 | getcwd      | buf, len                            | returns the length (no NUL) |-//! |  80 | chdir       | path, path_len                      | |-//! |  83 | mkdir       | path, path_len                      | |-//! |  84 | rmdir       | path, path_len                      | only empty directories |-//! |  87 | unlink      | path, path_len                      | files and symlinks (not what a symlink points to) |-//! |  89 | readlink    | path, path_len, buf, len            | a symlink's target; returns its length (cut to `len`, no NUL) | //! |  41 | socket      | domain, type, protocol              | AF_INET (2) only; SOCK_STREAM (1) = TCP, SOCK_DGRAM (2) = UDP, SOCK_DGRAM + IPPROTO_ICMP (1) = ping socket | //! |  42 | connect     | fd, *sockaddr_in, len               | TCP: handshake, blocks (10 s timeout); UDP/ICMP: set default destination | //! |  43 | accept      | fd, *sockaddr_in or 0, *u32 len or 0 | blocks for a connection (SO_RCVTIMEO applies); returns the new fd |@@ -40,14 +44,37 @@ //! |  49 | bind        | fd, *sockaddr_in, len               | sets the local port (address ignored: we have one) | //! |  50 | listen      | fd, backlog                         | TCP; backlog 1..4 | //! |  54 | setsockopt  | fd, level, name, *value, len        | only SOL_SOCKET (1) / SO_RCVTIMEO (20) with a `struct timeval` |+//! |  56 | clone       | flags, stack, ...                   | only fork-like flags: SIGCHLD + optional CLONE_VFORK (else ENOSYS) |+//! |  57 | fork        |                                     | copy-on-write; parent gets the pid, child 0 |+//! |  58 | vfork       |                                     | fork + the parent waits until the child execs or exits |+//! |  59 | execve      | path, argv, envp (C strings)        | Linux's stack layout: argc, argv, envp, auxv |+//! |  60 | exit        | code                                | |+//! |  61 | wait4       | pid, *status, options, *rusage      | WNOHANG 1, WUNTRACED 2, WCONTINUED 8; rusage must be 0 |+//! |  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, F_DUPFD_CLOEXEC 1030 |+//! |  79 | getcwd      | buf, len                            | returns the length (no NUL) |+//! |  80 | chdir       | path, path_len                      | |+//! |  83 | mkdir       | path, path_len                      | |+//! |  84 | rmdir       | path, path_len                      | only empty directories |+//! |  87 | unlink      | path, path_len                      | files and symlinks (not what a symlink points to) |+//! |  89 | readlink    | path, path_len, buf, len            | a symlink's target; returns its length (cut to `len`, no NUL) |+//! | 109 | setpgid     | pid, pgid                           | 0 = myself / a new group |+//! | 110 | getppid     |                                     | 0 if started by the kernel or orphaned |+//! | 111 | getpgrp     |                                     | |+//! | 121 | getpgid     | pid                                 | | //! | 217 | getdents64  | fd, buf, len                        | Linux `struct linux_dirent64` records | //! | 228 | clock_gettime | clock, *timespec                  | CLOCK_REALTIME/MONOTONIC both = time since boot (we have no RTC driver) |+//! | 231 | exit_group  | code                                | = exit (one thread per process) |+//! | 292 | dup3        | old, new, flags                     | flags: O_CLOEXEC |+//! | 293 | pipe2       | *[u32; 2], flags                    | flags: O_CLOEXEC | //! | 318 | getrandom   | buf, len, flags                     | random bytes (virtio-rng); up to 64 KiB per call; flags ignored |-//! | 500 | spawn       | path, path_len, argv, argv_len, *SpawnOptions | ours: start a program, returns its pid (see process.rs for why not fork/exec) |+//! | 500 | spawn       | path, path_len, argv, argv_len, *SpawnOptions | ours: new process running a program (fork+exec in one step, like posix_spawn) | //! | 501 | mounts      | buf, len                            | ours: "path fstype\n" per mount (Linux: read /proc/mounts) | //! | 502 | sysinfo     | *SysInfo                            | ours: CPUs, threads, memory, per-CPU busy time (Linux: /proc/stat etc.) | //! | 503 | resolve     | name, name_len, *[[u8; 4]], max     | ours: DNS lookup (A records), returns the count. Linux does DNS in libc | //! | 504 | netinfo     | *NetInfo                            | ours: MAC, IP, gateway, DNS, packet counters (Linux: netlink/ioctls) |+//! | 505 | trace_me    | on                                  | ours: print my syscalls from now on (survives execve; `run --trace`) | //! //! spawn's `argv` is all arguments separated by NUL bytes ("ls\0-l\0/bin"); //! empty means just [program name]. `SpawnOptions` (or a null pointer for@@ -71,8 +98,10 @@ //!   ...but RSP is still the *user* stack! The CPU doesn't switch stacks for //!   `syscall` (unlike interrupts, which use TSS.RSP0). So `syscall_entry` //!   must do it first: stash the user RSP, load this thread's kernel stack,-//!   save registers, re-enable interrupts and call `syscall_dispatch` in Rust.-//!   `sysretq` reverses it: RIP = rcx, RFLAGS = r11, CPL = 3.+//!   save all registers as a `TrapFrame` (trap.rs), re-enable interrupts and+//!   call `syscall_dispatch` in Rust. The way back is trap.rs's common exit,+//!   `iretq` (stage 13 used the faster `sysretq`, but that can't restore+//!   rcx/r11, which a signal handler's return needs). //! //! **Several CPUs.** The "kernel stack of the running thread" differs per //! CPU, but at the entry we have no free register to compute which CPU we@@ -90,7 +119,8 @@ use crate::errno::Errno; use crate::fs::file::{InodeFile, Stat}; use crate::fs::{self, InodeKind};-use crate::{memory, process, thread};+use crate::trap::TrapFrame;+use crate::{memory, process, signal, thread}; use alloc::{string::String, sync::Arc, vec::Vec}; use core::arch::global_asm; use core::sync::atomic::Ordering;@@ -108,7 +138,6 @@ pub const SYS_SCHED_YIELD: u64 = 24; pub const SYS_SLEEP_MS: u64 = 35; pub const SYS_GETPID: u64 = 39; pub const SYS_EXIT: u64 = 60;-pub const SYS_WAIT: u64 = 61; pub const SYS_UNAME: u64 = 63; pub const SYS_GETCWD: u64 = 79; pub const SYS_CHDIR: u64 = 80;@@ -132,6 +161,31 @@ pub const SYS_MOUNTS: u64 = 501; pub const SYS_SYSINFO: u64 = 502; pub const SYS_RESOLVE: u64 = 503; pub const SYS_NETINFO: u64 = 504;+pub const SYS_MMAP: u64 = 9;+pub const SYS_MPROTECT: u64 = 10;+pub const SYS_MUNMAP: u64 = 11;+pub const SYS_BRK: u64 = 12;+pub const SYS_RT_SIGACTION: u64 = 13;+pub const SYS_RT_SIGPROCMASK: u64 = 14;+pub const SYS_RT_SIGRETURN: u64 = 15;+pub const SYS_DUP: u64 = 32;+pub const SYS_DUP2: u64 = 33;+pub const SYS_PAUSE: u64 = 34;+pub const SYS_CLONE: u64 = 56;+pub const SYS_FORK: u64 = 57;+pub const SYS_VFORK: u64 = 58;+pub const SYS_EXECVE: u64 = 59;+pub const SYS_WAIT4: u64 = 61;+pub const SYS_KILL: u64 = 62;+pub const SYS_FCNTL: u64 = 72;+pub const SYS_SETPGID: u64 = 109;+pub const SYS_GETPPID: u64 = 110;+pub const SYS_GETPGRP: u64 = 111;+pub const SYS_GETPGID: u64 = 121;+pub const SYS_EXIT_GROUP: u64 = 231;+pub const SYS_DUP3: u64 = 292;+pub const SYS_PIPE2: u64 = 293;+pub const SYS_TRACE_ME: u64 = 505;  // Socket constants (Linux's values). pub const AF_INET: u64 = 2;@@ -144,6 +198,27 @@ pub const SO_RCVTIMEO: u64 = 20; pub const O_CREAT: u64 = 0o100; pub const O_TRUNC: u64 = 0o1000; pub const O_APPEND: u64 = 0o2000;+pub const O_NONBLOCK: u64 = 0o4000;+pub const O_CLOEXEC: u64 = 0o2000000;+/// fcntl commands.+pub const F_DUPFD: u64 = 0;+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_DUPFD_CLOEXEC: u64 = 1030;+pub const FD_CLOEXEC: u64 = 1;+/// Terminal ioctls (Linux's numbers).+pub const TIOCGPGRP: u64 = 0x540F;+pub const TIOCSPGRP: u64 = 0x5410;+pub const TIOCGWINSZ: u64 = 0x5413;+/// mmap flags.+pub const MAP_SHARED: u64 = 0x01;+pub const MAP_PRIVATE: u64 = 0x02;+pub const MAP_FIXED: u64 = 0x10;+pub const MAP_ANONYMOUS: u64 = 0x20;+/// clone flags we understand.+pub const CLONE_VFORK: u64 = 0x4000; /// ioctl request (ours): arg 1 = raw mode, 0 = cooked; returns the old mode. pub const TTY_SET_RAW: u64 = 1; pub const SPAWN_TRACE: u64 = 1;@@ -169,7 +244,31 @@ pub fn name(nr: u64) -> &'static str {         SYS_SLEEP_MS => "sleep_ms",         SYS_GETPID => "getpid",         SYS_EXIT => "exit",-        SYS_WAIT => "wait",+        SYS_WAIT4 => "wait4",+        SYS_MMAP => "mmap",+        SYS_MPROTECT => "mprotect",+        SYS_MUNMAP => "munmap",+        SYS_BRK => "brk",+        SYS_RT_SIGACTION => "rt_sigaction",+        SYS_RT_SIGPROCMASK => "rt_sigprocmask",+        SYS_RT_SIGRETURN => "rt_sigreturn",+        SYS_DUP => "dup",+        SYS_DUP2 => "dup2",+        SYS_DUP3 => "dup3",+        SYS_PAUSE => "pause",+        SYS_CLONE => "clone",+        SYS_FORK => "fork",+        SYS_VFORK => "vfork",+        SYS_EXECVE => "execve",+        SYS_KILL => "kill",+        SYS_FCNTL => "fcntl",+        SYS_SETPGID => "setpgid",+        SYS_GETPPID => "getppid",+        SYS_GETPGRP => "getpgrp",+        SYS_GETPGID => "getpgid",+        SYS_EXIT_GROUP => "exit_group",+        SYS_PIPE2 => "pipe2",+        SYS_TRACE_ME => "trace_me",         SYS_UNAME => "uname",         SYS_GETCWD => "getcwd",         SYS_CHDIR => "chdir",@@ -250,54 +349,66 @@ global_asm!(     "    swapgs",     "    mov gs:[8], rsp",     "    mov rsp, gs:[0]",-    "    push qword ptr gs:[8]", // user rsp-    "    swapgs",                // GS back to normal right away-    "    push rcx",                                // user rip-    "    push r11",                                // user rflags-    // Preserve the argument registers for the user (Rust may clobber them).-    // rbx, rbp, r12-r15 are callee-saved: the Rust code keeps them intact.-    "    push rdi",-    "    push rsi",+    // Build a TrapFrame (trap.rs), the same layout an interrupt makes:+    "    push 0",                 // ss (filled in by syscall_dispatch)+    "    push qword ptr gs:[8]",  // user rsp+    "    swapgs",                 // GS back to normal right away+    "    push r11",               // user rflags (saved there by `syscall`)+    "    push 0",                 // cs (filled in by syscall_dispatch)+    "    push rcx",               // user rip (saved there by `syscall`)+    "    push rax",               // `error` slot: the syscall number+    "    push 0x100",             // `vector`: TRAP_SYSCALL+    "    push rax",+    "    push rbx",+    "    push rcx",     "    push rdx",+    "    push rsi",+    "    push rdi",+    "    push rbp",     "    push r8",     "    push r9",     "    push r10",-    "    sub rsp, 8", // 10 pushes total -> stack 16-byte aligned for the call-    // syscall_dispatch(nr, a1, a2, a3, a4, a5) in the C calling convention:-    // shuffle rax, rdi, rsi, rdx, r10, r8 -> rdi, rsi, rdx, rcx, r8, r9.-    "    mov r9, r8",-    "    mov r8, r10",-    "    mov rcx, rdx",-    "    mov rdx, rsi",-    "    mov rsi, rdi",-    "    mov rdi, rax",-    "    sti",  // we're on our own stack now: preemption is fine-    "    call {dispatch}", // result in rax-    "    cli",  // no interrupts while we're back on the user stack in ring 0-    "    add rsp, 8",-    "    pop r10",-    "    pop r9",-    "    pop r8",-    "    pop rdx",-    "    pop rsi",-    "    pop rdi",-    "    pop r11",-    "    pop rcx",-    "    pop rsp",-    // (rcx came from the `syscall` itself, so it's a canonical address;-    // sysret with a non-canonical rcx would fault *in ring 0* -- a classic bug.)-    "    sysretq",+    "    push r11",+    "    push r12",+    "    push r13",+    "    push r14",+    "    push r15",+    "    mov rdi, rsp",   // syscall_dispatch(&mut TrapFrame)+    "    sti",            // we're on our own stack now: preemption is fine+    "    call {dispatch}",+    // Back to user mode with `iretq` through the common exit (which+    // restores every register from the frame). Not `sysretq`: fork, execve,+    // signals and rt_sigreturn may have changed *any* register, and+    // sysret can't set rcx and r11.+    "    jmp {ret}",     dispatch = sym syscall_dispatch,+    ret = sym crate::trap::trap_return, ); -/// Rust side of every system call. Returns the value for rax.-extern "C" fn syscall_dispatch(nr: u64, a1: u64, a2: u64, a3: u64, a4: u64, a5: u64) -> i64 {+/// Rust side of every system call. Arguments are in the saved registers+/// (rdi, rsi, rdx, r10, r8, r9 = a1..a6), the number in `error` (= rax at+/// entry); the result goes into the saved rax.+extern "C" fn syscall_dispatch(frame: &mut TrapFrame) {+    let sel = crate::gdt::selectors();+    frame.cs = sel.user_code.0 as u64;+    frame.ss = sel.user_data.0 as u64;+    let nr = frame.error;+    let (a1, a2, a3, a4, a5) = (frame.rdi, frame.rsi, frame.rdx, frame.r10, frame.r8);     let process = thread::current_process();-    let trace = process.as_ref().is_some_and(|p| p.trace);-    if trace && nr == SYS_EXIT {+    let trace = process.as_ref().is_some_and(|p| p.tracing());+    if trace && (nr == SYS_EXIT || nr == SYS_EXIT_GROUP) {         crate::kprintln!("[trace {}] exit({})", process.as_ref().unwrap().pid, a1 as i64);     }     drop(process); // don't hold an Arc while blocking/exiting+    // A successful execve throws away the memory its path lived in, so the+    // trace line for it is made before the call.+    let exec_line = (trace && nr == SYS_EXECVE).then(|| trace_line(nr, [a1, a2, a3, a4], 0));+    if nr == SYS_RT_SIGRETURN {+        // Restores *all* registers (rax included) from the signal frame.+        signal::sys_rt_sigreturn(frame);+        signal::deliver(frame); // signals unblocked by the restored mask+        return;+    }     let result = match nr {         SYS_READ => sys_read(a1, a2, a3),         SYS_WRITE => sys_write(a1, a2, a3),@@ -306,27 +417,47 @@ extern "C" fn syscall_dispatch(nr: u64, a1: u64, a2: u64, a3: u64, a4: u64, a5:          SYS_STAT => sys_stat(a1, a2, a3),         SYS_FSTAT => sys_fstat(a1, a2),         SYS_LSEEK => current().and_then(|p| p.file(a1)).and_then(|f| f.seek(a2 as i64, a3)),+        SYS_MMAP => current().and_then(|p| sys_mmap(&p, a1, a2, a3, a4, a5, frame.r9)),+        SYS_MPROTECT => current().and_then(|p| sys_mprotect(&p, a1, a2, a3)),+        SYS_MUNMAP => current().and_then(|p| sys_munmap(&p, a1, a2)),+        SYS_BRK => current().map(|p| p.mm.lock().set_brk(a1)),+        SYS_RT_SIGACTION => sys_rt_sigaction(a1, a2, a3, a4),+        SYS_RT_SIGPROCMASK => sys_rt_sigprocmask(a1, a2, a3, a4),         SYS_IOCTL => sys_ioctl(a1, a2, a3),-        SYS_PIPE => sys_pipe(a1),+        SYS_PIPE => sys_pipe2(a1, 0),+        SYS_PIPE2 => sys_pipe2(a1, a2),         SYS_SCHED_YIELD => {             thread::yield_now();             Ok(0)         }+        SYS_DUP => current().and_then(|p| p.file(a1).and_then(|f| p.add_file(f))),+        SYS_DUP2 => current().and_then(|p| if a1 == a2 { p.file(a1).map(|_| a1) } else { p.dup_to(a1, a2, false) }),+        SYS_DUP3 => sys_dup3(a1, a2, a3),+        SYS_PAUSE => Err(thread::pause()),         SYS_SLEEP_MS => {             // The PIT fires ~18.2 times a second: 1 tick = ~55 ms. Round up.-            thread::sleep_ticks(a1.div_ceil(55));-            Ok(0)+            thread::sleep_ticks_intr(a1.div_ceil(55)).map(|_| 0)         }         SYS_GETPID => current().map(|p| p.pid),-        SYS_EXIT => process::exit_current(process::ExitStatus::Exited(a1 as i64)),-        SYS_WAIT => current().and_then(|p| process::wait_child(p.pid, a1, a2)).map(|s| s.code() as u64),+        SYS_CLONE => sys_clone(frame, a1, a2),+        SYS_FORK => current().and_then(|p| process::fork(&p, frame, 0, false)),+        SYS_VFORK => current().and_then(|p| process::fork(&p, frame, 0, true)),+        SYS_EXECVE => sys_execve(frame, a1, a2, a3),+        SYS_EXIT | SYS_EXIT_GROUP => process::exit_current(process::ExitStatus::Exited(a1 as i32 as i64)),+        SYS_WAIT4 => sys_wait4(a1 as i32 as i64, a2, a3),+        SYS_KILL => current().and_then(|p| signal::sys_kill(&p, a1 as i32 as i64, a2)),         SYS_UNAME => sys_uname(a1),+        SYS_FCNTL => sys_fcntl(a1, a2, a3),         SYS_GETCWD => sys_getcwd(a1, a2),         SYS_CHDIR => sys_chdir(a1, a2),         SYS_MKDIR => user_path(a1, a2).and_then(|p| fs::create(&p, InodeKind::Directory)).map(|_| 0),         SYS_RMDIR => user_path(a1, a2).and_then(|p| fs::remove(&p, true)).map(|_| 0),         SYS_UNLINK => user_path(a1, a2).and_then(|p| fs::remove(&p, false)).map(|_| 0),         SYS_READLINK => sys_readlink(a1, a2, a3, a4),+        SYS_SETPGID => current().and_then(|p| process::setpgid(&p, a1, a2)).map(|_| 0),+        SYS_GETPPID => current().map(|p| if p.parent() == process::ORPHAN { 0 } else { p.parent() }),+        SYS_GETPGRP => current().map(|p| p.pgid()),+        SYS_GETPGID => current().and_then(|p| if a1 == 0 { Ok(p.pgid()) } else { process::get(a1).map(|t| t.pgid()).ok_or(Errno::ESRCH) }),         SYS_GETDENTS64 => sys_getdents64(a1, a2, a3),         SYS_SPAWN => sys_spawn(a1, a2, a3, a4, a5),         SYS_MOUNTS => sys_mounts(a1, a2),@@ -343,17 +474,28 @@ extern "C" fn syscall_dispatch(nr: u64, a1: u64, a2: u64, a3: u64, a4: u64, a5:          SYS_RESOLVE => sys_resolve(a1, a2, a3, a4),         SYS_NETINFO => copy_to_user(a1, &crate::net::info()).map(|_| 0),         SYS_GETRANDOM => sys_getrandom(a1, a2),+        SYS_TRACE_ME => current().map(|p| p.trace.store(a1 != 0, Ordering::Relaxed)).map(|_| 0),         _ => Err(Errno::ENOSYS),     };     let ret = match result {         Ok(value) => value as i64,         Err(errno) => -(errno as i64),     };+    // execve: success means "the new program starts with these registers"+    // (rax = 0 there anyway); the frame was replaced, so don't touch it.+    if !(nr == SYS_EXECVE && ret == 0) {+        frame.rax = ret as u64;+    }     if trace {         let pid = current().map(|p| p.pid).unwrap_or(0);-        crate::kprintln!("[trace {}] {}", pid, trace_line(nr, [a1, a2, a3, a4], ret));+        match exec_line {+            Some(line) if ret == 0 => crate::kprintln!("[trace {}] {}", pid, line),+            _ => crate::kprintln!("[trace {}] {}", pid, trace_line(nr, [a1, a2, a3, a4], ret)),+        }     }-    ret+    // A signal may have arrived meanwhile (or the call was interrupted by+    // one): act on it before returning to the program.+    signal::deliver(frame); }  /// One syscall in strace style: `open("/mnt/disk0/readme.txt", 0) = 3`.@@ -378,6 +520,12 @@ fn trace_line(nr: u64, a: [u64; 4], ret: i64) -> String {         }         s     };+    let cstr = |ptr: u64| -> String {+        match user_cstr(ptr, 4096) {+            Ok(bytes) => format!("\"{}\"", String::from_utf8_lossy(&bytes)),+            Err(_) => String::from("<bad pointer>"),+        }+    };     let args = match nr {         SYS_READ => format!("{}, {}, {}", a[0], if ret > 0 { text(a[1], ret as u64) } else { String::from("\"\"") }, a[2]),         SYS_WRITE => format!("{}, {}, {}", a[0], text(a[1], a[2]), a[2]),@@ -387,9 +535,14 @@ fn trace_line(nr: u64, a: [u64; 4], ret: i64) -> String {         SYS_CHDIR | SYS_MKDIR | SYS_RMDIR | SYS_UNLINK => text(a[0], a[1]),         SYS_SPAWN => format!("{}, {}", text(a[0], a[1]), text(a[2], a[3])),         SYS_SCHED_YIELD | SYS_GETPID => String::new(),-        SYS_CLOSE | SYS_SLEEP_MS | SYS_EXIT => format!("{}", a[0] as i64),+        SYS_CLOSE | SYS_SLEEP_MS | SYS_EXIT | SYS_EXIT_GROUP | SYS_DUP => format!("{}", a[0] as i64),         SYS_PIPE | SYS_UNAME => format!("{:#x}", a[0]),-        SYS_FSTAT | SYS_WAIT | SYS_GETCWD | SYS_MOUNTS => format!("{}, {:#x}", a[0], a[1]),+        SYS_FSTAT | SYS_GETCWD | SYS_MOUNTS => format!("{}, {:#x}", a[0], a[1]),+        SYS_WAIT4 => format!("{}, {:#x}, {}", a[0] as i32, a[1], a[2]),+        SYS_KILL => format!("{}, {}", a[0] as i32, a[1]),+        SYS_FORK | SYS_VFORK | SYS_GETPPID | SYS_GETPGRP | SYS_PAUSE => String::new(),+        SYS_EXECVE => format!("{}, ...", cstr(a[0])),+        SYS_MMAP => format!("{:#x}, {}, {}, {:#x}", a[0], a[1], a[2], a[3]),         _ => format!("{}, {:#x}, {:#x}", a[0], a[1], a[2]),     };     match Errno::from_ret(ret) {@@ -408,7 +561,7 @@ pub fn user_slice(ptr: u64, len: u64) -> Result<&'static [u8], Errno> {     if len == 0 {         return Ok(&[]); // nothing to access, so any pointer is fine (like Linux)     }-    if !memory::user_range_ok(ptr, len, false) {+    if !crate::mm::user_access_ok(ptr, len, false) {         return Err(Errno::EFAULT);     }     Ok(unsafe { core::slice::from_raw_parts(ptr as *const u8, len as usize) })@@ -419,7 +572,7 @@ pub fn user_slice_mut(ptr: u64, len: u64) -> Result<&'static mut [u8], Errno> {     if len == 0 {         return Ok(&mut []);     }-    if !memory::user_range_ok(ptr, len, true) {+    if !crate::mm::user_access_ok(ptr, len, true) {         return Err(Errno::EFAULT);     }     Ok(unsafe { core::slice::from_raw_parts_mut(ptr as *mut u8, len as usize) })@@ -432,9 +585,16 @@ fn sys_read(fd: u64, buf: u64, len: u64) -> Result<u64, Errno> { }  fn sys_write(fd: u64, buf: u64, len: u64) -> Result<u64, Errno> {-    let file = current()?.file(fd)?;+    let process = current()?;+    let file = process.file(fd)?;     let buf = user_slice(buf, len)?;-    file.write(buf).map(|n| n as u64)+    let result = file.write(buf).map(|n| n as u64);+    if result == Err(Errno::EPIPE) {+        // Nobody will ever read this: SIGPIPE, which by default ends the+        // writer. That's how `yes | head -1` stops `yes`.+        signal::send(&process, signal::SIGPIPE, signal::SigInfo::from_pid(signal::SIGPIPE, 0));+    }+    result }  /// Read a path argument and make it absolute (relative to the cwd).@@ -472,14 +632,15 @@ fn sys_open(path_ptr: u64, path_len: u64, flags: u64) -> Result<u64, Errno> {         other => other?,     };     // Device files (/dev/...) come with their own File.+    let cloexec = flags & O_CLOEXEC != 0;     if let Some(device) = inode.open_device() {-        return current()?.add_file(device);+        return current()?.add_file_with(device, 0, cloexec);     }     if flags & O_TRUNC != 0 && inode.kind() == InodeKind::File {         inode.truncate()?;     }     let file = if flags & O_APPEND != 0 { InodeFile::open_append(inode)? } else { InodeFile::open(inode)? };-    current()?.add_file(Arc::new(file))+    current()?.add_file_with(Arc::new(file), 0, cloexec) }  fn sys_stat(path_ptr: u64, path_len: u64, stat_ptr: u64) -> Result<u64, Errno> {@@ -494,12 +655,16 @@ fn sys_fstat(fd: u64, stat_ptr: u64) -> Result<u64, Errno> {     Ok(0) } -fn sys_pipe(fds_ptr: u64) -> Result<u64, Errno> {+fn sys_pipe2(fds_ptr: u64, flags: u64) -> Result<u64, Errno> {+    if flags & !O_CLOEXEC != 0 {+        return Err(Errno::EINVAL); // O_NONBLOCK, O_DIRECT: not supported+    }+    let cloexec = flags & O_CLOEXEC != 0;     user_slice_mut(fds_ptr, 8)?; // check before creating anything     let process = current()?;     let (reader, writer) = fs::pipe::new();-    let read_fd = process.add_file(Arc::new(reader))?;-    let write_fd = match process.add_file(Arc::new(writer)) {+    let read_fd = process.add_file_with(Arc::new(reader), 0, cloexec)?;+    let write_fd = match process.add_file_with(Arc::new(writer), 0, cloexec) {         Ok(fd) => fd,         Err(e) => {             process.close(read_fd)?;@@ -510,6 +675,31 @@ fn sys_pipe(fds_ptr: u64) -> Result<u64, Errno> {     Ok(0) } +fn sys_dup3(old: u64, new: u64, flags: u64) -> Result<u64, Errno> {+    if old == new || flags & !O_CLOEXEC != 0 {+        return Err(Errno::EINVAL);+    }+    current()?.dup_to(old, new, flags & O_CLOEXEC != 0)+}++/// fcntl: just the fd flags (close-on-exec) and duplication. Status flags:+/// everything is blocking, read/write (O_NONBLOCK isn't supported yet).+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 => 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), // O_RDWR+        F_SETFL if arg & O_NONBLOCK != 0 => Err(Errno::EINVAL),+        F_SETFL => Ok(0),+        _ => Err(Errno::EINVAL),+    }+}+ fn sys_getcwd(buf: u64, len: u64) -> Result<u64, Errno> {     let cwd = current()?.cwd.lock().clone();     if (len as usize) < cwd.len() {@@ -562,9 +752,11 @@ fn sys_spawn(path_ptr: u64, path_len: u64, argv_ptr: u64, argv_len: u64, opts_pt     let stdio = [pick(0, opts.stdin)?, pick(1, opts.stdout)?, pick(2, opts.stderr)?];     let spawn = process::SpawnArgs {         args,+        env: parent.env(),         stdio,         cwd: parent.cwd.lock().clone(),         parent: parent.pid,+        pgid: parent.pgid(), // same process group, like fork         trace: opts.flags & SPAWN_TRACE != 0,     };     drop(parent);@@ -582,6 +774,20 @@ fn sys_ioctl(fd: u64, request: u64, arg: u64) -> Result<u64, Errno> {     }     match request {         TTY_SET_RAW => Ok(crate::tty::set_raw(arg != 0, process.pid) as u64),+        // Which process group is in the foreground (gets Ctrl+C)?+        TIOCGPGRP => copy_to_user(arg, &(crate::tty::foreground() as u32)).map(|_| 0),+        TIOCSPGRP => {+            let pgid = read_user_u32s::<1>(arg)?[0] as u64;+            if process::group_members(pgid).is_empty() {+                return Err(Errno::ESRCH);+            }+            crate::tty::set_foreground(pgid);+            Ok(0)+        }+        TIOCGWINSZ => {+            let (cols, rows) = crate::vga_buffer::size();+            copy_to_user(arg, &[rows as u16, cols as u16, 0, 0]).map(|_| 0)+        }         _ => Err(Errno::EINVAL),     } }@@ -627,7 +833,7 @@ fn sys_uname(buf: u64) -> Result<u64, Errno> {         "Rustbelt",                     // sysname (`uname -s`)         "qemu",                         // nodename (hostname)         env!("CARGO_PKG_VERSION"),      // release (`uname -r`)-        "stage 13: virtual drivers",    // version+        "stage 14: processes",          // version         "x86_64",                       // machine         "(none)",                       // domainname     ];@@ -799,3 +1005,180 @@ fn sys_resolve(name_ptr: u64, name_len: u64, out: u64, max: u64) -> Result<u64,      }     Ok(n as u64) }++// --- Processes, memory, signals (stage 14) --------------------------------------++/// A NUL-terminated string from user memory (at most `max` bytes).+pub fn user_cstr(ptr: u64, max: usize) -> Result<Vec<u8>, Errno> {+    let mut out = Vec::new();+    // Page by page: the string may end right before an unmapped page.+    let mut addr = ptr;+    loop {+        let chunk = (4096 - addr % 4096).min((max + 1 - out.len()) as u64);+        let bytes = user_slice(addr, chunk)?;+        if let Some(end) = bytes.iter().position(|&b| b == 0) {+            out.extend_from_slice(&bytes[..end]);+            return Ok(out);+        }+        out.extend_from_slice(bytes);+        if out.len() > max {+            return Err(Errno::ENAMETOOLONG);+        }+        addr += chunk;+    }+}++/// A NULL-terminated array of C strings (execve's argv and envp).+fn user_cstr_array(ptr: u64) -> Result<Vec<Vec<u8>>, Errno> {+    let mut out = Vec::new();+    if ptr == 0 {+        return Ok(out); // Linux allows NULL here (an empty list)+    }+    let mut total = 0;+    for i in 0.. {+        let p = u64::from_le_bytes(user_slice(ptr + 8 * i, 8)?.try_into().unwrap());+        if p == 0 {+            break;+        }+        let s = user_cstr(p, process::MAX_ARGS_SIZE)?;+        total += s.len() + 1;+        if total > process::MAX_ARGS_SIZE {+            return Err(Errno::E2BIG);+        }+        out.push(s);+    }+    Ok(out)+}++fn sys_execve(frame: &mut TrapFrame, path: u64, argv: u64, envp: u64) -> Result<u64, Errno> {+    let p = current()?;+    let path = String::from_utf8(user_cstr(path, 4096)?).map_err(|_| Errno::EINVAL)?;+    let path = p.resolve(&path)?;+    let argv = user_cstr_array(argv)?;+    let envp = user_cstr_array(envp)?;+    process::execve(&p, frame, &path, &argv, &envp)?;+    Ok(0)+}++/// clone: only the fork-like uses (no threads yet): exit signal SIGCHLD+/// (or 0) in the low byte, optionally CLONE_VFORK, optionally a new stack.+fn sys_clone(frame: &TrapFrame, flags: u64, stack: u64) -> Result<u64, Errno> {+    let exit_signal = flags & 0xff;+    if flags & !(0xff | CLONE_VFORK) != 0 || (exit_signal != 0 && exit_signal != signal::SIGCHLD as u64) {+        return Err(Errno::ENOSYS); // CLONE_VM, CLONE_THREAD...: threads are future work+    }+    process::fork(&current()?, frame, stack, flags & CLONE_VFORK != 0)+}++fn sys_wait4(pid: i64, status_ptr: u64, options: u64) -> Result<u64, Errno> {+    if status_ptr != 0 {+        user_slice_mut(status_ptr, 4)?; // check before reaping anything+    }+    let (pid, status) = process::wait4(&*current()?, pid, options)?;+    if status_ptr != 0 && pid != 0 {+        copy_to_user(status_ptr, &status)?;+    }+    Ok(pid)+}++fn sys_rt_sigaction(sig: u64, new_ptr: u64, old_ptr: u64, size: u64) -> Result<u64, Errno> {+    let new = if new_ptr == 0 {+        None+    } else {+        let bytes = user_slice(new_ptr, core::mem::size_of::<signal::SigAction>() as u64)?;+        Some(unsafe { core::ptr::read_unaligned(bytes.as_ptr() as *const signal::SigAction) })+    };+    if old_ptr != 0 {+        user_slice_mut(old_ptr, 32)?;+    }+    let old = signal::sys_rt_sigaction(&*current()?, sig, new, size)?;+    if old_ptr != 0 {+        copy_to_user(old_ptr, &old)?;+    }+    Ok(0)+}++fn sys_rt_sigprocmask(how: u64, set_ptr: u64, old_ptr: u64, size: u64) -> Result<u64, Errno> {+    let set = if set_ptr == 0 { None } else { Some(u64::from_le_bytes(user_slice(set_ptr, 8)?.try_into().unwrap())) };+    let old = signal::sys_rt_sigprocmask(&*current()?, how, set, size)?;+    if old_ptr != 0 {+        copy_to_user(old_ptr, &old)?;+    }+    Ok(0)+}++/// mmap(addr, len, prot, flags, fd, offset). The new region is empty: pages+/// appear on first touch (demand paging, mm.rs).+fn sys_mmap(p: &process::Process, addr: u64, len: u64, prot: u64, flags: u64, fd: u64, offset: u64) -> Result<u64, Errno> {+    use crate::mm::{Kind, Mm, Vma};+    let len = len.checked_next_multiple_of(4096).ok_or(Errno::ENOMEM)?;+    let shared = match flags & (MAP_SHARED | MAP_PRIVATE) {+        MAP_SHARED => true,+        MAP_PRIVATE => false,+        _ => return Err(Errno::EINVAL), // exactly one of the two+    };+    if len == 0 || prot & !7 != 0 || offset % 4096 != 0 {+        return Err(Errno::EINVAL);+    }+    let file = if flags & MAP_ANONYMOUS != 0 {+        None+    } else {+        // A file: only read-only sharing of its bytes is supported -- a+        // private copy of the file's pages (writes stay in this process).+        // MAP_SHARED of a file would need writes to reach the file (a page+        // cache), which we don't have.+        if shared {+            return Err(Errno::ENODEV);+        }+        let inode = p.file(fd)?.inode().ok_or(Errno::ENODEV)?;+        Some((inode, offset))+    };+    let mut mm = p.mm.lock();+    let start = if flags & MAP_FIXED != 0 {+        if addr % 4096 != 0 || !Mm::range_ok(addr, addr.saturating_add(len)) {+            return Err(Errno::EINVAL);+        }+        mm.unmap(addr, addr + len); // MAP_FIXED replaces whatever was there+        addr+    } else {+        mm.find_free(len).ok_or(Errno::ENOMEM)?+    };+    mm.insert(Vma { start, end: start + len, prot, shared, file, kind: Kind::Mmap });+    if shared {+        // Shared memory gets its frames now, so a fork before the first+        // touch still shares them (mm.rs).+        if mm.populate(start, start + len).is_err() {+            mm.unmap(start, start + len);+            return Err(Errno::ENOMEM);+        }+    }+    Ok(start)+}++fn page_range(addr: u64, len: u64) -> Result<(u64, u64), Errno> {+    let end = addr.checked_add(len).ok_or(Errno::EINVAL)?.next_multiple_of(4096);+    if addr % 4096 != 0 || !crate::mm::Mm::range_ok(addr, end.max(addr + 1)) {+        return Err(Errno::EINVAL);+    }+    Ok((addr, end))+}++fn sys_munmap(p: &process::Process, addr: u64, len: u64) -> Result<u64, Errno> {+    let (start, end) = page_range(addr, len)?;+    if len == 0 {+        return Err(Errno::EINVAL);+    }+    p.mm.lock().unmap(start, end);+    Ok(0)+}++fn sys_mprotect(p: &process::Process, addr: u64, len: u64, prot: u64) -> Result<u64, Errno> {+    if prot & !7 != 0 {+        return Err(Errno::EINVAL);+    }+    let (start, end) = page_range(addr, len)?;+    if start == end {+        return Ok(0);+    }+    p.mm.lock().protect(start, end, prot).map(|_| 0)+}
@@ -84,7 +84,9 @@ impl Stream for ScancodeStream { /// The keyboard task: decode scancodes and echo characters forever. pub async fn print_keypresses() {     let mut scancodes = ScancodeStream::new();-    let mut keyboard = Keyboard::new(ScancodeSet1::new(), layouts::Us104Key, HandleControl::Ignore);+    // MapLettersToUnicode: Ctrl+C arrives as character 3, Ctrl+Z as 26 (the+    // ASCII control codes), for tty.rs to turn into signals.+    let mut keyboard = Keyboard::new(ScancodeSet1::new(), layouts::Us104Key, HandleControl::MapLettersToUnicode);      while let Some(scancode) = scancodes.next().await {         if let Ok(Some(mut key_event)) = keyboard.add_byte(scancode)
@@ -60,6 +60,7 @@ //! fake stack that looks like `switch_context` saved it, with the "return //! address" pointing at `thread_trampoline`, which calls the thread's function. +use crate::errno::Errno; use crate::process::Process; use crate::smp::{self, MAX_CPUS}; use crate::stack::KernelStack;@@ -623,6 +624,36 @@ pub fn sleep_ticks(ticks: u64) {     } } +/// `sleep_ticks` that a signal can cut short: `Err(ERESTARTNOHAND)` then+/// (nanosleep and pause are never restarted after a handler ran).+pub fn sleep_ticks_intr(ticks: u64) -> Result<(), Errno> {+    let was_enabled = interrupts::are_enabled();+    interrupts::disable();+    let wake_at = crate::interrupts::ticks() + ticks.max(1);+    clear_woken();+    let mut result = Ok(());+    while crate::interrupts::ticks() < wake_at {+        if crate::signal::interrupted() {+            result = Err(Errno::ERESTARTNOHAND);+            break;+        }+        block_current(Some(wake_at));+    }+    if was_enabled {+        interrupts::enable();+    }+    result+}++/// Block until a signal arrives (pause()).+pub fn pause() -> Errno {+    static NEVER: WaitQueue = WaitQueue::new();+    match NEVER.wait_until_intr(|| None::<()>) {+        Err(e) => if e == Errno::ERESTARTSYS { Errno::ERESTARTNOHAND } else { e },+        Ok(()) => unreachable!(),+    }+}+ /// The running thread overflowed its kernel stack (the page-fault or /// double-fault handler saw an access to a guard page). Say so, and kill /// it: its process if it has one, else just the thread.@@ -635,7 +666,7 @@ pub fn die_of_stack_overflow(addr: u64) -> ! {         addr & !0xfff     );     if current_process().is_some() {-        crate::process::exit_current(crate::process::ExitStatus::Killed("kernel stack overflow"));+        crate::process::exit_current(crate::process::ExitStatus::Killed(crate::signal::SIGSEGV, "kernel stack overflow"));     }     exit_current() }@@ -655,55 +686,65 @@ impl WaitQueue {         WaitQueue { waiting: Mutex::new(Vec::new()) }     } -    pub fn wait_until<T>(&self, mut condition: impl FnMut() -> Option<T>) -> T {-        let was_enabled = interrupts::are_enabled();-        let result = loop {-            interrupts::disable();-            // The order matters (the classic "lost wake-up" bug): put-            // ourselves on the list *first*, then check. If the condition-            // becomes true on another CPU after our check, its wake_all()-            // finds us on the list and sets `woken`, so block_current-            // returns straight away instead of sleeping forever.-            let me = current_id();-            clear_woken();-            self.waiting.lock().push(me);-            if let Some(value) = condition() {-                self.waiting.lock().retain(|&id| id != me);-                break value;-            }-            block_current(None);-            // Woken up: loop and check the condition again (somebody else may-            // have consumed what we were waiting for).-        };-        if was_enabled {-            interrupts::enable();+    pub fn wait_until<T>(&self, condition: impl FnMut() -> Option<T>) -> T {+        match self.wait(None, false, condition) {+            Ok(Some(value)) => value,+            _ => unreachable!("no deadline, not interruptible"),         }-        result     }      /// Like `wait_until`, but give up at PIT tick `deadline`: returns     /// `None` if the condition still wasn't true by then. (Network code uses     /// this for timeouts: connect, DNS, ping, `SO_RCVTIMEO`.)-    pub fn wait_until_deadline<T>(&self, deadline: u64, mut condition: impl FnMut() -> Option<T>) -> Option<T> {+    pub fn wait_until_deadline<T>(&self, deadline: u64, condition: impl FnMut() -> Option<T>) -> Option<T> {+        self.wait(Some(deadline), false, condition).ok().flatten()+    }++    /// Like `wait_until`, but a signal for the process interrupts the wait:+    /// `Err(ERESTARTSYS)`. Every wait a program can get stuck in (reading+    /// the keyboard or a pipe, wait4, sockets, ...) uses this, or Ctrl+C+    /// couldn't stop a program blocked in `read`. On the way back to user+    /// space the signal code turns ERESTARTSYS into EINTR or restarts the+    /// call (see signal.rs).+    pub fn wait_until_intr<T>(&self, condition: impl FnMut() -> Option<T>) -> Result<T, Errno> {+        self.wait(None, true, condition).map(|v| v.expect("no deadline"))+    }++    /// Interruptible *and* with a deadline (`Ok(None)` = timed out).+    pub fn wait_until_deadline_intr<T>(&self, deadline: u64, condition: impl FnMut() -> Option<T>) -> Result<Option<T>, Errno> {+        self.wait(Some(deadline), true, condition)+    }++    fn wait<T>(&self, deadline: Option<u64>, interruptible: bool, mut condition: impl FnMut() -> Option<T>) -> Result<Option<T>, Errno> {         let was_enabled = interrupts::are_enabled();+        let me = current_id();         let result = loop {             interrupts::disable();-            let me = current_id();+            // The order matters (the classic "lost wake-up" bug): put+            // ourselves on the list *first*, then check. If the condition+            // becomes true on another CPU after our check, its wake_all()+            // finds us on the list and sets `woken`, so block_current+            // returns straight away instead of sleeping forever. (Signals+            // wake us with `wake(tid)` directly, which works the same way.)             clear_woken();             self.waiting.lock().push(me);             if let Some(value) = condition() {-                self.waiting.lock().retain(|&id| id != me);-                break Some(value);+                break Ok(Some(value));+            }+            if interruptible && crate::signal::interrupted() {+                break Err(Errno::ERESTARTSYS);             }-            if crate::interrupts::ticks() >= deadline {-                self.waiting.lock().retain(|&id| id != me);-                break None;+            if deadline.is_some_and(|d| crate::interrupts::ticks() >= d) {+                break Ok(None);             }-            // Sleep until woken *or* the deadline tick (whichever is first).-            block_current(Some(deadline));-            // A timer wake-up leaves us on the list: take us off.+            // Sleep until woken (or the deadline tick, whichever is first).+            block_current(deadline);+            // Woken up: take us off the list (a timer or signal wake-up+            // leaves us on it), then check again -- somebody else may have+            // consumed what we were waiting for.             self.waiting.lock().retain(|&id| id != me);         };+        self.waiting.lock().retain(|&id| id != me);         if was_enabled {             interrupts::enable();         }
@@ -0,0 +1,189 @@+//! Trap frames: the user's registers, saved on the kernel stack.+//!+//! Every way into the kernel from ring 3 -- a `syscall`, a page fault or+//! other exception, the timer interrupt -- now saves *all* of the user's+//! registers in one layout, `TrapFrame`, at the top of the thread's kernel+//! stack, and every way back goes through one exit path (`trap_return`)+//! that reloads them and `iretq`s. Linux calls this `struct pt_regs`.+//!+//! Why bother (stage 13 used `sysretq` and the ready-made+//! `extern "x86-interrupt"` handlers)? Because the new process features+//! need to *read and change* the user's registers:+//!   * fork copies them for the child (with rax = 0, "you're the child"),+//!   * execve replaces them (new rip, new rsp, everything else zero),+//!   * a signal saves them on the user stack and points rip at the handler;+//!     rt_sigreturn puts them back,+//!   * and signals must be checked on *every* return to user mode, also+//!     after a timer interrupt (that's how Ctrl+C stops a program that+//!     never makes a syscall, like `spin`).+//!+//! Layout on the kernel stack (lowest address first; `rsp` points at r15+//! while the Rust handler runs):+//!+//!   r15 r14 r13 r12 r11 r10 r9 r8 rbp rdi rsi rdx rcx rbx rax   (we push)+//!   vector  error                                               (we push)+//!   rip cs rflags rsp ss                     (the CPU pushes, or we do)+//!+//! For a syscall `vector` is TRAP_SYSCALL and `error` holds the syscall+//! number (Linux's "orig_rax": rax gets the result, but a restarted call+//! needs the number again).++use core::arch::global_asm;++/// `vector` value of a frame built by the syscall entry.+pub const TRAP_SYSCALL: u64 = 0x100;++#[repr(C)]+#[derive(Clone, Copy, Debug, Default)]+pub struct TrapFrame {+    pub r15: u64,+    pub r14: u64,+    pub r13: u64,+    pub r12: u64,+    pub r11: u64,+    pub r10: u64,+    pub r9: u64,+    pub r8: u64,+    pub rbp: u64,+    pub rdi: u64,+    pub rsi: u64,+    pub rdx: u64,+    pub rcx: u64,+    pub rbx: u64,+    pub rax: u64,+    pub vector: u64,+    pub error: u64,+    pub rip: u64,+    pub cs: u64,+    pub rflags: u64,+    pub rsp: u64,+    pub ss: u64,+}++impl TrapFrame {+    /// Registers for starting a program: everything zero except the entry+    /// point, the stack, and RFLAGS = interrupts on (0x200, plus the+    /// always-one bit 1).+    pub fn new_user(rip: u64, rsp: u64) -> TrapFrame {+        let s = crate::gdt::selectors();+        TrapFrame {+            rip,+            rsp,+            rflags: 0x202,+            cs: s.user_code.0 as u64,+            ss: s.user_data.0 as u64,+            ..Default::default()+        }+    }++    /// Did this trap come from ring 3? (The low two bits of CS are the+    /// privilege level the CPU was running at.)+    pub fn from_user(&self) -> bool {+        self.cs & 3 == 3+    }++    pub fn is_syscall(&self) -> bool {+        self.vector == TRAP_SYSCALL+    }+}++unsafe extern "C" {+    /// Pops a TrapFrame (rsp must point at it) and `iretq`s.+    pub fn trap_return();+}++/// Leave the kernel for good: load `frame` into the registers and jump to+/// user mode. Used by a new process's first instructions (spawned or+/// forked). The frame is copied onto our own stack first; the stack below+/// it doesn't matter any more.+pub fn return_to_user(frame: TrapFrame) -> ! {+    unsafe {+        core::arch::asm!(+            "mov rsp, {frame}",+            "jmp {ret}",+            frame = in(reg) &frame as *const TrapFrame,+            ret = sym trap_return,+            options(noreturn)+        )+    }+}++// The common exit. Interrupts off: from here on we're restoring user+// registers and must not be interrupted half-way (the CPU turns them back+// on itself, because the saved RFLAGS has IF set).+global_asm!(+    ".global trap_return",+    "trap_return:",+    "    cli",+    "    pop r15",+    "    pop r14",+    "    pop r13",+    "    pop r12",+    "    pop r11",+    "    pop r10",+    "    pop r9",+    "    pop r8",+    "    pop rbp",+    "    pop rdi",+    "    pop rsi",+    "    pop rdx",+    "    pop rcx",+    "    pop rbx",+    "    pop rax",+    "    add rsp, 16", // skip vector and error+    "    iretq",+);++/// An interrupt/exception entry stub. The CPU has pushed ss, rsp, rflags,+/// cs, rip (and for some exceptions an error code). We push a fake error+/// code if needed, the vector number and all general registers, then call+/// `interrupts::trap_handler(&mut TrapFrame)` and leave through+/// `trap_return`. 22 slots of 8 bytes: the CPU aligned rsp to 16 before+/// its pushes, so it's still 16-aligned for the call.+macro_rules! trap_stub {+    ($name:literal, $vector:literal, $push_error:literal) => {+        global_asm!(+            concat!(".global ", $name),+            concat!($name, ":"),+            $push_error,+            concat!("    push ", $vector),+            "    push rax",+            "    push rbx",+            "    push rcx",+            "    push rdx",+            "    push rsi",+            "    push rdi",+            "    push rbp",+            "    push r8",+            "    push r9",+            "    push r10",+            "    push r11",+            "    push r12",+            "    push r13",+            "    push r14",+            "    push r15",+            "    mov rdi, rsp",+            "    cld", // Rust expects the direction flag clear+            "    call {handler}",+            "    jmp {ret}",+            handler = sym crate::interrupts::trap_handler,+            ret = sym trap_return,+        );+    };+}++// Exceptions that a user program can cause (and get a signal for), plus+// the local APIC timer (so a running program can be stopped by a signal).+trap_stub!("trap_divide_error", "0", "push 0"); // SIGFPE+trap_stub!("trap_invalid_opcode", "6", "push 0"); // SIGILL+trap_stub!("trap_general_protection", "13", ""); // SIGSEGV (CPU pushes an error code)+trap_stub!("trap_page_fault", "14", ""); // SIGSEGV or demand paging+trap_stub!("trap_apic_timer", "0x40", "push 0");++unsafe extern "C" {+    pub fn trap_divide_error();+    pub fn trap_invalid_opcode();+    pub fn trap_general_protection();+    pub fn trap_page_fault();+    pub fn trap_apic_timer();+}
@@ -12,12 +12,18 @@ //!   switches to raw while it edits its command line, so it can do its own //!   echo, Backspace and up-arrow history. (Linux: `tcsetattr` / `ioctl`.) //!+//! In cooked mode Ctrl+C and Ctrl+Z don't become input: they send SIGINT /+//! SIGTSTP to the *foreground process group* (set by the shell with+//! ioctl TIOCSPGRP) -- that's how Ctrl+C stops the running program but not+//! the shell. (The virtio console, hvc0, has no job control.)+//! //! Output (`Console::write`) understands two control characters besides //! '\n': '\x08' erases the character before the cursor, and '\x0c' (form //! feed) clears the screen.  use crate::errno::Errno; use crate::fs::file::{File, KIND_CONSOLE, Stat};+use crate::signal; use crate::thread::WaitQueue; use crate::{print, serial_print, vga_buffer}; use alloc::string::String;@@ -37,6 +43,18 @@ static RAW: AtomicBool = AtomicBool::new(false); /// without switching back, the console goes back to cooked mode. static RAW_OWNER: AtomicU64 = AtomicU64::new(0); +/// The foreground process group: Ctrl+C / Ctrl+Z signal it (0 = none).+/// The shell sets it to each job it runs, and back to itself after.+static FOREGROUND: AtomicU64 = AtomicU64::new(0);++pub fn foreground() -> u64 {+    FOREGROUND.load(Ordering::SeqCst)+}++pub fn set_foreground(pgid: u64) {+    FOREGROUND.store(pgid, Ordering::SeqCst);+}+ /// Threads blocked in `Console::read`, waiting for input. static INPUT_WAIT: WaitQueue = WaitQueue::new(); @@ -95,6 +113,21 @@ pub fn input_char(c: char) {             deliver(line.as_bytes());             line.clear();         }+        // Ctrl+C / Ctrl+Z: the "line discipline" turns them into signals+        // for the foreground job (Linux: the ISIG flag of termios). Raw mode+        // passes them through as bytes 3 / 26 instead (above).+        '\u{3}' | '\u{1a}' => {+            let (echo, sig) = if c == '\u{3}' { ("^C\n", signal::SIGINT) } else { ("^Z\n", signal::SIGTSTP) };+            print!("{}", echo);+            serial_print!("{}", echo);+            line.clear(); // the half-typed line is thrown away+            drop(line);+            let fg = foreground();+            if fg != 0 {+                crate::process::signal_group(fg, sig);+            }+        }+        c if (c as u32) < 0x20 && c != '\t' => {} // other control keys: ignore         c => {             print!("{}", c);             serial_print!("{}", c);@@ -141,7 +174,7 @@ impl File for Console {         // Block until input arrives: the thread is parked on INPUT_WAIT         // (using no CPU) and `deliver` wakes it. The closure runs with         // interrupts off; the ArrayQueue is lock-free, so that's fine.-        Ok(INPUT_WAIT.wait_until(|| {+        INPUT_WAIT.wait_until_intr(|| {             let mut n = 0;             while n < buf.len() {                 let Some(b) = input().pop() else { break };@@ -152,7 +185,7 @@ impl File for Console {                 }             }             if n > 0 { Some(n) } else { None }-        }))+        })     }      fn write(&self, buf: &[u8]) -> Result<usize, Errno> {
@@ -198,10 +198,11 @@ pub fn log(args: core::fmt::Arguments) {     write(&buf.data[..buf.len]); } -/// Read received bytes into `buf`; blocks until at least one is there.-pub fn read(buf: &mut [u8]) -> usize {+/// Read received bytes into `buf`; blocks until at least one is there+/// (or a signal arrives: ERESTARTSYS).+pub fn read(buf: &mut [u8]) -> Result<usize, crate::errno::Errno> {     if buf.is_empty() {-        return 0;+        return Ok(0);     }     loop {         let take = |buf: &mut [u8]| {@@ -221,11 +222,11 @@ pub fn read(buf: &mut [u8]) -> usize {         });         let n = take(buf);         if n > 0 {-            return n;+            return Ok(n);         }         // Sleep until the interrupt handler has data (or 1 tick, then poll).         let deadline = crate::interrupts::ticks() + 1;-        INPUT_READY.wait_until_deadline(deadline, || (!input().is_empty()).then_some(()));+        INPUT_READY.wait_until_deadline_intr(deadline, || (!input().is_empty()).then_some(()))?;     } } 
@@ -86,9 +86,9 @@ fn user_stack_overflow_kills_the_process() {         let p = process::spawn_path("/mnt/disk0/bin/overflow", spawn).expect("spawn failed");         process::wait(&p)     };-    assert_eq!(run(), ExitStatus::Killed("stack overflow")); // warm up+    assert_eq!(run(), ExitStatus::Killed(rustbelt::signal::SIGSEGV, "stack overflow")); // warm up     let frames = memory::frames_in_use();-    assert_eq!(run(), ExitStatus::Killed("stack overflow"));+    assert_eq!(run(), ExitStatus::Killed(rustbelt::signal::SIGSEGV, "stack overflow"));     // Everything it had (incl. the 16 KiB of stack it filled) is free again.     assert_eq!(memory::frames_in_use(), frames); }
@@ -0,0 +1,158 @@+//! Stage 14, the process model: fork with copy-on-write, execve with a+//! Linux-style initial stack, mmap and demand paging, signals, wait4, job+//! control. Most checks run inside the user test programs (forktest,+//! cowtest, mmaptest, sigtest: exit code = failed checks); this file starts+//! them and adds what only the kernel can see: memory leaks, the process+//! table limit, Ctrl+C from the keyboard.+#![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, sync::Arc};+use bootloader::{BootInfo, entry_point};+use core::panic::PanicInfo;+use rustbelt::process::{self, ExitStatus, SpawnArgs};+use rustbelt::{fs, memory, signal, thread, tty};++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();+}++const BIN: &str = "/mnt/disk0/bin";++fn spawn(name: &str, args: &[&str], env: &[&str]) -> Arc<process::Process> {+    let mut spawn = SpawnArgs::kernel(name);+    spawn.args.extend(args.iter().map(|a| String::from(*a)));+    if !env.is_empty() {+        spawn.env = env.iter().map(|e| String::from(*e)).collect();+    }+    process::spawn_path(&alloc::format!("{}/{}", BIN, name), spawn).expect("spawn failed")+}++fn run(name: &str, args: &[&str]) -> ExitStatus {+    let p = spawn(name, args, &[]);+    process::wait(&p) // (also removes it from the process table)+}++/// Writes in a forked child don't show up in the parent (and back),+/// MAP_SHARED pages are shared.+#[test_case]+fn copy_on_write_isolation() {+    assert_eq!(run("cowtest", &[]), ExitStatus::Exited(0));+}++/// fork, wait4, execve, vfork, dup2 through pipes, a SIGSEGV'd child.+#[test_case]+fn fork_exec_wait() {+    assert_eq!(run("forktest", &[]), ExitStatus::Exited(0));+}++/// A program started by the kernel gets argv, envp and auxv exactly like+/// on Linux (AT_PHDR, AT_PAGESZ, AT_ENTRY, AT_RANDOM, AT_EXECFN...).+#[test_case]+fn exec_argv_envp_auxv() {+    let p = spawn("forktest", &["args", "two words", ""], &["FOO=bar", "EMPTY="]);+    assert_eq!(process::wait(&p), ExitStatus::Exited(0));+}++/// mmap/munmap/mprotect/brk, demand paging, and faults delivered to a+/// SIGSEGV handler that fixes them.+#[test_case]+fn mmap_and_fault_handlers() {+    assert_eq!(run("mmaptest", &[]), ExitStatus::Exited(0));+}++/// Without a handler, a bad write is SIGSEGV's default: terminate, 139.+#[test_case]+fn unhandled_fault_is_sigsegv() {+    let status = run("mmaptest", &["crash"]);+    assert!(matches!(status, ExitStatus::Killed(signal::SIGSEGV, _)), "{:?}", status);+    assert_eq!(status.code(), 139);+    assert_eq!(status.wait_status(), 11);+}++/// Handlers + sigreturn, masks, default actions, SIGCHLD, stop/continue,+/// SIGPIPE, EINTR and SA_RESTART, kill on process groups.+#[test_case]+fn signals() {+    assert_eq!(run("sigtest", &[]), ExitStatus::Exited(0));+}++/// wait4's status word, the way the W* macros decode it.+#[test_case]+fn wait_status_encoding() {+    assert_eq!(ExitStatus::Exited(3).wait_status(), 0x300);+    assert_eq!(ExitStatus::Exited(256 + 7).wait_status(), 0x700); // only 8 bits survive+    assert_eq!(ExitStatus::Killed(signal::SIGKILL, "x").wait_status(), 9);+    assert_eq!(ExitStatus::Killed(signal::SIGINT, "x").code(), 130);+}++/// A fork bomb hits the process limit (EAGAIN) instead of eating all+/// memory, and after killing it everything is given back.+#[test_case]+fn fork_bomb_is_bounded_and_leaks_nothing() {+    assert_eq!(run("forktest", &["bomb"]), ExitStatus::Exited(0)); // warm up (caches, page tables)+    let (frames, processes) = (memory::frames_in_use(), process::count());+    assert_eq!(run("forktest", &["bomb"]), ExitStatus::Exited(0));+    thread::sleep_ticks(2); // let exited threads' stacks be freed+    assert_eq!(process::count(), processes);+    assert_eq!(memory::frames_in_use(), frames, "frames leaked by the fork bomb");+}++/// Many rounds of fork + exec + exit + wait: the frame count comes back+/// exactly (shared-frame refcounts drop to zero, page tables are freed).+#[test_case]+fn no_frame_leaks_across_fork_exec_exit() {+    run("forktest", &[]);+    run("cowtest", &[]);+    let frames = memory::frames_in_use();+    let shared = memory::shared_frame_count();+    for _ in 0..3 {+        assert_eq!(run("forktest", &[]), ExitStatus::Exited(0));+        assert_eq!(run("cowtest", &[]), ExitStatus::Exited(0));+    }+    thread::sleep_ticks(2);+    assert_eq!(memory::shared_frame_count(), shared, "copy-on-write refcounts left behind");+    assert_eq!(memory::frames_in_use(), frames, "frames leaked across fork/exec/exit");+}++/// Ctrl+C on the console: the tty sends SIGINT to the foreground process+/// group, and `spin` (which never makes a syscall) dies of it.+#[test_case]+fn ctrl_c_interrupts_the_foreground_job() {+    let p = spawn("spin", &["1000"], &[]); // ~minutes of spinning+    thread::sleep_ticks(4);+    assert!(p.status().is_none(), "spin ended by itself");+    tty::set_foreground(p.pgid());+    tty::input_char('\u{3}'); // what the keyboard driver delivers for Ctrl+C+    let status = process::wait(&p);+    tty::set_foreground(0);+    assert!(matches!(status, ExitStatus::Killed(signal::SIGINT, _)), "{:?}", status);+    assert_eq!(status.code(), 130);+}++/// Ctrl+C with nobody in the foreground hits nobody.+#[test_case]+fn ctrl_c_without_a_foreground_job_does_nothing() {+    let p = spawn("spin", &["2"], &[]);+    tty::set_foreground(0);+    tty::input_char('\u{3}');+    assert_eq!(process::wait(&p), ExitStatus::Exited(0));+}++#[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.13.0",+        "Rustbelt 0.14.0",         "B:/ $ exit 3\n",     ] {         assert!(out.contains(expected), "missing {:?} in:\n{}", expected, out);
@@ -66,7 +66,7 @@ fn demo_program_passes_its_checks() { fn kernel_memory_access_kills_only_the_process() {     let p = run("evil", &[]);     let status = process::wait(&p);-    assert_eq!(status, ExitStatus::Killed("page fault"));+    assert_eq!(status, ExitStatus::Killed(rustbelt::signal::SIGSEGV, "page fault"));     assert_eq!(status.code(), 139); } 
@@ -0,0 +1,107 @@+//! cowtest -- checks that fork really gives the child its *own* memory.+//!+//! fork doesn't copy anything up front: parent and child share every page,+//! read-only, and the first write to a page makes the copy (copy-on-write).+//! This checks that the copies are made at the right moments: writes in the+//! child don't show in the parent and vice versa -- for global data, the+//! heap and the stack -- while MAP_SHARED memory *is* shared.+#![no_std]+#![no_main]++extern crate alloc;++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use alloc::vec;+use ulib::*;++/// 64 KiB of global data (16 pages, in .bss).+static mut GLOBAL: [u8; 65536] = [0; 65536];++fn global() -> &'static mut [u8; 65536] {+    unsafe { &mut *core::ptr::addr_of_mut!(GLOBAL) }+}++fn all(buf: &[u8], value: u8) -> bool {+    buf.iter().all(|&b| b == value)+}++fn main() -> i64 {+    let mut c = Checks::new("cowtest");+    global().fill(1);+    let mut heap = vec![2u8; 100_000];+    let mut stack = [3u8; 2000];++    // 1. The child writes; the parent must not see it.+    let pid = fork();+    if pid == 0 {+        let before = all(global(), 1) && all(&heap, 2) && all(&stack, 3);+        global().fill(0xc1);+        heap.fill(0xc2);+        stack.fill(0xc3);+        let after = all(global(), 0xc1) && all(&heap, 0xc2) && all(core::hint::black_box(&stack), 0xc3);+        exit(if before && after { 0 } else { 1 });+    }+    c.check(wait(pid) == 0, "the child starts with the parent's data and can change its copy");+    c.check(all(global(), 1), "the child's writes to global data stay in the child");+    c.check(all(&heap, 2), "... and to the heap");+    c.check(all(core::hint::black_box(&stack), 3), "... and to the stack");++    // 2. The parent writes after fork; the child must keep the old data.+    // A pipe orders things: the child only checks after we wrote.+    let (r, w) = pipe().unwrap_or((-1, -1));+    let pid = fork();+    if pid == 0 {+        close(w);+        let mut b = [0u8];+        read(r, &mut b); // wait for the parent's go+        exit(if all(global(), 1) && all(&heap, 2) { 0 } else { 1 });+    }+    close(r);+    global().fill(0x77);+    heap.fill(0x77);+    write(w, b"!");+    close(w);+    c.check(wait(pid) == 0, "the parent's writes after fork stay in the parent");+    c.check(all(global(), 0x77), "and the parent sees its own writes");++    // 3. A grandchild: three generations share pages, each writes.+    let pid = fork();+    if pid == 0 {+        let pid2 = fork();+        if pid2 == 0 {+            global()[0] = 3;+            exit(if global()[1] == 0x77 { 0 } else { 1 });+        }+        global()[0] = 2;+        let code = wait(pid2);+        exit(if code == 0 && global()[0] == 2 { 0 } else { 1 });+    }+    c.check(wait(pid) == 0 && global()[0] == 0x77, "three generations: each keeps its own copy");++    // 4. MAP_SHARED memory is *not* copied: both see the same pages.+    let shared = mmap(0, 8192, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);+    let private = mmap(0, 8192, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);+    c.check(shared > 0 && private > 0, "mmap shared + private anonymous memory");+    let (sp, pp) = (shared as *mut u8, private as *mut u8);+    unsafe {+        sp.write_volatile(10);+        pp.write_volatile(10);+    }+    let pid = fork();+    if pid == 0 {+        unsafe {+            sp.write_volatile(42);+            sp.add(4096).write_volatile(43); // a page nobody touched before fork+            pp.write_volatile(42);+        }+        exit(0);+    }+    wait(pid);+    let (s0, s1, p0) = unsafe { (sp.read_volatile(), sp.add(4096).read_volatile(), pp.read_volatile()) };+    c.check(s0 == 42 && s1 == 43, "MAP_SHARED: the child's writes show up in the parent");+    c.check(p0 == 10, "MAP_PRIVATE: they don't");+    c.done()+}
@@ -0,0 +1,186 @@+//! forktest -- checks fork, execve, wait4, vfork and fd inheritance.+//!+//!   forktest          run all the checks (exit code = failed checks)+//!   forktest bomb     fork until the kernel says no (EAGAIN), then clean+//!                     up: the process table is bounded, nothing leaks+//!   forktest args ... (used by the exec check) verify argv, envp and the+//!                     auxiliary vector the kernel built for us+#![no_std]+#![no_main]++extern crate alloc;++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use alloc::vec::Vec;+use ulib::*;++const SELF: &str = "/mnt/disk0/bin/forktest";++fn main() -> i64 {+    match arg(1) {+        Some("bomb") => bomb(),+        Some("args") => check_args(),+        _ => all(),+    }+}++fn all() -> i64 {+    let mut c = Checks::new("forktest");+    let me = getpid();++    // 1. The basic dance: fork returns twice.+    let pid = fork();+    if pid == 0 {+        // The child: a new pid, and its parent is the process that forked.+        exit(if getppid() == me && getpid() != me { 7 } else { 1 });+    }+    c.check(pid > 0, "fork returns the child's pid in the parent");+    let r = wait4(pid, 0);+    c.check(matches!(r, Ok((p, s)) if p == pid && wifexited(s) && wexitstatus(s) == 7), "child saw getppid() == parent; wait4 gives its exit code 7");++    // 2. Many children, reaped in any order with wait4(-1).+    let mut sum = 0;+    for i in 1..=10 {+        if fork() == 0 {+            exit(i);+        }+    }+    let mut reaped = 0;+    while let Ok((_, status)) = wait4(-1, 0) {+        sum += wexitstatus(status);+        reaped += 1;+    }+    c.check(reaped == 10 && sum == 55, "10 children, wait4(-1) reaps them all (exit codes add up to 55)");+    c.check(wait4(-1, 0) == Err(-ECHILD), "no children left: ECHILD");++    // 3. execve: the child becomes another program (here: forktest itself+    // in "args" mode, which checks what it was given).+    let pid = fork();+    if pid == 0 {+        let err = execve(SELF, &["forktest", "args", "two words", ""], &["FOO=bar", "EMPTY="]);+        exit(100 + (-err).min(50)); // execve only returns on failure+    }+    let code = wait(pid);+    c.check(code == 0, "execve: argv, envp and auxv arrive the Linux way (see the [forktest args] lines)");+    let pid = fork();+    if pid == 0 {+        let err = execve("/mnt/disk0/bin/no-such-program", &["x"], &[]);+        exit(if err == -ENOENT { 0 } else { 1 });+    }+    c.check(wait(pid) == 0, "execve of a missing file fails with ENOENT (and the caller keeps running)");++    // 4. vfork: the parent waits until the child has exec'd.+    let pid = vfork();+    if pid == 0 {+        execve("/mnt/disk0/bin/child", &["child", "5"], &[]);+        exit(99);+    }+    c.check(wait(pid) == 5, "vfork + execve child 5 -> exit code 5");++    // 5. fds are inherited: the child writes into our pipe via dup2.+    let (r, w) = pipe2(O_CLOEXEC).unwrap_or((-1, -1));+    c.check(fcntl(r, F_GETFD, 0) == FD_CLOEXEC as i64, "pipe2(O_CLOEXEC) sets FD_CLOEXEC");+    let pid = fork();+    if pid == 0 {+        dup2(w, 1); // stdout = the pipe (dup2 clears close-on-exec on fd 1)+        execve("/mnt/disk0/bin/child", &["child", "pipe"], &[]);+        exit(99);+    }+    close(w); // else the read below would never see end-of-file+    let mut out = Vec::new();+    let mut buf = [0u8; 128];+    loop {+        let n = read(r, &mut buf);+        if n <= 0 {+            break;+        }+        out.extend_from_slice(&buf[..n as usize]);+    }+    close(r);+    c.check(wait(pid) == 0 && out.starts_with(b"hello through a pipe"), "fork + dup2 + execve: the child's stdout comes through our pipe");++    // 6. A killed child: the status says which signal.+    let pid = fork();+    if pid == 0 {+        unsafe { core::ptr::read_volatile(0x10 as *const u8) }; // NULL-ish pointer+        exit(0);+    }+    let r = wait4(pid, 0);+    c.check(matches!(r, Ok((_, s)) if wifsignaled(s) && wtermsig(s) == SIGSEGV), "a child that touches address 0x10 dies of SIGSEGV");+    c.done()+}++/// The exec'd side of check 3.+fn check_args() -> i64 {+    let mut c = Checks::new("forktest args");+    c.check(argc() == 4, "argc == 4");+    c.check(arg(0) == Some("forktest") && arg(2) == Some("two words") && arg(3) == Some(""), "argv[0..4] as passed (incl. an empty one)");+    c.check(env(0) == Some("FOO=bar") && env(1) == Some("EMPTY=") && env(2).is_none(), "envp exactly as passed");+    c.check(getenv("FOO") == Some("bar"), "getenv(\"FOO\") == \"bar\"");+    c.check(getauxval(AT_PAGESZ) == Some(4096), "AT_PAGESZ = 4096");+    let entry = getauxval(AT_ENTRY).unwrap_or(0);+    c.check(entry != 0 && entry == ulib_start(), "AT_ENTRY = the address of _start");+    // AT_PHDR points at our own ELF program headers: the first loadable+    // one (type 1 = PT_LOAD) should contain the entry point.+    let (phdr, phent, phnum) = (getauxval(AT_PHDR).unwrap_or(0), getauxval(AT_PHENT).unwrap_or(0), getauxval(AT_PHNUM).unwrap_or(0));+    let mut found = false;+    if phdr != 0 && phent == 56 {+        for i in 0..phnum {+            let h = (phdr + i * phent) as *const u64;+            let (ty, vaddr, memsz) = unsafe { (*(h as *const u32), *h.add(2), *h.add(5)) };+            found |= ty == 1 && (vaddr..vaddr + memsz).contains(&entry);+        }+    }+    c.check(found, "AT_PHDR/AT_PHENT/AT_PHNUM: our program headers, a PT_LOAD contains the entry");+    let random = getauxval(AT_RANDOM).map(|p| unsafe { *(p as *const [u8; 16]) });+    c.check(random.is_some_and(|r| r != [0; 16]), "AT_RANDOM: 16 random bytes");+    let execfn = getauxval(AT_EXECFN).map(|p| unsafe { cstr(p as *const u8) });+    c.check(execfn == Some(SELF), "AT_EXECFN = the path we were run as");+    let sp = &c as *const _ as u64;+    c.check(sp > 0xff_0000_0000 && sp < 0x100_0000_0000, "the stack is at the top of user space");+    c.done()+}++fn ulib_start() -> u64 {+    unsafe extern "C" {+        fn _start();+    }+    _start as usize as u64+}++/// Fork until fork fails. Every child just waits (pause) for a signal.+/// Then kill them all and reap them; the kernel tests check that no memory+/// leaked and that the system still works afterwards.+fn bomb() -> i64 {+    let mut pids = Vec::new();+    let error = loop {+        let pid = fork();+        if pid == 0 {+            loop {+                pause(); // until SIGKILL+            }+        }+        if pid < 0 {+            break pid;+        }+        pids.push(pid);+        if pids.len() > 1000 {+            break 0; // unbounded? stop anyway+        }+    };+    println!("[forktest bomb] {} children, then fork failed with {} ({})", pids.len(), error, strerror(error));+    for &pid in &pids {+        kill(pid, SIGKILL);+    }+    let mut reaped = 0;+    while let Ok((_, status)) = wait4(-1, 0) {+        if wifsignaled(status) && wtermsig(status) == SIGKILL {+            reaped += 1;+        }+    }+    println!("[forktest bomb] killed and reaped {}", reaped);+    if error == -EAGAIN && reaped == pids.len() && !pids.is_empty() { 0 } else { 1 }+}
@@ -0,0 +1,130 @@+//! mmaptest -- checks mmap / munmap / mprotect / brk and demand paging.+//!+//!   mmaptest         run all the checks (exit code = failed checks)+//!   mmaptest crash   write to read-only memory without a handler: the+//!                    default action of SIGSEGV ends the program (139)+//!+//! Faults are caught with a SIGSEGV handler that records what happened+//! (address, SEGV_MAPERR = nothing mapped / SEGV_ACCERR = not allowed) and+//! then *fixes* it (mprotect / mmap the page), so the faulting instruction+//! can run again when the handler returns. Garbage collectors and+//! user-space pagers use the same trick.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use core::sync::atomic::{AtomicU64, Ordering::SeqCst};+use ulib::*;++const PAGE: u64 = 4096;+const SEGV_MAPERR: i32 = 1;+const SEGV_ACCERR: i32 = 2;++static FAULTS: AtomicU64 = AtomicU64::new(0);+static ADDR: AtomicU64 = AtomicU64::new(0);+static CODE: AtomicU64 = AtomicU64::new(0);++extern "C" fn on_segv(sig: i32, info: *const SigInfo, _context: *mut u8) {+    let info = unsafe { &*info };+    FAULTS.fetch_add(1, SeqCst);+    ADDR.store(info.addr_or_pid, SeqCst);+    CODE.store(info.code as u64, SeqCst);+    let page = info.addr_or_pid & !(PAGE - 1);+    // Fix it: make the page readable and writable (mapping it if needed).+    if sig != SIGSEGV || mprotect(page, PAGE, PROT_READ | PROT_WRITE) < 0 {+        mmap(page, PAGE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);+    }+}++fn peek(addr: u64) -> u8 {+    unsafe { (addr as *const u8).read_volatile() }+}+fn poke(addr: u64, value: u8) {+    unsafe { (addr as *mut u8).write_volatile(value) }+}++fn main() -> i64 {+    if arg(1) == Some("crash") {+        let p = mmap(0, PAGE, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) as u64;+        println!("[mmaptest] writing to read-only memory at {:#x}...", p);+        poke(p, 1);+        println!("[mmaptest] not reached");+        return 1;+    }+    let mut c = Checks::new("mmaptest");+    let before = sysinfo().frames_in_use;++    // 1. Anonymous memory: zeroed, readable, writable.+    let a = mmap(0, 3 * PAGE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) as u64;+    c.check(a as i64 > 0 && a % PAGE == 0, "mmap 3 pages of anonymous memory");+    c.check((0..3 * PAGE).all(|i| peek(a + i) == 0), "it starts zeroed");+    for i in 0..3 * PAGE {+        poke(a + i, i as u8);+    }+    c.check((0..3 * PAGE).all(|i| peek(a + i) == i as u8), "it keeps what we write");++    // 2. Demand paging: a big mapping costs (almost) nothing until touched.+    let big = mmap(0, 64 << 20, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) as u64;+    let after_map = sysinfo().frames_in_use;+    poke(big, 1);+    poke(big + (32 << 20), 2);+    poke(big + (64 << 20) - 1, 3);+    let after_touch = sysinfo().frames_in_use;+    c.check(big as i64 > 0 && after_map < before + 8, "mmap 64 MiB: no memory used yet (pages come on first touch)");+    c.check(after_touch >= after_map + 3 && after_touch < after_map + 16, "touching 3 pages of it uses ~3 frames");+    c.check(munmap(big, 64 << 20) == 0, "munmap the 64 MiB");++    // 3. Faults become signals our handler sees.+    c.check(sigaction_info(SIGSEGV, on_segv, 0) == 0, "install a SIGSEGV handler (SA_SIGINFO)");+    c.check(mprotect(a + PAGE, PAGE, PROT_READ) == 0, "mprotect the middle page read-only");+    c.check(peek(a + PAGE + 5) == 5 && FAULTS.load(SeqCst) == 0, "reading it still works");+    poke(a + PAGE + 7, 99);+    c.check(FAULTS.load(SeqCst) == 1 && ADDR.load(SeqCst) == a + PAGE + 7 && CODE.load(SeqCst) == SEGV_ACCERR as u64, "writing it: SIGSEGV, si_addr = that byte, SEGV_ACCERR");+    c.check(peek(a + PAGE + 7) == 99, "the handler's mprotect fixed it and the write was retried");++    c.check(munmap(a + PAGE, PAGE) == 0, "munmap the middle page");+    c.check(peek(a) == 0 && peek(a + 2 * PAGE + 1) == 1, "the pages around the hole are untouched");+    let v = peek(a + PAGE + 7);+    c.check(FAULTS.load(SeqCst) == 2 && CODE.load(SeqCst) == SEGV_MAPERR as u64 && v == 0, "reading the hole: SEGV_MAPERR; the handler maps a fresh (zero) page");+    c.check(mprotect(a, PAGE, PROT_NONE) == 0 && { poke(a, 4); FAULTS.load(SeqCst) == 3 }, "PROT_NONE: even reading faults");+    c.check(munmap(a, 3 * PAGE) == 0, "munmap all of it");++    // 4. A file mapped read-only (private): the same bytes read() gives.+    let fd = open("/mnt/disk0/readme.txt", 0);+    let mut buf = [0u8; 211];+    let n = read(fd, &mut buf);+    let f = mmap(0, 2 * PAGE, PROT_READ, MAP_PRIVATE, fd, 0) as u64;+    c.check(fd >= 0 && n == 211 && f as i64 > 0, "mmap /mnt/disk0/readme.txt");+    c.check((0..211).all(|i| peek(f + i) == buf[i as usize]), "the mapping has the file's bytes");+    c.check(peek(f + 211) == 0 && peek(f + PAGE - 1) == 0, "after the end of the file: zeros");+    let faults = FAULTS.load(SeqCst);+    poke(f, b'J');+    c.check(FAULTS.load(SeqCst) == faults + 1 && peek(f) == b'J', "writing it faults; after mprotect we write our private copy");+    let mut again = [0u8; 1];+    lseek(fd, 0, SEEK_SET);+    read(fd, &mut again);+    c.check(again[0] == b'H', "the file itself is unchanged");+    c.check(mmap(0, PAGE, PROT_READ, MAP_SHARED, fd, 0) < 0, "MAP_SHARED of a file isn't supported (an error, not silent loss)");+    munmap(f, 2 * PAGE);+    close(fd);++    // 5. brk: grow the heap by 1 MiB, use the end, shrink back.+    let start = brk(0);+    let end = brk(start + (1 << 20));+    c.check(end == start + (1 << 20), "brk grows the heap by 1 MiB");+    poke(end - 1, 7);+    c.check(peek(end - 1) == 7, "the new end is usable");+    c.check(brk(start) == start, "brk shrinks it back");++    // 6. Bad requests.+    c.check(mmap(0, 0, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) == -22, "mmap of 0 bytes: EINVAL");+    c.check(mmap(0x1000, PAGE, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) == -22, "MAP_FIXED outside user space: EINVAL");++    sigaction_raw(SIGSEGV, SIG_DFL, 0, 0);+    let leaked = sysinfo().frames_in_use as i64 - before as i64;+    c.check(leaked < 16, "memory use is back to (about) where it started");+    c.done()+}
@@ -5,9 +5,17 @@ //! //! It reads a line, parses it (shparse.rs: words, `|`, `<`, `>`, `>>`, `&`, //! drive letters), then either runs a *builtin* (cd, pwd, echo, ... -- these-//! must change the shell itself, or are just tiny) or *spawns* a program-//! from a `bin` directory, connecting pipes and files to its stdin/stdout.-//! Everything goes through the same syscalls any program could use.+//! must change the shell itself, or are just tiny) or starts a program from+//! a `bin` directory the Unix way: `fork` a copy of the shell, and in the+//! copy connect pipes and files to stdin/stdout with `dup2`, then `execve`+//! the program. Everything goes through the same syscalls any program+//! could use.+//!+//! Job control: every pipeline gets its own *process group*, and while it+//! runs in the foreground the console's foreground group is set to it, so+//! Ctrl+C (SIGINT) and Ctrl+Z (SIGTSTP) reach the program, not the shell+//! (which ignores both). `fg` / `bg` continue a stopped job; `kill` sends+//! any signal.  #![no_std] #![no_main]@@ -40,21 +48,27 @@ const HELP: &str = "\ Rustbelt shell commands   builtins   cd [dir]  pwd  echo [text]  ver  drives  alias [name=value]              history  jobs  wait [pid]  cls / clear  help  exit [code]+             kill [-SIG] pid|%job ...  kill -l  fg [%job]  bg [%job]   programs   ls [-l]  cat  wc  grep [-i] [-v]  mkdir  rm  touch  cp  sleep  sysinfo   network    ifconfig (ip)  ping [-c n] host  host name (nslookup)              fetch [-o file] http://... (wget)  httpd [-p port] [-n count]   devices    random [-n bytes | -d sides [n]]  getty [-1] /dev/hvc0  tile [n] | tile -r   (bin dirs) hello  demo  evil  spin [rounds]  overflow  child  sh+  tests      forktest  cowtest  mmaptest  sigtest   run [--trace] prog args   run a program (--trace prints its syscalls)   start prog args           run it in the background (or: prog args &)   a | b | c                 pipes;  > file  >> file  < file  redirection+  Ctrl+C                    interrupt the running program (SIGINT); Ctrl+Z stops it   A: is /, B: C: ... are the disks (see `drives`): cd B:, cat B:/readme.txt "; -/// A background job started with `start` or `&`.+/// A background (or stopped) job: one pipeline, one process group. struct Job {-    pid: i64,+    pid: i64,       // the last program of the pipeline (its exit code counts)+    pgid: i64,      // the group: the first program's pid+    pids: Vec<i64>, // programs not reaped yet     command: String,+    stopped: bool, }  /// One command of a pipeline, after alias, drive letter and `run`/`start`@@ -74,6 +88,7 @@ struct Shell {     jobs: Vec<Job>,     status: i64,            // exit code of the last command     exit: Option<i64>,      // set by the `exit` builtin+    job_control: bool,      // interactive: process groups + the console's foreground }  fn main() -> i64 {@@ -84,6 +99,7 @@ fn main() -> i64 {         jobs: Vec::new(),         status: 0,         exit: None,+        job_control: false,     };      // Where do commands come from? A script file, or stdin. If stdin is the@@ -91,7 +107,8 @@ fn main() -> i64 {     // lines just like from a script.     let fd = match arg(1) {         Some(path) => {-            let fd = open(path, 0);+            // O_CLOEXEC: the programs we fork + exec mustn't inherit it.+            let fd = open(path, O_CLOEXEC);             if fd < 0 {                 eprintln!("sh: {}: {}", path, strerror(fd));                 return 127;@@ -106,6 +123,15 @@ fn main() -> i64 {      sh.refresh_drives();     if interactive {+        // Job control: be the leader of our own process group, own the+        // console, and ignore the keyboard signals -- they're for the+        // programs we run, not for us.+        sh.job_control = true;+        setpgid(0, 0);+        tcsetpgrp(0, getpgrp());+        for sig in [SIGINT, SIGQUIT, SIGTSTP, SIGTTIN, SIGTTOU] {+            sigaction_raw(sig, SIG_IGN, 0, 0);+        }         let disks: Vec<String> = sh.drives.iter().map(|d| format!("{}: = {}", d.letter, d.path)).collect();         println!("Rustbelt shell. Type `help` for commands. Drives: {}", disks.join(", "));     }@@ -150,11 +176,25 @@ impl Shell {     }      /// Print "[done]" for background jobs that have finished (without-    /// blocking: WNOHANG returns -EAGAIN while a job is still running).+    /// blocking: WNOHANG says 0 while a program is still running).     fn reap_jobs(&mut self) {-        self.jobs.retain(|job| {-            let code = try_wait(job.pid);-            if code == -EAGAIN {+        self.jobs.retain_mut(|job| {+            let mut code = 0;+            job.pids.retain(|&pid| match wait4(pid, WNOHANG | WUNTRACED) {+                Ok((0, _)) => true, // still running+                Ok((_, status)) if wifstopped(status) => {+                    job.stopped = true;+                    true+                }+                Ok((_, status)) => {+                    if pid == job.pid {+                        code = exit_code(status);+                    }+                    false+                }+                Err(_) => false, // gone already+            });+            if !job.pids.is_empty() {                 return true;             }             println!("[done] pid {} ({}), exit code {}", job.pid, job.command, code);@@ -207,6 +247,13 @@ impl Shell {                             erase(1);                         }                     }+                    (_, 0x03) => {+                        // Ctrl+C while typing: throw the line away (in raw+                        // mode the console doesn't make it a signal).+                        println!("^C");+                        line.clear();+                        break 'read;+                    }                     (_, 0x20..=0x7e) => {                         line.push(b);                         write(1, &[b]); // echo@@ -307,6 +354,7 @@ impl Shell {          let mut opened: Vec<i64> = Vec::new(); // fds to close when done         let mut children: Vec<(i64, String)> = Vec::new();+        let mut pgid = 0; // the job's process group = its first program's pid         let mut status = 0;         let mut prev_pipe = -1; // read end of the pipe from the previous stage         let mut last_pid = None; // the last stage, if it's a program@@ -314,7 +362,7 @@ impl Shell {             // stdin: `< file`, else the previous pipe, else ours (-1).             let mut stdin = prev_pipe;             if let Some(file) = &stage.stdin {-                stdin = open(file, 0);+                stdin = open(file, O_CLOEXEC);                 if stdin < 0 {                     eprintln!("sh: {}: {}", file, strerror(stdin));                     status = 1;@@ -327,13 +375,15 @@ impl Shell {             let mut stdout = -1;             prev_pipe = -1;             if i + 1 < stages.len() {-                let (r, w) = pipe().unwrap_or((-1, -1));+                // O_CLOEXEC: the programs get these only as their stdin+                // / stdout (dup2), not as extra open fds.+                let (r, w) = pipe2(O_CLOEXEC).unwrap_or((-1, -1));                 opened.extend([r, w]);                 stdout = w;                 prev_pipe = r;             }             if let Some((file, append)) = &stage.stdout {-                let flags = O_CREAT | if *append { O_APPEND } else { O_TRUNC };+                let flags = O_CREAT | O_CLOEXEC | if *append { O_APPEND } else { O_TRUNC };                 stdout = open(file, flags);                 if stdout < 0 {                     eprintln!("sh: {}: {}", file, strerror(stdout));@@ -349,8 +399,11 @@ impl Shell {                     continue;                 }             }-            match self.spawn(stage, stdin, stdout) {+            match self.spawn(stage, stdin, stdout, pgid, !background) {                 Ok(pid) => {+                    if pgid == 0 {+                        pgid = pid;+                    }                     children.push((pid, stage.argv[0].clone()));                     if i + 1 == stages.len() {                         last_pid = Some(pid);@@ -368,23 +421,85 @@ impl Shell {             close(fd);         } +        if children.is_empty() {+            return status;+        }+        let mut job = Job {+            pid: last_pid.unwrap_or(children[children.len() - 1].0),+            pgid,+            pids: children.iter().map(|(pid, _)| *pid).collect(),+            command: line.to_string(),+            stopped: false,+        };         if background {-            for (pid, name) in children {+            for (pid, name) in &children {                 println!("[pid {}] {} started in the background", pid, name);-                self.jobs.push(Job { pid, command: line.to_string() });             }+            self.jobs.push(job);             return 0;         }-        for (pid, _) in &children {-            let code = wait(*pid);-            if Some(*pid) == last_pid {-                status = code; // the last stage's exit code is the pipeline's+        match self.wait_job(&mut job) {+            Some(code) if last_pid.is_some() => code,+            Some(_) => status, // the last stage was a builtin+            None => 148,       // stopped (128 + SIGTSTP)+        }+    }++    /// Wait for a foreground job: it owns the console until all of its+    /// programs end (-> the exit code of the last one) or it's stopped by+    /// Ctrl+Z (-> None, and it goes on the job list).+    fn wait_job(&mut self, job: &mut Job) -> Option<i64> {+        if self.job_control {+            tcsetpgrp(0, job.pgid);+        }+        let mut code = 0;+        let mut signal = 0;+        while let Some(&next) = job.pids.first() {+            match wait4(next, WUNTRACED) {+                Ok((_, status)) if wifstopped(status) => {+                    job.stopped = true;+                    break;+                }+                Ok((pid, status)) => {+                    job.pids.retain(|&p| p != pid);+                    if pid == job.pid {+                        code = exit_code(status);+                        signal = if wifsignaled(status) { wtermsig(status) } else { 0 };+                    }+                }+                Err(e) if e == -EINTR => {}+                Err(_) => break, // no children left (someone else reaped them)             }         }-        if status != 0 && last_pid.is_some() {-            println!("[exit code {}]", status);+        if self.job_control {+            tcsetpgrp(0, getpgrp()); // the console is ours again+        }+        if job.stopped {+            println!("\n[stopped] pid {} ({})  -- `fg` to continue", job.pid, job.command);+            self.jobs.push(Job { pids: core::mem::take(&mut job.pids), command: core::mem::take(&mut job.command), ..*job });+            return None;+        }+        match signal {+            0 if code != 0 => println!("[exit code {}]", code),+            0 => {}+            SIGINT => println!("[exit code {}] (interrupted: SIGINT)", code),+            sig => println!("[exit code {}] (killed by SIG{})", code, SIGNAL_NAMES.get(sig as usize).unwrap_or(&"?")),+        }+        Some(code)+    }++    /// `%2` (job number), `%` (the latest job) or a pid -> index in jobs.+    fn find_job(&self, spec: Option<&str>) -> Option<usize> {+        match spec {+            None | Some("%") => self.jobs.len().checked_sub(1),+            Some(s) => match s.strip_prefix('%') {+                Some(n) => n.parse::<usize>().ok().filter(|&n| n >= 1 && n <= self.jobs.len()).map(|n| n - 1),+                None => {+                    let pid: i64 = s.parse().ok()?;+                    self.jobs.iter().position(|j| j.pid == pid || j.pgid == pid)+                }+            },         }-        status     }      /// Find a program: names with a `/` are paths; others are looked up in@@ -397,20 +512,51 @@ impl Shell {         self.drives.iter().map(|d| format!("{}/bin/{}", d.path.trim_end_matches('/'), name)).find(|p| stat(p, &mut st) == 0 && st.kind == KIND_FILE)     } -    fn spawn(&self, stage: &Stage, stdin: i64, stdout: i64) -> Result<i64, i64> {+    /// Start one program: fork, then in the child set up its process+    /// group, stdin/stdout and signals, and execve it.+    fn spawn(&self, stage: &Stage, stdin: i64, stdout: i64, pgid: i64, foreground: bool) -> Result<i64, i64> {         let name = &stage.argv[0];         let Some(path) = self.find_program(name) else {             eprintln!("sh: {}: command not found (try `help`)", name);             return Err(127);         };-        // spawn takes the arguments as one NUL-separated string.-        let argv = stage.argv.join("\0");-        let opts = SpawnOptions { stdin, stdout, stderr: -1, flags: if stage.trace { SPAWN_TRACE } else { 0 } };-        let pid = spawn(&path, &argv, &opts);+        let pid = fork();         if pid < 0 {-            eprintln!("sh: {}: {}", name, strerror(pid));+            eprintln!("sh: {}: fork: {}", name, strerror(pid));             return Err(126);         }+        if pid == 0 {+            // --- the child: still the shell's code, in a copy of its memory.+            if self.job_control {+                setpgid(0, pgid); // pgid 0: a new group, named after me+                if foreground {+                    tcsetpgrp(0, getpgrp());+                }+                // Ignored signals stay ignored across execve: undo ours.+                for sig in [SIGINT, SIGQUIT, SIGTSTP, SIGTTIN, SIGTTOU] {+                    sigaction_raw(sig, SIG_DFL, 0, 0);+                }+            }+            if stdin >= 0 {+                dup2(stdin, 0); // the new fd 0 doesn't close on exec+            }+            if stdout >= 0 {+                dup2(stdout, 1);+            }+            if stage.trace {+                trace_me(true);+            }+            let argv: Vec<&str> = stage.argv.iter().map(|s| s.as_str()).collect();+            let err = execve(&path, &argv, &environ());+            // Only reached if execve failed.+            eprintln!("sh: {}: {}", name, strerror(err));+            exit(126);+        }+        // --- the parent. Set the group here too: whichever of us runs+        // first, the child is in its group before we wait or tcsetpgrp.+        if self.job_control {+            setpgid(pid, if pgid == 0 { pid } else { pgid });+        }         Ok(pid)     } @@ -493,8 +639,9 @@ impl Shell {             }             "jobs" => {                 self.reap_jobs();-                for job in &self.jobs {-                    let _ = writeln!(out, "[pid {}] running  {}", job.pid, job.command);+                for (i, job) in self.jobs.iter().enumerate() {+                    let state = if job.stopped { "stopped" } else { "running" };+                    let _ = writeln!(out, "[{}] [pid {}] {}  {}", i + 1, job.pid, state, job.command);                 }                 0             }@@ -506,18 +653,94 @@ impl Shell {                     if only.is_some_and(|pid| pid != job.pid) {                         return true;                     }-                    code = wait(job.pid);+                    for &pid in &job.pids {+                        let c = wait(pid);+                        if pid == job.pid {+                            code = c;+                        }+                    }                     let _ = writeln!(out, "[done] pid {} ({}), exit code {}", job.pid, job.command, code);                     false                 });                 code             }+            "kill" => self.kill(&argv[1..], &mut out),+            "fg" | "bg" => {+                let Some(i) = self.find_job(arg1) else {+                    eprintln!("{}: no such job", argv[0]);+                    return Some(1);+                };+                let mut job = self.jobs.remove(i);+                kill(-job.pgid, SIGCONT); // wake the whole pipeline up+                job.stopped = false;+                if argv[0] == "bg" {+                    let _ = writeln!(out, "[pid {}] {} continued in the background", job.pid, job.command);+                    self.jobs.push(job);+                    0+                } else {+                    let _ = writeln!(out, "{}", job.command);+                    self.wait_job(&mut job).unwrap_or(148)+                }+            }             _ => return None,         };         Some(code)     } } +impl Shell {+    /// `kill [-SIG] pid|%job ...` and `kill -l`. Default signal: SIGTERM.+    fn kill(&self, args: &[String], out: &mut Fd) -> i64 {+        let mut sig = SIGTERM;+        let mut args = args;+        match args.first().map(|s| s.as_str()) {+            Some("-l") => {+                for (i, name) in SIGNAL_NAMES.iter().enumerate().skip(1) {+                    let _ = write!(out, "{:2}) SIG{:<7}{}", i, name, if i % 5 == 0 { "\n" } else { " " });+                }+                let _ = writeln!(out);+                return 0;+            }+            Some(opt) if opt.starts_with('-') && opt.len() > 1 && !opt[1..].starts_with(|c: char| c == '-') => {+                let name = opt[1..].strip_prefix("s ").unwrap_or(&opt[1..]);+                match signal_number(name) {+                    Some(n) => sig = n,+                    None => {+                        eprintln!("kill: {}: unknown signal (see `kill -l`)", name);+                        return 2;+                    }+                }+                args = &args[1..];+            }+            _ => {}+        }+        if args.is_empty() {+            eprintln!("usage: kill [-SIGNAL] pid|%job ...   (kill -l lists signals)");+            return 2;+        }+        let mut code = 0;+        for target in args {+            // %n = a job's whole process group (negative pid).+            let pid = if target.starts_with('%') {+                // (Without job control the job has no group of its own.)+                let job = |i: usize| if self.job_control { -self.jobs[i].pgid } else { self.jobs[i].pid };+                self.find_job(Some(target)).map(job)+            } else {+                target.parse::<i64>().ok()+            };+            let r = match pid {+                Some(pid) => kill(pid, sig),+                None => -ESRCH,+            };+            if r < 0 {+                eprintln!("kill: {}: {}", target, if r == -ESRCH { "No such process" } else { strerror(r) });+                code = 1;+            }+        }+        code+    }+}+ /// Erase `n` characters before the cursor (the console treats \x08 as /// "back one column and blank it"). fn erase(n: usize) {
@@ -0,0 +1,189 @@+//! sigtest -- checks signals: handlers and sigreturn, masks, default+//! actions, SIGCHLD, stop/continue, SIGPIPE and interrupted system calls.+//!+//! A signal handler is a normal function that the kernel makes the program+//! "call" at a moment it didn't choose: it saves the interrupted registers+//! in a frame on the user stack, runs the handler, and when the handler+//! returns (into ulib's `__restore_rt`), rt_sigreturn puts everything back.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use core::sync::atomic::{AtomicI64, Ordering::SeqCst};+use ulib::*;++static HITS: AtomicI64 = AtomicI64::new(0);+static LAST_SIG: AtomicI64 = AtomicI64::new(0);+static MASK_IN_HANDLER: AtomicI64 = AtomicI64::new(0);+static CHLD_PID: AtomicI64 = AtomicI64::new(0);+static CHLD_STATUS: AtomicI64 = AtomicI64::new(0);+static CHLD_CODE: AtomicI64 = AtomicI64::new(0);++extern "C" fn on_signal(sig: i32) {+    HITS.fetch_add(1, SeqCst);+    LAST_SIG.store(sig as i64, SeqCst);+    // While a handler runs, its own signal is blocked.+    MASK_IN_HANDLER.store(sigprocmask(SIG_BLOCK, 0), SeqCst);+}++extern "C" fn on_child(_sig: i32, info: *const SigInfo, _ctx: *mut u8) {+    let info = unsafe { &*info };+    CHLD_PID.store(info.addr_or_pid as u32 as i64, SeqCst);+    CHLD_STATUS.store(info.status as i64, SeqCst);+    CHLD_CODE.store(info.code as i64, SeqCst);+}++/// Busy work that keeps values in registers across a signal: if sigreturn+/// restored anything wrong, the sum comes out different.+#[inline(never)]+fn compute_with_signal(sig: i32) -> u64 {+    let mut x = core::hint::black_box(0x1234_5678_u64);+    let mut y = core::hint::black_box(0x0bad_cafe_u64);+    for i in 0..1000u64 {+        if i == 500 {+            kill(getpid(), sig); // the handler runs before kill returns+        }+        x = x.wrapping_mul(31).wrapping_add(i);+        y ^= x.rotate_left(7);+    }+    x ^ y+}++/// Fork a child that runs `f` and return its wait4 status.+fn in_child(f: fn()) -> i32 {+    let pid = fork();+    if pid == 0 {+        f();+        exit(0);+    }+    wait4(pid, 0).map(|(_, s)| s).unwrap_or(-1)+}++fn main() -> i64 {+    let mut c = Checks::new("sigtest");+    // 1. A handler round trip, with every register restored.+    sigaction_raw(SIGUSR2, SIG_IGN, 0, 0);+    let without = compute_with_signal(SIGUSR2); // ignored: nothing happens+    c.check(HITS.load(SeqCst) == 0, "SIG_IGN: an ignored signal does nothing");+    c.check(signal(SIGUSR1, on_signal) == 0, "rt_sigaction installs a handler");+    let with = compute_with_signal(SIGUSR1);+    c.check(HITS.load(SeqCst) == 1 && LAST_SIG.load(SeqCst) == SIGUSR1 as i64, "kill(getpid(), SIGUSR1) runs the handler once, with sig = 10");+    c.check(with == without, "after rt_sigreturn the interrupted code computes the same result");+    c.check(MASK_IN_HANDLER.load(SeqCst) as u64 & sigbit(SIGUSR1) != 0, "the signal is blocked while its handler runs");+    c.check(sigprocmask(SIG_BLOCK, 0) as u64 & sigbit(SIGUSR1) == 0, "... and unblocked again after sigreturn");++    // 2. Blocking: a blocked signal waits (pending) until unblocked.+    sigprocmask(SIG_BLOCK, sigbit(SIGUSR1));+    kill(getpid(), SIGUSR1);+    c.check(HITS.load(SeqCst) == 1, "blocked: kill doesn't run the handler yet");+    sigprocmask(SIG_UNBLOCK, sigbit(SIGUSR1));+    c.check(HITS.load(SeqCst) == 2, "unblocking delivers the pending signal");+    c.check(sigaction_raw(SIGKILL, SIG_IGN, 0, 0) == -22, "SIGKILL can't be caught or ignored (EINVAL)");++    // 3. Default actions, seen through wait4's status.+    let s = in_child(|| {+        kill(getpid(), SIGTERM);+    });+    c.check(wifsignaled(s) && wtermsig(s) == SIGTERM, "SIGTERM's default action terminates");+    let s = in_child(|| {+        kill(getpid(), SIGQUIT);+    });+    c.check(wifsignaled(s) && wtermsig(s) == SIGQUIT, "SIGQUIT (\"core\") terminates too (no core files here)");+    let s = in_child(|| {+        kill(getpid(), SIGCHLD); // default: ignore+        kill(getpid(), SIGURG_NUM);+    });+    c.check(wifexited(s) && wexitstatus(s) == 0, "SIGCHLD and SIGURG are ignored by default");+    let s = in_child(|| {+        // Write to a pipe nobody reads: SIGPIPE.+        let (r, w) = pipe().unwrap();+        close(r);+        write(w, b"x");+    });+    c.check(wifsignaled(s) && wtermsig(s) == SIGPIPE, "writing to a pipe without readers: SIGPIPE");+    let s = in_child(|| {+        sigaction_raw(SIGPIPE, SIG_IGN, 0, 0);+        let (r, w) = pipe().unwrap();+        close(r);+        exit(if write(w, b"x") == -32 { 0 } else { 1 });+    });+    c.check(wifexited(s) && wexitstatus(s) == 0, "... with SIGPIPE ignored, write fails with EPIPE");++    // 4. SIGCHLD: the parent hears when a child ends.+    sigaction_info(SIGCHLD, on_child, SA_RESTART);+    let pid = fork();+    if pid == 0 {+        exit(3);+    }+    while CHLD_PID.load(SeqCst) == 0 {+        pause();+    }+    c.check(CHLD_PID.load(SeqCst) == pid && CHLD_STATUS.load(SeqCst) == 3 && CHLD_CODE.load(SeqCst) == 1, "SIGCHLD arrives with si_pid, status 3, CLD_EXITED");+    c.check(wait(pid) == 3, "and the child can still be reaped");+    sigaction_raw(SIGCHLD, SIG_DFL, 0, 0);++    // 5. Stop and continue.+    let pid = fork();+    if pid == 0 {+        loop {+            sleep_ms(10);+        }+    }+    kill(pid, SIGSTOP);+    let r = wait4(pid, WUNTRACED);+    c.check(matches!(r, Ok((p, s)) if p == pid && wifstopped(s) && wstopsig(s) == SIGSTOP), "SIGSTOP stops a child; wait4(WUNTRACED) reports it");+    kill(pid, SIGCONT);+    let r = wait4(pid, WCONTINUED);+    c.check(matches!(r, Ok((_, s)) if wifcontinued(s)), "SIGCONT continues it; wait4(WCONTINUED) reports that");+    kill(pid, SIGKILL);+    let r = wait4(pid, 0);+    c.check(matches!(r, Ok((_, s)) if wifsignaled(s) && wtermsig(s) == SIGKILL), "SIGKILL ends it");++    // 6. Interrupted system calls: a signal during a blocking read.+    for restart in [false, true] {+        sigaction_raw(SIGUSR1, on_signal as usize, if restart { SA_RESTART } else { 0 }, 0);+        let (r, w) = pipe().unwrap();+        let me = getpid();+        let pid = fork();+        if pid == 0 {+            sleep_ms(120);+            kill(me, SIGUSR1); // interrupts the parent's read+            sleep_ms(120);+            write(w, b"y");+            exit(0);+        }+        let hits = HITS.load(SeqCst);+        let mut b = [0u8; 1];+        let n = read(r, &mut b);+        if restart {+            c.check(n == 1 && HITS.load(SeqCst) == hits + 1, "SA_RESTART: the handler runs, then read continues and gets the data");+        } else {+            c.check(n == -EINTR && HITS.load(SeqCst) == hits + 1, "no SA_RESTART: read returns EINTR after the handler");+        }+        wait(pid);+        close(r);+        close(w);+    }++    // 7. kill errors and process groups.+    c.check(kill(99999, SIGTERM) == -ESRCH, "kill of a pid that doesn't exist: ESRCH");+    c.check(kill(getpid(), 99) == -22, "kill with signal 99: EINVAL");+    c.check(kill(getpid(), 0) == 0, "signal 0 only checks that the process exists");+    let pid = fork();+    if pid == 0 {+        setpgid(0, 0);+        loop {+            pause();+        }+    }+    setpgid(pid, pid);+    c.check(kill(-pid, SIGTERM) == 0, "kill(-pgid) signals a whole process group");+    let r = wait4(pid, 0);+    c.check(matches!(r, Ok((_, s)) if wtermsig(s) == SIGTERM), "the group's member got it");+    c.done()+}++const SIGURG_NUM: i32 = 23;
@@ -12,6 +12,8 @@  #![allow(dead_code, unused_macros)] +extern crate alloc;+ use core::alloc::{GlobalAlloc, Layout}; use core::arch::asm; use core::cell::UnsafeCell;@@ -30,7 +32,28 @@ pub const SYS_SCHED_YIELD: u64 = 24; pub const SYS_SLEEP_MS: u64 = 35; pub const SYS_GETPID: u64 = 39; pub const SYS_EXIT: u64 = 60;-pub const SYS_WAIT: u64 = 61;+pub const SYS_WAIT4: u64 = 61;+pub const SYS_MMAP: u64 = 9;+pub const SYS_MPROTECT: u64 = 10;+pub const SYS_MUNMAP: u64 = 11;+pub const SYS_BRK: u64 = 12;+pub const SYS_RT_SIGACTION: u64 = 13;+pub const SYS_RT_SIGPROCMASK: u64 = 14;+pub const SYS_RT_SIGRETURN: u64 = 15;+pub const SYS_DUP: u64 = 32;+pub const SYS_DUP2: u64 = 33;+pub const SYS_PAUSE: u64 = 34;+pub const SYS_FORK: u64 = 57;+pub const SYS_VFORK: u64 = 58;+pub const SYS_EXECVE: u64 = 59;+pub const SYS_KILL: u64 = 62;+pub const SYS_FCNTL: u64 = 72;+pub const SYS_SETPGID: u64 = 109;+pub const SYS_GETPPID: u64 = 110;+pub const SYS_GETPGRP: u64 = 111;+pub const SYS_EXIT_GROUP: u64 = 231;+pub const SYS_PIPE2: u64 = 293;+pub const SYS_TRACE_ME: u64 = 505; pub const SYS_UNAME: u64 = 63; pub const SYS_GETCWD: u64 = 79; pub const SYS_CHDIR: u64 = 80;@@ -65,12 +88,23 @@ pub const FB_FLUSH: u64 = 0x46f0; pub const FB_FILL: u64 = 0x46f1; pub const FB_SET_CONSOLE: u64 = 0x46f2; pub const WNOHANG: u64 = 1;+pub const WUNTRACED: u64 = 2;+pub const WCONTINUED: u64 = 8;+pub const O_CLOEXEC: u64 = 0o2000000;+pub const F_GETFD: u64 = 1;+pub const F_SETFD: u64 = 2;+pub const FD_CLOEXEC: u64 = 1;+pub const TIOCGPGRP: u64 = 0x540F;+pub const TIOCSPGRP: u64 = 0x5410; pub const SEEK_SET: u64 = 0; pub const SEEK_CUR: u64 = 1; pub const SEEK_END: u64 = 2; pub const SPAWN_TRACE: u64 = 1; +pub const EPERM: i64 = 1; pub const ENOENT: i64 = 2;+pub const ESRCH: i64 = 3;+pub const EINTR: i64 = 4; pub const EAGAIN: i64 = 11; pub const EBADF: i64 = 9; pub const ECHILD: i64 = 10;@@ -132,6 +166,11 @@ pub fn syscall3(nr: u64, a1: u64, a2: u64, a3: u64) -> i64 {  /// 5-argument version (4th argument goes in r10, 5th in r8). pub fn syscall5(nr: u64, a1: u64, a2: u64, a3: u64, a4: u64, a5: u64) -> i64 {+    syscall6(nr, a1, a2, a3, a4, a5, 0)+}++/// 6-argument version (6th in r9). mmap is the only one that needs it.+pub fn syscall6(nr: u64, a1: u64, a2: u64, a3: u64, a4: u64, a5: u64, a6: u64) -> i64 {     let ret: i64;     unsafe {         asm!(@@ -142,6 +181,7 @@ pub fn syscall5(nr: u64, a1: u64, a2: u64, a3: u64, a4: u64, a5: u64) -> i64 {             in("rdx") a3,             in("r10") a4,             in("r8") a5,+            in("r9") a6,             lateout("rcx") _,             lateout("r11") _,             options(nostack),@@ -173,8 +213,12 @@ pub fn lseek(fd: i64, offset: i64, whence: u64) -> i64 { } /// Returns (read end, write end), or the negative errno. pub fn pipe() -> Result<(i64, i64), i64> {+    pipe2(0)+}+/// pipe with flags (O_CLOEXEC: both ends close on exec).+pub fn pipe2(flags: u64) -> Result<(i64, i64), i64> {     let mut fds = [0u32; 2];-    match syscall3(SYS_PIPE, fds.as_mut_ptr() as u64, 0, 0) {+    match syscall3(SYS_PIPE2, fds.as_mut_ptr() as u64, flags, 0) {         0 => Ok((fds[0] as i64, fds[1] as i64)),         e => Err(e),     }@@ -189,17 +233,314 @@ pub fn getpid() -> i64 {     syscall3(SYS_GETPID, 0, 0, 0) } pub fn exit(code: i64) -> ! {-    syscall3(SYS_EXIT, code as u64, 0, 0);+    syscall3(SYS_EXIT_GROUP, code as u64, 0, 0);     loop {} // not reached }-/// Wait for child `pid` to end; returns its exit code (139 = killed).++// --- Processes (fork, exec, wait), stage 14 --------------------------------++/// fork: 0 in the child, the child's pid in the parent (or -errno).+pub fn fork() -> i64 {+    syscall3(SYS_FORK, 0, 0, 0)+}+/// vfork: like fork, but the parent sleeps until the child calls execve or+/// exits. (Here the child gets a copy-on-write copy anyway, so it's safe to+/// do more than exec in it; on Linux it borrows the parent's memory.)+pub fn vfork() -> i64 {+    syscall3(SYS_VFORK, 0, 0, 0)+}+pub fn getppid() -> i64 {+    syscall3(SYS_GETPPID, 0, 0, 0)+}+pub fn getpgrp() -> i64 {+    syscall3(SYS_GETPGRP, 0, 0, 0)+}+/// Put process `pid` (0 = me) into group `pgid` (0 = a new group named+/// after the process).+pub fn setpgid(pid: i64, pgid: i64) -> i64 {+    syscall3(SYS_SETPGID, pid as u64, pgid as u64, 0)+}+/// Which process group gets the console's Ctrl+C.+pub fn tcgetpgrp(fd: i64) -> i64 {+    let mut pgid = 0u32;+    match ioctl(fd, TIOCGPGRP, &mut pgid as *mut u32 as u64) {+        0 => pgid as i64,+        e => e,+    }+}+pub fn tcsetpgrp(fd: i64, pgid: i64) -> i64 {+    let pgid = pgid as u32;+    ioctl(fd, TIOCSPGRP, &pgid as *const u32 as u64)+}+/// Print my system calls from now on (inherited across fork and exec).+pub fn trace_me(on: bool) -> i64 {+    syscall3(SYS_TRACE_ME, on as u64, 0, 0)+}+pub fn dup(fd: i64) -> i64 {+    syscall3(SYS_DUP, fd as u64, 0, 0)+}+pub fn dup2(old: i64, new: i64) -> i64 {+    syscall3(SYS_DUP2, old as u64, new as u64, 0)+}+pub fn fcntl(fd: i64, cmd: u64, arg: u64) -> i64 {+    syscall3(SYS_FCNTL, fd as u64, cmd, arg)+}++/// A list of C strings (NUL-terminated) plus the NULL-terminated array of+/// pointers to them that execve wants.+pub struct CStrings {+    data: alloc::vec::Vec<u8>,+    ends: alloc::vec::Vec<usize>,+    ptrs: alloc::vec::Vec<u64>,+}++impl CStrings {+    pub fn new<'a>(items: impl IntoIterator<Item = &'a str>) -> CStrings {+        let mut data = alloc::vec::Vec::new();+        let mut ends = alloc::vec::Vec::new();+        for item in items {+            data.extend_from_slice(item.as_bytes());+            data.push(0);+            ends.push(data.len());+        }+        CStrings { data, ends, ptrs: alloc::vec::Vec::new() }+    }+    /// The `char **`: pointers into `data`, then NULL.+    pub fn as_ptr(&mut self) -> *const u64 {+        let base = self.data.as_ptr() as u64;+        let mut start = 0;+        self.ptrs.clear();+        for &end in &self.ends {+            self.ptrs.push(base + start as u64);+            start = end;+        }+        self.ptrs.push(0);+        self.ptrs.as_ptr()+    }+}++/// execve(path, argv, envp): only returns on failure (-errno).+pub fn execve(path: &str, argv: &[&str], envp: &[&str]) -> i64 {+    let mut cpath = alloc::vec::Vec::from(path.as_bytes());+    cpath.push(0);+    let mut args = CStrings::new(argv.iter().copied());+    let mut env = CStrings::new(envp.iter().copied());+    syscall3(SYS_EXECVE, cpath.as_ptr() as u64, args.as_ptr() as u64, env.as_ptr() as u64)+}++/// The same environment I got: for passing on to execve.+pub fn environ() -> alloc::vec::Vec<&'static str> {+    (0..).map_while(env).collect()+}++/// wait4(pid, &status, options): returns (pid, status), or -errno. pid -1 =+/// any child; WNOHANG and nothing finished: pid 0.+pub fn wait4(pid: i64, options: u64) -> Result<(i64, i32), i64> {+    let mut status = 0i32;+    match syscall5(SYS_WAIT4, pid as u64, &mut status as *mut i32 as u64, options, 0, 0) {+        n if n < 0 => Err(n),+        n => Ok((n, status)),+    }+}++// The status macros (C's WIFEXITED etc.): wait4's `status` packs how the+// child ended into one int -- exit code << 8, or the signal number, or+// 0x7f for "stopped".+pub fn wifexited(status: i32) -> bool {+    status & 0x7f == 0+}+pub fn wexitstatus(status: i32) -> i32 {+    (status >> 8) & 0xff+}+pub fn wifsignaled(status: i32) -> bool {+    (status & 0x7f) + 1 >= 2 && status & 0x7f != 0x7f+}+pub fn wtermsig(status: i32) -> i32 {+    status & 0x7f+}+pub fn wcoredump(status: i32) -> bool {+    status & 0x80 != 0+}+pub fn wifstopped(status: i32) -> bool {+    status & 0xff == 0x7f+}+pub fn wstopsig(status: i32) -> i32 {+    wexitstatus(status)+}+pub fn wifcontinued(status: i32) -> bool {+    status == 0xffff+}+/// A shell-style exit code: the code itself, or 128 + the signal number.+pub fn exit_code(status: i32) -> i64 {+    if wifsignaled(status) { 128 + wtermsig(status) as i64 } else { wexitstatus(status) as i64 }+}++/// Wait for child `pid` to end; returns its exit code (139 = killed by+/// SIGSEGV: 128 + 11), or -errno. pub fn wait(pid: i64) -> i64 {-    syscall3(SYS_WAIT, pid as u64, 0, 0)+    loop {+        match wait4(pid, 0) {+            Ok((_, status)) => return exit_code(status),+            Err(e) if e == -EINTR => continue,+            Err(e) => return e,+        }+    } } /// Like `wait`, but don't block: -EAGAIN while the child is still running. pub fn try_wait(pid: i64) -> i64 {-    syscall3(SYS_WAIT, pid as u64, WNOHANG, 0)+    match wait4(pid, WNOHANG) {+        Ok((0, _)) => -EAGAIN,+        Ok((_, status)) => exit_code(status),+        Err(e) => e,+    }+}++// --- Signals -----------------------------------------------------------------++pub const SIGHUP: i32 = 1;+pub const SIGINT: i32 = 2;+pub const SIGQUIT: i32 = 3;+pub const SIGILL: i32 = 4;+pub const SIGABRT: i32 = 6;+pub const SIGBUS: i32 = 7;+pub const SIGFPE: i32 = 8;+pub const SIGKILL: i32 = 9;+pub const SIGUSR1: i32 = 10;+pub const SIGSEGV: i32 = 11;+pub const SIGUSR2: i32 = 12;+pub const SIGPIPE: i32 = 13;+pub const SIGALRM: i32 = 14;+pub const SIGTERM: i32 = 15;+pub const SIGCHLD: i32 = 17;+pub const SIGCONT: i32 = 18;+pub const SIGSTOP: i32 = 19;+pub const SIGTSTP: i32 = 20;+pub const SIGTTIN: i32 = 21;+pub const SIGTTOU: i32 = 22;++/// Signal names, index = number ("" = unused).+pub const SIGNAL_NAMES: [&str; 32] = [+    "", "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "BUS", "FPE", "KILL", "USR1", "SEGV", "USR2", "PIPE", "ALRM", "TERM",+    "STKFLT", "CHLD", "CONT", "STOP", "TSTP", "TTIN", "TTOU", "URG", "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "IO", "PWR",+    "SYS",+];+/// "INT" or "SIGINT" or "2" -> 2.+pub fn signal_number(name: &str) -> Option<i32> {+    if let Ok(n) = name.parse::<i32>() {+        return (1..64).contains(&n).then_some(n);+    }+    let name = name.strip_prefix("SIG").unwrap_or(name);+    SIGNAL_NAMES.iter().position(|&n| !n.is_empty() && n.eq_ignore_ascii_case(name)).map(|i| i as i32) }++pub const SIG_DFL: usize = 0;+pub const SIG_IGN: usize = 1;+pub const SA_SIGINFO: u64 = 4;+pub const SA_RESTORER: u64 = 0x0400_0000;+pub const SA_RESTART: u64 = 0x1000_0000;+pub const SA_NODEFER: u64 = 0x4000_0000;+pub const SIG_BLOCK: u64 = 0;+pub const SIG_UNBLOCK: u64 = 1;+pub const SIG_SETMASK: u64 = 2;++/// The kernel's `struct sigaction` (not quite libc's: no sa_sigaction+/// union, the mask is 64 bits).+#[repr(C)]+#[derive(Clone, Copy, Default)]+pub struct SigAction {+    pub handler: usize, // fn(i32) or fn(i32, *SigInfo, *ucontext); or SIG_DFL / SIG_IGN+    pub flags: u64,+    pub restorer: usize,+    pub mask: u64,+}++/// The first fields of Linux's `siginfo_t` (it's 128 bytes in all).+#[repr(C)]+pub struct SigInfo {+    pub signo: i32,+    pub errno: i32,+    pub code: i32,+    _pad: i32,+    /// SIGSEGV/SIGBUS: the faulting address. SIGCHLD/kill: the sender's+    /// pid (low 32 bits) and uid (high 32 bits).+    pub addr_or_pid: u64,+    /// SIGCHLD: the exit code (or the signal that ended/stopped the child).+    pub status: i32,+}++// After a handler returns it "returns" into this: the restorer, which asks+// the kernel to put everything back as it was before the signal (from the+// frame the kernel saved on our stack). Linux's libc does exactly this.+core::arch::global_asm!(+    ".global __restore_rt",+    "__restore_rt:",+    "mov rax, 15", // rt_sigreturn+    "syscall",+);+unsafe extern "C" {+    fn __restore_rt();+}++/// Install `handler` for `sig` (plain `fn(i32)`). Returns the old action.+pub fn signal(sig: i32, handler: extern "C" fn(i32)) -> i64 {+    sigaction_raw(sig, handler as usize, 0, 0)+}+/// SA_SIGINFO handler: also gets the SigInfo (fault address, sender pid...).+pub fn sigaction_info(sig: i32, handler: extern "C" fn(i32, *const SigInfo, *mut u8), flags: u64) -> i64 {+    sigaction_raw(sig, handler as usize, flags | SA_SIGINFO, 0)+}+/// Set SIG_DFL / SIG_IGN / a handler address, with flags and a mask of+/// signals to block while it runs.+pub fn sigaction_raw(sig: i32, handler: usize, flags: u64, mask: u64) -> i64 {+    let act = SigAction { handler, flags: flags | SA_RESTORER, restorer: __restore_rt as usize, mask };+    syscall5(SYS_RT_SIGACTION, sig as u64, &act as *const _ as u64, 0, 8, 0)+}+/// Change the blocked set; returns the old one (or -errno).+pub fn sigprocmask(how: u64, set: u64) -> i64 {+    let mut old = 0u64;+    match syscall5(SYS_RT_SIGPROCMASK, how, &set as *const u64 as u64, &mut old as *mut u64 as u64, 8, 0) {+        0 => old as i64,+        e => e,+    }+}+pub fn sigbit(sig: i32) -> u64 {+    1 << (sig - 1)+}+/// kill(pid, sig): pid > 0 one process, 0 my group, -pgid a group, -1 all.+pub fn kill(pid: i64, sig: i32) -> i64 {+    syscall3(SYS_KILL, pid as u64, sig as u64, 0)+}+/// Sleep until a signal handler has run (always returns -EINTR).+pub fn pause() -> i64 {+    syscall3(SYS_PAUSE, 0, 0, 0)+}++// --- Memory: mmap and brk ----------------------------------------------------++pub const PROT_NONE: u64 = 0;+pub const PROT_READ: u64 = 1;+pub const PROT_WRITE: u64 = 2;+pub const PROT_EXEC: u64 = 4;+pub const MAP_SHARED: u64 = 0x01;+pub const MAP_PRIVATE: u64 = 0x02;+pub const MAP_FIXED: u64 = 0x10;+pub const MAP_ANONYMOUS: u64 = 0x20;++/// mmap: the address of the new mapping, or -errno. fd -1 = anonymous.+pub fn mmap(addr: u64, len: u64, prot: u64, flags: u64, fd: i64, offset: u64) -> i64 {+    syscall6(SYS_MMAP, addr, len, prot, flags, fd as u64, offset)+}+pub fn munmap(addr: u64, len: u64) -> i64 {+    syscall3(SYS_MUNMAP, addr, len, 0)+}+pub fn mprotect(addr: u64, len: u64, prot: u64) -> i64 {+    syscall3(SYS_MPROTECT, addr, len, prot)+}+/// brk: move the end of the heap; returns the new end (brk(0) = current).+pub fn brk(addr: u64) -> u64 {+    syscall3(SYS_BRK, addr, 0, 0) as u64+}+ /// Console raw mode on/off (keys arrive one by one, no echo). Returns the /// old mode, or -ENOTTY if `fd` isn't the console. pub fn set_raw(fd: i64, raw: bool) -> i64 {@@ -299,23 +640,64 @@ pub fn for_each_dirent(buf: &[u8], len: usize, mut f: impl FnMut(&str, u8)) {     } } -// --- program arguments ---------------------------------------------------+// --- program arguments, environment, auxiliary vector -------------------------+//+// The kernel starts a program the way Linux does: rsp points at+//     argc, argv[0..argc], NULL, envp[0..], NULL, auxv pairs..., AT_NULL 0+// and the strings themselves sit higher up on the stack. -/// `argv` as the kernel passes it: (pointer, length) pairs. static mut ARGC: usize = 0;-static mut ARGV: *const [usize; 2] = core::ptr::null();+static mut ARGV: *const *const u8 = core::ptr::null();+static mut ENVP: *const *const u8 = core::ptr::null();+static mut AUXV: *const [u64; 2] = core::ptr::null();++/// A C string as &str (up to its NUL).+pub unsafe fn cstr(ptr: *const u8) -> &'static str {+    unsafe {+        let mut len = 0;+        while *ptr.add(len) != 0 {+            len += 1;+        }+        core::str::from_utf8(core::slice::from_raw_parts(ptr, len)).unwrap_or("?")+    }+}  /// The program's arguments; `arg(0)` is the program name. pub fn argc() -> usize {     unsafe { ARGC } } pub fn arg(i: usize) -> Option<&'static str> {+    unsafe { if i < ARGC { Some(cstr(*ARGV.add(i))) } else { None } }+}+/// The i-th environment string ("NAME=value").+pub fn env(i: usize) -> Option<&'static str> {     unsafe {-        if i >= ARGC {-            return None;+        let p = *ENVP.add(i);+        if p.is_null() { None } else { Some(cstr(p)) }+    }+}+/// getenv("PATH") -> the value after "PATH=".+pub fn getenv(name: &str) -> Option<&'static str> {+    (0..).map_while(env).find_map(|e| e.strip_prefix(name)?.strip_prefix('='))+}+pub const AT_PHDR: u64 = 3;+pub const AT_PHENT: u64 = 4;+pub const AT_PHNUM: u64 = 5;+pub const AT_PAGESZ: u64 = 6;+pub const AT_ENTRY: u64 = 9;+pub const AT_RANDOM: u64 = 25;+pub const AT_EXECFN: u64 = 31;+/// An auxiliary vector entry (AT_PAGESZ -> 4096 ...), if the kernel gave it.+pub fn getauxval(kind: u64) -> Option<u64> {+    unsafe {+        let mut p = AUXV;+        while (*p)[0] != 0 {+            if (*p)[0] == kind {+                return Some((*p)[1]);+            }+            p = p.add(1);         }-        let [ptr, len] = *ARGV.add(i);-        core::str::from_utf8(core::slice::from_raw_parts(ptr as *const u8, len)).ok()+        None     } } @@ -365,7 +747,10 @@ pub fn for_each_line(fd: i64, mut f: impl FnMut(&[u8])) -> i64 { /// `ls`, `cat` etc. print errors like "cat: x: No such file or directory". pub fn strerror(errno: i64) -> &'static str {     match errno.abs() {+        1 => "Operation not permitted",         2 => "No such file or directory",+        3 => "No such process",+        4 => "Interrupted system call",         5 => "I/O error",         7 => "Argument list too long",         8 => "Exec format error",@@ -641,13 +1026,30 @@ macro_rules! println {     }}; } -/// The kernel jumps here (see process.rs) with rdi = argc, rsi = argv.-/// Save the arguments, run main, exit with its result.+// The entry point. The kernel jumps here with rsp at `argc` (see above);+// hand that pointer to Rust (rdi = first argument), keeping rsp 16-byte+// aligned the way a `call` expects.+core::arch::global_asm!(+    ".global _start",+    "_start:",+    "mov rdi, rsp",+    "and rsp, -16",+    "call start_rust",+    "ud2",+);++/// Find argv/envp/auxv on the initial stack, run main, exit with its result. #[unsafe(no_mangle)]-pub extern "C" fn _start(argc: usize, argv: *const [usize; 2]) -> ! {+extern "C" fn start_rust(sp: *const usize) -> ! {     unsafe {-        ARGC = argc;-        ARGV = argv;+        ARGC = *sp;+        ARGV = sp.add(1) as *const *const u8;+        ENVP = ARGV.add(ARGC + 1);+        let mut p = ENVP;+        while !(*p).is_null() {+            p = p.add(1);+        }+        AUXV = p.add(1) as *const [u64; 2];     }     exit(crate::main()) }@@ -661,29 +1063,30 @@ fn panic(info: &core::panic::PanicInfo) -> ! { // --- heap ---------------------------------------------------------------- // // A tiny allocator so programs can use `Vec`, `String`, `format!` (with-// `extern crate alloc;`). Memory comes from a 64 KiB array in .bss, which-// the ELF loader maps as zeroed pages -- no `brk`/`mmap` syscall needed.+// `extern crate alloc;`). Memory comes from `brk`: the kernel's "program+// break" is the end of a heap area right after the program, and moving it+// up gives us more (zeroed, demand-paged) memory. // Same idea as the kernel's fixed-size-block allocator: sizes are rounded up-// to a power of two (16 B .. 32 KiB) and freed blocks go on one free list+// to a power of two (16 B .. 1 MiB) and freed blocks go on one free list // per size, to be reused by the next allocation of that size. Programs are // single-threaded, so no lock. -const HEAP_SIZE: usize = 64 * 1024;-const CLASSES: usize = 12; // 16 << 11 = 32 KiB+const CLASSES: usize = 17; // 16 << 16 = 1 MiB -#[repr(C, align(4096))] struct Heap {-    memory: UnsafeCell<[u8; HEAP_SIZE]>,-    used: UnsafeCell<usize>,               // bump pointer: bytes handed out so far-    free: UnsafeCell<[usize; CLASSES]>,    // free list heads (addresses, 0 = empty)+    start: UnsafeCell<u64>, // where the heap began (0 = not set up yet)+    used: UnsafeCell<u64>,  // bump pointer: end of what's handed out+    end: UnsafeCell<u64>,   // the current break+    free: UnsafeCell<[usize; CLASSES]>, // free list heads (addresses, 0 = empty) }  unsafe impl Sync for Heap {} // single-threaded programs  #[global_allocator] static HEAP: Heap = Heap {-    memory: UnsafeCell::new([0; HEAP_SIZE]),+    start: UnsafeCell::new(0),     used: UnsafeCell::new(0),+    end: UnsafeCell::new(0),     free: UnsafeCell::new([0; CLASSES]), }; @@ -707,16 +1110,27 @@ unsafe impl GlobalAlloc for Heap {                 free[class] = *(block as *const usize);                 return block;             }+            let (used, end) = (&mut *self.used.get(), &mut *self.end.get());+            if *self.start.get() == 0 {+                *self.start.get() = brk(0);+                *used = *self.start.get();+                *end = *used;+            }             // Carve a new block. Blocks are aligned to their size, which             // covers any alignment up to that size.-            let size = 16 << class;-            let used = &mut *self.used.get();+            let size = 16u64 << class;             let start = (*used).next_multiple_of(size);-            if start + size > HEAP_SIZE {-                return core::ptr::null_mut();+            if start + size > *end {+                // Grow the heap (at least 64 KiB at a time).+                let want = (start + size).max(*end + 64 * 1024);+                let got = brk(want);+                if got < start + size {+                    return core::ptr::null_mut(); // the kernel said no+                }+                *end = got;             }             *used = start + size;-            (self.memory.get() as *mut u8).add(start)+            start as *mut u8         }     } @@ -729,3 +1143,35 @@ unsafe impl GlobalAlloc for Heap {         }     } }++// --- Self-checking test programs ---------------------------------------------++/// Counts failed checks for the test programs (forktest, sigtest, ...):+/// each prints "ok" or "FAILED" per check and exits with the failures.+pub struct Checks {+    pub name: &'static str,+    pub failed: i64,+}++impl Checks {+    pub fn new(name: &'static str) -> Checks {+        Checks { name, failed: 0 }+    }+    pub fn check(&mut self, ok: bool, what: &str) {+        if ok {+            println!("[{}] ok: {}", self.name, what);+        } else {+            println!("[{}] FAILED: {}", self.name, what);+            self.failed += 1;+        }+    }+    /// Print the summary; the result is the exit code.+    pub fn done(self) -> i64 {+        if self.failed == 0 {+            println!("[{}] all checks passed", self.name);+        } else {+            println!("[{}] {} check(s) failed", self.name, self.failed);+        }+        self.failed+    }+}