rust-kernel
Rustbelt: a hobby x86_64 kernel written in Rust
| author | Tyler Fowler <tyler@localhost> | 2026-10-30 09:46:08 -0700 |
|---|---|---|
| commit | 02590b467a004b14ab80c26c0860b4a39fcfaee9 | |
| parent | 2a105612c9f6eeb692b8d5862fb2b27ae68a3ee9 | |
| tree | browse 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.
| M | Cargo.lock | +1 -1 |
| M | Cargo.toml | +1 -1 |
| M | README.md | +725 -174 |
| M | boot.png | +0 -0 |
| M | build.rs | +1 -0 |
| M | disk/scripts/selftest.sh | +11 -0 |
| M | scripts/run-headless.sh | +12 -5 |
| M | scripts/screenshot.sh | +23 -12 |
| M | src/dev.rs | +1 -1 |
| M | src/elf.rs | +76 -27 |
| M | src/errno.rs | +10 -2 |
| M | src/fs/file.rs | +8 -0 |
| M | src/fs/pipe.rs | +3 -3 |
| M | src/interrupts.rs | +80 -58 |
| M | src/lib.rs | +5 -2 |
| M | src/memory.rs | +181 -17 |
| A | src/mm.rs | +465 -0 |
| M | src/net/socket.rs | +4 -2 |
| M | src/process.rs | +577 -203 |
| A | src/signal.rs | +715 -0 |
| M | src/syscall.rs | +464 -81 |
| M | src/task/keyboard.rs | +3 -1 |
| M | src/thread.rs | +75 -34 |
| A | src/trap.rs | +189 -0 |
| M | src/tty.rs | +35 -2 |
| M | src/virtio/console.rs | +6 -5 |
| M | tests/guard_pages.rs | +2 -2 |
| A | tests/process.rs | +158 -0 |
| M | tests/shell.rs | +1 -1 |
| M | tests/user_mode.rs | +1 -1 |
| A | user/cowtest.rs | +107 -0 |
| A | user/forktest.rs | +186 -0 |
| A | user/mmaptest.rs | +130 -0 |
| M | user/sh.rs | +255 -32 |
| A | user/sigtest.rs | +189 -0 |
| M | user/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.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(¤t()?, 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+ }+}