RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

authorTyler Fowler <tyler@localhost>2026-10-30 00:52:14 -0700
commit20756a688a45abfec809ec6ee710c35307350834
parent176e770e4a2ec63a4e05a273a139907cc241fb0d
treebrowse files at this commit
Stage 6: PCI, virtio-blk, partitions, FAT32, spawn/wait and shell syscalls

- PCI bus scan (printed at boot); BlockDevice trait + device registry
- legacy virtio-blk driver (polled virtqueue, bounce buffer); RamDisk for tests
- MBR + GPT parsing, partitions exposed as block devices (vda1...)
- FAT32 via fatfs (git, no_std+alloc) behind a BlockIo adapter; mounted at
  /mnt/disk0; build.rs builds target/disk.img (sfdisk + mkfs.vfat + mtools)
  with text files and the user program ELFs; programs now load from disk
- syscalls: stat, fstat, lseek, pipe, sleep_ms, wait, getcwd, chdir, mkdir,
  rmdir, unlink, getdents64, spawn(path, argv, stdio, trace)
- real blocking: blocked threads, wake_at, WaitQueue; console read, pipes and
  wait block instead of spinning
- 64 KiB thread stacks with a canary, finished stacks freed, 4 MiB heap,
  allocator takes its lock with interrupts off
- demo + child user programs; tests for block, partitions, FAT, spawn/wait
- README: disks section, syscall table, spawn vs fork/exec, shell design
MCargo.lock+16 -0
MCargo.toml+13 -0
MREADME.md+329 -109
Mboot.png+0 -0
Mbuild.rs+122 -22
Adisk/notes/todo.txt+2 -0
Adisk/readme.txt+2 -0
Mscripts/run-headless.sh+1 -0
Mscripts/screenshot.sh+1 -0
Msrc/allocator.rs+1 -1
Msrc/allocator/bump.rs+14 -1
Msrc/allocator/fixed_size_block.rs+14 -1
Asrc/block/mod.rs+127 -0
Asrc/block/partition.rs+158 -0
Asrc/block/virtio_blk.rs+281 -0
Msrc/errno.rs+11 -0
Asrc/fs/fat.rs+360 -0
Msrc/fs/file.rs+99 -8
Msrc/fs/mod.rs+144 -31
Asrc/fs/pipe.rs+102 -0
Msrc/fs/ramfs.rs+23 -3
Msrc/lib.rs+22 -8
Msrc/main.rs+31 -12
Msrc/memory.rs+22 -0
Asrc/pci.rs+121 -0
Msrc/process.rs+174 -52
Msrc/syscall.rs+226 -21
Msrc/thread.rs+179 -24
Msrc/tty.rs+26 -17
Atests/block.rs+133 -0
Atests/fat.rs+131 -0
Mtests/user_mode.rs+115 -23
Mtests/vfs.rs+81 -12
Auser/child.rs+41 -0
Auser/demo.rs+152 -0
Muser/ulib.rs+167 -2

36 files changed, 3441 insertions, 347 deletions

@@ -63,6 +63,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"  [[package]]+name = "fatfs"+version = "0.4.0"+source = "git+https://github.com/rafalh/rust-fatfs?rev=38b3a7950bf0e765e34286d1dfe76c519ee347e2#38b3a7950bf0e765e34286d1dfe76c519ee347e2"+dependencies = [+ "bitflags 2.13.2",+ "log",+]++[[package]] name = "futures-core" version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index"@@ -114,6 +123,12 @@ dependencies = [ ]  [[package]]+name = "log"+version = "0.4.34"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"++[[package]] name = "pc-keyboard" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index"@@ -150,6 +165,7 @@ dependencies = [  "bootloader",  "conquer-once",  "crossbeam-queue",+ "fatfs",  "futures-util",  "lazy_static",  "linked_list_allocator",
@@ -23,6 +23,11 @@ crossbeam-queue = { version = "0.3.11", default-features = false, features = ["a conquer-once = { version = "0.4", default-features = false }   # OnceCell: init a static at runtime, once futures-util = { version = "0.3.4", default-features = false, features = ["alloc"] } # Stream, AtomicWaker +# FAT12/16/32 file system library. The crates.io release (0.3.6) needs the+# abandoned `core_io` crate for no_std, so we use the git version, pinned to+# an exact commit so builds are reproducible. Only `alloc` + long file names.+fatfs = { git = "https://github.com/rafalh/rust-fatfs", rev = "38b3a7950bf0e765e34286d1dfe76c519ee347e2", default-features = false, features = ["alloc", "lfn"] }+ [dependencies.lazy_static] version = "1.0" features = ["spin_no_std"]@@ -64,11 +69,19 @@ harness = false # Extra QEMU arguments used by `cargo run` (via `bootimage runner`). [package.metadata.bootimage] run-args = [+    # Second disk: the FAT32 image build.rs makes (target/disk.img), as a+    # virtio-blk PCI device. snapshot=on: writes go to a temporary overlay,+    # so the image is the same at every boot (and tests can't corrupt it).+    "-drive", "file=target/disk.img,format=raw,if=virtio,snapshot=on",     "-device", "isa-debug-exit,iobase=0xf4,iosize=0x04",  # lets the kernel exit QEMU     "-serial", "stdio",                                    # serial port -> your terminal ] # Used by `cargo test`: headless, results on serial, exit code 33 = pass. test-args = [+    # Second disk: the FAT32 image build.rs makes (target/disk.img), as a+    # virtio-blk PCI device. snapshot=on: writes go to a temporary overlay,+    # so the image is the same at every boot (and tests can't corrupt it).+    "-drive", "file=target/disk.img,format=raw,if=virtio,snapshot=on",     "-device", "isa-debug-exit,iobase=0xf4,iosize=0x04",     "-serial", "stdio",     "-display", "none",
@@ -12,14 +12,18 @@ keyboard is one of them) and **preemptive kernel threads** switched by the timer interrupt (threads A/B/C count on the top line while you type). It runs **user programs in ring 3** (small Rust ELF binaries) as processes with their own address spaces, which talk to the kernel via `syscall`-(`read`, `write`, `open`, `close`, `getpid`, `sched_yield`, `exit`) and read-files from an in-memory filesystem behind a Linux-style **VFS**.+(about 20 calls, numbered like Linux: `read`, `write`, `open`, `stat`, `lseek`,+`getdents64`, `chdir`, `pipe`, plus our own `spawn` and `wait`...). Files live+behind a Linux-style **VFS**: an in-memory ramfs at `/` and `/tmp`, and a+**FAT32** partition on a second **virtio-blk** disk (found by **PCI**+enumeration, **GPT/MBR** partitions parsed) mounted at `/mnt/disk0`. The user+programs themselves are loaded from that disk. It has a `cargo test` suite that runs inside QEMU.  Most code follows the blog_os sources (MIT / Apache-2.0) closely, with extra comments; additions are the serial mirroring, uptime clock, demo features, scripts, and everything past chapter 12: preemptive threads, user mode,-syscalls, ELF loading and the VFS.+syscalls, ELF loading, the VFS, PCI, virtio-blk, partitions and FAT32.  ![boot screenshot](boot.png) @@ -33,6 +37,7 @@ syscalls, ELF loading and the VFS. | `llvm-tools-preview` component | – | `llvm-objcopy` etc., used by `bootimage` | | `bootimage` (cargo subcommand) | 0.10.5 | links kernel + bootloader into a bootable disk image | | QEMU (`qemu-system-x86`, `qemu-system-gui`) | 10.0.13 | the emulator we boot in (`-gui` adds the GTK/SDL window) |+| `dosfstools`, `mtools`, `fdisk` | Debian packages | `build.rs` makes the FAT32 test disk with `sfdisk` + `mkfs.vfat` + `mmd`/`mcopy` |  `rust-toolchain.toml` pins the project to nightly with the two components, so `cargo` inside this directory picks them up automatically. It also installs@@ -41,7 +46,8 @@ the built-in `x86_64-unknown-none` target, used to compile the user programs. Crates: `bootloader` 0.9.35 (BIOS bootloader used by the 2nd edition), `volatile` 0.2, `spin` 0.5, `x86_64` 0.15, `uart_16550` 0.3, `lazy_static` 1.5, `pic8259` 0.11 (interrupt controller), `pc-keyboard` 0.7 (scancode decoding),-`linked_list_allocator` 0.9 (fallback heap allocator).+`linked_list_allocator` 0.9 (fallback heap allocator), `fatfs` (git, `no_std` ++`alloc`; FAT12/16/32 reading and writing).  Fresh setup on another machine: @@ -49,7 +55,7 @@ Fresh setup on another machine: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh   # rustup rustup toolchain install nightly -c rust-src -c llvm-tools-preview -t x86_64-unknown-none cargo install bootimage-sudo apt install qemu-system-x86 qemu-system-gui+sudo apt install qemu-system-x86 qemu-system-gui dosfstools mtools fdisk ```  ## Build and run@@ -93,75 +99,90 @@ Cargo features (in `Cargo.toml`): | `stack-overflow` | after boot, recurse forever to test the double-fault handler (with `headless`: exit success when caught) | | `page-fault` | after reading CR3, write to unmapped address `0xdeadbeaf` (with `headless`: exit success when caught) | -Expected serial output of `./scripts/run-headless.sh` (stack frame dump trimmed):+Expected serial output of `./scripts/run-headless.sh` (stack frame dump and+some timer dots / thread lines trimmed):  ``` Hello from Tyler's Rust kernel! EXCEPTION: BREAKPOINT-InterruptStackFrame { instruction_pointer: ..., ... }-Survived the breakpoint, still running.-[paging] CR3 (level 4 table) at phys 0x1000-... (paging + memory lines as before)-[paging] mapped page 0xdeadbeaf000 -> VGA frame, wrote "New!" through it-.[heap] 256 KiB heap at 0x444444440000 (fixed-size-block allocator)-[heap] Box::new(41) = 41 at 0x444444440000-[heap] Vec of 500 numbers at 0x444444441000, sum = 124750-[heap] Rc count after clone = 2-[heap] Rc count after drop  = 1+... (breakpoint, paging and memory lines as before)+[heap] 4096 KiB heap at 0x444444440000 (fixed-size-block allocator)+... (heap demo lines) [thread] spawned threads A, B, C (status on the top line)-.[thread A] count 1-.[thread B] count 1-..[vfs] mounted ramfs at /+[pci] 00:00.0 8086:1237 host bridge+[pci] 00:01.0 8086:7000 ISA bridge+[pci] 00:01.1 8086:7010 IDE controller+[pci] 00:01.3 8086:7113 bridge (other)+[pci] 00:02.0 1234:1111 display controller+[pci] 00:03.0 8086:100e network controller+[pci] 00:04.0 1af4:1001 SCSI storage (virtio-blk)+[pci] 7 devices on the PCI bus+[disk] vda: virtio-blk, 131072 sectors (64 MiB)+[disk] vda: GPT partition table, 1 partition(s)+[disk] vda1: start 2048 sectors 126976 type Microsoft basic data (FAT/exFAT/NTFS) name "fat32"+[disk] vda1: FAT32 volume "TYLERDISK" mounted at /mnt/disk0+[vfs] mounted ramfs at / [vfs] mounted ramfs at /tmp+[vfs] mounted fat32 at /mnt/disk0 [process] started pid 1 (hello), entry 0x8000400180-[process] started pid 2 (evil), entry 0x80004000f0-[process] started pid 3 (spin), entry 0x80004000e0-[process] started pid 4 (echo), entry 0x8000400180-Type a line + Enter: the echo program (ring 3) reads it-[async] async number: 42-[async] example task step 1 (after yield)-[async] example task step 2 (after yield)-[async] example task step 3 (after yield)-....Hello from user mode! I'm pid 1.+Hello from user mode! I'm pid 1. [hello] /hello.txt says: Hello from the ramfs! This is /hello.txt. [hello] wrote and read back /tmp/note.txt [hello] all syscall checks passed, exiting with 0 [process] pid 1 (hello) exited with code 0+[process] started pid 2 (evil), entry 0x80004000f0+[process] started pid 3 (spin), entry 0x80004000e0 [evil] pid 2: reading kernel memory at 0x200000... [kernel] user page fault: address Ok(VirtAddr(0x200000)), error PageFaultErrorCode(PROTECTION_VIOLATION | USER_MODE) [kernel] page fault in user mode at rip 0x800040017e [process] pid 2 (evil) killed: page fault [spin] pid 3: spinning without syscalls...-.[echo] pid 4: type a line and press Enter-..[thread A] count 2-.[thread B] count 2-.....[thread A] count 3-. 18 ticks-.....[thread A] count 4-.[thread B] count 3-.[thread C] busy loop round 1 (count = 2000000)-.[spin] round 1 done (count = 30000000)-...[thread A] count 5-....... 36 ticks-[thread B] count 4-..[spin] round 2 done (count = 60000000)-..........[thread B] count 5-.[thread C] busy loop round 2 (count = 4000000)-.[spin] round 3 done (count = 90000000)+[process] started pid 4 (demo), entry 0x8000400320+[process] started pid 5 (echo), entry 0x8000400180+Type a line + Enter: the echo program (ring 3) reads it+[async] async number: 42+...+[demo] pid 4 started from /mnt/disk0/bin/demo+[demo] readme.txt from FAT32: Hello from the FAT32 disk! This file lives on a virtio-blk disk, in a GPT+[demo] /mnt/disk0/bin: hello evil spin echo demo child+[echo] pid 5: type a line and press Enter+[spin] round 1 done (count = 30000000)+[demo] mkdir/unlink/rmdir work on ramfs and FAT32+[process] started pid 6 (child), entry 0x8000400240+[process] pid 6 (child) exited with code 0+[demo] read from pipe: "hello through a pipe from pid 6"+[process] started pid 7 (child), entry 0x8000400240+[child] pid 7 got 3 args: "child" "with" "three args"+[process] pid 7 (child) exited with code 0+[process] started pid 8 (child), entry 0x8000400240+[process] pid 8 (child) exited with code 42+[spin] round 3 done (count = 90000000) [process] pid 3 (spin) exited with code 0-.... 54 ticks-.................. 72 ticks-..[thread C] busy loop round 3 (count = 6000000)-...............[thread C] busy loop round 4 (count = 8000000)-. 90 ticks+[demo] spawned child 42 (pid 8), wait -> exit code 42+[process] started pid 9 (evil), entry 0x80004000f0+[evil] pid 9: reading kernel memory at 0x200000...+...+[process] pid 9 (evil) killed: page fault+[demo] all stage 6 checks passed, exiting with 0+[process] pid 4 (demo) exited with code 0+[thread C] done, exiting+.................. 90 ticks [ok] timer works, exiting+[run-headless] kernel exited OK ```  Each `.` is a timer tick. Threads A and B only print their first 5 counts to serial (so the keyboard echo in your terminal stays readable); after that they keep counting on the VGA top line. (Exact interleaving and dot counts vary a little from run to run -- that's preemption at work.) Spin's rounds interleave-with threads A/B/C: that's preemption of ring-3 code by the timer.+with threads A/B/C and the demo: that's preemption of ring-3 code by the timer.+The `[pci]` lines only go to serial (the VGA screen gets the count).++The kernel boots from the bootimage on the first (IDE) disk; the scripts and+`Cargo.toml` attach a **second** disk, `target/disk.img`, as+`-drive file=target/disk.img,format=raw,if=virtio,snapshot=on`. `snapshot=on`+makes QEMU throw away writes when it exits, so every boot and every test sees+the same fresh image even though the kernel writes to it.  ## Tests @@ -192,8 +213,10 @@ heap_allocation::simple_allocation...               [ok] | `tests/should_panic.rs` | a failing `assert_eq!` reaches the panic handler (`harness = false`) | | `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/user_mode.rs` | `hello` exits 0 (all its syscall checks pass) and its `/tmp/note.txt` is visible to the kernel; `evil` is `Killed("page fault")`; `spin` finishes while the test thread keeps running (preemption of ring 3); the ELF loader rejects garbage/truncated files; user pointers to kernel/unmapped/overflowing ranges give `EFAULT` |-| `tests/vfs.rs` | read `/hello.txt` to EOF, path normalisation, `ENOENT`/`ENOTDIR`/`EINVAL`/`EISDIR`/`EEXIST`, `/tmp` is a separate ramfs, a third mount wins by longest prefix, sparse writes zero-fill, directory listing |+| `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`); 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` | read `/hello.txt` to EOF, path normalisation (`.`, `..`, relative to a cwd), `ENOENT`/`ENOTDIR`/`EINVAL`/`EISDIR`/`EEXIST`, `/tmp` is a separate ramfs, longest-prefix mounts, 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; 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 | | `tests/threads.rs` | two busy-looping threads that never yield both make progress while the main thread busy-waits too (only possible with preemption); a thread can exit and the others keep running |  How it works: `#![feature(custom_test_frameworks)]` collects every `#[test_case]`@@ -230,18 +253,27 @@ src/task/executor.rs      real executor: task queue + wakers, sleeps with hlt wh src/task/keyboard.rs      lock-free scancode queue, ScancodeStream, async keyboard echo task src/thread.rs             preemptive threads: spawn, round-robin schedule(), asm context switch,                           per-thread kernel stack (TSS.rsp0) + CR3 switching-src/syscall.rs            syscall/sysret MSR setup, asm entry stub, dispatch, read/write/open/close/...-src/process.rs            Process (pid, fd table, exit status), spawn from ELF, iretq into ring 3, kill+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/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/errno.rs              Linux errno numbers src/tty.rs                keyboard line buffer + Console (the File behind fds 0/1/2)-src/fs/mod.rs             VFS: Inode + FileSystem traits, mount table, path lookup, init (boot files)-src/fs/file.rs            File trait (what an fd points to), InodeFile (inode + offset)+src/fs/mod.rs             VFS: Inode + FileSystem traits, mount table, path normalize/lookup/create/remove,+                          init (boot files), mount_disks (FAT partitions -> /mnt/diskN)+src/fs/file.rs            File trait (what an fd points to), InodeFile (inode + offset), Stat, getdents64 src/fs/ramfs.rs           in-memory filesystem-build.rs                  compiles user/*.rs to ELF files in OUT_DIR+src/fs/fat.rs             FAT32 via the fatfs crate: BlockIo adapter + FatFs/FatInode for the VFS+src/fs/pipe.rs            pipes: a byte queue with a blocking read end and a write end+build.rs                  compiles user/*.rs to ELF files, then builds target/disk.img (GPT + FAT32)+disk/                     files copied onto the FAT32 disk (readme.txt, notes/todo.txt) user/ulib.rs              tiny user-space "libc": syscall wrappers, println!, _start, panic handler user/link.ld              linker script for user programs (base 0x80_0040_0000)-user/{hello,evil,spin,echo}.rs  the user programs+user/{hello,evil,spin,echo,demo,child}.rs  the user programs (copied to /mnt/disk0/bin) src/vga_buffer.rs         VGA text-mode writer, print!/println!, print_at (status line), backspace src/serial.rs             COM1 UART writer + serial_print!/serial_println! tests/                    integration tests, one QEMU-booted kernel each@@ -297,7 +329,7 @@ physical RAM is also mapped at `physical_memory_offset` (`0x18000000000` here),  | `0x200000`... | kernel code + data (the ELF; maps to phys around `0x400000`) | | `0x0100_0000_0000`... | kernel stack (80 pages, guard page below it) | | `0x0180_0000_0000`... | all physical memory (`physical_memory_offset`) |-| `0x4444_4444_0000` | kernel heap, 256 KiB (also holds thread stacks) (we map it in `allocator::init_heap`) |+| `0x4444_4444_0000` | kernel heap, 4 MiB (also holds thread stacks) (we map it in `allocator::init_heap`) | | `0xdead_beaf_000` | demo page we map to the VGA frame (writes "New!" on screen) |  The "New!" demo writes to screen row 20 through the new mapping; by the time you look,@@ -307,7 +339,7 @@ later lines have scrolled it up, so in `boot.png` it overwrote the start of the  the BIOS memory map marks `Usable` (~121 MiB of QEMU's default 128 MiB). It never frees; fine for now. -**Heap.** `allocator::init_heap` maps the 25 pages of `HEAP_START..+HEAP_SIZE` to fresh+**Heap.** `allocator::init_heap` maps the 1024 pages (4 MiB) of `HEAP_START..+HEAP_SIZE` to fresh frames, then gives the range to the `#[global_allocator]`. From then on the `alloc` crate (`Box`, `Vec`, `Rc`, `String`, `BTreeMap`...) works. `alloc` is compiled for our target by `build-std` in `.cargo/config.toml`.@@ -346,7 +378,7 @@ cheap (just return from `poll`), and all tasks share one stack.  ### Preemptive: kernel threads (`src/thread.rs`, beyond the blog series) -Each thread has its own 16 KiB stack (a `Vec<u8>` on the heap). On every timer+Each thread has its own 64 KiB stack (a `Vec<u8>` on the heap). On every timer interrupt (~18 times a second) the handler sends the EOI to the PIC and then calls `thread::schedule()`, which picks the next thread round-robin and calls `switch_context(&mut old.rsp, new.rsp)` (a dozen lines of `global_asm!`):@@ -382,19 +414,50 @@ taken with interrupts disabled. Here: the print macros and `print_at` already hold their spinlocks with interrupts off (`without_interrupts`), so a thread can't be switched away while holding them; the scheduler lock is only taken with interrupts off and is released *before* switching (the next thread would-otherwise wait forever for a lock held by a paused thread). The heap allocator's-lock is never taken in an interrupt handler, so a thread preempted while-holding it only makes others spin until it runs again -- slow, not a deadlock.--Known simplifications (good exercises): sleeping threads (`sleep_ticks`) just-`hlt` and still get scheduled -- a real scheduler has a *blocked* state and a-sleep queue; finished threads' stacks are never freed; no guard page under-thread stacks (an overflow silently corrupts the heap); no priorities; a single-CPU, so `spin::Mutex` is enough.+otherwise wait forever for a lock held by a paused thread).++**The locking rule:** a lock that is *ever* taken with interrupts off must+*always* be taken with interrupts off. Otherwise: thread T takes the lock with+interrupts on, gets preempted, and some code running with interrupts off (so+it can never be preempted back to T) spins on that lock forever. Stage 6 hit+exactly this with the heap allocator: wait-queue code runs with interrupts off+and allocates, so `GlobalAlloc::alloc`/`dealloc` now take the heap lock inside+`without_interrupts`.++**Blocking (stage 6).** A thread can be *blocked*: the scheduler skips it until+someone wakes it. Two ways to be woken:++- `sleep_ticks(n)` sets `wake_at = now + n`; `schedule()` (run on every timer+  tick) unblocks it once that tick has passed (the `sleep_ms` syscall rounds+  up to 55 ms ticks).+- A `WaitQueue` (a list of waiting thread ids). `wait_until(|| condition)`+  turns interrupts off, checks the condition, and if it's false puts the+  thread on the queue, marks it blocked and switches away; when woken it checks+  again. `wake_all()` unblocks everyone on the queue. Checking with interrupts+  off is what prevents a *lost wakeup* (the event firing between "check" and+  "sleep"). Users: the console (`read(0)` waits for Enter; the keyboard path+  wakes it), pipes (reader waits for data or EOF) and `wait(pid)` (parent+  waits on the child's `exited` queue).++If every thread is blocked, the scheduler idles with `sti; hlt` until an+interrupt makes something runnable.++**Stack size and the canary.** Stage 6 first used 16 KiB stacks and crashed+with a ring-0 jump to address 0: the syscall -> VFS -> FAT -> ELF loader path in+an unoptimized debug build used more than that, overflowed into the heap and+smashed a neighbour. Now stacks are 64 KiB, and the lowest 8 bytes of each+hold a canary value (`0x5AFE_57AC_C0DE_CAFE`). `schedule()` checks it on every+switch and panics with "kernel stack overflow in thread" if it changed: a+cheap stand-in for a real guard page. Finished threads' stacks are freed (the+next `spawn` drops them, since a thread can't free the stack it's running on).++Known simplifications (good exercises): no guard page under thread stacks (the+canary only catches overflows after the fact); no priorities; a single CPU,+so `spin::Mutex` is enough.  ## User mode, syscalls and processes -Four tiny Rust programs in `user/` run in **ring 3** as separate processes, each+Six tiny Rust programs in `user/` run in **ring 3** as separate processes, each with its own address space and file-descriptor table:  | Program | What it does |@@ -402,7 +465,12 @@ with its own address space and file-descriptor table: | `hello` | prints "Hello from user mode!", `getpid`, reads `/hello.txt` from the ramfs, creates and reads back `/tmp/note.txt`, checks error cases (`ENOENT`, `EBADF`, `EFAULT`, `ENOSYS`), exits with the number of failed checks (0) | | `evil` | reads kernel memory at `0x200000` -> page fault in ring 3 -> the kernel kills it and keeps going | | `spin` | busy-loops ~2 s with no syscalls; it still gets preempted (threads A/B/C keep printing) |-| `echo` | `read(0)` a line from the keyboard, prints `[echo] you typed: ...`; type `exit` to end it |+| `echo` | `read(0)` a line from the keyboard (the thread is blocked until Enter), prints `[echo] you typed: ...`; type `exit` to end it |+| `demo` | stage 6 checks: cwd, reads `/mnt/disk0/readme.txt`, `stat`/`fstat`/`lseek`, lists `bin/` with `getdents64`, `chdir` errors, `mkdir`/`unlink`/`rmdir` on ramfs and FAT, a pipe from a child, arguments, `wait` exit codes (42, 139 for killed `evil`), `ECHILD`/`ENOENT`/`ENOEXEC`, `sleep_ms`; exits with the number of failed checks |+| `child` | helper for `demo` and tests: `child pipe` writes a line to stdout, `child cat` copies stdin to stdout, `child 42` exits with 42, otherwise prints its arguments |++The kernel starts them all from `/mnt/disk0/bin/` -- they're read from the FAT32+partition on the virtio disk, not compiled into the kernel any more.  ### Rings in one paragraph @@ -460,15 +528,54 @@ One call, step by step: Single CPU, so the "current kernel stack" and "saved user RSP" are plain globals; SMP kernels use per-CPU data reached with `swapgs`. +Numbers are Linux's wherever Linux has the same call, so man pages and+`strace` habits mostly apply. Args 4 and 5 go in `r10` and `r8` like Linux.+ | nr | name | args | notes | |---|---|---|---|-| 0 | `read` | fd, buf, len | fd 0 = keyboard (blocks until Enter), others = files |-| 1 | `write` | fd, buf, len | fd 1/2 = console (VGA + serial) |-| 2 | `open` | path, path_len, flags | **differs from Linux**: path is ptr+len, not a C string; `O_CREAT` = 0o100 |+| 0 | `read` | fd, buf, len | console: blocks until Enter; pipe: blocks until data or EOF |+| 1 | `write` | fd, buf, len | console = VGA + serial; pipe: `EPIPE` once the reader is gone |+| 2 | `open` | path, path_len, flags | `O_CREAT` 0o100, `O_TRUNC` 0o1000; directories can be opened (for `getdents64`) | | 3 | `close` | fd | |+| 4 | `stat` | path, path_len, *Stat | |+| 5 | `fstat` | fd, *Stat | |+| 8 | `lseek` | fd, offset, whence | `SEEK_SET/CUR/END` = 0/1/2; `ESPIPE` on console/pipes |+| 22 | `pipe` | *[u32; 2] | [read end, write end] | | 24 | `sched_yield` | | |+| 35 | `sleep_ms` | ms | **ours**: Linux nr 35 is `nanosleep(timespec)` | | 39 | `getpid` | | | | 60 | `exit` | code | |+| 61 | `wait` | pid | **simpler than Linux `wait4`**: blocks, returns the exit code (139 = killed, like a shell shows SIGSEGV), reaps the child; `ECHILD` if not your child |+| 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 |+| 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 only (`EISDIR`) |+| 217 | `getdents64` | fd, buf, len | real Linux `struct linux_dirent64` records (ino, off, reclen, type, name\0) |+| 500 | `spawn` | path, path_len, argv, argv_len, *SpawnOptions | **ours**: start a program, returns its pid |++Deviations from Linux worth knowing:++- **Paths are pointer + length**, not NUL-terminated C strings (easier to+  validate, and Rust `&str`s aren't NUL-terminated). Relative paths start at+  the process's cwd; `.` and `..` are resolved in the kernel (`fs::normalize`).+- **`Stat` is tiny**: `#[repr(C)] struct Stat { kind: u64, size: u64 }`, kind+  1 = file, 2 = directory, 3 = console, 4 = pipe. No inode numbers, owners,+  modes 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] name(a1, a2, a3) = result` -- that's what the shell's+  `run --trace` will use. 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)`.  Errors are negative errno values like Linux (`-2` ENOENT, `-9` EBADF, `-14` EFAULT, `-38` ENOSYS...). Unknown numbers return `-ENOSYS`.@@ -521,8 +628,10 @@ and fails with "relocation overflow"), `-T user/link.ld` (one PT_LOAD segment per permission, page-aligned). The large code model names its sections `.ltext`/`.lrodata`/`.ldata`/`.lbss`, so the linker script lists those too -- forgetting that put the code into a non-executable segment and-every program died at its first instruction. The ELFs (~15-20 KB) are embedded in the-kernel with `include_bytes!` (`rust_kernel::user_programs`).+every program died at its first instruction. The ELFs (~15-20 KB) used to be+embedded in the kernel with `include_bytes!`; since stage 6 `build.rs` copies+them onto the FAT32 disk image instead and `process::spawn_path` reads them+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!`, `_start` (calls `main`,@@ -539,19 +648,22 @@ physical-memory map and leaves the rest (`.bss`) zero. Designed like Linux's so real filesystems can plug in later:  - `trait Inode` (`fs/mod.rs`): a file or directory: `kind`, `size`,-  `read_at(offset, buf)`, `write_at`, `lookup(name)`, `create(name, kind)`, `list`.+  `read_at(offset, buf)`, `write_at`, `truncate`, `lookup(name)`,+  `create(name, kind)`, `list` (names + kinds), `remove(name)`.   Methods have default impls returning an error, so a read-only fs implements   only what it supports. - `trait FileSystem`: `name()` and `root() -> Arc<dyn Inode>`. A disk filesystem   will hold its block device and parsed superblock. - **Mount table**: `mount("/tmp", fs)`; `lookup(path)` picks the longest mount   point that prefixes the path, then walks the rest with `Inode::lookup`.-  Boot mounts a ramfs on `/` (`/hello.txt`, `/etc/motd`, `/tmp`) and a-  second, separate ramfs on `/tmp`.+  Boot mounts a ramfs on `/` (`/hello.txt`, `/etc/motd`, `/tmp`, `/mnt`), a+  second, separate ramfs on `/tmp`, and each FAT partition at `/mnt/diskN`.+  `fs::normalize(cwd, path)` turns any path into a clean absolute one first. - `trait File` (`fs/file.rs`): what a file descriptor points to: `read(buf)`,-  `write(buf)`. `InodeFile` = an inode + its own offset (two opens of the same+  `write(buf)`, `seek`, `stat`, `getdents`. `InodeFile` = an inode + its own offset (two opens of the same   file have separate offsets, like Linux's `struct file`). `tty::Console` is-  another `File`: writes go to VGA+serial, reads come from the keyboard line buffer.+  another `File`: writes go to VGA+serial, reads come from the keyboard line+  buffer. `fs/pipe.rs` gives two more: a pipe's read and write ends. - **fd table**: `Vec<Option<Arc<dyn File>>>` per process (max 16), fds 0-2 =   Console. `open` = `fs::lookup` (or `fs::create` with `O_CREAT`) + `InodeFile::open`   + first free slot.@@ -561,16 +673,125 @@ Keyboard input for `read(0)` goes through a minimal tty line discipline current line, Enter makes the line (with `\n`) available to readers.  Known simplifications (exercises): a process's page tables and frames are-never freed on exit; `read(0)` waits with `hlt` instead of blocking the-thread; no `fork`/`exec`/`wait` syscalls (the kernel spawns programs and-`process::wait` is kernel-only); no `lseek`, `stat`, `readdir` yet; user-pointer checks walk the page table but don't guard against another thread-unmapping the page mid-syscall (there's no `munmap` yet, so that's fine for now).+never freed on exit; orphans aren't reparented; no signals; user pointer+checks walk the page table but don't guard against another thread unmapping+the page mid-syscall (there's no `munmap` yet, so that's fine for now).++## Disks: PCI, virtio-blk, partitions, FAT32 (stage 6)++```+/mnt/disk0/readme.txt+   VFS (fs/mod.rs)          longest mount prefix -> FatFs, rest "readme.txt"+   FatInode (fs/fat.rs)     fatfs crate: clusters, FAT chains, directory entries+   BlockIo adapter          byte offsets -> whole 512-byte sectors+   Partition vda1           sector + 2048, bounds-checked+   VirtioBlk vda            virtqueue request to QEMU+   PCI                      found the device and its I/O port at boot+```++**PCI** (`src/pci.rs`). Every PCI device has a 256-byte configuration space+(vendor/device id, class code, BARs = where its registers live). On a PC you+read it by writing `bus/device/function/offset` to port `0xCF8` and reading+port `0xCFC`. `pci::scan()` tries every bus/device/function, keeps the ones+whose vendor id isn't `0xFFFF`, and prints them with a class name. QEMU's+default machine has 7: host bridge, ISA/IDE/ACPI parts of the PIIX chipset,+VGA, the e1000 network card, and our disk, `1af4:1001`.++**Why virtio-blk and not ATA PIO?** ATA PIO is simpler (a few port writes,+then 256 `in` instructions per sector) but it's a 1980s interface that only+emulators and old hardware have. virtio is what QEMU/KVM and cloud VMs+actually use, and it teaches the pattern every modern device uses: the driver+and device share **queues of descriptors in RAM**. The legacy (virtio 0.9.5)+interface is used, through I/O port BAR0, because it needs no MMIO mapping or+PCI capability parsing; QEMU's device is "transitional" and speaks both.++**virtio-blk in one paragraph** (`src/block/virtio_blk.rs`, ~280 lines).+Reset the device, set ACKNOWLEDGE + DRIVER, accept no optional features,+read the queue size, allocate the virtqueue (descriptor table + available+ring + used ring, physically contiguous: `memory::alloc_contiguous_frames`)+and give its page number to the device, set DRIVER_OK. A request is a chain+of three descriptors: a 16-byte header (`IN` = read or `OUT` = write, sector+number), the data buffer, and a 1-byte status the device fills in. Put the+chain's head in the available ring, write the queue number to the notify+port, then **poll** the used ring until the device hands it back. Data goes+through a one-page **bounce buffer** whose physical address we know (heap+buffers are virtually contiguous but not physically). Interrupts are switched+off for the device (`VRING_AVAIL_F_NO_INTERRUPT`); polling is fine because QEMU+completes requests almost instantly. Exercises: IRQ-driven completion (block+the thread on a wait queue), several requests in flight, virtio 1.0 "modern"+MMIO.++**`BlockDevice`** (`src/block/mod.rs`): `sector_count()`,+`read_sectors(lba, buf)`, `write_sectors(lba, buf)`, buffers a whole number of+512-byte sectors. Devices get Linux-style names in a registry+(`block::devices()`, `block::get("vda1")`). `RamDisk` is a `Vec<u8>` disk used+by tests.++**Partitions** (`src/block/partition.rs`). Sector 0 is the MBR: four 16-byte+entries at offset 446 and the `55 AA` signature. A single entry of type `0xEE`+is a "protective MBR" meaning *look for a GPT*: sector 1 has the `EFI PART`+header, which says where the partition entry array is (128 bytes each: type+GUID, unique GUID, first/last LBA, UTF-16 name). Each partition becomes a+`Partition` -- itself a `BlockDevice` that adds its start sector and refuses+to go past its end -- registered as `vda1`, `vda2`... CRCs aren't checked yet.++**FAT32 via the `fatfs` crate** (`src/fs/fat.rs`). `fatfs` does the+filesystem logic; it wants something implementing its `Read + Write + Seek`+traits on *bytes*. `BlockIo` is that adapter: it keeps a byte position, sends+whole aligned sectors straight to the device, and handles partial sectors+through a one-sector buffer (read the sector, change the bytes, write it back).+Writes go straight through, so `flush` has nothing to do. (`fatfs` 0.3 from+crates.io depends on the abandoned `core_io` crate, which doesn't build on+current nightlies, so `Cargo.toml` pins a git revision of the 0.4 branch with+its own `no_std` IO traits.) For the VFS, `FatFs` holds the `fatfs::FileSystem`+in an `Arc<Mutex<..>>`, and a `FatInode` is just *(that filesystem, a path, a+kind)*: each call reopens its path from the root. Slower than caching+directory entries, but simple and never stale. Lookups are case-insensitive,+like FAT itself. No timestamps (`NullTimeProvider`) and long file names on.++**The test disk** (`build.rs`). After compiling the user programs,+`build.rs` makes `target/disk.img` (64 MiB): `sfdisk` writes a GPT with one+"Microsoft basic data" partition at sector 2048, `mkfs.vfat -F 32 -s 1 -n+TYLERDISK` formats a separate partition image (1 sector per cluster so 62 MiB+is enough clusters for FAT32), `mmd`/`mcopy` (mtools, which edit FAT images+without mounting, so no root) copy in `disk/` and `bin/<programs>`, and the+partition image is spliced into the disk at sector 2048. If a tool is missing+the build stops with `sudo apt install dosfstools mtools fdisk`+(`mkfs.vfat` lives in `/usr/sbin`, which isn't in a normal user's `PATH` on+Debian, so `build.rs` looks there too). Look inside it from Linux:++```sh+/usr/sbin/sfdisk -l target/disk.img             # not in a normal user's PATH on Debian+export MTOOLS_SKIP_CHECK=1                     # mtools: don't complain about the small-cluster FAT32+mdir -i target/disk.img@@1M ::/bin          # @@1M = partition offset (2048 * 512)+mtype -i target/disk.img@@1M ::/readme.txt+```++## Next stage: the shell (Tyler's design)++A **hybrid** shell: Unix navigation with a DOS flavour on top. All the+syscalls it needs exist now (`read(0)` blocks, `spawn` + `wait`, `pipe`,+`getdents64`, `stat`, `chdir`/`getcwd`, `mkdir`/`rmdir`/`unlink`, `lseek`,+`SPAWN_TRACE`).++- **Unix navigation**: `cd`, `ls`, `pwd`, `cat`, `mkdir`, `rm`, `echo`.+- **DOS-style program running**: `run prog args` (foreground: spawn + wait,+  show the exit code) and `start prog args` (background: spawn, don't wait),+  plus aliases `dir` (= ls), `type` (= cat), `cls` (clear screen), `ver`.+- **Pipes and redirection**: `a | b` (pipe + two spawns with stdout/stdin set)+  and `> file` (open with `O_CREAT|O_TRUNC`, pass it as the child's stdout).+- `help`, `exit`.+- **`run --trace prog`**: spawn with `SPAWN_TRACE` to print every syscall the+  program makes (a built-in mini `strace`).+- **Drive letters as aliases over the single tree**: `C:` = `/`, and each+  mount gets the next letter automatically (`D:` = `/mnt/disk0`, ...). It's+  only shell syntax: the kernel keeps one Unix-style tree.  ## Roadmap  Done: all 12 chapters of the 2nd edition, plus preemptive kernel threads,-user mode + syscalls, ELF loading and a VFS with a ramfs.+user mode + syscalls, ELF loading, a VFS with a ramfs, PCI, virtio-blk,+MBR/GPT partitions, FAT32, blocking threads, pipes, spawn/wait.  - [x] Freestanding binary — [phil-opp](https://os.phil-opp.com/freestanding-rust-binary/) - [x] Minimal kernel booting in QEMU — [phil-opp](https://os.phil-opp.com/minimal-rust-kernel/), [OSDev: Bare Bones](https://wiki.osdev.org/Bare_Bones)@@ -592,6 +813,11 @@ user mode + syscalls, ELF loading and a VFS with a ramfs. - [x] User mode (ring 3) & syscalls — GDT user segments, TSS.rsp0 per thread, `syscall`/`sysret`, per-process page tables, user faults kill the process. [OSDev: Getting to Ring 3](https://wiki.osdev.org/Getting_to_Ring_3), [OSDev: System Calls](https://wiki.osdev.org/System_Calls), [OSDev: SYSENTER/SYSCALL](https://wiki.osdev.org/SYSENTER) - [x] ELF loading — hand-written PT_LOAD loader, user programs built by `build.rs`. [OSDev: ELF](https://wiki.osdev.org/ELF), [OSDev: ELF Tutorial](https://wiki.osdev.org/ELF_Tutorial) - [x] VFS skeleton + ramfs — Inode/FileSystem/File traits, mount table, fd table, `open/read/write/close`. [OSDev: VFS](https://wiki.osdev.org/VFS), [Linux VFS overview](https://www.kernel.org/doc/html/latest/filesystems/vfs.html)+- [x] PCI enumeration + virtio-blk driver + `BlockDevice` trait — [OSDev: PCI](https://wiki.osdev.org/PCI), [OSDev: Virtio](https://wiki.osdev.org/Virtio), [virtio spec (legacy interface)](https://docs.oasis-open.org/virtio/virtio/v1.2/virtio-v1.2.html)+- [x] MBR + GPT partitions — [OSDev: MBR](https://wiki.osdev.org/MBR_(x86)), [OSDev: GPT](https://wiki.osdev.org/GPT)+- [x] FAT32 through `fatfs`, mounted at `/mnt/disk0`; programs loaded from disk — [OSDev: FAT](https://wiki.osdev.org/FAT)+- [x] Blocking threads + wait queues, pipes, `spawn`/`wait`, cwd, `stat`/`lseek`/`getdents64` — [OSDev: Blocking Process](https://wiki.osdev.org/Blocking_Process)+- [ ] Shell (next stage, design above) — [OSDev: Shell](https://wiki.osdev.org/Shell)  ### Filesystem roadmap (Tyler's plan) @@ -599,19 +825,12 @@ Everything below plugs into the VFS above: a filesystem is a `FileSystem` whose `root()` returns `Inode`s; mounting it is one `fs::mount` call.  1. **ramfs / tmpfs** — done (`src/fs/ramfs.rs`).-2. **Block device driver** — a `BlockDevice` trait (`read_blocks(lba, buf)`,-   `write_blocks`, `block_size`) with a first driver: **virtio-blk** (modern,-   fast, what QEMU/KVM use; needs PCI enumeration + virtqueues) or **ATA PIO**-   (dead simple port I/O, good first step). Then a small block cache.-   [OSDev: ATA PIO Mode](https://wiki.osdev.org/ATA_PIO_Mode),-   [OSDev: Virtio](https://wiki.osdev.org/Virtio), [OSDev: PCI](https://wiki.osdev.org/PCI),-   [virtio spec](https://docs.oasis-open.org/virtio/virtio/v1.2/virtio-v1.2.html)-3. **Partition tables** — MBR and GPT parsing so a disk can hold several filesystems.-   [OSDev: GPT](https://wiki.osdev.org/GPT)-4. **FAT32** — the easiest real on-disk format, using the `fatfs` crate (`no_std`-   with `alloc`) behind an adapter that implements `FileSystem`/`Inode`. Test-   images are easy to make with `mkfs.vfat` + `mtools`.-   [OSDev: FAT](https://wiki.osdev.org/FAT), [fatfs crate](https://crates.io/crates/fatfs)+2. **Block device driver** — done: `BlockDevice` trait + legacy virtio-blk+   (stage 6). Still to do: a block cache, IRQ-driven completion, maybe an+   ATA PIO driver for comparison ([OSDev: ATA PIO Mode](https://wiki.osdev.org/ATA_PIO_Mode)).+3. **Partition tables** — done: MBR + GPT (no CRC checks yet).+4. **FAT32** — done via the `fatfs` crate (`src/fs/fat.rs`). A good exercise+   later: a hand-written read-only FAT32 to see what the crate does. 5. **ext2** — written by hand: superblock, block groups, inode table, direct/indirect    blocks, directory entries. The classic "learn how Unix filesystems work" step,    and the base for understanding ext4 later. [OSDev: Ext2](https://wiki.osdev.org/Ext2),@@ -633,15 +852,10 @@ whose `root()` returns `Inode`s; mounting it is one `fs::mount` call.  Other next stages: -- **Shell** — a user-mode shell using `read(0)`/`write`; needs `fork`/`exec`-  (or a simpler `spawn(path)`) + `wait` syscalls and `readdir`/`stat` for `ls`/`cat`.-  [OSDev: Shell](https://wiki.osdev.org/Shell)-- **Process cleanup** — free a process's page tables, frames and kernel stack on exit.-- **Real blocking** — a blocked thread state + wait queues so `read(0)` and-  `sleep` don't spin with `hlt`.+- **Shell** — next stage; design in "Next stage: the shell" above.+- **Process cleanup** — free a process's page tables and frames on exit. -Small exercises along the way: a *blocked* state + sleep queue for threads,-free finished threads' stacks, guard pages under thread stacks, a timer-based+Small exercises along the way: guard pages under thread stacks, a timer-based async `sleep` future, speed up the PIT ([OSDev: PIT](https://wiki.osdev.org/Programmable_Interval_Timer)), add handlers for divide error / general protection fault, make `BootInfoFrameAllocator` O(1), add a@@ -670,3 +884,9 @@ and for a UEFI/framebuffer setup later, the newer [`bootloader` 0.11](https://gi   and boots the wrong build. `scripts/runner.sh` and the scripts `touch` the ELF   first to avoid this. If you call `cargo bootimage --features ...` by hand after   switching features, `touch target/x86_64-rust_kernel/debug/rust_kernel` first.+- `build.rs` reruns when `user/`, `disk/` or `build.rs` change and rebuilds+  `target/disk.img`. QEMU opens it with `snapshot=on`, so the kernel's writes+  (the tests create and delete files) never reach the file on disk.+- Debug builds use a lot of stack: deep call chains (syscall -> VFS -> fatfs+  -> ELF loader) needed more than 16 KiB. If you see a weird ring-0 fault at+  address 0 or "kernel stack overflow in thread", suspect the stack first.

Binary file changed.

@@ -1,33 +1,133 @@-//! Build script: compile the user programs in user/ into ELF executables in-//! OUT_DIR. The kernel embeds them with `include_bytes!` (src/lib.rs).+//! Build script, two jobs: //!-//! They're compiled with plain `rustc` for the built-in `x86_64-unknown-none`-//! target (prebuilt `core`, so no build-std needed), as static executables-//! (`relocation-model=static`) linked at a high user address -- which needs-//! the `large` code model (the default ones assume code in the low/high 2 GiB).+//! 1. Compile the user programs in user/ into ELF executables in OUT_DIR.+//!    They're compiled with plain `rustc` for the built-in+//!    `x86_64-unknown-none` target (prebuilt `core`, so no build-std+//!    needed), as static executables (`relocation-model=static`) linked at a+//!    high user address -- which needs the `large` code model (the default+//!    ones assume code in the low/high 2 GiB).+//!+//! 2. Build the second QEMU disk, `target/disk.img` (64 MiB):+//!      * a GPT partition table (made with `sfdisk`) with one partition+//!        starting at sector 2048 (1 MiB, the usual alignment),+//!      * in it a FAT32 file system (`mkfs.vfat`), filled with `mtools`+//!        (`mmd`/`mcopy` write into an image file directly -- no root, no+//!        loop-mounting needed): everything in disk/ plus the user programs+//!        in /bin.+//!    The kernel finds the disk on the PCI bus, reads the GPT, mounts the+//!    FAT32 partition at /mnt/disk0 and loads programs from /mnt/disk0/bin.+//!    QEMU opens it with `snapshot=on`, so the kernel's writes never change+//!    this file (every boot and every test starts from the same disk).+//!+//! Needs dosfstools (mkfs.vfat), mtools (mmd, mcopy) and fdisk (sfdisk):+//!     sudo apt install dosfstools mtools fdisk++use std::{env, fs, io::Write, path::{Path, PathBuf}, process::{Command, Stdio}}; -use std::{env, path::PathBuf, process::Command};+const PROGRAMS: &[&str] = &["hello", "evil", "spin", "echo", "demo", "child"]; -const PROGRAMS: &[&str] = &["hello", "evil", "spin", "echo"];+const DISK_SECTORS: u64 = 64 * 1024 * 1024 / 512;+const PART_START: u64 = 2048;+/// Leave 1 MiB at the end too (the backup GPT lives in the last 33 sectors).+const PART_SECTORS: u64 = DISK_SECTORS - 2 * PART_START;  fn main() {+    let manifest = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());     let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());-    let user_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("user");-    let rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".into());+    let user_dir = manifest.join("user");     println!("cargo:rerun-if-changed=user");+    println!("cargo:rerun-if-changed=disk");+    println!("cargo:rerun-if-changed=build.rs");++    for program in PROGRAMS {+        build_program(&user_dir, &out_dir, program);+    }+    build_disk(&manifest, &out_dir);+}++fn build_program(user_dir: &Path, out_dir: &Path, program: &str) {+    let rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".into());+    let status = Command::new(&rustc)+        .args(["--edition", "2024", "--target", "x86_64-unknown-none"])+        .args(["-C", "panic=abort", "-C", "opt-level=2", "-C", "debuginfo=0", "-C", "strip=debuginfo"])+        .args(["-C", "relocation-model=static", "-C", "code-model=large"])+        .arg("-C")+        .arg(format!("link-arg=-T{}", user_dir.join("link.ld").display()))+        .arg("-o")+        .arg(out_dir.join(program))+        .arg(user_dir.join(format!("{program}.rs")))+        .status()+        .expect("failed to run rustc for a user program");+    assert!(status.success(), "building user program {program} failed");+}++/// Find a tool in PATH or the sbin directories (often not in a user's PATH).+fn tool(name: &str) -> PathBuf {+    let path = env::var("PATH").unwrap_or_default();+    for dir in path.split(':').chain(["/usr/sbin", "/sbin", "/usr/bin"]) {+        let candidate = Path::new(dir).join(name);+        if candidate.is_file() {+            return candidate;+        }+    }+    panic!("`{name}` not found: install it with `sudo apt install dosfstools mtools fdisk`");+}++fn run(cmd: &mut Command, stdin: Option<&str>) {+    cmd.stdin(if stdin.is_some() { Stdio::piped() } else { Stdio::null() });+    let mut child = cmd.spawn().unwrap_or_else(|e| panic!("can't run {cmd:?}: {e}"));+    if let Some(input) = stdin {+        child.stdin.take().unwrap().write_all(input.as_bytes()).unwrap();+    }+    let status = child.wait().unwrap();+    assert!(status.success(), "{cmd:?} failed");+}++fn build_disk(manifest: &Path, out_dir: &Path) {+    let target_dir = env::var("CARGO_TARGET_DIR").map(PathBuf::from).unwrap_or_else(|_| manifest.join("target"));+    fs::create_dir_all(&target_dir).unwrap();+    let disk = target_dir.join("disk.img");+    let part = out_dir.join("part.img"); +    // 1. The FAT32 file system, as its own image first. -s 1: one sector per+    //    cluster (FAT32 needs >= 65525 clusters, so small disks need small+    //    clusters). -n: volume label.+    let _ = fs::remove_file(&part);+    run(+        Command::new(tool("mkfs.vfat"))+            .args(["-F", "32", "-s", "1", "-n", "TYLERDISK", "-C"])+            .arg(&part)+            .arg((PART_SECTORS / 2).to_string()), // size in KiB+        None,+    );+    let mtools = |name: &str, args: &[&str]| {+        let mut cmd = Command::new(tool(name));+        cmd.env("MTOOLS_SKIP_CHECK", "1").arg("-i").arg(&part).args(args);+        run(&mut cmd, None);+    };+    // Copy disk/ (text files, subdirectories) to the root of the volume...+    for entry in fs::read_dir(manifest.join("disk")).unwrap() {+        let path = entry.unwrap().path();+        mtools("mcopy", &["-s", path.to_str().unwrap(), "::/"]);+    }+    // ...and the user programs to /bin.+    mtools("mmd", &["::/bin"]);     for program in PROGRAMS {-        let status = Command::new(&rustc)-            .args(["--edition", "2024", "--target", "x86_64-unknown-none"])-            .args(["-C", "panic=abort", "-C", "opt-level=2", "-C", "debuginfo=0", "-C", "strip=debuginfo"])-            .args(["-C", "relocation-model=static", "-C", "code-model=large"])-            .arg("-C")-            .arg(format!("link-arg=-T{}", user_dir.join("link.ld").display()))-            .arg("-o")-            .arg(out_dir.join(program))-            .arg(user_dir.join(format!("{program}.rs")))-            .status()-            .expect("failed to run rustc for a user program");-        assert!(status.success(), "building user program {program} failed");+        mtools("mcopy", &[out_dir.join(program).to_str().unwrap(), &format!("::/bin/{program}")]);     }++    // 2. The whole disk: zeros + GPT partition table (sfdisk works on image+    //    files too), then the FAT32 image copied into the partition.+    let _ = fs::remove_file(&disk);+    fs::File::create(&disk).unwrap().set_len(DISK_SECTORS * 512).unwrap();+    let layout = format!(+        "label: gpt\nstart={PART_START}, size={PART_SECTORS}, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7, name=\"fat32\"\n"+    );+    run(Command::new(tool("sfdisk")).arg("--quiet").arg(&disk), Some(&layout));+    let fat = fs::read(&part).unwrap();+    assert_eq!(fat.len() as u64, PART_SECTORS * 512);+    let mut file = fs::OpenOptions::new().write(true).open(&disk).unwrap();+    use std::io::{Seek, SeekFrom};+    file.seek(SeekFrom::Start(PART_START * 512)).unwrap();+    file.write_all(&fat).unwrap(); }
@@ -0,0 +1,2 @@+Next stage: the shell (cd, ls, pwd, cat, run, start, pipes, > redirection).+Later: ext2, exFAT, btrfs.
@@ -0,0 +1,2 @@+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.
@@ -17,6 +17,7 @@ IMG=target/x86_64-rust_kernel/debug/bootimage-rust_kernel.bin set +e timeout 30 qemu-system-x86_64 \     -drive format=raw,file="$IMG" \+    -drive file=target/disk.img,format=raw,if=virtio,snapshot=on \     -device isa-debug-exit,iobase=0xf4,iosize=0x04 \     -serial stdio -display none -no-reboot code=$?
@@ -23,6 +23,7 @@ SOCK=$(mktemp -u /tmp/qemu-mon.XXXXXX) SERIAL_LOG=$(mktemp /tmp/serial.XXXXXX)  qemu-system-x86_64 -drive format=raw,file="$IMG" \+    -drive file=target/disk.img,format=raw,if=virtio,snapshot=on \     -serial file:"$SERIAL_LOG" -display none -monitor unix:"$SOCK",server,nowait & QPID=$! sleep 6   # boot, and let the demo threads finish their serial output
@@ -28,7 +28,7 @@ pub mod fixed_size_block; /// Where the heap lives in *virtual* memory. Any unused, easy-to-recognise /// address works; this one stands out in a debugger. pub const HEAP_START: usize = 0x_4444_4444_0000;-pub const HEAP_SIZE: usize = 256 * 1024; // 256 KiB (thread stacks live here too)+pub const HEAP_SIZE: usize = 4 * 1024 * 1024; // 4 MiB (thread stacks live here too)  #[global_allocator] static ALLOCATOR: Locked<fixed_size_block::FixedSizeBlockAllocator> =
@@ -26,8 +26,21 @@ impl BumpAllocator {     } } +// The heap lock is taken with interrupts off: code that runs with+// interrupts off (the scheduler, wait queues, ...) may allocate, and if it+// found the lock held by a thread that was preempted mid-allocation, it+// would spin forever -- that thread can't run again until interrupts are on. unsafe impl GlobalAlloc for Locked<BumpAllocator> {     unsafe fn alloc(&self, layout: Layout) -> *mut u8 {+        x86_64::instructions::interrupts::without_interrupts(|| unsafe { self.alloc_inner(layout) })+    }+    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {+        x86_64::instructions::interrupts::without_interrupts(|| unsafe { self.dealloc_inner(ptr, layout) })+    }+}++impl Locked<BumpAllocator> {+    unsafe fn alloc_inner(&self, layout: Layout) -> *mut u8 {         let mut bump = self.lock();         let alloc_start = align_up(bump.next, layout.align());         let alloc_end = match alloc_start.checked_add(layout.size()) {@@ -43,7 +56,7 @@ unsafe impl GlobalAlloc for Locked<BumpAllocator> {         }     } -    unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {+    unsafe fn dealloc_inner(&self, _ptr: *mut u8, _layout: Layout) {         let mut bump = self.lock();         bump.allocations -= 1;         if bump.allocations == 0 {
@@ -56,8 +56,21 @@ impl FixedSizeBlockAllocator {     } } +// The heap lock is taken with interrupts off: code that runs with+// interrupts off (the scheduler, wait queues, ...) may allocate, and if it+// found the lock held by a thread that was preempted mid-allocation, it+// would spin forever -- that thread can't run again until interrupts are on. unsafe impl GlobalAlloc for Locked<FixedSizeBlockAllocator> {     unsafe fn alloc(&self, layout: Layout) -> *mut u8 {+        x86_64::instructions::interrupts::without_interrupts(|| unsafe { self.alloc_inner(layout) })+    }+    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {+        x86_64::instructions::interrupts::without_interrupts(|| unsafe { self.dealloc_inner(ptr, layout) })+    }+}++impl Locked<FixedSizeBlockAllocator> {+    unsafe fn alloc_inner(&self, layout: Layout) -> *mut u8 {         let mut allocator = self.lock();         match list_index(&layout) {             Some(index) => match allocator.list_heads[index].take() {@@ -77,7 +90,7 @@ unsafe impl GlobalAlloc for Locked<FixedSizeBlockAllocator> {         }     } -    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {+    unsafe fn dealloc_inner(&self, ptr: *mut u8, layout: Layout) {         let mut allocator = self.lock();         match list_index(&layout) {             Some(index) => {
@@ -0,0 +1,127 @@+//! Block devices: things that store fixed-size *sectors* (512 bytes) you can+//! read and write by number (LBA = logical block address). Disks, partitions+//! of disks, and a RAM disk for tests all implement [`BlockDevice`], so file+//! systems don't care which one they sit on.+//!+//! Layers, bottom to top:+//!   virtio_blk (a QEMU disk)  ->  partition (a slice of a disk)  ->  fs::fat (FAT32)+//!+//! Devices are registered by name ("vda", "vda1", like Linux) in a global+//! list, see [`register`] and [`devices`].++pub mod partition;+pub mod virtio_blk;++use crate::errno::Errno;+use alloc::{string::String, sync::Arc, vec, vec::Vec};+use spin::Mutex;++pub const SECTOR_SIZE: usize = 512;++pub trait BlockDevice: Send + Sync {+    /// Number of sectors.+    fn sector_count(&self) -> u64;+    /// Read `buf.len() / 512` sectors starting at `lba` (buf.len() must be a+    /// multiple of 512).+    fn read_sectors(&self, lba: u64, buf: &mut [u8]) -> Result<(), Errno>;+    /// Write `buf.len() / 512` sectors starting at `lba`.+    fn write_sectors(&self, lba: u64, buf: &[u8]) -> Result<(), Errno>;+}++/// Bounds check shared by all devices: whole sectors, inside the device.+pub fn check_range(dev: &dyn BlockDevice, lba: u64, len: usize) -> Result<u64, Errno> {+    if len % SECTOR_SIZE != 0 {+        return Err(Errno::EINVAL);+    }+    let count = (len / SECTOR_SIZE) as u64;+    match lba.checked_add(count) {+        Some(end) if end <= dev.sector_count() => Ok(count),+        _ => Err(Errno::EIO), // like reading past the end of a real disk+    }+}++/// A disk in RAM (for tests, and handy for experiments).+pub struct RamDisk {+    data: Mutex<Vec<u8>>,+}++impl RamDisk {+    pub fn new(sectors: usize) -> Self {+        RamDisk { data: Mutex::new(vec![0; sectors * SECTOR_SIZE]) }+    }+}++impl BlockDevice for RamDisk {+    fn sector_count(&self) -> u64 {+        (self.data.lock().len() / SECTOR_SIZE) as u64+    }+    fn read_sectors(&self, lba: u64, buf: &mut [u8]) -> Result<(), Errno> {+        check_range(self, lba, buf.len())?;+        let start = lba as usize * SECTOR_SIZE;+        buf.copy_from_slice(&self.data.lock()[start..start + buf.len()]);+        Ok(())+    }+    fn write_sectors(&self, lba: u64, buf: &[u8]) -> Result<(), Errno> {+        check_range(self, lba, buf.len())?;+        let start = lba as usize * SECTOR_SIZE;+        self.data.lock()[start..start + buf.len()].copy_from_slice(buf);+        Ok(())+    }+}++static DEVICES: Mutex<Vec<(String, Arc<dyn BlockDevice>)>> = Mutex::new(Vec::new());++pub fn register(name: &str, dev: Arc<dyn BlockDevice>) {+    DEVICES.lock().push((String::from(name), dev));+}++/// All registered devices, (name, device), in registration order.+pub fn devices() -> Vec<(String, Arc<dyn BlockDevice>)> {+    DEVICES.lock().clone()+}++pub fn get(name: &str) -> Option<Arc<dyn BlockDevice>> {+    DEVICES.lock().iter().find(|(n, _)| n == name).map(|(_, d)| d.clone())+}++/// Boot-time storage setup: find virtio disks on the PCI bus ("vda", "vdb"...),+/// register them, and register their partitions ("vda1", ...). Returns+/// lines describing what was found, for printing.+pub fn init(pci_devices: &[crate::pci::PciDevice]) -> Vec<String> {+    use alloc::format;+    let mut log = Vec::new();+    let mut letter = b'a';+    for pci in pci_devices.iter().filter(|d| virtio_blk::is_virtio_blk(d)) {+        let name = format!("vd{}", letter as char);+        match virtio_blk::VirtioBlk::new(pci) {+            Ok(disk) => {+                let disk: Arc<dyn BlockDevice> = Arc::new(disk);+                log.push(format!(+                    "{}: virtio-blk, {} sectors ({} MiB)",+                    name,+                    disk.sector_count(),+                    disk.sector_count() * SECTOR_SIZE as u64 / (1024 * 1024)+                ));+                register(&name, disk.clone());+                match partition::scan(&disk) {+                    Ok((scheme, parts)) => {+                        log.push(format!("{}: {} partition table, {} partition(s)", name, scheme, parts.len()));+                        for (i, p) in parts.into_iter().enumerate() {+                            let pname = format!("{}{}", name, i + 1);+                            log.push(format!(+                                "{}: start {} sectors {} type {}{}",+                                pname, p.start, p.sectors, p.type_name,+                                if p.name.is_empty() { String::new() } else { format!(" name \"{}\"", p.name) }+                            ));+                            register(&pname, Arc::new(partition::Partition::new(disk.clone(), p.start, p.sectors)));+                        }+                    }+                    Err(e) => log.push(format!("{}: no partition table ({:?})", name, e)),+                }+            }+            Err(e) => log.push(format!("{}: virtio-blk init failed: {:?}", name, e)),+        }+        letter += 1;+    }+    log+}
@@ -0,0 +1,158 @@+//! Partition tables: MBR (1983, max 4 primary partitions, 2 TiB) and GPT+//! (UEFI era, 128 partitions, 64-bit sizes). A [`Partition`] is itself a+//! `BlockDevice` -- a window onto part of the disk -- so a file system on+//! "vda1" doesn't know (or care) that it starts at sector 2048 of "vda".+//!+//! MBR (sector 0): boot code, then 4 entries of 16 bytes at offset 446, then+//! the signature 0x55 0xAA. Each entry: type byte (offset 4), first sector+//! (u32 at 8), sector count (u32 at 12). Type 0xEE = "this disk is GPT, the+//! MBR is just a protective placeholder".+//!+//! GPT: header in sector 1 ("EFI PART"; where the entry array is, how many+//! entries, how big each is), entry array usually in sectors 2-33. Each+//! entry: type GUID (all zero = unused), unique GUID, first/last LBA, flags,+//! UTF-16 name. A backup copy lives at the end of the disk; we don't check+//! it, nor the CRC32 checksums (exercise!). Extended/logical MBR partitions+//! aren't supported either. References: wiki.osdev.org/MBR_(x86),+//! wiki.osdev.org/GPT++use super::{BlockDevice, SECTOR_SIZE, check_range};+use crate::errno::Errno;+use alloc::{string::String, sync::Arc, vec, vec::Vec};++/// What the partition table says about one partition.+#[derive(Debug, Clone, PartialEq, Eq)]+pub struct PartitionInfo {+    pub start: u64,   // first sector+    pub sectors: u64, // length+    pub type_name: &'static str,+    pub name: String, // GPT only+}++#[derive(Debug, PartialEq, Eq)]+pub enum PartitionError {+    NoSignature, // sector 0 doesn't end in 55 AA: not partitioned+    BadGpt,+    Io(Errno),+}++fn u32_at(b: &[u8], at: usize) -> u32 {+    u32::from_le_bytes(b[at..at + 4].try_into().unwrap())+}+fn u64_at(b: &[u8], at: usize) -> u64 {+    u64::from_le_bytes(b[at..at + 8].try_into().unwrap())+}++/// Read the partition table of `disk`: ("MBR" or "GPT", partitions).+pub fn scan(disk: &Arc<dyn BlockDevice>) -> Result<(&'static str, Vec<PartitionInfo>), PartitionError> {+    let mut mbr = [0u8; SECTOR_SIZE];+    disk.read_sectors(0, &mut mbr).map_err(PartitionError::Io)?;+    if mbr[510..512] != [0x55, 0xaa] {+        return Err(PartitionError::NoSignature);+    }+    let entries: Vec<&[u8]> = (0..4).map(|i| &mbr[446 + 16 * i..446 + 16 * (i + 1)]).collect();+    if entries.iter().any(|e| e[4] == 0xee) {+        return Ok(("GPT", scan_gpt(disk)?));+    }+    let parts = entries+        .iter()+        .filter(|e| e[4] != 0 && u32_at(e, 12) != 0) // type 0 = empty slot+        .map(|e| PartitionInfo {+            start: u32_at(e, 8) as u64,+            sectors: u32_at(e, 12) as u64,+            type_name: mbr_type_name(e[4]),+            name: String::new(),+        })+        .collect();+    Ok(("MBR", parts))+}++fn scan_gpt(disk: &Arc<dyn BlockDevice>) -> Result<Vec<PartitionInfo>, PartitionError> {+    let mut header = [0u8; SECTOR_SIZE];+    disk.read_sectors(1, &mut header).map_err(PartitionError::Io)?;+    if &header[0..8] != b"EFI PART" {+        return Err(PartitionError::BadGpt);+    }+    let entries_lba = u64_at(&header, 72);+    let count = u32_at(&header, 80) as usize;+    let entry_size = u32_at(&header, 84) as usize;+    if entry_size < 128 || count > 1024 || entry_size > SECTOR_SIZE || SECTOR_SIZE % entry_size != 0 {+        return Err(PartitionError::BadGpt);+    }+    let sectors = (count * entry_size).div_ceil(SECTOR_SIZE);+    let mut table = vec![0u8; sectors * SECTOR_SIZE];+    disk.read_sectors(entries_lba, &mut table).map_err(PartitionError::Io)?;++    let mut parts = Vec::new();+    for e in table.chunks(entry_size).take(count) {+        let type_guid = &e[0..16];+        if type_guid.iter().all(|&b| b == 0) {+            continue; // unused entry+        }+        let (first, last) = (u64_at(e, 32), u64_at(e, 40));+        if last < first {+            return Err(PartitionError::BadGpt);+        }+        // Name: UTF-16LE, up to 36 code units, zero-padded.+        let units: Vec<u16> = e[56..128].chunks(2).map(|c| u16::from_le_bytes([c[0], c[1]])).take_while(|&u| u != 0).collect();+        parts.push(PartitionInfo {+            start: first,+            sectors: last - first + 1,+            type_name: gpt_type_name(type_guid),+            name: char::decode_utf16(units).map(|c| c.unwrap_or('?')).collect(),+        });+    }+    Ok(parts)+}++fn mbr_type_name(t: u8) -> &'static str {+    match t {+        0x0b | 0x0c => "FAT32",+        0x07 => "NTFS/exFAT",+        0x83 => "Linux",+        0x05 | 0x0f => "extended (unsupported)",+        _ => "unknown",+    }+}++/// GUIDs are stored with their first three fields little-endian, so the+/// bytes look scrambled compared to the usual text form.+fn gpt_type_name(guid: &[u8]) -> &'static str {+    const BASIC_DATA: [u8; 16] = [0xa2, 0xa0, 0xd0, 0xeb, 0xe5, 0xb9, 0x33, 0x44, 0x87, 0xc0, 0x68, 0xb6, 0xb7, 0x26, 0x99, 0xc7]; // EBD0A0A2-B9E5-4433-87C0-68B6B72699C7+    const LINUX_FS: [u8; 16] = [0xaf, 0x3d, 0xc6, 0x0f, 0x83, 0x84, 0x72, 0x47, 0x8e, 0x79, 0x3d, 0x69, 0xd8, 0x47, 0x7d, 0xe4]; // 0FC63DAF-8483-4772-8E79-3D69D8477DE4+    const EFI_SYSTEM: [u8; 16] = [0x28, 0x73, 0x2a, 0xc1, 0x1f, 0xf8, 0xd2, 0x11, 0xba, 0x4b, 0x00, 0xa0, 0xc9, 0x3e, 0xc9, 0x3b]; // C12A7328-F81F-11D2-BA4B-00A0C93EC93B+    match guid {+        g if g == BASIC_DATA => "Microsoft basic data (FAT/exFAT/NTFS)",+        g if g == LINUX_FS => "Linux filesystem",+        g if g == EFI_SYSTEM => "EFI system (FAT)",+        _ => "unknown",+    }+}++/// A partition as a block device: sector numbers are shifted by `start` and+/// checked against `sectors`, so a file system can't scribble outside it.+pub struct Partition {+    disk: Arc<dyn BlockDevice>,+    start: u64,+    sectors: u64,+}++impl Partition {+    pub fn new(disk: Arc<dyn BlockDevice>, start: u64, sectors: u64) -> Self {+        Partition { disk, start, sectors }+    }+}++impl BlockDevice for Partition {+    fn sector_count(&self) -> u64 {+        self.sectors+    }+    fn read_sectors(&self, lba: u64, buf: &mut [u8]) -> Result<(), Errno> {+        check_range(self, lba, buf.len())?;+        self.disk.read_sectors(self.start + lba, buf)+    }+    fn write_sectors(&self, lba: u64, buf: &[u8]) -> Result<(), Errno> {+        check_range(self, lba, buf.len())?;+        self.disk.write_sectors(self.start + lba, buf)+    }+}
@@ -0,0 +1,281 @@+//! Driver for virtio-blk: the paravirtual disk QEMU/KVM offer+//! (`-drive file=disk.img,if=virtio`).+//!+//! Why virtio and not ATA PIO? ATA PIO is simpler (copy 256 words through an+//! I/O port per sector) but it's a 1980s interface that real machines barely+//! have any more. virtio is what VMs actually use, and it teaches the core+//! idea of every modern device: *shared memory queues + DMA*. We use the+//! **legacy** (virtio 0.9.5) interface, which QEMU's "transitional" virtio+//! devices still offer: its registers are a handful of I/O ports (BAR0), and+//! setting it up is ~40 lines. The modern (1.0) interface uses PCI+//! capabilities + MMIO; the queue format is the same.+//!+//! The virtqueue: three arrays in physical memory that driver and device share.+//!   * descriptor table: {physical addr, length, flags, next} -- buffers+//!   * available ring: driver writes "descriptor chain N is ready"+//!   * used ring: device writes "I finished chain N"+//! A block request is a chain of 3 descriptors:+//!   header {type: read/write, sector}  ->  data (512*n bytes)  ->  status byte+//! We put the chain in the available ring, write the queue number to the+//! "notify" port, and wait until the used ring's index moves.+//!+//! Simplifications: one request at a time; we *poll* the used ring instead of+//! using the interrupt (we even ask the device not to interrupt us), and we+//! copy data through one page-sized "bounce buffer", so a request moves at+//! most 8 sectors. Reference: virtio spec 1.2 sections 2.7 (split virtqueues),+//! 4.1.4.8 (legacy PCI interface), 5.2 (block device); wiki.osdev.org/Virtio++use super::{BlockDevice, SECTOR_SIZE, check_range};+use crate::errno::Errno;+use crate::memory;+use crate::pci::PciDevice;+use core::ptr::{read_volatile, write_volatile};+use core::sync::atomic::{Ordering, fence};+use spin::Mutex;+use x86_64::PhysAddr;+use x86_64::instructions::port::Port;++// Legacy virtio PCI registers (offsets into the I/O BAR).+const REG_DEVICE_FEATURES: u16 = 0x00;+const REG_GUEST_FEATURES: u16 = 0x04;+const REG_QUEUE_ADDRESS: u16 = 0x08; // physical page number of the queue+const REG_QUEUE_SIZE: u16 = 0x0c;+const REG_QUEUE_SELECT: u16 = 0x0e;+const REG_QUEUE_NOTIFY: u16 = 0x10;+const REG_DEVICE_STATUS: u16 = 0x12;+const REG_ISR_STATUS: u16 = 0x13;+const REG_CAPACITY: u16 = 0x14; // device config: disk size in sectors (u64)++// Device status bits (we tell the device how far initialization got).+const STATUS_ACKNOWLEDGE: u8 = 1; // "I see you"+const STATUS_DRIVER: u8 = 2; // "I know how to drive you"+const STATUS_DRIVER_OK: u8 = 4; // "ready to go"++// Descriptor flags.+const DESC_NEXT: u16 = 1; // chain continues at `next`+const DESC_WRITE: u16 = 2; // device writes into this buffer (vs. reads it)+const AVAIL_NO_INTERRUPT: u16 = 1; // "don't interrupt me, I'm polling"++const REQ_READ: u32 = 0; // VIRTIO_BLK_T_IN+const REQ_WRITE: u32 = 1; // VIRTIO_BLK_T_OUT++/// Max sectors per request = what fits in our one-page bounce buffer.+const MAX_SECTORS: usize = 4096 / SECTOR_SIZE;++#[repr(C)]+struct Descriptor {+    addr: u64,+    len: u32,+    flags: u16,+    next: u16,+}++#[derive(Debug)]+pub enum VirtioError {+    NoIoBar,+    QueueUnavailable,+    OutOfMemory,+}++/// PCI vendor 0x1af4 (Red Hat/virtio), device 0x1001 = transitional block device.+pub fn is_virtio_blk(dev: &PciDevice) -> bool {+    dev.vendor_id == 0x1af4 && dev.device_id == 0x1001+}++pub struct VirtioBlk {+    io_base: u16,+    sectors: u64,+    queue: Mutex<Queue>,+}++struct Queue {+    size: u16,+    /// Physical address of the queue memory (descriptors, then available+    /// ring, then -- at the next page boundary -- the used ring).+    phys: PhysAddr,+    used_offset: usize,+    /// Our copy of the used ring index: how many requests we've seen finish.+    last_used: u16,+    /// One page for the request header (offset 0) and status byte (offset 16).+    header_page: PhysAddr,+    /// One page bounce buffer for the data.+    data_page: PhysAddr,+}++impl VirtioBlk {+    pub fn new(pci: &PciDevice) -> Result<Self, VirtioError> {+        let bar0 = pci.bar(0);+        if bar0 & 1 == 0 {+            return Err(VirtioError::NoIoBar); // legacy interface lives in I/O space+        }+        pci.enable_io_and_bus_master(); // bus master = allowed to DMA into our RAM+        let io_base = (bar0 & !0x3) as u16;+        let dev = VirtioBlk { io_base, sectors: 0, queue: Mutex::new(Queue::empty()) };++        // 1. Reset, then announce ourselves.+        dev.write8(REG_DEVICE_STATUS, 0);+        dev.write8(REG_DEVICE_STATUS, STATUS_ACKNOWLEDGE);+        dev.write8(REG_DEVICE_STATUS, STATUS_ACKNOWLEDGE | STATUS_DRIVER);+        // 2. Feature negotiation: the device lists what it can do; we accept+        //    nothing optional (plain read/write is always supported).+        let _offered = dev.read32(REG_DEVICE_FEATURES);+        dev.write32(REG_GUEST_FEATURES, 0);+        // 3. Set up queue 0 (block devices have one request queue).+        dev.write16(REG_QUEUE_SELECT, 0);+        let size = dev.read16(REG_QUEUE_SIZE); // the device decides (QEMU: 256)+        if size == 0 {+            return Err(VirtioError::QueueUnavailable);+        }+        // Legacy layout: descriptors (16 bytes each) + available ring+        // (2+2+2*size+2), then the used ring (2+2+8*size+2) page-aligned.+        let n = size as usize;+        let used_offset = align_up(16 * n + 6 + 2 * n, 4096);+        let total = used_offset + align_up(6 + 8 * n, 4096);+        let phys = memory::alloc_contiguous_frames(total / 4096).ok_or(VirtioError::OutOfMemory)?;+        let header_page = memory::alloc_contiguous_frames(1).ok_or(VirtioError::OutOfMemory)?;+        let data_page = memory::alloc_contiguous_frames(1).ok_or(VirtioError::OutOfMemory)?;+        dev.write32(REG_QUEUE_ADDRESS, (phys.as_u64() / 4096) as u32);+        // 4. Read the disk size from the device config area, then go.+        let lo = dev.read32(REG_CAPACITY) as u64;+        let hi = dev.read32(REG_CAPACITY + 4) as u64;+        dev.write8(REG_DEVICE_STATUS, STATUS_ACKNOWLEDGE | STATUS_DRIVER | STATUS_DRIVER_OK);++        let queue = Queue { size, phys, used_offset, last_used: 0, header_page, data_page };+        unsafe { write_volatile(queue.avail_flags(), AVAIL_NO_INTERRUPT) };+        Ok(VirtioBlk { io_base, sectors: hi << 32 | lo, queue: Mutex::new(queue) })+    }++    /// Do one request of up to MAX_SECTORS. For writes, the data must+    /// already be in the bounce buffer; for reads, it's there afterwards.+    fn request(&self, q: &mut Queue, kind: u32, lba: u64, sectors: usize) -> Result<(), Errno> {+        let header = memory::phys_to_virt(q.header_page).as_mut_ptr::<u8>();+        unsafe {+            write_volatile(header as *mut u32, kind); // type+            write_volatile(header.add(4) as *mut u32, 0); // reserved+            write_volatile(header.add(8) as *mut u64, lba); // first sector+            write_volatile(header.add(16), 0xff); // status: device sets 0 = OK+        }+        // The chain: descriptors 0 -> 1 -> 2 (always the same three, since we+        // only ever have one request in flight).+        let data_flags = if kind == REQ_READ { DESC_WRITE } else { 0 };+        q.set_desc(0, q.header_page.as_u64(), 16, DESC_NEXT, 1);+        q.set_desc(1, q.data_page.as_u64(), (sectors * SECTOR_SIZE) as u32, data_flags | DESC_NEXT, 2);+        q.set_desc(2, q.header_page.as_u64() + 16, 1, DESC_WRITE, 0);++        // Publish chain 0 in the available ring, then bump its index.+        unsafe {+            let idx = read_volatile(q.avail_idx());+            write_volatile(q.avail_ring(idx % q.size), 0);+            fence(Ordering::SeqCst); // ring entry visible before the index+            write_volatile(q.avail_idx(), idx.wrapping_add(1));+            fence(Ordering::SeqCst);+        }+        self.write16(REG_QUEUE_NOTIFY, 0); // "queue 0 has work" -> QEMU does the I/O++        // Poll the used ring until the device reports completion.+        while unsafe { read_volatile(q.used_idx()) } == q.last_used {+            core::hint::spin_loop();+        }+        fence(Ordering::SeqCst);+        q.last_used = q.last_used.wrapping_add(1);+        let _ = self.read8(REG_ISR_STATUS); // reading acknowledges (clears) it++        match unsafe { read_volatile(header.add(16)) } {+            0 => Ok(()),+            _ => Err(Errno::EIO),+        }+    }++    fn read8(&self, reg: u16) -> u8 {+        unsafe { Port::<u8>::new(self.io_base + reg).read() }+    }+    fn write8(&self, reg: u16, v: u8) {+        unsafe { Port::<u8>::new(self.io_base + reg).write(v) }+    }+    fn read16(&self, reg: u16) -> u16 {+        unsafe { Port::<u16>::new(self.io_base + reg).read() }+    }+    fn write16(&self, reg: u16, v: u16) {+        unsafe { Port::<u16>::new(self.io_base + reg).write(v) }+    }+    fn read32(&self, reg: u16) -> u32 {+        unsafe { Port::<u32>::new(self.io_base + reg).read() }+    }+    fn write32(&self, reg: u16, v: u32) {+        unsafe { Port::<u32>::new(self.io_base + reg).write(v) }+    }+}++impl BlockDevice for VirtioBlk {+    fn sector_count(&self) -> u64 {+        self.sectors+    }++    fn read_sectors(&self, lba: u64, buf: &mut [u8]) -> Result<(), Errno> {+        check_range(self, lba, buf.len())?;+        // Interrupts off while we hold the queue lock (see thread.rs locking+        // rule): a request takes microseconds in QEMU.+        x86_64::instructions::interrupts::without_interrupts(|| {+            let mut q = self.queue.lock();+            for (i, chunk) in buf.chunks_mut(MAX_SECTORS * SECTOR_SIZE).enumerate() {+                let sectors = chunk.len() / SECTOR_SIZE;+                self.request(&mut q, REQ_READ, lba + (i * MAX_SECTORS) as u64, sectors)?;+                let src = memory::phys_to_virt(q.data_page).as_ptr::<u8>();+                unsafe { core::ptr::copy_nonoverlapping(src, chunk.as_mut_ptr(), chunk.len()) };+            }+            Ok(())+        })+    }++    fn write_sectors(&self, lba: u64, buf: &[u8]) -> Result<(), Errno> {+        check_range(self, lba, buf.len())?;+        x86_64::instructions::interrupts::without_interrupts(|| {+            let mut q = self.queue.lock();+            for (i, chunk) in buf.chunks(MAX_SECTORS * SECTOR_SIZE).enumerate() {+                let dst = memory::phys_to_virt(q.data_page).as_mut_ptr::<u8>();+                unsafe { core::ptr::copy_nonoverlapping(chunk.as_ptr(), dst, chunk.len()) };+                self.request(&mut q, REQ_WRITE, lba + (i * MAX_SECTORS) as u64, chunk.len() / SECTOR_SIZE)?;+            }+            Ok(())+        })+    }+}++impl Queue {+    const fn empty() -> Self {+        Queue {+            size: 0,+            phys: PhysAddr::zero(),+            used_offset: 0,+            last_used: 0,+            header_page: PhysAddr::zero(),+            data_page: PhysAddr::zero(),+        }+    }+    fn base(&self) -> *mut u8 {+        memory::phys_to_virt(self.phys).as_mut_ptr()+    }+    fn set_desc(&self, i: usize, addr: u64, len: u32, flags: u16, next: u16) {+        let desc = unsafe { (self.base() as *mut Descriptor).add(i) };+        unsafe { write_volatile(desc, Descriptor { addr, len, flags, next }) };+    }+    fn avail_base(&self) -> *mut u16 {+        unsafe { self.base().add(16 * self.size as usize) as *mut u16 }+    }+    fn avail_flags(&self) -> *mut u16 {+        self.avail_base()+    }+    fn avail_idx(&self) -> *mut u16 {+        unsafe { self.avail_base().add(1) }+    }+    fn avail_ring(&self, i: u16) -> *mut u16 {+        unsafe { self.avail_base().add(2 + i as usize) }+    }+    fn used_idx(&self) -> *const u16 {+        unsafe { (self.base().add(self.used_offset) as *const u16).add(1) }+    }+}++fn align_up(x: usize, align: usize) -> usize {+    (x + align - 1) & !(align - 1)+}
@@ -5,13 +5,24 @@ #[repr(i64)] pub enum Errno {     ENOENT = 2,        // no such file or directory+    EIO = 5,           // input/output error (disk trouble)+    E2BIG = 7,         // argument list too long+    ENOEXEC = 8,       // not a valid executable     EBADF = 9,         // bad file descriptor+    ECHILD = 10,       // no such child process (wait)+    ENOMEM = 12,       // out of memory     EFAULT = 14,       // bad address (pointer not in user space / not mapped)+    EBUSY = 16,        // busy (e.g. removing a mount point)     EEXIST = 17,       // file exists     ENOTDIR = 20,      // a path component isn't a directory     EISDIR = 21,       // is a directory     EINVAL = 22,       // invalid argument     EMFILE = 24,       // too many open files+    ENOSPC = 28,       // no space left on device+    ESPIPE = 29,       // can't seek (pipe, console)+    EPIPE = 32,        // writing to a pipe nobody reads+    ERANGE = 34,       // result doesn't fit (getcwd buffer too small)     ENAMETOOLONG = 36, // path too long     ENOSYS = 38,       // no such system call+    ENOTEMPTY = 39,    // directory not empty }
@@ -0,0 +1,360 @@+//! FAT32 support, using the `fatfs` crate (no_std + alloc mode).+//!+//! Two adapters glue it in:+//!+//! 1. [`BlockIo`]: `fatfs` wants something that looks like a `std::fs::File`+//!    (byte-addressed Read + Write + Seek). Our disks only do whole 512-byte+//!    sectors, so `BlockIo` keeps a byte position and turns byte requests+//!    into sector requests: whole aligned sectors go straight to the device;+//!    partial sectors go through a one-sector cache (read-modify-write for+//!    writes). Writes are write-through, so nothing is ever lost on "power+//!    off" because of the cache.+//!+//! 2. [`FatInode`]: our VFS `Inode` for one FAT file/directory. `fatfs`'s+//!    own `File`/`Dir` objects *borrow* the FileSystem (they carry a lifetime)+//!    so we can't store them in a long-lived `Arc<dyn Inode>`. Instead a+//!    FatInode just remembers its **path** and re-opens it from the root on+//!    every operation. That's slower (each read walks the directories again)+//!    but very simple and obviously correct -- fine for a learning kernel.+//!    A faster design would cache directory entries (like Linux's dcache).+//!+//! Locking: the whole FileSystem sits behind one Mutex, so FAT operations+//! are serialized. The virtio driver underneath polls with interrupts off;+//! this lock is only taken from thread context, never from an interrupt.++use super::{DirEntry, FileSystem, Inode, InodeKind};+use crate::block::{BlockDevice, SECTOR_SIZE};+use crate::errno::Errno;+use alloc::{format, string::String, sync::Arc, vec::Vec};+use fatfs::{IoBase, Read, Seek, SeekFrom, Write};+use spin::Mutex;++// ---------------------------------------------------------------------------+// 1. Byte-stream view of a block device+// ---------------------------------------------------------------------------++/// The error type `fatfs` reports back to us: just our errno.+#[derive(Debug)]+pub struct IoErr(pub Errno);++impl fatfs::IoError for IoErr {+    fn is_interrupted(&self) -> bool {+        false+    }+    fn new_unexpected_eof_error() -> Self {+        IoErr(Errno::EIO)+    }+    fn new_write_zero_error() -> Self {+        IoErr(Errno::EIO)+    }+}++pub struct BlockIo {+    dev: Arc<dyn BlockDevice>,+    pos: u64,+    size: u64,+    /// One cached sector: (lba, contents).+    cache: Option<(u64, [u8; SECTOR_SIZE])>,+}++impl BlockIo {+    pub fn new(dev: Arc<dyn BlockDevice>) -> Self {+        let size = dev.sector_count() * SECTOR_SIZE as u64;+        BlockIo { dev, pos: 0, size, cache: None }+    }++    /// Get sector `lba` into the cache and return it.+    fn cached(&mut self, lba: u64) -> Result<&mut [u8; SECTOR_SIZE], IoErr> {+        if self.cache.map(|(l, _)| l) != Some(lba) {+            let mut data = [0u8; SECTOR_SIZE];+            self.dev.read_sectors(lba, &mut data).map_err(IoErr)?;+            self.cache = Some((lba, data));+        }+        Ok(&mut self.cache.as_mut().unwrap().1)+    }+}++impl IoBase for BlockIo {+    type Error = IoErr;+}++impl Read for BlockIo {+    fn read(&mut self, buf: &mut [u8]) -> Result<usize, IoErr> {+        let len = buf.len().min(self.size.saturating_sub(self.pos) as usize);+        if len == 0 {+            return Ok(0);+        }+        let lba = self.pos / SECTOR_SIZE as u64;+        let offset = (self.pos % SECTOR_SIZE as u64) as usize;+        let n = if offset == 0 && len >= SECTOR_SIZE {+            // Fast path: whole sectors straight into the caller's buffer.+            let n = len / SECTOR_SIZE * SECTOR_SIZE;+            self.dev.read_sectors(lba, &mut buf[..n]).map_err(IoErr)?;+            // Writes keep the cache up to date, so it can't be stale here.+            n+        } else {+            // Part of one sector, via the cache.+            let n = len.min(SECTOR_SIZE - offset);+            let sector = self.cached(lba)?;+            buf[..n].copy_from_slice(&sector[offset..offset + n]);+            n+        };+        self.pos += n as u64;+        Ok(n)+    }+}++impl Write for BlockIo {+    fn write(&mut self, buf: &[u8]) -> Result<usize, IoErr> {+        let len = buf.len().min(self.size.saturating_sub(self.pos) as usize);+        if len == 0 {+            return Ok(0); // fatfs turns this into WriteZero (disk full)+        }+        let lba = self.pos / SECTOR_SIZE as u64;+        let offset = (self.pos % SECTOR_SIZE as u64) as usize;+        let n = if offset == 0 && len >= SECTOR_SIZE {+            let n = len / SECTOR_SIZE * SECTOR_SIZE;+            self.dev.write_sectors(lba, &buf[..n]).map_err(IoErr)?;+            // Drop the cached sector if we just overwrote it.+            if let Some((cached, _)) = self.cache {+                if cached >= lba && cached < lba + (n / SECTOR_SIZE) as u64 {+                    self.cache = None;+                }+            }+            n+        } else {+            // Read-modify-write one sector.+            let n = len.min(SECTOR_SIZE - offset);+            let sector = self.cached(lba)?;+            sector[offset..offset + n].copy_from_slice(&buf[..n]);+            let copy = *sector;+            self.dev.write_sectors(lba, &copy).map_err(IoErr)?;+            n+        };+        self.pos += n as u64;+        Ok(n)+    }++    fn flush(&mut self) -> Result<(), IoErr> {+        Ok(()) // write-through: nothing buffered+    }+}++impl Seek for BlockIo {+    fn seek(&mut self, pos: SeekFrom) -> Result<u64, IoErr> {+        let new = match pos {+            SeekFrom::Start(n) => Some(n),+            SeekFrom::End(d) => self.size.checked_add_signed(d),+            SeekFrom::Current(d) => self.pos.checked_add_signed(d),+        };+        match new {+            Some(n) if n <= self.size => {+                self.pos = n;+                Ok(n)+            }+            _ => Err(IoErr(Errno::EINVAL)),+        }+    }+}++/// Translate a fatfs error into an errno.+fn errno(e: fatfs::Error<IoErr>) -> Errno {+    match e {+        fatfs::Error::Io(IoErr(e)) => e,+        fatfs::Error::NotFound => Errno::ENOENT,+        fatfs::Error::AlreadyExists => Errno::EEXIST,+        fatfs::Error::DirectoryIsNotEmpty => Errno::ENOTEMPTY,+        fatfs::Error::NotEnoughSpace | fatfs::Error::WriteZero => Errno::ENOSPC,+        fatfs::Error::InvalidFileNameLength => Errno::ENAMETOOLONG,+        fatfs::Error::InvalidInput | fatfs::Error::UnsupportedFileNameCharacter => Errno::EINVAL,+        _ => Errno::EIO, // corrupted file system, unexpected EOF, ...+    }+}++// ---------------------------------------------------------------------------+// 2. The VFS side+// ---------------------------------------------------------------------------++type Fs = fatfs::FileSystem<BlockIo, fatfs::NullTimeProvider, fatfs::LossyOemCpConverter>;+type FatDir<'a> = fatfs::Dir<'a, BlockIo, fatfs::NullTimeProvider, fatfs::LossyOemCpConverter>;++/// A mounted FAT volume. The fatfs `FileSystem` sits behind one Mutex+/// (shared via Arc with every FatInode), so FAT operations are serialized.+pub struct FatFs {+    fs: Arc<Mutex<Fs>>,+}++impl FatFs {+    /// Read the boot sector of `dev` and mount it if it's FAT.+    pub fn mount(dev: Arc<dyn BlockDevice>) -> Result<Arc<FatFs>, Errno> {+        // No real-time clock driver yet, so new files get the FAT epoch date+        // (1980-01-01) via NullTimeProvider.+        let options = fatfs::FsOptions::new().time_provider(fatfs::NullTimeProvider::new());+        let fs = fatfs::FileSystem::new(BlockIo::new(dev), options).map_err(errno)?;+        Ok(Arc::new(FatFs { fs: Arc::new(Mutex::new(fs)) }))+    }++    /// The volume label (from `mkfs.vfat -n`).+    pub fn label(&self) -> String {+        String::from(self.fs.lock().volume_label().trim_end())+    }+}++impl FileSystem for FatFs {+    fn name(&self) -> &'static str {+        "fat32"+    }+    fn root(&self) -> Arc<dyn Inode> {+        Arc::new(FatInode { fs: self.fs.clone(), path: String::new(), kind: InodeKind::Directory })+    }+}++/// A file or directory on a FAT volume, identified by its path from the+/// volume root ("" = the root, "bin/hello" = a file).+pub struct FatInode {+    fs: Arc<Mutex<Fs>>,+    path: String,+    kind: InodeKind,+}++impl FatInode {+    fn child_path(&self, name: &str) -> String {+        if self.path.is_empty() { String::from(name) } else { format!("{}/{}", self.path, name) }+    }++    /// Run `f` on this inode's directory (re-opened from the root).+    fn with_dir<T>(&self, f: impl FnOnce(&FatDir) -> Result<T, Errno>) -> Result<T, Errno> {+        let fs = self.fs.lock();+        let root = fs.root_dir();+        if self.path.is_empty() {+            f(&root)+        } else {+            f(&root.open_dir(&self.path).map_err(errno)?)+        }+    }++    /// Run `f` on this inode's file (re-opened from the root).+    fn with_file<T>(&self, f: impl FnOnce(&mut fatfs::File<BlockIo, fatfs::NullTimeProvider, fatfs::LossyOemCpConverter>) -> Result<T, Errno>) -> Result<T, Errno> {+        if self.kind == InodeKind::Directory {+            return Err(Errno::EISDIR);+        }+        let fs = self.fs.lock();+        let mut file = fs.root_dir().open_file(&self.path).map_err(errno)?;+        f(&mut file)+    }+}++impl Inode for FatInode {+    fn kind(&self) -> InodeKind {+        self.kind+    }++    fn size(&self) -> usize {+        match self.kind {+            InodeKind::File => self.with_file(|f| f.seek(SeekFrom::End(0)).map_err(errno)).unwrap_or(0) as usize,+            InodeKind::Directory => self.list().map(|l| l.len()).unwrap_or(0),+        }+    }++    fn read_at(&self, offset: usize, buf: &mut [u8]) -> Result<usize, Errno> {+        self.with_file(|file| {+            let size = file.seek(SeekFrom::End(0)).map_err(errno)? as usize;+            if offset >= size {+                return Ok(0);+            }+            file.seek(SeekFrom::Start(offset as u64)).map_err(errno)?;+            let len = buf.len().min(size - offset);+            // fatfs reads at most one cluster per call; loop to fill buf.+            let mut done = 0;+            while done < len {+                match file.read(&mut buf[done..len]).map_err(errno)? {+                    0 => break,+                    n => done += n,+                }+            }+            Ok(done)+        })+    }++    fn write_at(&self, offset: usize, buf: &[u8]) -> Result<usize, Errno> {+        self.with_file(|file| {+            let size = file.seek(SeekFrom::End(0)).map_err(errno)? as usize;+            if offset > size {+                // Writing past the end: fill the gap with zeros (FAT has no+                // sparse files).+                let zeros = [0u8; 512];+                let mut gap = offset - size;+                while gap > 0 {+                    let n = gap.min(zeros.len());+                    file.write_all(&zeros[..n]).map_err(errno)?;+                    gap -= n;+                }+            } else {+                file.seek(SeekFrom::Start(offset as u64)).map_err(errno)?;+            }+            file.write_all(buf).map_err(errno)?;+            Ok(buf.len())+        })+    }++    fn truncate(&self) -> Result<(), Errno> {+        self.with_file(|file| file.truncate().map_err(errno)) // truncates at the position: 0+    }++    fn lookup(&self, name: &str) -> Result<Arc<dyn Inode>, Errno> {+        if self.kind != InodeKind::Directory {+            return Err(Errno::ENOTDIR);+        }+        let kind = self.with_dir(|dir| {+            for entry in dir.iter() {+                let entry = entry.map_err(errno)?;+                // FAT names are case-insensitive (README.TXT == readme.txt).+                if entry.file_name().eq_ignore_ascii_case(name) {+                    return Ok(if entry.is_dir() { InodeKind::Directory } else { InodeKind::File });+                }+            }+            Err(Errno::ENOENT)+        })?;+        Ok(Arc::new(FatInode { fs: self.fs.clone(), path: self.child_path(name), kind }))+    }++    fn create(&self, name: &str, kind: InodeKind) -> Result<Arc<dyn Inode>, Errno> {+        if self.lookup(name).is_ok() {+            return Err(Errno::EEXIST);+        }+        self.with_dir(|dir| {+            match kind {+                InodeKind::File => dir.create_file(name).map(|_| ()),+                InodeKind::Directory => dir.create_dir(name).map(|_| ()),+            }+            .map_err(errno)+        })?;+        Ok(Arc::new(FatInode { fs: self.fs.clone(), path: self.child_path(name), kind }))+    }++    fn list(&self) -> Result<Vec<DirEntry>, Errno> {+        if self.kind != InodeKind::Directory {+            return Err(Errno::ENOTDIR);+        }+        self.with_dir(|dir| {+            let mut out = Vec::new();+            for entry in dir.iter() {+                let entry = entry.map_err(errno)?;+                let name = entry.file_name();+                if name == "." || name == ".." {+                    continue; // the VFS doesn't list these (like our ramfs)+                }+                let kind = if entry.is_dir() { InodeKind::Directory } else { InodeKind::File };+                out.push(DirEntry { name, kind });+            }+            Ok(out)+        })+    }++    fn remove(&self, name: &str) -> Result<(), Errno> {+        if self.kind != InodeKind::Directory {+            return Err(Errno::ENOTDIR);+        }+        self.with_dir(|dir| dir.remove(name).map_err(errno))+    }+}
@@ -1,23 +1,65 @@ //! Open files: what a file descriptor refers to. //!-//! `File` is the interface the read/write syscalls use. It's separate from-//! `Inode` because the same inode can be open several times with different-//! offsets, and because some files aren't in any file system at all (the-//! console). Linux calls this `struct file` + `file_operations`.+//! `File` is the interface the read/write/lseek/fstat/getdents syscalls use.+//! It's separate from `Inode` because the same inode can be open several+//! times with different offsets, and because some files aren't in any file+//! system at all (the console, pipes). Linux calls this `struct file` ++//! `file_operations`.  use super::{Inode, InodeKind}; use crate::errno::Errno; use alloc::sync::Arc; use spin::Mutex; +/// What `stat`/`fstat` return. Our own small layout (Linux's `struct stat`+/// has ~15 fields we have no use for yet). `#[repr(C)]` because it's copied+/// to user space as is; user/ulib.rs has the same struct.+#[repr(C)]+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]+pub struct Stat {+    pub kind: u64, // one of the KIND_* constants+    pub size: u64, // bytes (files), entries (directories)+}++pub const KIND_FILE: u64 = 1;+pub const KIND_DIR: u64 = 2;+pub const KIND_CONSOLE: u64 = 3;+pub const KIND_PIPE: u64 = 4;++impl Stat {+    pub fn of(inode: &dyn Inode) -> Stat {+        let kind = match inode.kind() {+            InodeKind::File => KIND_FILE,+            InodeKind::Directory => KIND_DIR,+        };+        Stat { kind, size: inode.size() as u64 }+    }+}++/// `whence` values for `seek`, as in Linux's lseek.+pub const SEEK_SET: u64 = 0;+pub const SEEK_CUR: u64 = 1;+pub const SEEK_END: u64 = 2;+ pub trait File: Send + Sync {     /// Read up to `buf.len()` bytes; 0 means end of file.     fn read(&self, buf: &mut [u8]) -> Result<usize, Errno>;     /// Write bytes; returns how many were written.     fn write(&self, buf: &[u8]) -> Result<usize, Errno>;+    /// Move the offset; returns the new offset. Consoles and pipes can't.+    fn seek(&self, _offset: i64, _whence: u64) -> Result<u64, Errno> {+        Err(Errno::ESPIPE)+    }+    fn stat(&self) -> Result<Stat, Errno>;+    /// Fill `buf` with Linux `dirent64` records for the next directory+    /// entries; returns bytes used, 0 at the end.+    fn getdents(&self, _buf: &mut [u8]) -> Result<usize, Errno> {+        Err(Errno::ENOTDIR)+    } } -/// A regular file opened through the VFS: inode + current position.+/// A file or directory opened through the VFS: inode + current position.+/// For directories the "position" is the index of the next entry to list. pub struct InodeFile {     inode: Arc<dyn Inode>,     offset: Mutex<usize>,@@ -25,9 +67,6 @@ pub struct InodeFile {  impl InodeFile {     pub fn open(inode: Arc<dyn Inode>) -> Result<Self, Errno> {-        if inode.kind() == InodeKind::Directory {-            return Err(Errno::EISDIR); // no getdents yet-        }         Ok(InodeFile { inode, offset: Mutex::new(0) })     } }@@ -46,4 +85,56 @@ impl File for InodeFile {         *offset += n;         Ok(n)     }++    fn seek(&self, offset: i64, whence: u64) -> Result<u64, Errno> {+        if self.inode.kind() == InodeKind::Directory {+            return if offset == 0 && whence == SEEK_SET { *self.offset.lock() = 0; Ok(0) } else { Err(Errno::EINVAL) };+        }+        let mut current = self.offset.lock();+        let base = match whence {+            SEEK_SET => 0,+            SEEK_CUR => *current as i64,+            SEEK_END => self.inode.size() as i64,+            _ => return Err(Errno::EINVAL),+        };+        let new = base.checked_add(offset).filter(|&n| n >= 0).ok_or(Errno::EINVAL)?;+        *current = new as usize;+        Ok(new as u64)+    }++    fn stat(&self) -> Result<Stat, Errno> {+        Ok(Stat::of(&*self.inode))+    }++    fn getdents(&self, buf: &mut [u8]) -> Result<usize, Errno> {+        // Re-list the directory each call and skip what we've returned+        // already (simple; a real fs would keep a cursor into the directory).+        let entries = self.inode.list()?;+        let mut index = self.offset.lock();+        let mut used = 0;+        for entry in entries.iter().skip(*index) {+            // struct linux_dirent64 { u64 ino; i64 off; u16 reclen; u8 type; char name[]; }+            // padded to a multiple of 8 bytes.+            let reclen = (19 + entry.name.len() + 1).next_multiple_of(8);+            if used + reclen > buf.len() {+                if used == 0 {+                    return Err(Errno::EINVAL); // buffer too small for even one entry+                }+                break;+            }+            let rec = &mut buf[used..used + reclen];+            rec.fill(0);+            rec[0..8].copy_from_slice(&(*index as u64 + 1).to_le_bytes()); // fake inode number+            rec[8..16].copy_from_slice(&(*index as i64 + 1).to_le_bytes()); // offset of next+            rec[16..18].copy_from_slice(&(reclen as u16).to_le_bytes());+            rec[18] = match entry.kind {+                InodeKind::Directory => 4, // DT_DIR+                InodeKind::File => 8,      // DT_REG+            };+            rec[19..19 + entry.name.len()].copy_from_slice(entry.name.as_bytes());+            used += reclen;+            *index += 1;+        }+        Ok(used)+    } }
@@ -3,26 +3,31 @@ //! Modelled (very loosely) on Linux: //! //! * [`Inode`] -- one file or directory *inside some file system*. Knows how to-//!   read/write its bytes at an offset and (for directories) look up / create-//!   children. A FAT32, ext2, exFAT or btrfs driver implements this trait.-//! * [`FileSystem`] -- a mounted instance (e.g. "the FAT32 volume on disk 0");+//!   read/write its bytes at an offset and (for directories) look up, create,+//!   list and remove children. ramfs and FAT32 implement it today; ext2, exFAT+//!   and btrfs will later.+//! * [`FileSystem`] -- a mounted instance (e.g. "the FAT32 volume on vda1"); //!   gives you its root inode. //! * the **mount table** -- which file system is attached at which path ("/",-//!   "/tmp", ...). `lookup("/tmp/x")` finds the longest matching mount point-//!   and walks the rest of the path from that file system's root.+//!   "/tmp", "/mnt/disk0"). `lookup("/tmp/x")` finds the longest matching+//!   mount point and walks the rest of the path from that file system's root. //! * [`file::File`] -- an *open* file (what a file descriptor points to): an-//!   inode plus a current offset, or a device like the console. The per-process+//!   inode plus a current offset, or the console, or a pipe. The per-process //!   fd table holds `Arc<dyn File>`s, so syscalls don't care what's behind it. //!-//! Today there is one implementation, [`ramfs`] (everything in RAM). Later: a-//! block device layer (virtio-blk / ATA PIO), then FAT32, ext2, exFAT and-//! btrfs (read-only first) as further `FileSystem`s.+//! Paths: every path is first turned into a clean absolute path by+//! [`normalize`] (relative paths are joined to the process's current+//! directory; ".", ".." and "//" are resolved by plain string processing).+//! That makes ".." work across mount points for free -- real kernels track+//! parent dentries instead, which also handles symlinks (we have none). +pub mod fat; pub mod file;+pub mod pipe; pub mod ramfs;  use crate::errno::Errno;-use alloc::{string::String, sync::Arc, vec::Vec};+use alloc::{format, string::String, sync::Arc, vec::Vec}; use spin::Mutex;  #[derive(Debug, Clone, Copy, PartialEq, Eq)]@@ -31,6 +36,13 @@ pub enum InodeKind {     Directory, } +/// One entry of a directory listing.+#[derive(Debug, Clone, PartialEq, Eq)]+pub struct DirEntry {+    pub name: String,+    pub kind: InodeKind,+}+ /// A file or directory inside a file system. /// /// Default method bodies return the error a "wrong kind" of inode would give@@ -48,6 +60,10 @@ pub trait Inode: Send + Sync {     fn write_at(&self, _offset: usize, _buf: &[u8]) -> Result<usize, Errno> {         Err(Errno::EISDIR)     }+    /// Cut the file to length 0 (for `open(..., O_TRUNC)`, i.e. shell `>`).+    fn truncate(&self) -> Result<(), Errno> {+        Err(Errno::EISDIR)+    }     /// Find a child of this directory by name.     fn lookup(&self, _name: &str) -> Result<Arc<dyn Inode>, Errno> {         Err(Errno::ENOTDIR)@@ -56,8 +72,12 @@ pub trait Inode: Send + Sync {     fn create(&self, _name: &str, _kind: InodeKind) -> Result<Arc<dyn Inode>, Errno> {         Err(Errno::ENOTDIR)     }-    /// Names of all children of this directory.-    fn list(&self) -> Result<Vec<String>, Errno> {+    /// All children of this directory (without "." and "..").+    fn list(&self) -> Result<Vec<DirEntry>, Errno> {+        Err(Errno::ENOTDIR)+    }+    /// Delete a child (a file, or an *empty* directory).+    fn remove(&self, _name: &str) -> Result<(), Errno> {         Err(Errno::ENOTDIR)     } }@@ -78,12 +98,12 @@ struct Mount { /// I/O), so it's fine as a spinlock. Not used from interrupt handlers. static MOUNTS: Mutex<Vec<Mount>> = Mutex::new(Vec::new()); -/// Attach `fs` at `path` (must be absolute, like "/" or "/tmp").+/// Attach `fs` at `path` (absolute, like "/" or "/tmp"). pub fn mount(path: &str, fs: Arc<dyn FileSystem>) -> Result<(), Errno> {     if !path.starts_with('/') {         return Err(Errno::EINVAL);     }-    let path = String::from(if path.len() > 1 { path.trim_end_matches('/') } else { path });+    let path = normalize("/", path)?;     let mut mounts = MOUNTS.lock();     if mounts.iter().any(|m| m.path == path) {         return Err(Errno::EEXIST);@@ -105,45 +125,107 @@ fn mount_contains(mount: &str, path: &str) -> bool {         || (path.starts_with(mount) && path.as_bytes().get(mount.len()) == Some(&b'/')) } +/// Turn `path` into a clean absolute path. Relative paths start at `cwd`+/// (which must be absolute). "." is dropped, ".." removes the previous+/// component (".." of "/" is "/", like Unix).+///   normalize("/home", "../etc//motd") == "/etc/motd"+pub fn normalize(cwd: &str, path: &str) -> Result<String, Errno> {+    if path.is_empty() {+        return Err(Errno::ENOENT); // like Linux: "" is not a path+    }+    let mut parts: Vec<&str> = Vec::new();+    let joined = if path.starts_with('/') { String::from(path) } else { format!("{}/{}", cwd, path) };+    for component in joined.split('/') {+        match component {+            "" | "." => {}+            ".." => {+                parts.pop();+            }+            name if name.len() > 255 => return Err(Errno::ENAMETOOLONG),+            name => parts.push(name),+        }+    }+    let result = format!("/{}", parts.join("/"));+    if result.len() > 4096 {+        return Err(Errno::ENAMETOOLONG);+    }+    Ok(result)+}+ /// Resolve an absolute path to an inode. pub fn lookup(path: &str) -> Result<Arc<dyn Inode>, Errno> {     if !path.starts_with('/') {-        return Err(Errno::EINVAL); // no current directory yet-    }-    if path.len() > 255 {-        return Err(Errno::ENAMETOOLONG);+        return Err(Errno::EINVAL); // callers resolve relative paths first     }+    let path = normalize("/", path)?;     // Longest mount point that contains the path wins.     let (root, rest) = {         let mounts = MOUNTS.lock();         let m = mounts             .iter()-            .filter(|m| mount_contains(&m.path, path))+            .filter(|m| mount_contains(&m.path, &path))             .max_by_key(|m| m.path.len())             .ok_or(Errno::ENOENT)?;-        let rest = if m.path == "/" { path } else { &path[m.path.len()..] };+        let rest = String::from(if m.path == "/" { &path[..] } else { &path[m.path.len()..] });         (m.fs.root(), rest)     };     // Walk the remaining components ("a/b/c") from that file system's root.     let mut inode = root;-    for name in rest.split('/').filter(|c| !c.is_empty() && *c != ".") {-        if name == ".." {-            return Err(Errno::EINVAL); // not supported yet-        }+    for name in rest.split('/').filter(|c| !c.is_empty()) {         inode = inode.lookup(name)?;     }     Ok(inode) } -/// Create a file at `path` (its parent directory must exist).-pub fn create(path: &str, kind: InodeKind) -> Result<Arc<dyn Inode>, Errno> {-    let path = path.trim_end_matches('/');+/// Split a normalized path into (parent directory inode, last component).+fn parent_of(path: &str) -> Result<(Arc<dyn Inode>, String), Errno> {+    let path = normalize("/", path)?;     let (parent, name) = path.rsplit_once('/').ok_or(Errno::EINVAL)?;     if name.is_empty() {-        return Err(Errno::EINVAL);+        return Err(Errno::EEXIST); // "/" itself     }-    let parent = lookup(if parent.is_empty() { "/" } else { parent })?;-    parent.create(name, kind)+    Ok((lookup(if parent.is_empty() { "/" } else { parent })?, String::from(name)))+}++/// Create a file or directory at absolute `path` (parent must exist).+pub fn create(path: &str, kind: InodeKind) -> Result<Arc<dyn Inode>, Errno> {+    let (parent, name) = parent_of(path)?;+    parent.create(&name, kind)+}++/// Delete the file (`want_dir` false, like `unlink`) or empty directory+/// (`want_dir` true, like `rmdir`) at absolute `path`.+pub fn remove(path: &str, want_dir: bool) -> Result<(), Errno> {+    let normalized = normalize("/", path)?;+    if MOUNTS.lock().iter().any(|m| m.path == normalized) {+        return Err(Errno::EBUSY); // can't delete a mount point+    }+    let inode = lookup(&normalized)?;+    match (inode.kind(), want_dir) {+        (InodeKind::Directory, false) => return Err(Errno::EISDIR),+        (InodeKind::File, true) => return Err(Errno::ENOTDIR),+        _ => {}+    }+    let (parent, name) = parent_of(&normalized)?;+    parent.remove(&name)+}++/// Read a whole file into memory (used to load programs).+pub fn read_all(path: &str) -> Result<Vec<u8>, Errno> {+    let inode = lookup(path)?;+    if inode.kind() == InodeKind::Directory {+        return Err(Errno::EISDIR);+    }+    let mut data = alloc::vec![0u8; inode.size()];+    let mut done = 0;+    while done < data.len() {+        match inode.read_at(done, &mut data[done..])? {+            0 => break,+            n => done += n,+        }+    }+    data.truncate(done);+    Ok(data) }  /// Boot-time file system setup: a ramfs as "/", with a few files, and a@@ -155,8 +237,39 @@ pub fn init() {     hello.write_at(0, b"Hello from the ramfs! This is /hello.txt.\n").unwrap();     let etc = dir.create("etc", InodeKind::Directory).unwrap();     let motd = etc.create("motd", InodeKind::File).unwrap();-    motd.write_at(0, b"Welcome to Tyler's kernel. Next up: FAT32, ext2, exFAT, btrfs.\n").unwrap();+    motd.write_at(0, b"Welcome to Tyler's kernel. Next up: ext2, exFAT, btrfs.\n").unwrap();     dir.create("tmp", InodeKind::Directory).unwrap(); // mount point+    dir.create("mnt", InodeKind::Directory).unwrap(); // disks go to /mnt/diskN     mount("/", root).unwrap();     mount("/tmp", ramfs::RamFs::new()).unwrap(); }++/// Try to mount every registered partition (and unpartitioned disk) that+/// holds a FAT file system at /mnt/disk0, /mnt/disk1, ... Returns log lines.+/// (Later: also probe for ext2, exFAT, btrfs superblocks here.)+pub fn mount_disks() -> Vec<String> {+    let mut log = Vec::new();+    let devices = crate::block::devices();+    let mut n = 0;+    for (name, dev) in &devices {+        // Skip a whole disk if it has partitions ("vda" when "vda1" exists).+        let has_partitions = devices.iter().any(|(other, _)| other.len() > name.len() && other.starts_with(name.as_str()));+        if has_partitions {+            continue;+        }+        match fat::FatFs::mount(dev.clone()) {+            Ok(fs) => {+                let path = format!("/mnt/disk{}", n);+                let _ = create(&path, InodeKind::Directory); // mount point (may exist)+                let label = fs.label();+                match mount(&path, fs) {+                    Ok(()) => log.push(format!("{}: FAT32 volume \"{}\" mounted at {}", name, label, path)),+                    Err(e) => log.push(format!("{}: mount at {} failed: {:?}", name, path, e)),+                }+                n += 1;+            }+            Err(e) => log.push(format!("{}: not a FAT file system ({:?})", name, e)),+        }+    }+    log+}
@@ -0,0 +1,102 @@+//! Pipes: a byte queue with a read end and a write end, each a `File`.+//! `pipe()` puts both in the caller's fd table; the shell will hand the+//! write end to one program as stdout and the read end to the next as stdin.+//!+//! Semantics like Unix: reading an empty pipe *blocks* until data arrives or+//! every write end is closed (then read returns 0 = EOF); writing when every+//! read end is closed fails with EPIPE. Closing = dropping the last `Arc`+//! of an end (fd closed, process exited), which our `Drop` impls notice.+//! Simplification: the buffer is unbounded, so writers never block.++use super::file::{File, KIND_PIPE, Stat};+use crate::errno::Errno;+use crate::thread::WaitQueue;+use alloc::{collections::VecDeque, sync::Arc};+use spin::Mutex;+use x86_64::instructions::interrupts::without_interrupts;++struct Pipe {+    // Locked with interrupts off only (it's touched in `wait_until`'s+    // condition, see the locking rule in thread.rs).+    state: Mutex<PipeState>,+    readers_wait: WaitQueue,+}++struct PipeState {+    buffer: VecDeque<u8>,+    write_closed: bool,+    read_closed: bool,+}++pub struct PipeReader(Arc<Pipe>);+pub struct PipeWriter(Arc<Pipe>);++/// A new pipe: (read end, write end).+pub fn new() -> (PipeReader, PipeWriter) {+    let pipe = Arc::new(Pipe {+        state: Mutex::new(PipeState { buffer: VecDeque::new(), write_closed: false, read_closed: false }),+        readers_wait: WaitQueue::new(),+    });+    (PipeReader(pipe.clone()), PipeWriter(pipe))+}++impl File for PipeReader {+    fn read(&self, buf: &mut [u8]) -> Result<usize, Errno> {+        if buf.is_empty() {+            return Ok(0);+        }+        // Block until there's data or no writer is left.+        Ok(self.0.readers_wait.wait_until(|| {+            let mut state = self.0.state.lock();+            if state.buffer.is_empty() {+                return if state.write_closed { Some(0) } else { None };+            }+            let n = buf.len().min(state.buffer.len());+            for (dst, src) in buf.iter_mut().zip(state.buffer.drain(..n)) {+                *dst = src;+            }+            Some(n)+        }))+    }+    fn write(&self, _buf: &[u8]) -> Result<usize, Errno> {+        Err(Errno::EBADF) // wrong end+    }+    fn stat(&self) -> Result<Stat, Errno> {+        let size = without_interrupts(|| self.0.state.lock().buffer.len());+        Ok(Stat { kind: KIND_PIPE, size: size as u64 })+    }+}++impl File for PipeWriter {+    fn read(&self, _buf: &mut [u8]) -> Result<usize, Errno> {+        Err(Errno::EBADF)+    }+    fn write(&self, buf: &[u8]) -> Result<usize, Errno> {+        without_interrupts(|| {+            let mut state = self.0.state.lock();+            if state.read_closed {+                return Err(Errno::EPIPE);+            }+            state.buffer.extend(buf);+            Ok(buf.len())+        })?;+        self.0.readers_wait.wake_all();+        Ok(buf.len())+    }+    fn stat(&self) -> Result<Stat, Errno> {+        Ok(Stat { kind: KIND_PIPE, size: 0 })+    }+}++impl Drop for PipeWriter {+    fn drop(&mut self) {+        without_interrupts(|| self.0.state.lock().write_closed = true);+        self.0.readers_wait.wake_all(); // readers see EOF+    }+}++impl Drop for PipeReader {+    fn drop(&mut self) {+        without_interrupts(|| self.0.state.lock().read_closed = true);+    }+}
@@ -2,7 +2,7 @@ //! tmpfs without the size limit). The simplest possible `FileSystem`: a tree //! of nodes; files are a `Vec<u8>`, directories a sorted map name -> node. -use super::{FileSystem, Inode, InodeKind};+use super::{DirEntry, FileSystem, Inode, InodeKind}; use crate::errno::Errno; use alloc::{collections::BTreeMap, string::String, sync::Arc, vec::Vec}; use spin::Mutex;@@ -91,8 +91,28 @@ impl Inode for RamInode {         Ok(node)     } -    fn list(&self) -> Result<Vec<String>, Errno> {+    fn list(&self) -> Result<Vec<DirEntry>, Errno> {         let RamInode::Dir(entries) = self else { return Err(Errno::ENOTDIR) };-        Ok(entries.lock().keys().cloned().collect())+        let entries = entries.lock();+        Ok(entries.iter().map(|(name, node)| DirEntry { name: name.clone(), kind: node.kind() }).collect())+    }++    fn truncate(&self) -> Result<(), Errno> {+        let RamInode::File(data) = self else { return Err(Errno::EISDIR) };+        data.lock().clear();+        Ok(())+    }++    fn remove(&self, name: &str) -> Result<(), Errno> {+        let RamInode::Dir(entries) = self else { return Err(Errno::ENOTDIR) };+        let mut entries = entries.lock();+        let node = entries.get(name).ok_or(Errno::ENOENT)?;+        if let RamInode::Dir(children) = &**node {+            if !children.lock().is_empty() {+                return Err(Errno::ENOTEMPTY);+            }+        }+        entries.remove(name);+        Ok(())     } }
@@ -22,12 +22,14 @@ extern crate alloc; use core::panic::PanicInfo;  pub mod allocator; // heap: maps heap pages + the #[global_allocator]+pub mod block; // block devices: BlockDevice trait, virtio-blk, partitions pub mod elf; // ELF64 loader for user programs pub mod errno; // Linux-compatible error numbers 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 memory; // paging, frame allocator, per-process address spaces+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 serial; // COM1 serial port pub mod syscall; // syscall/sysret entry + system call implementations@@ -36,14 +38,6 @@ pub mod thread; // preemptive kernel threads + context switch pub mod tty; // console: keyboard line discipline + fd 0/1/2 pub mod vga_buffer; // 80x25 VGA text mode screen -/// The user programs (user/*.rs), compiled to ELF by build.rs and embedded.-pub mod user_programs {-    pub static HELLO: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/hello"));-    pub static EVIL: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/evil"));-    pub static SPIN: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/spin"));-    pub static ECHO: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/echo"));-}- /// Set up everything interrupt-related. Order matters: /// GDT/TSS first (the IDT refers to the IST stack), then the IDT, then the /// PICs, and only then enable interrupts (`sti`).@@ -55,6 +49,26 @@ pub fn init() {     x86_64::instructions::interrupts::enable(); } +/// Paging + heap + the global frame allocator, in one call (what the test+/// kernels need; src/main.rs does the same steps one by one with demos).+pub fn init_memory(boot_info: &'static bootloader::BootInfo) {+    let phys_mem_offset = x86_64::VirtAddr::new(boot_info.physical_memory_offset);+    let mut mapper = unsafe { memory::init(phys_mem_offset) };+    let mut frame_allocator = unsafe { memory::BootInfoFrameAllocator::init(&boot_info.memory_map) };+    allocator::init_heap(&mut mapper, &mut frame_allocator).expect("heap initialization failed");+    memory::init_frame_allocator(frame_allocator);+}++/// Storage bring-up: scan the PCI bus, start the disk drivers, read the+/// partition tables and mount FAT volumes under /mnt. Needs the heap, the+/// frame allocator and `fs::init()`. Returns (PCI devices, log lines).+pub fn init_storage() -> (alloc::vec::Vec<pci::PciDevice>, alloc::vec::Vec<alloc::string::String>) {+    let devices = pci::scan();+    let mut log = block::init(&devices);+    log.extend(fs::mount_disks());+    (devices, log)+}+ /// Print a line to *both* the VGA screen and the serial port. #[macro_export] macro_rules! kprintln {
@@ -15,7 +15,7 @@ use bootloader::{BootInfo, entry_point}; use core::panic::PanicInfo; use rust_kernel::task::{Task, executor::Executor, keyboard}; use rust_kernel::{allocator, fs, kprintln, memory, println, process, serial_println, thread};-use rust_kernel::{user_programs, vga_buffer};+use rust_kernel::vga_buffer; use x86_64::{VirtAddr, structures::paging::{Page, Translate}};  // `entry_point!` defines the real `_start` for us and checks at compile time@@ -48,22 +48,41 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! {     thread::spawn("C", thread_c);     kprintln!("[thread] spawned threads A, B, C (status on the top line)"); -    // --- VFS + user mode (fs/, process.rs, syscall.rs) ----------------------+    // --- VFS, PCI, disks (fs/, pci.rs, block/) ------------------------------     fs::init();+    let (pci_devices, storage_log) = rust_kernel::init_storage();+    for d in &pci_devices {+        serial_println!(+            "[pci] {:02x}:{:02x}.{} {:04x}:{:04x} {}",+            d.bus, d.device, d.function, d.vendor_id, d.device_id, d.class_name()+        );+    }+    kprintln!("[pci] {} devices on the PCI bus", pci_devices.len());+    for line in &storage_log {+        kprintln!("[disk] {}", line);+    }     for (path, fs_type) in fs::mounts() {         serial_println!("[vfs] mounted {} at {}", fs_type, path);     }-    // Each program is an ELF file built from user/*.rs and embedded in the-    // kernel. Each gets its own address space and runs in ring 3.-    let programs: [(&'static str, &'static [u8]); 4] = [-        ("hello", user_programs::HELLO), // syscalls + ramfs demo, exits 0-        ("evil", user_programs::EVIL),   // touches kernel memory -> killed-        ("spin", user_programs::SPIN),   // busy loop, gets preempted-        ("echo", user_programs::ECHO),   // reads keyboard lines via read(0)++    // --- User mode (process.rs, syscall.rs) ---------------------------------+    // The programs (built from user/*.rs) are ELF files on the FAT32 disk.+    // Each gets its own address space and runs in ring 3.+    const BIN: &str = "/mnt/disk0/bin";+    if fs::lookup(BIN).is_err() {+        kprintln!("[process] no {} -- was QEMU started with the disk image?", BIN);+    }+    let programs = [+        "hello", // syscalls + ramfs demo, exits 0+        "evil",  // touches kernel memory -> killed+        "spin",  // busy loop, gets preempted+        "demo",  // stage 6: FAT file, cwd, getdents, spawn + wait, pipes+        "echo",  // reads keyboard lines via read(0)     ];-    for (name, elf) in programs {-        if let Err(e) = process::spawn(name, elf) {-            kprintln!("[process] could not start {}: {:?}", name, e);+    for name in programs {+        let path = alloc::format!("{}/{}", BIN, name);+        if let Err(e) = process::spawn_path(&path, process::SpawnArgs::kernel(name)) {+            kprintln!("[process] could not start {}: {:?}", path, e);         }     } 
@@ -170,6 +170,28 @@ pub fn nx_enabled() -> bool {     Efer::read().contains(EferFlags::NO_EXECUTE_ENABLE) } +/// `count` physically *contiguous*, zeroed frames (device DMA buffers like+/// virtio queues must be contiguous in physical memory). Our allocator hands+/// out frames in address order, so consecutive frames are usually adjacent;+/// if a run gets broken (end of a memory region) we start over and simply+/// leak the frames we skipped. Fine for a few boot-time allocations.+pub fn alloc_contiguous_frames(count: usize) -> Option<PhysAddr> {+    let mut start = alloc_frame()?.start_address();+    let mut got = 1;+    while got < count {+        let frame = alloc_frame()?.start_address();+        if frame == start + (got as u64) * 4096 {+            got += 1;+        } else {+            start = frame; // not adjacent: restart the run here+            got = 1;+        }+    }+    let ptr: *mut u8 = phys_to_virt(start).as_mut_ptr();+    unsafe { core::ptr::write_bytes(ptr, 0, count * 4096) };+    Some(start)+}+ /// A frame filled with zeros (new page tables and user pages must not leak /// old data). fn alloc_zeroed_frame() -> Option<PhysFrame> {
@@ -0,0 +1,121 @@+//! PCI bus enumeration (the legacy "configuration mechanism #1").+//!+//! Every PCI function has a 256-byte *configuration space*: vendor/device+//! ids, class code, BARs (where its registers live), the command register...+//! On x86 we reach it through two I/O ports: write the address+//! (bus, device, function, register) to 0xCF8, then read/write the data at+//! 0xCFC. Enumerating = asking every possible (bus, device, function) for its+//! vendor id; 0xFFFF means "nobody here".+//!+//! Modern machines also offer memory-mapped config space (PCIe ECAM, found+//! via ACPI's MCFG table); the port method is simpler and QEMU supports both.+//! Reference: https://wiki.osdev.org/PCI++use alloc::vec::Vec;+use x86_64::instructions::port::Port;++const CONFIG_ADDRESS: u16 = 0xCF8;+const CONFIG_DATA: u16 = 0xCFC;++#[derive(Debug, Clone, Copy)]+pub struct PciDevice {+    pub bus: u8,+    pub device: u8,+    pub function: u8,+    pub vendor_id: u16,+    pub device_id: u16,+    pub class: u8,+    pub subclass: u8,+}++/// Read a 32-bit register (`offset` must be 4-byte aligned).+pub fn read_config(bus: u8, device: u8, function: u8, offset: u8) -> u32 {+    let address = 1u32 << 31 // enable bit+        | (bus as u32) << 16+        | (device as u32) << 11+        | (function as u32) << 8+        | (offset as u32 & 0xfc);+    unsafe {+        Port::<u32>::new(CONFIG_ADDRESS).write(address);+        Port::<u32>::new(CONFIG_DATA).read()+    }+}++pub fn write_config(bus: u8, device: u8, function: u8, offset: u8, value: u32) {+    let address = 1u32 << 31 | (bus as u32) << 16 | (device as u32) << 11 | (function as u32) << 8 | (offset as u32 & 0xfc);+    unsafe {+        Port::<u32>::new(CONFIG_ADDRESS).write(address);+        Port::<u32>::new(CONFIG_DATA).write(value);+    }+}++impl PciDevice {+    fn read(&self, offset: u8) -> u32 {+        read_config(self.bus, self.device, self.function, offset)+    }++    /// Base address register `n` (0-5), raw. Bit 0 set = I/O port space.+    pub fn bar(&self, n: u8) -> u32 {+        self.read(0x10 + 4 * n)+    }++    /// Let the device respond to I/O port accesses and do DMA (bus mastering).+    pub fn enable_io_and_bus_master(&self) {+        let command = self.read(0x04);+        write_config(self.bus, self.device, self.function, 0x04, command | 0b101);+    }++    /// Human-readable class (just the common ones).+    pub fn class_name(&self) -> &'static str {+        match (self.class, self.subclass) {+            (0x01, 0x01) => "IDE controller",+            (0x01, 0x00) => "SCSI storage (virtio-blk)",+            (0x01, 0x06) => "SATA controller",+            (0x01, _) => "storage controller",+            (0x02, _) => "network controller",+            (0x03, _) => "display controller",+            (0x06, 0x00) => "host bridge",+            (0x06, 0x01) => "ISA bridge",+            (0x06, 0x80) => "bridge (other)",+            (0x06, _) => "bridge",+            (0x0c, 0x03) => "USB controller",+            _ => "other",+        }+    }+}++fn probe(bus: u8, device: u8, function: u8) -> Option<PciDevice> {+    let id = read_config(bus, device, function, 0x00);+    let vendor_id = id as u16;+    if vendor_id == 0xffff {+        return None; // no such device+    }+    let class = read_config(bus, device, function, 0x08);+    Some(PciDevice {+        bus,+        device,+        function,+        vendor_id,+        device_id: (id >> 16) as u16,+        class: (class >> 24) as u8,+        subclass: (class >> 16) as u8,+    })+}++/// Every PCI function on every bus (brute force: 256 buses x 32 devices --+/// a few thousand port reads, instant in QEMU).+pub fn scan() -> Vec<PciDevice> {+    let mut found = Vec::new();+    for bus in 0..=255u8 {+        for device in 0..32u8 {+            let Some(first) = probe(bus, device, 0) else { continue };+            found.push(first);+            // Header type bit 7: multi-function device (functions 1-7 exist).+            let header_type = (read_config(bus, device, 0, 0x0c) >> 16) as u8;+            if header_type & 0x80 != 0 {+                found.extend((1..8).filter_map(|f| probe(bus, device, f)));+            }+        }+    }+    found+}
@@ -1,29 +1,42 @@-//! Processes = a thread + its own address space + a file descriptor table.+//! Processes = a thread + its own address space + a file descriptor table+//! (+ arguments, a current directory and a parent). //!-//! Starting one (`spawn`):+//! 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 = the console (tty.rs)-//!   5. spawn a thread with that CR3. The thread starts in the kernel-//!      (`user_thread_entry`) and "returns" into ring 3 with `iretq`.+//!   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`. //!-//! From then on the process only comes back into the kernel through-//! interrupts (timer -> maybe another thread runs), exceptions (page fault in-//! user mode -> the process is killed, see interrupts.rs) and syscalls.+//! 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.+//!+//! 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, then+//! removes the child from the process table ("reaps" it -- until then it's+//! what Unix calls a zombie). //! //! Not done yet (exercises): freeing a dead process's frames and page tables,-//! fork/exec/wait syscalls, passing arguments, more than one thread per process.+//! reparenting orphans, signals/kill, more than one thread per process.  use crate::elf::{self, ElfError}; use crate::errno::Errno;-use crate::fs::file::File;+use crate::fs::{self, file::File}; use crate::memory::{self, USER_END};-use crate::{serial_println, thread, tty::Console};-use alloc::{sync::Arc, vec, vec::Vec};+use crate::thread::{self, WaitQueue};+use crate::{serial_println, tty::Console};+use alloc::{boxed::Box, collections::BTreeMap, string::String, sync::Arc, vec, vec::Vec}; use core::sync::atomic::{AtomicU64, Ordering}; use spin::Mutex; use x86_64::VirtAddr;+use x86_64::instructions::interrupts::without_interrupts; use x86_64::structures::paging::{Page, PageTableFlags, Size4KiB};  /// The user stack: 16 KiB, ending one (unmapped) page below USER_END.@@ -31,6 +44,11 @@ pub const USER_STACK_TOP: u64 = USER_END - 0x1000; pub const USER_STACK_SIZE: u64 = 16 * 1024; /// 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;  #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ExitStatus {@@ -40,18 +58,77 @@ pub enum ExitStatus {     Killed(&'static str), } +impl ExitStatus {+    /// The number `wait` returns.+    pub fn code(self) -> i64 {+        match self {+            ExitStatus::Exited(code) => code,+            ExitStatus::Killed(_) => KILLED_EXIT_CODE,+        }+    }+}+ pub struct Process {     pub pid: u64,-    pub name: &'static str,+    /// Parent pid; 0 = started by the kernel itself.+    pub parent: u64,+    pub name: String,+    /// Print every syscall this process makes (`spawn` flag; `run --trace`).+    pub trace: bool,+    /// Current directory (always a normalized absolute path).+    pub cwd: Mutex<String>,     entry: u64,+    args: Vec<String>,     fds: Mutex<Vec<Option<Arc<dyn File>>>>,+    /// Locked with interrupts off only (it's read in a wait_until condition).     status: Mutex<Option<ExitStatus>>,+    exited: WaitQueue, }  static NEXT_PID: AtomicU64 = AtomicU64::new(1);+/// All processes that haven't been reaped yet, by pid.+static PROCESSES: Mutex<BTreeMap<u64, Arc<Process>>> = Mutex::new(BTreeMap::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 stdio: [Arc<dyn File>; 3],+    pub cwd: String,+    pub parent: u64,+    pub trace: bool,+}++impl SpawnArgs {+    /// Defaults for a process started by the kernel: console stdio, cwd "/".+    pub fn kernel(name: &str) -> SpawnArgs {+        let console: Arc<dyn File> = Arc::new(Console);+        SpawnArgs {+            args: vec![String::from(name)],+            stdio: [console.clone(), console.clone(), console],+            cwd: String::from("/"),+            parent: 0,+            trace: false,+        }+    }+}++/// 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"+    })+}++/// Start ELF program `elf_image` from the kernel (console stdio, no args).+pub fn spawn(name: &str, elf_image: &[u8]) -> Result<Arc<Process>, ElfError> {+    spawn_image(name, elf_image, SpawnArgs::kernel(name))+} -/// Load ELF program `elf` into a new address space and start it.-pub fn spawn(name: &'static str, elf_image: &[u8]) -> Result<Arc<Process>, ElfError> {+/// 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)?;     let entry = elf::load(elf_image, l4)?; @@ -65,16 +142,24 @@ pub fn spawn(name: &'static str, elf_image: &[u8]) -> Result<Arc<Process>, ElfEr         memory::map_user_page(l4, page, stack_flags).ok_or(ElfError::OutOfMemory)?;     } -    let console: Arc<dyn File> = Arc::new(Console);+    let [stdin, stdout, stderr] = spawn.stdio;     let process = Arc::new(Process {         pid: NEXT_PID.fetch_add(1, Ordering::Relaxed),-        name,+        parent: spawn.parent,+        name: String::from(name),+        trace: spawn.trace,+        cwd: Mutex::new(spawn.cwd),         entry,-        fds: Mutex::new(vec![Some(console.clone()), Some(console.clone()), Some(console)]),+        args: spawn.args,+        fds: Mutex::new(vec![Some(stdin), Some(stdout), Some(stderr)]),         status: Mutex::new(None),+        exited: WaitQueue::new(),     });+    PROCESSES.lock().insert(process.pid, process.clone());     serial_println!("[process] started pid {} ({}), entry {:#x}", process.pid, name, entry);-    thread::spawn_with(name, user_thread_entry, l4, Some(process.clone()));+    // Thread names are &'static str; leaking a few bytes per process is fine here.+    let thread_name: &'static str = Box::leak(String::from(name).into_boxed_str());+    thread::spawn_with(thread_name, user_thread_entry, l4, Some(process.clone()));     Ok(process) } @@ -82,15 +167,37 @@ pub fn spawn(name: &'static str, elf_image: &[u8]) -> Result<Arc<Process>, ElfEr /// with the process's CR3 and kernel stack). 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 top.-    unsafe { enter_user_mode(process.entry, USER_STACK_TOP - 8) }+    // 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) } }  /// 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).-unsafe fn enter_user_mode(rip: u64, rsp: u64) -> ! {+/// 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!(@@ -101,7 +208,7 @@ unsafe fn enter_user_mode(rip: u64, rsp: u64) -> ! {             "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 esi, esi", "xor edi, edi", "xor ebp, ebp",+            "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",@@ -110,6 +217,8 @@ unsafe fn enter_user_mode(rip: u64, rsp: u64) -> ! {             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)         );     }@@ -137,50 +246,63 @@ impl Process {     }      pub fn close(&self, fd: u64) -> Result<(), Errno> {-        let mut fds = self.fds.lock();-        match fds.get_mut(fd as usize) {-            Some(slot @ Some(_)) => {-                *slot = None; // dropping the Arc closes the file-                Ok(())+        let file = {+            let mut fds = self.fds.lock();+            match fds.get_mut(fd as usize) {+                Some(slot @ Some(_)) => slot.take(),+                _ => return Err(Errno::EBADF),             }-            _ => Err(Errno::EBADF),-        }+        };+        drop(file); // dropping the last Arc closes the file (after unlocking)+        Ok(())     }      pub fn status(&self) -> Option<ExitStatus> {-        *self.status.lock()+        without_interrupts(|| *self.status.lock())     }++    /// Make `path` absolute, relative to this process's current directory.+    pub fn resolve(&self, path: &str) -> Result<String, Errno> {+        let cwd = self.cwd.lock().clone();+        fs::normalize(&cwd, path)+    }+}++/// Look up a live (or not yet reaped) process.+pub fn get(pid: u64) -> Option<Arc<Process>> {+    PROCESSES.lock().get(&pid).cloned() }  /// End the current process (exit syscall, or killed by an exception). pub fn exit_current(status: ExitStatus) -> ! {     if let Some(process) = thread::current_process() {-        process.fds.lock().clear(); // close all files-        *process.status.lock() = Some(status);+        // Close all files (a pipe's reader sees EOF once its writers are gone).+        let files = core::mem::take(&mut *process.fds.lock());+        drop(files);         match status {-            ExitStatus::Exited(code) => crate::kprintln!(-                "[process] pid {} ({}) exited with code {}",-                process.pid,-                process.name,-                code-            ),-            ExitStatus::Killed(why) => crate::kprintln!(-                "[process] pid {} ({}) killed: {}",-                process.pid,-                process.name,-                why-            ),+            ExitStatus::Exited(code) => {+                crate::kprintln!("[process] pid {} ({}) exited with code {}", process.pid, process.name, code)+            }+            ExitStatus::Killed(why) => {+                crate::kprintln!("[process] pid {} ({}) killed: {}", process.pid, process.name, why)+            }         }+        without_interrupts(|| *process.status.lock() = Some(status));+        process.exited.wake_all(); // the parent may be blocked in wait()     }     thread::exit_current() } -/// Block (the calling kernel thread) until `process` has ended.+/// Block (the calling thread) until `process` has ended, then remove it from+/// the process table. Used by the kernel and by the `wait` syscall. pub fn wait(process: &Process) -> ExitStatus {-    loop {-        if let Some(status) = process.status() {-            return status;-        }-        x86_64::instructions::hlt(); // the timer lets the process run meanwhile-    }+    let status = process.exited.wait_until(|| *process.status.lock());+    PROCESSES.lock().remove(&process.pid);+    status+}++/// The `wait(pid)` syscall: only a process's parent may wait for it.+pub fn wait_child(parent: u64, pid: u64) -> Result<ExitStatus, Errno> {+    let child = get(pid).filter(|c| c.parent == parent).ok_or(Errno::ECHILD)?;+    Ok(wait(&child)) }
@@ -5,15 +5,37 @@ //! `syscall` instruction itself clobbers rcx (return address) and r11 (saved //! RFLAGS); every other register is preserved for the caller. //!-//! | nr | name        | arguments                      | note |-//! |----|-------------|--------------------------------|------|-//! |  0 | read        | fd, buf, len                   | fd 0 blocks until a line is typed |-//! |  1 | write       | fd, buf, len                   | fd 1/2 = console |-//! |  2 | open        | path_ptr, path_len, flags      | Linux takes a C string; we take ptr+len. flags: O_CREAT = 0o100 |-//! |  3 | close       | fd                             | |-//! | 24 | sched_yield |                                | |-//! | 39 | getpid      |                                | |-//! | 60 | exit        | code                           | |+//! 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.+//!+//! | 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) |+//! |   3 | close       | fd                                  | |+//! |   4 | stat        | path, path_len, *Stat               | our own small `Stat {kind, size}` |+//! |   5 | fstat       | fd, *Stat                           | |+//! |   8 | lseek       | fd, offset, whence                  | SEEK_SET/CUR/END = 0/1/2 |+//! |  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 |+//! |  39 | getpid      |                                     | |+//! |  60 | exit        | code                                | |+//! |  61 | wait        | pid                                 | returns the exit code (139 if killed). Linux wait4 is fancier |+//! |  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 only |+//! | 217 | getdents64  | fd, buf, len                        | Linux `struct linux_dirent64` records |+//! | 500 | spawn       | path, path_len, argv, argv_len, *SpawnOptions | ours: start a program, returns its pid (see process.rs for why not fork/exec) |+//!+//! spawn's `argv` is all arguments separated by NUL bytes ("ls\0-l\0/bin");+//! empty means just [program name]. `SpawnOptions` (or a null pointer for+//! the defaults) picks the child's stdin/stdout/stderr from the caller's fds+//! (-1 = same as mine) and flags (SPAWN_TRACE = print its syscalls). //! //! What happens on `syscall` (the CPU does steps 1-3 in one instruction): //!   1. rcx = user RIP (next instruction), r11 = user RFLAGS@@ -33,9 +55,10 @@ //! OS use, and it's faster.  use crate::errno::Errno;-use crate::fs::{self, file::InodeFile};+use crate::fs::file::{InodeFile, Stat};+use crate::fs::{self, InodeKind}; use crate::{memory, process, thread};-use alloc::sync::Arc;+use alloc::{string::String, sync::Arc, vec::Vec}; use core::arch::global_asm; use core::sync::atomic::{AtomicU64, Ordering}; @@ -43,11 +66,63 @@ pub const SYS_READ: u64 = 0; pub const SYS_WRITE: u64 = 1; pub const SYS_OPEN: u64 = 2; pub const SYS_CLOSE: u64 = 3;+pub const SYS_STAT: u64 = 4;+pub const SYS_FSTAT: u64 = 5;+pub const SYS_LSEEK: u64 = 8;+pub const SYS_PIPE: u64 = 22; 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_GETCWD: u64 = 79;+pub const SYS_CHDIR: u64 = 80;+pub const SYS_MKDIR: u64 = 83;+pub const SYS_RMDIR: u64 = 84;+pub const SYS_UNLINK: u64 = 87;+pub const SYS_GETDENTS64: u64 = 217;+pub const SYS_SPAWN: u64 = 500;  pub const O_CREAT: u64 = 0o100;+pub const O_TRUNC: u64 = 0o1000;+pub const SPAWN_TRACE: u64 = 1;++/// Name of a syscall number (for tracing).+pub fn name(nr: u64) -> &'static str {+    match nr {+        SYS_READ => "read",+        SYS_WRITE => "write",+        SYS_OPEN => "open",+        SYS_CLOSE => "close",+        SYS_STAT => "stat",+        SYS_FSTAT => "fstat",+        SYS_LSEEK => "lseek",+        SYS_PIPE => "pipe",+        SYS_SCHED_YIELD => "sched_yield",+        SYS_SLEEP_MS => "sleep_ms",+        SYS_GETPID => "getpid",+        SYS_EXIT => "exit",+        SYS_WAIT => "wait",+        SYS_GETCWD => "getcwd",+        SYS_CHDIR => "chdir",+        SYS_MKDIR => "mkdir",+        SYS_RMDIR => "rmdir",+        SYS_UNLINK => "unlink",+        SYS_GETDENTS64 => "getdents64",+        SYS_SPAWN => "spawn",+        _ => "?",+    }+}++/// spawn's options, as laid out in user memory.+#[repr(C)]+#[derive(Clone, Copy)]+pub struct SpawnOptions {+    pub stdin: i64, // caller's fd to use as the child's fd 0; -1 = caller's fd 0+    pub stdout: i64,+    pub stderr: i64,+    pub flags: u64,+}  /// Kernel stack top of the running thread (set by the scheduler). #[unsafe(no_mangle)]@@ -133,24 +208,52 @@ global_asm!( );  /// 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 {+extern "C" fn syscall_dispatch(nr: u64, a1: u64, a2: u64, a3: u64, a4: u64, a5: u64) -> i64 {+    let process = thread::current_process();+    let trace = process.as_ref().is_some_and(|p| p.trace);+    if trace && nr == SYS_EXIT {+        crate::kprintln!("[trace {}] exit({})", process.as_ref().unwrap().pid, a1 as i64);+    }+    drop(process); // don't hold an Arc while blocking/exiting     let result = match nr {         SYS_READ => sys_read(a1, a2, a3),         SYS_WRITE => sys_write(a1, a2, a3),         SYS_OPEN => sys_open(a1, a2, a3),-        SYS_CLOSE => sys_close(a1),+        SYS_CLOSE => current().and_then(|p| p.close(a1)).map(|_| 0),+        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_PIPE => sys_pipe(a1),         SYS_SCHED_YIELD => {             thread::yield_now();             Ok(0)         }+        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)+        }         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)).map(|s| s.code() as u64),+        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_GETDENTS64 => sys_getdents64(a1, a2, a3),+        SYS_SPAWN => sys_spawn(a1, a2, a3, a4, a5),         _ => Err(Errno::ENOSYS),     };-    match result {+    let ret = match result {         Ok(value) => value as i64,         Err(errno) => -(errno as i64),+    };+    if trace {+        let pid = current().map(|p| p.pid).unwrap_or(0);+        crate::kprintln!("[trace {}] {}({:#x}, {:#x}, {:#x}) = {}", pid, name(nr), a1, a2, a3, ret);     }+    ret }  fn current() -> Result<Arc<process::Process>, Errno> {@@ -160,6 +263,9 @@ fn current() -> Result<Arc<process::Process>, Errno> { /// Turn a user pointer + length into a slice, after checking that the user /// may really access that memory. Never trust pointers from user space! 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) {         return Err(Errno::EFAULT);     }@@ -168,6 +274,9 @@ pub fn user_slice(ptr: u64, len: u64) -> Result<&'static [u8], Errno> {  /// Like `user_slice`, but the user must be allowed to *write* the memory. 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) {         return Err(Errno::EFAULT);     }@@ -186,19 +295,115 @@ fn sys_write(fd: u64, buf: u64, len: u64) -> Result<u64, Errno> {     file.write(buf).map(|n| n as u64) } -fn sys_open(path_ptr: u64, path_len: u64, flags: u64) -> Result<u64, Errno> {-    if path_len > 255 {+/// Read a path argument and make it absolute (relative to the cwd).+fn user_path(ptr: u64, len: u64) -> Result<String, Errno> {+    if len > 4096 {         return Err(Errno::ENAMETOOLONG);     }-    let path = core::str::from_utf8(user_slice(path_ptr, path_len)?).map_err(|_| Errno::EINVAL)?;-    let inode = match fs::lookup(path) {-        Err(Errno::ENOENT) if flags & O_CREAT != 0 => fs::create(path, fs::InodeKind::File)?,+    let path = core::str::from_utf8(user_slice(ptr, len)?).map_err(|_| Errno::EINVAL)?;+    current()?.resolve(path)+}++/// Copy a plain-data struct out to user memory.+fn copy_to_user<T: Copy>(ptr: u64, value: &T) -> Result<(), Errno> {+    let size = core::mem::size_of::<T>();+    let dst = user_slice_mut(ptr, size as u64)?;+    let src = unsafe { core::slice::from_raw_parts(value as *const T as *const u8, size) };+    dst.copy_from_slice(src);+    Ok(())+}++fn sys_open(path_ptr: u64, path_len: u64, flags: u64) -> Result<u64, Errno> {+    let path = user_path(path_ptr, path_len)?;+    let inode = match fs::lookup(&path) {+        Err(Errno::ENOENT) if flags & O_CREAT != 0 => fs::create(&path, InodeKind::File)?,         other => other?,     };+    if flags & O_TRUNC != 0 && inode.kind() == InodeKind::File {+        inode.truncate()?;+    }     let file = Arc::new(InodeFile::open(inode)?);     current()?.add_file(file) } -fn sys_close(fd: u64) -> Result<u64, Errno> {-    current()?.close(fd).map(|_| 0)+fn sys_stat(path_ptr: u64, path_len: u64, stat_ptr: u64) -> Result<u64, Errno> {+    let inode = fs::lookup(&user_path(path_ptr, path_len)?)?;+    copy_to_user(stat_ptr, &Stat::of(&*inode))?;+    Ok(0)+}++fn sys_fstat(fd: u64, stat_ptr: u64) -> Result<u64, Errno> {+    let stat = current()?.file(fd)?.stat()?;+    copy_to_user(stat_ptr, &stat)?;+    Ok(0)+}++fn sys_pipe(fds_ptr: u64) -> Result<u64, Errno> {+    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)) {+        Ok(fd) => fd,+        Err(e) => {+            process.close(read_fd)?;+            return Err(e);+        }+    };+    copy_to_user(fds_ptr, &[read_fd as u32, write_fd as u32])?;+    Ok(0)+}++fn sys_getcwd(buf: u64, len: u64) -> Result<u64, Errno> {+    let cwd = current()?.cwd.lock().clone();+    if (len as usize) < cwd.len() {+        return Err(Errno::ERANGE);+    }+    user_slice_mut(buf, cwd.len() as u64)?.copy_from_slice(cwd.as_bytes());+    Ok(cwd.len() as u64)+}++fn sys_chdir(path_ptr: u64, path_len: u64) -> Result<u64, Errno> {+    let path = user_path(path_ptr, path_len)?;+    if fs::lookup(&path)?.kind() != InodeKind::Directory {+        return Err(Errno::ENOTDIR);+    }+    *current()?.cwd.lock() = path;+    Ok(0)+}++fn sys_getdents64(fd: u64, buf: u64, len: u64) -> Result<u64, Errno> {+    let file = current()?.file(fd)?;+    file.getdents(user_slice_mut(buf, len)?).map(|n| n as u64)+}++fn sys_spawn(path_ptr: u64, path_len: u64, argv_ptr: u64, argv_len: u64, opts_ptr: u64) -> Result<u64, Errno> {+    let parent = current()?;+    let path = user_path(path_ptr, path_len)?;+    if argv_len as usize > process::MAX_ARGS_SIZE {+        return Err(Errno::E2BIG);+    }+    let blob = core::str::from_utf8(user_slice(argv_ptr, argv_len)?).map_err(|_| Errno::EINVAL)?;+    let mut args: Vec<String> = blob.split('\0').filter(|a| !a.is_empty()).map(String::from).collect();+    if args.is_empty() {+        args.push(String::from(path.rsplit('/').next().unwrap_or("")));+    }+    let opts = if opts_ptr == 0 {+        SpawnOptions { stdin: -1, stdout: -1, stderr: -1, flags: 0 }+    } else {+        let bytes = user_slice(opts_ptr, core::mem::size_of::<SpawnOptions>() as u64)?;+        unsafe { core::ptr::read_unaligned(bytes.as_ptr() as *const SpawnOptions) }+    };+    // Child fd n = the caller's fd chosen in the options (-1: the caller's fd n).+    let pick = |n: u64, choice: i64| parent.file(if choice < 0 { n } else { choice as u64 });+    let stdio = [pick(0, opts.stdin)?, pick(1, opts.stdout)?, pick(2, opts.stderr)?];+    let spawn = process::SpawnArgs {+        args,+        stdio,+        cwd: parent.cwd.lock().clone(),+        parent: parent.pid,+        trace: opts.flags & SPAWN_TRACE != 0,+    };+    drop(parent);+    process::spawn_path(&path, spawn).map(|child| child.pid) }
@@ -27,6 +27,18 @@ //! syscall entry at its kernel stack, so interrupts and syscalls from ring 3 //! land on the right stack. //!+//! Blocking: a thread waiting for something (keyboard input, a child+//! process, a pipe, a timeout) is marked `blocked` and the scheduler skips it+//! until someone calls `wake` (or its sleep deadline passes). That's what+//! `WaitQueue` is for. If *every* thread is blocked, the CPU idles with `hlt`+//! until an interrupt wakes somebody up.+//!+//! Locking rule (single CPU!): any lock that is also taken with interrupts+//! *off* (the scheduler's, a wait queue's, a pipe buffer's...) must *always*+//! be taken with interrupts off. Otherwise a thread could be preempted while+//! holding it, and the next thread would spin on it forever, unable to be+//! preempted itself.+//! //! A *new* thread has never been interrupted, so `spawn` builds a fake stack //! that looks like `switch_context` saved it, with the "return address" //! pointing at `thread_trampoline`, which enables interrupts and calls the@@ -40,16 +52,21 @@ use x86_64::instructions::interrupts; use x86_64::registers::control::Cr3; use x86_64::structures::paging::PhysFrame; -/// 16 KiB per thread stack (no guard page! a deep recursion in a thread-/// silently overwrites whatever is below its stack on the heap).-const STACK_SIZE: usize = 16 * 1024;+/// 64 KiB per thread stack. There's no guard page (stacks are plain heap+/// memory), so overflowing one silently overwrites whatever lies below it.+/// We at least *detect* that afterwards: the lowest 8 bytes hold+/// `STACK_CANARY`, checked whenever the scheduler switches away from a+/// thread. (16 KiB was enough until syscalls went through the FAT driver:+/// unoptimized debug builds use big stack frames.)+const STACK_SIZE: usize = 64 * 1024;+const STACK_CANARY: u64 = 0x5AFE_57AC_C0DE_CAFE;  struct Thread {     name: &'static str,     /// Saved stack pointer while the thread is not running.     rsp: u64,     /// The thread's stack. `None` for the boot thread (uses the bootloader's stack).-    _stack: Option<Vec<u8>>,+    stack: Option<Vec<u8>>,     /// Top of that stack (0 for the boot thread). Becomes TSS.RSP0 and the     /// syscall stack while this thread runs.     kernel_stack_top: u64,@@ -58,6 +75,10 @@ struct Thread {     /// The process this thread belongs to (None = pure kernel thread).     process: Option<Arc<Process>>,     finished: bool,+    /// Waiting for something; the scheduler skips blocked threads.+    blocked: bool,+    /// If blocked by `sleep_ticks`: wake up at this tick.+    wake_at: Option<u64>, }  struct Scheduler {@@ -78,6 +99,7 @@ pub fn spawn(name: &'static str, entry: fn()) { /// process's address space `cr3` active; `entry` then drops to ring 3. pub fn spawn_with(name: &'static str, entry: fn(), cr3: PhysFrame, process: Option<Arc<Process>>) {     let mut stack = vec![0u8; STACK_SIZE];+    stack[..8].copy_from_slice(&STACK_CANARY.to_ne_bytes());      // Build the initial stack, top down (stacks grow towards lower addresses):     //   [return address = thread_trampoline]@@ -99,35 +121,51 @@ pub fn spawn_with(name: &'static str, entry: fn(), cr3: PhysFrame, process: Opti     let thread = Box::new(Thread {         name,         rsp,-        _stack: Some(stack),+        stack: Some(stack),         kernel_stack_top: top,         cr3,         process,         finished: false,+        blocked: false,+        wake_at: None,     });      // The timer handler also takes this lock, so keep interrupts off while we     // hold it -- otherwise the handler would spin forever on our lock.     interrupts::without_interrupts(|| {         let mut scheduler = SCHEDULER.lock();-        if scheduler.threads.is_empty() {-            // First spawn: register the code that's running right now (the-            // boot/main thread) as thread 0. Its rsp is filled in the first-            // time we switch away from it.-            scheduler.threads.push(Box::new(Thread {-                name: "main",-                rsp: 0,-                _stack: None,-                kernel_stack_top: 0,-                cr3: Cr3::read().0,-                process: None,-                finished: false,-            }));+        register_main_thread(&mut scheduler);+        // Free the stacks of threads that have finished (we can't do that in+        // exit_current: the thread is still running on its stack there).+        let current = scheduler.current;+        for (i, t) in scheduler.threads.iter_mut().enumerate() {+            if t.finished && i != current {+                t.stack = None;+                t.process = None;+            }         }         scheduler.threads.push(thread);     }); } +/// On first use, register the code that's running right now (the boot/main+/// thread) as thread 0. Its rsp is filled in the first time we switch away.+fn register_main_thread(scheduler: &mut Scheduler) {+    if scheduler.threads.is_empty() {+        scheduler.threads.push(Box::new(Thread {+            name: "main",+            rsp: 0,+            stack: None,+            kernel_stack_top: 0,+            cr3: Cr3::read().0,+            process: None,+            finished: false,+            blocked: false,+            wake_at: None,+        }));+    }+}+ /// Called from the timer interrupt (interrupts disabled, EOI already sent). /// Switches to the next runnable thread, round-robin. Returns when this /// thread is scheduled again.@@ -139,12 +177,27 @@ pub fn schedule() {             return; // no threads spawned yet         }         let current = s.current;-        // Find the next thread that hasn't finished.-        let next = (1..=n).map(|i| (current + i) % n).find(|&i| !s.threads[i].finished);+        // Wake up sleepers whose time has come.+        let now = crate::interrupts::TICKS.load(core::sync::atomic::Ordering::Relaxed);+        for t in s.threads.iter_mut() {+            if t.wake_at.is_some_and(|at| now >= at) {+                t.wake_at = None;+                t.blocked = false;+            }+        }+        // Find the next thread that can run.+        let next = (1..=n)+            .map(|i| (current + i) % n)+            .find(|&i| !s.threads[i].finished && !s.threads[i].blocked);         let next = match next {             Some(next) if next != current => next,             _ => return, // nobody else to run         };+        if let Some(stack) = &s.threads[current].stack {+            if stack[..8] != STACK_CANARY.to_ne_bytes() {+                panic!("kernel stack overflow in thread '{}'", s.threads[current].name);+            }+        }         s.current = next;          // Prepare the CPU for the next thread *before* switching stacks.@@ -202,13 +255,115 @@ pub fn current_name() -> &'static str {     }) } -/// Sleep for `ticks` timer ticks (~55 ms each), letting other threads run.+/// Id of the running thread (its index in the thread list; ids are never reused).+pub fn current_id() -> usize {+    interrupts::without_interrupts(|| {+        let mut s = SCHEDULER.lock();+        register_main_thread(&mut s);+        s.current+    })+}++/// Make thread `id` runnable again (no-op if it isn't blocked).+pub fn wake(id: usize) {+    interrupts::without_interrupts(|| {+        if let Some(t) = SCHEDULER.lock().threads.get_mut(id) {+            t.blocked = false;+            t.wake_at = None;+        }+    });+}++fn is_blocked(id: usize) -> bool {+    SCHEDULER.lock().threads[id].blocked+}++/// Mark the running thread blocked, then run other threads until somebody+/// wakes it. Must be called with interrupts *off* (so a wake-up can't slip in+/// between the caller checking its condition and us blocking); returns with+/// interrupts still off.+fn block_current(wake_at: Option<u64>) {+    let me = {+        let mut s = SCHEDULER.lock();+        register_main_thread(&mut s);+        let me = s.current;+        s.threads[me].blocked = true;+        s.threads[me].wake_at = wake_at;+        me+    };+    loop {+        schedule(); // returns once we're picked again -- or if nobody else can run+        if !is_blocked(me) {+            return;+        }+        // Every thread is blocked: idle until an interrupt (timer, keyboard)+        // possibly wakes someone. `enable_and_hlt` = `sti; hlt` with no gap,+        // so the interrupt can't sneak in before the hlt.+        interrupts::enable_and_hlt();+        interrupts::disable();+        if !is_blocked(me) {+            return;+        }+    }+}++/// Sleep for `ticks` timer ticks (~55 ms each). The thread is *blocked*+/// meanwhile, so it costs no CPU time; the scheduler wakes it up. pub fn sleep_ticks(ticks: u64) {     use crate::interrupts::TICKS;     use core::sync::atomic::Ordering;-    let wake_at = TICKS.load(Ordering::Relaxed) + ticks;-    while TICKS.load(Ordering::Relaxed) < wake_at {-        x86_64::instructions::hlt(); // the timer interrupt may switch threads here+    let was_enabled = interrupts::are_enabled();+    interrupts::disable();+    let wake_at = TICKS.load(Ordering::Relaxed) + ticks.max(1);+    block_current(Some(wake_at));+    if was_enabled {+        interrupts::enable();+    }+}++/// Threads waiting for some event (like a Linux `wait_queue_head_t`).+///+/// Usage: `queue.wait_until(|| condition)` blocks until the closure returns+/// `Some(value)`; whoever makes the condition true calls `queue.wake_all()`.+/// The closure runs with interrupts off, so it must be quick and only touch+/// locks that follow the locking rule at the top of this file.+pub struct WaitQueue {+    waiting: Mutex<Vec<usize>>,+}++impl WaitQueue {+    pub const fn new() -> Self {+        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();+            // Check and go to sleep without interrupts in between: otherwise+            // a wake_all() could happen right after the check, and we'd+            // sleep forever (the classic "lost wake-up" bug).+            if let Some(value) = condition() {+                break value;+            }+            let me = current_id();+            self.waiting.lock().push(me);+            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();+        }+        result+    }++    pub fn wake_all(&self) {+        interrupts::without_interrupts(|| {+            for id in self.waiting.lock().drain(..) {+                wake(id);+            }+        });     } } 
@@ -7,7 +7,8 @@ //! `read(0, ...)` gets a complete, already-edited line.  use crate::errno::Errno;-use crate::fs::file::File;+use crate::fs::file::{File, KIND_CONSOLE, Stat};+use crate::thread::WaitQueue; use crate::{print, serial_print, vga_buffer}; use alloc::string::String; use conquer_once::spin::OnceCell;@@ -19,6 +20,9 @@ static LINE: Mutex<String> = Mutex::new(String::new()); /// Finished lines, as bytes, waiting for `read(0, ...)`. static INPUT: OnceCell<ArrayQueue<u8>> = OnceCell::uninit(); +/// Threads blocked in `Console::read`, waiting for a line.+static INPUT_WAIT: WaitQueue = WaitQueue::new();+ fn input() -> &'static ArrayQueue<u8> {     INPUT.get_or_init(|| ArrayQueue::new(4096)) }@@ -41,6 +45,7 @@ pub fn input_char(c: char) {                 let _ = input().push(b); // queue full: drop (nobody is reading)             }             line.clear();+            INPUT_WAIT.wake_all(); // a reader may be blocked in Console::read         }         c => {             print!("{}", c);@@ -61,23 +66,23 @@ impl File for Console {         if buf.is_empty() {             return Ok(0);         }-        // Sleep until a line arrives. Syscalls run with interrupts enabled,-        // so `hlt` wakes on the next interrupt (and the timer may switch to-        // other threads meanwhile). Lines are queued whole, so once the-        // first byte is there, the rest of the line is too.-        while input().is_empty() {-            x86_64::instructions::hlt();-        }-        let mut n = 0;-        while n < buf.len() {-            let Some(b) = input().pop() else { break };-            buf[n] = b;-            n += 1;-            if b == b'\n' {-                break;+        // Block until a line arrives: the thread is parked on INPUT_WAIT+        // (using no CPU) and `input_char` wakes it when Enter is pressed.+        // Lines are queued whole, so once the first byte is there the rest+        // of the line is too. (The closure runs with interrupts off; the+        // ArrayQueue is lock-free, so that's fine.)+        Ok(INPUT_WAIT.wait_until(|| {+            let mut n = 0;+            while n < buf.len() {+                let Some(b) = input().pop() else { break };+                buf[n] = b;+                n += 1;+                if b == b'\n' {+                    break;+                }             }-        }-        Ok(n)+            if n > 0 { Some(n) } else { None }+        }))     }      fn write(&self, buf: &[u8]) -> Result<usize, Errno> {@@ -92,4 +97,8 @@ impl File for Console {         }         Ok(buf.len())     }++    fn stat(&self) -> Result<Stat, Errno> {+        Ok(Stat { kind: KIND_CONSOLE, size: 0 })+    } }
@@ -0,0 +1,133 @@+//! Block layer: PCI enumeration, the virtio-blk driver (against the+//! target/disk.img QEMU attaches), partition table parsing, RAM disks.+#![no_std]+#![no_main]+#![feature(custom_test_frameworks)]+#![test_runner(rust_kernel::test_runner)]+#![reexport_test_harness_main = "test_main"]++extern crate alloc;++use alloc::{sync::Arc, vec};+use bootloader::{BootInfo, entry_point};+use core::panic::PanicInfo;+use rust_kernel::block::{self, BlockDevice, RamDisk, SECTOR_SIZE, partition};+use rust_kernel::errno::Errno;+use rust_kernel::pci;++entry_point!(main);++fn main(boot_info: &'static BootInfo) -> ! {+    rust_kernel::init();+    rust_kernel::init_memory(boot_info);+    block::init(&pci::scan());+    test_main();+    rust_kernel::hlt_loop();+}++#[test_case]+fn pci_scan_finds_the_virtio_disk() {+    let devices = pci::scan();+    // QEMU's i440FX machine always has the host bridge at 00:00.0.+    assert_eq!((devices[0].bus, devices[0].device, devices[0].vendor_id), (0, 0, 0x8086));+    assert!(devices.iter().any(|d| d.vendor_id == 0x1af4 && d.device_id == 0x1001));+}++#[test_case]+fn virtio_disk_reads_the_gpt_image() {+    let disk = block::get("vda").expect("no vda: is target/disk.img attached?");+    assert_eq!(disk.sector_count(), 64 * 1024 * 1024 / 512);+    let mut sector = [0u8; SECTOR_SIZE];+    disk.read_sectors(0, &mut sector).unwrap();+    assert_eq!(&sector[510..], &[0x55, 0xAA]); // protective MBR signature+    assert_eq!(sector[446 + 4], 0xEE); // partition type "GPT protective"+    disk.read_sectors(1, &mut sector).unwrap();+    assert_eq!(&sector[..8], b"EFI PART");+}++#[test_case]+fn multi_sector_reads_match_single_sector_reads() {+    // 20 sectors = more than one 4 KiB bounce-buffer page per request.+    let disk = block::get("vda").unwrap();+    let mut big = vec![0u8; 20 * SECTOR_SIZE];+    disk.read_sectors(2048, &mut big).unwrap();+    let mut one = [0u8; SECTOR_SIZE];+    for i in 0..20 {+        disk.read_sectors(2048 + i as u64, &mut one).unwrap();+        assert_eq!(&big[i * SECTOR_SIZE..(i + 1) * SECTOR_SIZE], &one[..]);+    }+}++#[test_case]+fn virtio_write_then_read_back() {+    // QEMU runs the disk with snapshot=on, so this doesn't touch the file.+    let disk = block::get("vda").unwrap();+    let data: alloc::vec::Vec<u8> = (0..2 * SECTOR_SIZE).map(|i| (i % 251) as u8).collect();+    disk.write_sectors(100, &data).unwrap();+    let mut back = vec![0u8; 2 * SECTOR_SIZE];+    disk.read_sectors(100, &mut back).unwrap();+    assert_eq!(back, data);+}++#[test_case]+fn out_of_range_and_partial_sectors_fail() {+    let disk = block::get("vda").unwrap();+    let mut sector = [0u8; SECTOR_SIZE];+    assert_eq!(disk.read_sectors(disk.sector_count(), &mut sector), Err(Errno::EIO));+    assert_eq!(disk.read_sectors(0, &mut sector[..100]), Err(Errno::EINVAL));+}++#[test_case]+fn gpt_partition_is_found_and_registered() {+    let disk = block::get("vda").unwrap();+    let (scheme, parts) = partition::scan(&disk).unwrap();+    assert_eq!(scheme, "GPT");+    assert_eq!(parts.len(), 1);+    assert_eq!((parts[0].start, parts[0].sectors), (2048, 126976));+    assert_eq!(parts[0].name, "fat32");+    // vda1 is a block device whose sector 0 is the disk's sector 2048:+    // the FAT boot sector (ends in 55 AA, says "FAT32   " at offset 82).+    let vda1 = block::get("vda1").unwrap();+    assert_eq!(vda1.sector_count(), 126976);+    let mut sector = [0u8; SECTOR_SIZE];+    vda1.read_sectors(0, &mut sector).unwrap();+    assert_eq!(&sector[82..90], b"FAT32   ");+    assert_eq!(vda1.read_sectors(126976, &mut sector), Err(Errno::EIO)); // past its end+}++#[test_case]+fn mbr_partitions_on_a_ram_disk() {+    // Build a classic MBR by hand: two primary partitions.+    let disk: Arc<dyn BlockDevice> = Arc::new(RamDisk::new(1000));+    let mut mbr = [0u8; SECTOR_SIZE];+    let entry = |mbr: &mut [u8; SECTOR_SIZE], i: usize, kind: u8, start: u32, len: u32| {+        let e = 446 + 16 * i;+        mbr[e + 4] = kind;+        mbr[e + 8..e + 12].copy_from_slice(&start.to_le_bytes());+        mbr[e + 12..e + 16].copy_from_slice(&len.to_le_bytes());+    };+    entry(&mut mbr, 0, 0x0c, 63, 400); // FAT32 (LBA)+    entry(&mut mbr, 1, 0x83, 500, 300); // Linux+    // No signature yet -> not a partition table.+    assert!(matches!(partition::scan(&disk), Err(partition::PartitionError::NoSignature)));+    mbr[510] = 0x55;+    mbr[511] = 0xAA;+    disk.write_sectors(0, &mbr).unwrap();+    let (scheme, parts) = partition::scan(&disk).unwrap();+    assert_eq!(scheme, "MBR");+    assert_eq!(parts.len(), 2);+    assert_eq!((parts[0].start, parts[0].sectors), (63, 400));+    assert_eq!((parts[1].start, parts[1].sectors), (500, 300));++    // A partition device maps its sector 0 to the partition's start.+    let part = partition::Partition::new(disk.clone(), 500, 300);+    part.write_sectors(0, &[7u8; SECTOR_SIZE]).unwrap();+    let mut back = [0u8; SECTOR_SIZE];+    disk.read_sectors(500, &mut back).unwrap();+    assert_eq!(back, [7u8; SECTOR_SIZE]);+}++#[panic_handler]+fn panic(info: &PanicInfo) -> ! {+    rust_kernel::test_panic_handler(info)+}
@@ -0,0 +1,131 @@+//! FAT32 through the VFS: the partition on target/disk.img is mounted at+//! /mnt/disk0 (writes are fine: QEMU uses snapshot=on).+#![no_std]+#![no_main]+#![feature(custom_test_frameworks)]+#![test_runner(rust_kernel::test_runner)]+#![reexport_test_harness_main = "test_main"]++extern crate alloc;++use alloc::{string::String, sync::Arc, vec, vec::Vec};+use bootloader::{BootInfo, entry_point};+use core::panic::PanicInfo;+use fatfs::{Read, Seek, SeekFrom, Write};+use rust_kernel::block::{BlockDevice, RamDisk, SECTOR_SIZE};+use rust_kernel::errno::Errno;+use rust_kernel::fs::fat::BlockIo;+use rust_kernel::fs::file::{File, InodeFile};+use rust_kernel::fs::{self, InodeKind};+use rust_kernel::elf;++entry_point!(main);++fn main(boot_info: &'static BootInfo) -> ! {+    rust_kernel::init();+    rust_kernel::init_memory(boot_info);+    fs::init();+    rust_kernel::init_storage();+    test_main();+    rust_kernel::hlt_loop();+}++#[test_case]+fn fat_volume_is_mounted() {+    assert!(fs::mounts().iter().any(|(path, kind)| path == "/mnt/disk0" && *kind == "fat32"));+    let mut names: Vec<String> = fs::lookup("/mnt/disk0").unwrap().list().unwrap().into_iter().map(|e| e.name).collect();+    names.sort(); // FAT keeps directory entries in creation order+    assert_eq!(names, vec!["bin", "notes", "readme.txt"]);+}++#[test_case]+fn read_text_file() {+    let file = InodeFile::open(fs::lookup("/mnt/disk0/readme.txt").unwrap()).unwrap();+    let mut buf = [0u8; 256];+    let n = file.read(&mut buf).unwrap();+    assert!(buf[..n].starts_with(b"Hello from the FAT32 disk!"));+    assert_eq!(n as u64, file.stat().unwrap().size);+    assert_eq!(file.read(&mut buf), Ok(0));+    // FAT is case-insensitive; subdirectories work.+    assert!(fs::lookup("/mnt/disk0/README.TXT").is_ok());+    assert_eq!(fs::lookup("/mnt/disk0/notes/todo.txt").unwrap().kind(), InodeKind::File);+    assert_eq!(fs::lookup("/mnt/disk0/nope").err(), Some(Errno::ENOENT));+    assert_eq!(fs::lookup("/mnt/disk0/readme.txt/x").err(), Some(Errno::ENOTDIR));+}++#[test_case]+fn programs_on_disk_are_valid_elf_files() {+    let bin = fs::lookup("/mnt/disk0/bin").unwrap();+    for entry in bin.list().unwrap() {+        let path = alloc::format!("/mnt/disk0/bin/{}", entry.name);+        let image = fs::read_all(&path).unwrap();+        assert_eq!(image.len(), fs::lookup(&path).unwrap().size());+        assert!(elf::check(&image).is_ok(), "{} is not a valid program", entry.name);+    }+}++#[test_case]+fn create_write_read_remove() {+    // 10 000 bytes = 20 clusters (1 sector each): exercises the FAT chain.+    let data: Vec<u8> = (0..10_000u32).map(|i| (i * 7 % 256) as u8).collect();+    let inode = fs::create("/mnt/disk0/notes/big.bin", InodeKind::File).unwrap();+    assert_eq!(inode.write_at(0, &data), Ok(data.len()));+    let mut back = vec![0u8; 12_000];+    assert_eq!(inode.read_at(0, &mut back), Ok(10_000));+    assert_eq!(&back[..10_000], &data[..]);+    // Overwrite in the middle, read across a sector boundary.+    inode.write_at(510, b"XYZW").unwrap();+    let mut four = [0u8; 4];+    inode.read_at(510, &mut four).unwrap();+    assert_eq!(&four, b"XYZW");+    // Writing past the end fills the gap with zeros.+    inode.write_at(10_010, b"!").unwrap();+    assert_eq!(inode.size(), 10_011);+    inode.read_at(10_000, &mut four).unwrap();+    assert_eq!(four, [0; 4]);+    // A fresh lookup sees the same data (it really went to the disk).+    assert_eq!(fs::lookup("/mnt/disk0/notes/big.bin").unwrap().size(), 10_011);+    inode.truncate().unwrap();+    assert_eq!(inode.size(), 0);+    fs::remove("/mnt/disk0/notes/big.bin", false).unwrap();+    assert_eq!(fs::lookup("/mnt/disk0/notes/big.bin").err(), Some(Errno::ENOENT));+}++#[test_case]+fn directories_on_fat() {+    fs::create("/mnt/disk0/newdir", InodeKind::Directory).unwrap();+    assert_eq!(fs::create("/mnt/disk0/newdir", InodeKind::Directory).err(), Some(Errno::EEXIST));+    fs::create("/mnt/disk0/newdir/a long file name.txt", InodeKind::File).unwrap();+    let list = fs::lookup("/mnt/disk0/newdir").unwrap().list().unwrap();+    assert_eq!(list.len(), 1);+    assert_eq!(list[0].name, "a long file name.txt"); // long file names (VFAT)+    assert_eq!(fs::remove("/mnt/disk0/newdir", true), Err(Errno::ENOTEMPTY));+    fs::remove("/mnt/disk0/newdir/a long file name.txt", false).unwrap();+    fs::remove("/mnt/disk0/newdir", true).unwrap();+    assert_eq!(fs::remove("/mnt/disk0", true), Err(Errno::EBUSY)); // mount point+}++#[test_case]+fn block_io_adapter_handles_unaligned_access() {+    // The byte-stream adapter fatfs uses, on a RAM disk.+    let disk: Arc<dyn BlockDevice> = Arc::new(RamDisk::new(4));+    let mut io = BlockIo::new(disk.clone());+    io.seek(SeekFrom::Start(508)).unwrap();+    io.write_all(b"spans two sectors").unwrap(); // bytes 508..525+    let mut sector = [0u8; SECTOR_SIZE];+    disk.read_sectors(1, &mut sector).unwrap();+    assert_eq!(&sector[..13], b"s two sectors"); // "span" went to sector 0+    disk.read_sectors(0, &mut sector).unwrap();+    assert_eq!(&sector[508..], b"span");+    io.seek(SeekFrom::Start(508)).unwrap();+    let mut back = [0u8; 17];+    io.read_exact(&mut back).unwrap();+    assert_eq!(&back, b"spans two sectors");+    assert_eq!(io.seek(SeekFrom::End(0)).unwrap(), 4 * SECTOR_SIZE as u64);+    assert!(io.seek(SeekFrom::End(1)).is_err()); // past the end of the disk+}++#[panic_handler]+fn panic(info: &PanicInfo) -> ! {+    rust_kernel::test_panic_handler(info)+}
@@ -1,60 +1,79 @@-//! User mode: real ring-3 programs (user/*.rs) run as processes, make-//! syscalls, get killed when they misbehave, and get preempted when they hog-//! the CPU. Plus the ELF loader and user-pointer checks.+//! User mode: real ring-3 programs (user/*.rs, loaded from the FAT32 disk)+//! run as processes, make syscalls, get killed when they misbehave, get+//! preempted when they hog the CPU, spawn children and wait for them.+//! Plus the ELF loader, user-pointer checks and blocking primitives. #![no_std] #![no_main] #![feature(custom_test_frameworks)] #![test_runner(rust_kernel::test_runner)] #![reexport_test_harness_main = "test_main"] +extern crate alloc;++use alloc::{string::String, sync::Arc, vec, vec::Vec}; use bootloader::{BootInfo, entry_point}; use core::panic::PanicInfo;-use core::sync::atomic::Ordering;+use core::sync::atomic::{AtomicBool, Ordering}; use rust_kernel::errno::Errno;+use rust_kernel::fs::file::File; use rust_kernel::interrupts::TICKS;-use rust_kernel::memory::{self, BootInfoFrameAllocator, USER_START};-use rust_kernel::process::{self, ExitStatus};-use rust_kernel::{allocator, elf, fs, syscall, user_programs};-use x86_64::VirtAddr;+use rust_kernel::memory::{self, USER_START};+use rust_kernel::process::{self, ExitStatus, SpawnArgs};+use rust_kernel::thread::{self, WaitQueue};+use rust_kernel::{elf, fs, syscall};  entry_point!(main);  fn main(boot_info: &'static BootInfo) -> ! {     rust_kernel::init();-    let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);-    let mut mapper = unsafe { memory::init(phys_mem_offset) };-    let mut frame_allocator = unsafe { BootInfoFrameAllocator::init(&boot_info.memory_map) };-    allocator::init_heap(&mut mapper, &mut frame_allocator).expect("heap initialization failed");-    memory::init_frame_allocator(frame_allocator);+    rust_kernel::init_memory(boot_info);     fs::init();-+    rust_kernel::init_storage();     test_main();     rust_kernel::hlt_loop(); } +const BIN: &str = "/mnt/disk0/bin";++/// Start /mnt/disk0/bin/<name> with these arguments (after the name).+fn run(name: &str, extra_args: &[&str]) -> Arc<process::Process> {+    let mut spawn = SpawnArgs::kernel(name);+    spawn.args.extend(extra_args.iter().map(|a| String::from(*a)));+    process::spawn_path(&alloc::format!("{}/{}", BIN, name), spawn).expect("spawn failed")+}+ /// hello does ~10 syscalls (open/read/write/close on the ramfs, error cases) /// and exits with the number of failed checks. #[test_case] fn hello_program_passes_its_syscall_checks() {-    let p = process::spawn("hello", user_programs::HELLO).expect("spawn hello");+    let p = run("hello", &[]);     assert_eq!(process::wait(&p), ExitStatus::Exited(0));     // It created /tmp/note.txt through the syscalls; the kernel sees it too.-    let inode = fs::lookup("/tmp/note.txt").expect("hello's file is missing");-    assert_eq!(inode.size(), 19);+    assert_eq!(fs::lookup("/tmp/note.txt").expect("hello's file is missing").size(), 19);+}++/// demo checks cwd, FAT reads, stat, lseek, getdents64, mkdir/rmdir/unlink,+/// pipes, spawn + wait (incl. a killed child) -- exit code = failures.+#[test_case]+fn demo_program_passes_its_checks() {+    let p = run("demo", &[]);+    assert_eq!(process::wait(&p), ExitStatus::Exited(0)); }  /// evil reads kernel memory -> page fault in ring 3 -> killed. We're still here. #[test_case] fn kernel_memory_access_kills_only_the_process() {-    let p = process::spawn("evil", user_programs::EVIL).expect("spawn evil");-    assert_eq!(process::wait(&p), ExitStatus::Killed("page fault"));+    let p = run("evil", &[]);+    let status = process::wait(&p);+    assert_eq!(status, ExitStatus::Killed("page fault"));+    assert_eq!(status.code(), 139); }  /// spin never makes a syscall while it loops. If preemption didn't work, /// this kernel thread couldn't run again until spin finished. #[test_case] fn spinning_process_is_preempted() {-    let p = process::spawn("spin", user_programs::SPIN).expect("spawn spin");+    let p = run("spin", &[]);     let start = TICKS.load(Ordering::Relaxed);     let mut samples_while_spinning = 0;     while p.status().is_none() {@@ -66,18 +85,62 @@ fn spinning_process_is_preempted() {         assert!(TICKS.load(Ordering::Relaxed) < start + 18 * 30, "spin didn't finish");         core::hint::spin_loop();     }-    assert_eq!(p.status(), Some(ExitStatus::Exited(0)));+    assert_eq!(process::wait(&p), ExitStatus::Exited(0));     assert!(samples_while_spinning > 0, "never ran while spin was running"); } +/// Arguments arrive in the child; its exit code comes back through wait.+/// (Tracing is on: its syscalls get printed to the serial log.)+#[test_case]+fn arguments_and_exit_codes() {+    let mut spawn = SpawnArgs::kernel("child");+    spawn.args.push(String::from("7"));+    spawn.trace = true;+    let p = process::spawn_path("/mnt/disk0/bin/child", spawn).unwrap();+    assert_eq!(process::wait(&p), ExitStatus::Exited(7));+    // Reaped: the pid is gone from the process table.+    assert!(process::get(p.pid).is_none());+}++/// A child's stdout can be a pipe: we read what it printed.+#[test_case]+fn child_stdout_into_a_pipe() {+    let (reader, writer) = fs::pipe::new();+    let mut spawn = SpawnArgs::kernel("child");+    spawn.args.push(String::from("pipe"));+    spawn.stdio[1] = Arc::new(writer);+    let p = process::spawn_path("/mnt/disk0/bin/child", spawn).unwrap();+    let mut out = Vec::new();+    let mut buf = [0u8; 32];+    loop {+        // Blocks until the child writes or exits (its exit closes the pipe).+        match reader.read(&mut buf).unwrap() {+            0 => break,+            n => out.extend_from_slice(&buf[..n]),+        }+    }+    assert!(out.starts_with(b"hello through a pipe from pid"));+    assert_eq!(process::wait(&p), ExitStatus::Exited(0));+}++#[test_case]+fn spawn_errors() {+    let spawn = || SpawnArgs::kernel("x");+    assert_eq!(process::spawn_path("/mnt/disk0/bin/nope", spawn()).err(), Some(Errno::ENOENT));+    assert_eq!(process::spawn_path("/mnt/disk0/readme.txt", spawn()).err(), Some(Errno::ENOEXEC));+    assert_eq!(process::spawn_path("/mnt/disk0/bin", spawn()).err(), Some(Errno::EISDIR));+    assert_eq!(process::wait_child(12345, 1).err(), Some(Errno::ECHILD));+}+ #[test_case] fn elf_loader_rejects_garbage() {+    let hello = fs::read_all("/mnt/disk0/bin/hello").unwrap();     assert_eq!(elf::check(&[b'x'; 128]), Err(elf::ElfError::NotElf));     assert_eq!(elf::check(b"\x7fELF"), Err(elf::ElfError::TooShort));-    let entry = elf::check(user_programs::HELLO).expect("hello should parse");+    let entry = elf::check(&hello).expect("hello should parse");     assert!(entry >= USER_START);     // A truncated program: the header is fine, the program headers are cut off.-    assert!(elf::check(&user_programs::HELLO[..70]).is_err());+    assert!(elf::check(&hello[..70]).is_err()); }  #[test_case]@@ -87,6 +150,35 @@ fn user_pointers_are_validated() {     assert_eq!(syscall::user_slice(USER_START, 16).err(), Some(Errno::EFAULT));     assert_eq!(syscall::user_slice(u64::MAX - 4, 16).err(), Some(Errno::EFAULT));     assert!(!memory::user_range_ok(USER_START - 8, 16, false));+    // Zero bytes: any pointer is fine.+    assert_eq!(syscall::user_slice(1, 0).map(|s| s.len()), Ok(0));+}++/// sleep_ticks really blocks: the thread is off the run queue meanwhile.+#[test_case]+fn sleep_blocks_for_the_requested_time() {+    let start = TICKS.load(Ordering::Relaxed);+    thread::sleep_ticks(4);+    let slept = TICKS.load(Ordering::Relaxed) - start;+    assert!((4..8).contains(&slept), "slept {} ticks", slept);+}++static FLAG: AtomicBool = AtomicBool::new(false);+static QUEUE: WaitQueue = WaitQueue::new();++/// A thread blocked on a WaitQueue wakes when another thread signals it.+#[test_case]+fn wait_queue_wakes_the_waiter() {+    thread::spawn("waker", || {+        thread::sleep_ticks(2);+        FLAG.store(true, Ordering::SeqCst);+        QUEUE.wake_all();+    });+    let start = TICKS.load(Ordering::Relaxed);+    let value = QUEUE.wait_until(|| FLAG.load(Ordering::SeqCst).then_some(42));+    assert_eq!(value, 42);+    assert!(TICKS.load(Ordering::Relaxed) - start >= 2);+    let _ = vec![0u8; 1]; // the heap still works after all the switching }  #[panic_handler]
@@ -7,24 +7,18 @@  extern crate alloc; -use alloc::{string::String, vec};+use alloc::{string::String, vec, vec::Vec}; use bootloader::{BootInfo, entry_point}; use core::panic::PanicInfo; use rust_kernel::errno::Errno;-use rust_kernel::fs::file::{File, InodeFile};+use rust_kernel::fs::file::{File, InodeFile, KIND_FILE, SEEK_CUR, SEEK_END, SEEK_SET, Stat}; use rust_kernel::fs::{self, InodeKind, ramfs::RamFs};-use rust_kernel::memory::{self, BootInfoFrameAllocator};-use rust_kernel::allocator;-use x86_64::VirtAddr;  entry_point!(main);  fn main(boot_info: &'static BootInfo) -> ! {     rust_kernel::init();-    let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);-    let mut mapper = unsafe { memory::init(phys_mem_offset) };-    let mut frame_allocator = unsafe { BootInfoFrameAllocator::init(&boot_info.memory_map) };-    allocator::init_heap(&mut mapper, &mut frame_allocator).expect("heap initialization failed");+    rust_kernel::init_memory(boot_info);     fs::init();      test_main();@@ -48,7 +42,9 @@ fn lookup_errors() {     assert_eq!(fs::lookup("/nope").err(), Some(Errno::ENOENT));     assert_eq!(fs::lookup("/hello.txt/x").err(), Some(Errno::ENOTDIR));     assert_eq!(fs::lookup("relative").err(), Some(Errno::EINVAL));-    assert_eq!(InodeFile::open(fs::lookup("/etc").unwrap()).err(), Some(Errno::EISDIR));+    // Directories can be opened (for getdents) but not read like files.+    let dir = InodeFile::open(fs::lookup("/etc").unwrap()).unwrap();+    assert_eq!(dir.read(&mut [0u8; 8]), Err(Errno::EISDIR)); }  #[test_case]@@ -75,8 +71,81 @@ fn write_grow_and_list() {     let mut buf = vec![0xffu8; 10];     assert_eq!(inode.read_at(0, &mut buf), Ok(7));     assert_eq!(&buf[..7], b"\0\0\0\0abc");-    let names = fs::lookup("/").unwrap().list().unwrap();-    assert_eq!(names, vec![String::from("etc"), String::from("hello.txt"), String::from("tmp")]);+    let names: Vec<String> = fs::lookup("/").unwrap().list().unwrap().into_iter().map(|e| e.name).collect();+    assert_eq!(names, vec!["etc", "hello.txt", "mnt", "tmp"]);+}++#[test_case]+fn paths_are_normalized() {+    assert_eq!(fs::normalize("/", "a/b").unwrap(), "/a/b");+    assert_eq!(fs::normalize("/home/tyler", "../etc//./motd").unwrap(), "/home/etc/motd");+    assert_eq!(fs::normalize("/x", "/../../y/").unwrap(), "/y"); // ".." of "/" is "/"+    assert_eq!(fs::normalize("/x", "..").unwrap(), "/");+    assert_eq!(fs::normalize("/", "").err(), Some(Errno::ENOENT));+    // ".." works across mount points: /tmp/.. is the root ramfs again.+    assert_eq!(fs::lookup("/tmp/../etc/motd").unwrap().kind(), InodeKind::File);+}++#[test_case]+fn remove_files_and_directories() {+    fs::create("/tmp/rm", InodeKind::Directory).unwrap();+    fs::create("/tmp/rm/f", InodeKind::File).unwrap();+    assert_eq!(fs::remove("/tmp/rm", true), Err(Errno::ENOTEMPTY));+    assert_eq!(fs::remove("/tmp/rm", false), Err(Errno::EISDIR)); // unlink a dir+    assert_eq!(fs::remove("/tmp/rm/f", true), Err(Errno::ENOTDIR)); // rmdir a file+    fs::remove("/tmp/rm/f", false).unwrap();+    fs::remove("/tmp/rm", true).unwrap();+    assert_eq!(fs::lookup("/tmp/rm").err(), Some(Errno::ENOENT));+    assert_eq!(fs::remove("/tmp", true), Err(Errno::EBUSY)); // mount point+}++#[test_case]+fn seek_stat_and_truncate() {+    let inode = fs::create("/tmp/seek.txt", InodeKind::File).unwrap();+    let file = InodeFile::open(inode.clone()).unwrap();+    file.write(b"0123456789").unwrap();+    assert_eq!(file.seek(-3, SEEK_END), Ok(7));+    let mut buf = [0u8; 8];+    assert_eq!(file.read(&mut buf), Ok(3));+    assert_eq!(&buf[..3], b"789");+    assert_eq!(file.seek(2, SEEK_SET), Ok(2));+    assert_eq!(file.seek(1, SEEK_CUR), Ok(3));+    assert_eq!(file.seek(-4, SEEK_CUR), Err(Errno::EINVAL)); // before the start+    assert_eq!(file.stat(), Ok(Stat { kind: KIND_FILE, size: 10 }));+    inode.truncate().unwrap();+    assert_eq!(inode.size(), 0);+}++#[test_case]+fn getdents_returns_linux_dirent64_records() {+    let dir = InodeFile::open(fs::lookup("/etc").unwrap()).unwrap();+    let mut buf = [0u8; 64];+    let n = dir.getdents(&mut buf).unwrap();+    // One entry, "motd": 19-byte header + "motd\0" = 24 bytes (already 8-aligned).+    assert_eq!(n, 24);+    assert_eq!(u16::from_le_bytes([buf[16], buf[17]]), 24); // d_reclen+    assert_eq!(buf[18], 8); // DT_REG+    assert_eq!(&buf[19..24], b"motd\0");+    assert_eq!(dir.getdents(&mut buf), Ok(0)); // end of directory+    assert_eq!(dir.seek(0, SEEK_SET), Ok(0)); // rewind+    assert_eq!(dir.getdents(&mut [0u8; 10]), Err(Errno::EINVAL)); // too small+}++#[test_case]+fn pipes_pass_bytes_and_report_eof() {+    let (reader, writer) = fs::pipe::new();+    assert_eq!(writer.write(b"through the pipe"), Ok(16));+    let mut buf = [0u8; 7];+    assert_eq!(reader.read(&mut buf), Ok(7));+    assert_eq!(&buf, b"through");+    assert_eq!(reader.seek(0, SEEK_SET), Err(Errno::ESPIPE));+    drop(writer); // last writer gone: remaining data, then EOF (no blocking)+    let mut rest = [0u8; 32];+    assert_eq!(reader.read(&mut rest), Ok(9));+    assert_eq!(reader.read(&mut rest), Ok(0));+    let (reader, writer) = fs::pipe::new();+    drop(reader);+    assert_eq!(writer.write(b"x"), Err(Errno::EPIPE)); }  #[panic_handler]
@@ -0,0 +1,41 @@+//! Helper program that `demo` (and the tests) spawn:+//!   child             -> print my arguments, exit 0+//!   child <number>    -> exit with that number (tests wait() exit codes)+//!   child pipe        -> write a line to stdout (demo reads it via a pipe)+//!   child cat         -> copy stdin to stdout until EOF+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++fn main() -> i64 {+    match arg(1) {+        Some("pipe") => {+            println!("hello through a pipe from pid {}", getpid());+            0+        }+        Some("cat") => {+            let mut buf = [0u8; 64];+            loop {+                let n = read(0, &mut buf);+                if n <= 0 {+                    return n;+                }+                write(1, &buf[..n as usize]);+            }+        }+        Some(number) if number.parse::<i64>().is_ok() => number.parse().unwrap(),+        _ => {+            print!("[child] pid {} got {} args:", getpid(), argc());+            for i in 0..argc() {+                print!(" {:?}", arg(i).unwrap_or("?"));+            }+            println!();+            0+        }+    }+}
@@ -0,0 +1,152 @@+//! Stage 6 demo: this program was itself loaded from the FAT32 disk.+//! It exercises the syscalls the shell (next stage) will need: cwd, reading+//! a file on the FAT disk, stat, lseek, getdents64, mkdir/unlink/rmdir,+//! pipes, and spawn + wait. Exit code = number of failed checks.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++static mut FAILURES: i64 = 0;++fn check(ok: bool, what: &str) {+    if !ok {+        println!("[demo] CHECK FAILED: {}", what);+        unsafe { FAILURES += 1 };+    }+}++fn cwd_is(expected: &str) -> bool {+    let mut buf = [0u8; 64];+    let n = getcwd(&mut buf);+    n >= 0 && &buf[..n as usize] == expected.as_bytes()+}++fn main() -> i64 {+    println!("[demo] pid {} started from /mnt/disk0/bin/demo", getpid());++    // --- current directory --------------------------------------------------+    check(cwd_is("/"), "cwd starts at /");+    check(chdir("/mnt/disk0") == 0, "chdir /mnt/disk0");+    check(cwd_is("/mnt/disk0"), "getcwd after chdir");++    // --- read a file from the FAT disk (relative path!) ---------------------+    let fd = open("readme.txt", 0);+    check(fd >= 3, "open readme.txt");+    let mut buf = [0u8; 256];+    let n = read(fd, &mut buf).max(0) as usize;+    let text = core::str::from_utf8(&buf[..n]).unwrap_or("?");+    print!("[demo] readme.txt from FAT32: {}", text.lines().next().unwrap_or(""));+    println!();+    check(text.starts_with("Hello from the FAT32 disk!"), "readme.txt contents");++    // --- stat / fstat / lseek ------------------------------------------------+    let mut st = Stat::default();+    check(stat("readme.txt", &mut st) == 0 && st.kind == KIND_FILE && st.size == n as u64, "stat readme.txt");+    check(fstat(1, &mut st) == 0 && st.kind == KIND_CONSOLE, "fstat stdout = console");+    check(stat("bin", &mut st) == 0 && st.kind == KIND_DIR, "stat bin is a directory");+    check(lseek(fd, 6, SEEK_SET) == 6, "lseek SEEK_SET");+    let mut word = [0u8; 4];+    check(read(fd, &mut word) == 4 && &word == b"from", "read after lseek");+    check(lseek(fd, 0, SEEK_END) == n as i64, "lseek SEEK_END = size");+    check(lseek(1, 0, SEEK_SET) == -ESPIPE, "can't seek the console");+    close(fd);++    // --- list a directory with getdents64 -----------------------------------+    let dir = open("bin", 0);+    let mut dents = [0u8; 512];+    let len = getdents64(dir, &mut dents);+    check(len > 0, "getdents64 bin");+    let (mut saw_demo, mut saw_child) = (false, false);+    print!("[demo] /mnt/disk0/bin:");+    for_each_dirent(&dents, len.max(0) as usize, |name, d_type| {+        print!(" {}{}", name, if d_type == 4 { "/" } else { "" });+        saw_demo |= name == "demo";+        saw_child |= name == "child";+    });+    println!();+    check(saw_demo && saw_child, "bin contains demo and child");+    check(getdents64(dir, &mut dents) == 0, "getdents64 at end returns 0");+    close(dir);++    // --- relative paths, "..", errors ----------------------------------------+    check(chdir("notes") == 0 && cwd_is("/mnt/disk0/notes"), "chdir notes");+    check(chdir("../..") == 0 && cwd_is("/mnt"), "chdir ../..");+    check(chdir("disk0/readme.txt") == -ENOTDIR, "chdir to a file -> ENOTDIR");+    check(chdir("/nope") == -ENOENT, "chdir to nothing -> ENOENT");++    // --- create and delete (ramfs /tmp and the FAT disk) ---------------------+    for base in ["/tmp", "/mnt/disk0/notes"] {+        let mut path_buf = [0u8; 64];+        let dir = join(&mut path_buf, base, "/newdir");+        check(mkdir(dir) == 0, "mkdir");+        check(mkdir(dir) == -EEXIST, "mkdir again -> EEXIST");+        let mut file_buf = [0u8; 64];+        let file = join(&mut file_buf, dir, "/f.txt");+        let fd = open(file, O_CREAT);+        check(write(fd, b"bytes") == 5, "write new file");+        close(fd);+        check(rmdir(dir) == -ENOTEMPTY, "rmdir non-empty -> ENOTEMPTY");+        check(unlink(dir) == -EISDIR, "unlink a directory -> EISDIR");+        check(unlink(file) == 0, "unlink file");+        check(rmdir(dir) == 0, "rmdir empty dir");+        check(stat(dir, &mut st) == -ENOENT, "gone after rmdir");+    }+    check(rmdir("/tmp") == -EBUSY, "rmdir a mount point -> EBUSY");+    println!("[demo] mkdir/unlink/rmdir work on ramfs and FAT32");++    // --- spawn a child with its stdout connected to a pipe -------------------+    chdir("/mnt/disk0");+    let (r, w) = pipe().unwrap_or((-1, -1));+    let opts = SpawnOptions { stdout: w, ..SpawnOptions::INHERIT };+    let pid = spawn("bin/child", "child\0pipe", &opts);+    check(pid > 0, "spawn child (pipe)");+    close(w); // only the child holds the write end now: EOF when it exits+    let mut got = [0u8; 128];+    let mut total = 0;+    loop {+        let n = read(r, &mut got[total..]); // blocks until data or EOF+        if n <= 0 {+            break;+        }+        total += n as usize;+    }+    close(r);+    let line = core::str::from_utf8(&got[..total]).unwrap_or("?").trim_end();+    println!("[demo] read from pipe: {:?}", line);+    check(line.starts_with("hello through a pipe"), "pipe contents");+    check(wait(pid) == 0, "child (pipe) exit code 0");++    // --- spawn + wait: exit codes ---------------------------------------------+    let pid = spawn("bin/child", "child\0with\0three args", &SpawnOptions::INHERIT);+    check(wait(pid) == 0, "child with args exits 0");+    let pid = spawn("bin/child", "child\042", &SpawnOptions::INHERIT);+    let code = wait(pid);+    println!("[demo] spawned child 42 (pid {}), wait -> exit code {}", pid, code);+    check(code == 42, "wait returns the exit code");+    check(wait(pid) == -ECHILD, "second wait -> ECHILD");+    let pid = spawn("/mnt/disk0/bin/evil", "", &SpawnOptions::INHERIT);+    check(wait(pid) == 139, "killed child -> 139");+    check(spawn("bin/nope", "", &SpawnOptions::INHERIT) == -ENOENT, "spawn missing -> ENOENT");+    check(spawn("readme.txt", "", &SpawnOptions::INHERIT) == -8, "spawn non-ELF -> ENOEXEC");+    check(wait(1) == -ECHILD, "wait for a non-child -> ECHILD");++    sleep_ms(100); // blocks in the kernel (no busy loop)++    let failures = unsafe { FAILURES };+    if failures == 0 {+        println!("[demo] all stage 6 checks passed, exiting with 0");+    }+    failures+}++/// Concatenate two strings into `buf` (no heap in user programs).+fn join<'a>(buf: &'a mut [u8], a: &str, b: &str) -> &'a str {+    buf[..a.len()].copy_from_slice(a.as_bytes());+    buf[a.len()..a.len() + b.len()].copy_from_slice(b.as_bytes());+    core::str::from_utf8(&buf[..a.len() + b.len()]).unwrap()+}
@@ -18,14 +18,65 @@ pub const SYS_READ: u64 = 0; pub const SYS_WRITE: u64 = 1; pub const SYS_OPEN: u64 = 2; pub const SYS_CLOSE: u64 = 3;+pub const SYS_STAT: u64 = 4;+pub const SYS_FSTAT: u64 = 5;+pub const SYS_LSEEK: u64 = 8;+pub const SYS_PIPE: u64 = 22; 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_GETCWD: u64 = 79;+pub const SYS_CHDIR: u64 = 80;+pub const SYS_MKDIR: u64 = 83;+pub const SYS_RMDIR: u64 = 84;+pub const SYS_UNLINK: u64 = 87;+pub const SYS_GETDENTS64: u64 = 217;+pub const SYS_SPAWN: u64 = 500;  pub const O_CREAT: u64 = 0o100;+pub const O_TRUNC: u64 = 0o1000;+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 ENOENT: i64 = 2; pub const EBADF: i64 = 9;+pub const ECHILD: i64 = 10; pub const EFAULT: i64 = 14;+pub const EBUSY: i64 = 16;+pub const EEXIST: i64 = 17;+pub const ENOTDIR: i64 = 20;+pub const EISDIR: i64 = 21;+pub const ESPIPE: i64 = 29;+pub const ENOTEMPTY: i64 = 39;++/// Same layout as the kernel's `fs::file::Stat`.+#[repr(C)]+#[derive(Default, Clone, Copy)]+pub struct Stat {+    pub kind: u64, // KIND_*+    pub size: u64,+}+pub const KIND_FILE: u64 = 1;+pub const KIND_DIR: u64 = 2;+pub const KIND_CONSOLE: u64 = 3;+pub const KIND_PIPE: u64 = 4;++/// Same layout as the kernel's `syscall::SpawnOptions`.+#[repr(C)]+pub struct SpawnOptions {+    pub stdin: i64, // my fd to give the child as its fd 0 (-1 = my fd 0)+    pub stdout: i64,+    pub stderr: i64,+    pub flags: u64, // SPAWN_TRACE+}++impl SpawnOptions {+    pub const INHERIT: SpawnOptions = SpawnOptions { stdin: -1, stdout: -1, stderr: -1, flags: 0 };+}  /// Raw system call: number in rax, args in rdi/rsi/rdx, result in rax. /// The `syscall` instruction overwrites rcx and r11.@@ -46,6 +97,26 @@ pub fn syscall3(nr: u64, a1: u64, a2: u64, a3: u64) -> i64 {     ret } +/// 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 {+    let ret: i64;+    unsafe {+        asm!(+            "syscall",+            inlateout("rax") nr as i64 => ret,+            in("rdi") a1,+            in("rsi") a2,+            in("rdx") a3,+            in("r10") a4,+            in("r8") a5,+            lateout("rcx") _,+            lateout("r11") _,+            options(nostack),+        );+    }+    ret+}+ pub fn read(fd: i64, buf: &mut [u8]) -> i64 {     syscall3(SYS_READ, fd as u64, buf.as_mut_ptr() as u64, buf.len() as u64) }@@ -58,9 +129,29 @@ pub fn open(path: &str, flags: u64) -> i64 { pub fn close(fd: i64) -> i64 {     syscall3(SYS_CLOSE, fd as u64, 0, 0) }+pub fn stat(path: &str, st: &mut Stat) -> i64 {+    syscall3(SYS_STAT, path.as_ptr() as u64, path.len() as u64, st as *mut Stat as u64)+}+pub fn fstat(fd: i64, st: &mut Stat) -> i64 {+    syscall3(SYS_FSTAT, fd as u64, st as *mut Stat as u64, 0)+}+pub fn lseek(fd: i64, offset: i64, whence: u64) -> i64 {+    syscall3(SYS_LSEEK, fd as u64, offset as u64, whence)+}+/// Returns (read end, write end), or the negative errno.+pub fn pipe() -> Result<(i64, i64), i64> {+    let mut fds = [0u32; 2];+    match syscall3(SYS_PIPE, fds.as_mut_ptr() as u64, 0, 0) {+        0 => Ok((fds[0] as i64, fds[1] as i64)),+        e => Err(e),+    }+} pub fn sched_yield() {     syscall3(SYS_SCHED_YIELD, 0, 0, 0); }+pub fn sleep_ms(ms: u64) {+    syscall3(SYS_SLEEP_MS, ms, 0, 0);+} pub fn getpid() -> i64 {     syscall3(SYS_GETPID, 0, 0, 0) }@@ -68,6 +159,75 @@ pub fn exit(code: i64) -> ! {     syscall3(SYS_EXIT, code as u64, 0, 0);     loop {} // not reached }+/// Wait for child `pid` to end; returns its exit code (139 = killed).+pub fn wait(pid: i64) -> i64 {+    syscall3(SYS_WAIT, pid as u64, 0, 0)+}+/// Current directory into `buf`; returns its length (or -errno).+pub fn getcwd(buf: &mut [u8]) -> i64 {+    syscall3(SYS_GETCWD, buf.as_mut_ptr() as u64, buf.len() as u64, 0)+}+pub fn chdir(path: &str) -> i64 {+    syscall3(SYS_CHDIR, path.as_ptr() as u64, path.len() as u64, 0)+}+pub fn mkdir(path: &str) -> i64 {+    syscall3(SYS_MKDIR, path.as_ptr() as u64, path.len() as u64, 0)+}+pub fn rmdir(path: &str) -> i64 {+    syscall3(SYS_RMDIR, path.as_ptr() as u64, path.len() as u64, 0)+}+pub fn unlink(path: &str) -> i64 {+    syscall3(SYS_UNLINK, path.as_ptr() as u64, path.len() as u64, 0)+}+pub fn getdents64(fd: i64, buf: &mut [u8]) -> i64 {+    syscall3(SYS_GETDENTS64, fd as u64, buf.as_mut_ptr() as u64, buf.len() as u64)+}+/// Start program `path` with arguments `argv` (NUL-separated, e.g.+/// "child\0hi"). Returns the child's pid (or -errno).+pub fn spawn(path: &str, argv: &str, opts: &SpawnOptions) -> i64 {+    syscall5(+        SYS_SPAWN,+        path.as_ptr() as u64,+        path.len() as u64,+        argv.as_ptr() as u64,+        argv.len() as u64,+        opts as *const SpawnOptions as u64,+    )+}++/// Walk the `linux_dirent64` records getdents64 put in `buf[..len]`,+/// calling `f(name, d_type)` for each (d_type: 4 = directory, 8 = file).+pub fn for_each_dirent(buf: &[u8], len: usize, mut f: impl FnMut(&str, u8)) {+    let mut pos = 0;+    while pos + 19 < len {+        let reclen = u16::from_le_bytes([buf[pos + 16], buf[pos + 17]]) as usize;+        let d_type = buf[pos + 18];+        let name = &buf[pos + 19..pos + reclen];+        let name = &name[..name.iter().position(|&b| b == 0).unwrap_or(name.len())];+        f(core::str::from_utf8(name).unwrap_or("?"), d_type);+        pos += reclen;+    }+}++// --- program arguments ---------------------------------------------------++/// `argv` as the kernel passes it: (pointer, length) pairs.+static mut ARGC: usize = 0;+static mut ARGV: *const [usize; 2] = core::ptr::null();++/// 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 {+            return None;+        }+        let [ptr, len] = *ARGV.add(i);+        core::str::from_utf8(core::slice::from_raw_parts(ptr as *const u8, len)).ok()+    }+}  pub struct Stdout; @@ -92,9 +252,14 @@ macro_rules! println {     }}; } -/// The kernel jumps here (see process.rs). Run main, exit with its result.+/// The kernel jumps here (see process.rs) with rdi = argc, rsi = argv.+/// Save the arguments, run main, exit with its result. #[unsafe(no_mangle)]-pub extern "C" fn _start() -> ! {+pub extern "C" fn _start(argc: usize, argv: *const [usize; 2]) -> ! {+    unsafe {+        ARGC = argc;+        ARGV = argv;+    }     exit(crate::main()) }