RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

authorTyler Fowler <tyler@localhost>2026-10-30 00:27:17 -0700
commit176e770e4a2ec63a4e05a273a139907cc241fb0d
parenta0fc1ab827ad7844fa38a7380fb140a1894ab230
treebrowse files at this commit
User mode (ring 3), syscalls, per-process address spaces, ELF loader, VFS + ramfs

- GDT: user code/data segments in sysret order; TSS.rsp0 updated per thread
- syscall/sysret entry (STAR/LSTAR/SFMASK, EFER.SCE) with kernel stack switch;
  Linux-numbered read/write/open/close/sched_yield/getpid/exit, -errno results,
  user pointer validation
- per-process L4 table (kernel entries copied, user region = L4 slot 1),
  CR3 switched by the scheduler; process = thread + address space + fd table
- user faults (page fault, GPF, invalid opcode) kill the process, not the kernel
- minimal ELF64 loader; user programs (hello, evil, spin, echo) built by build.rs
- VFS: Inode/FileSystem/File traits, mount table, ramfs on / and /tmp; tty line
  discipline for read(0)
- tests: user_mode, vfs; screenshot.sh drives the echo program; README
MREADME.md+305 -53
Mboot.png+0 -0
Abuild.rs+33 -0
Mrust-toolchain.toml+2 -0
Mscripts/screenshot.sh+10 -6
Asrc/elf.rs+163 -0
Asrc/errno.rs+17 -0
Asrc/fs/file.rs+49 -0
Asrc/fs/mod.rs+162 -0
Asrc/fs/ramfs.rs+98 -0
Msrc/gdt.rs+75 -37
Msrc/interrupts.rs+40 -1
Msrc/lib.rs+17 -2
Msrc/main.rs+26 -2
Msrc/memory.rs+162 -3
Asrc/process.rs+186 -0
Asrc/syscall.rs+204 -0
Msrc/task/keyboard.rs+5 -12
Msrc/thread.rs+77 -11
Asrc/tty.rs+95 -0
Atests/user_mode.rs+95 -0
Atests/vfs.rs+85 -0
Auser/echo.rs+23 -0
Auser/evil.rs+17 -0
Auser/hello.rs+57 -0
Auser/link.ld+23 -0
Auser/spin.rs+20 -0
Auser/ulib.rs+105 -0

28 files changed, 2151 insertions, 127 deletions

@@ -10,11 +10,16 @@ kernel heap (`Box`, `Vec`, `Rc` work) backed by a fixed-size-block allocator. It multitasks two ways: an **async/await executor** (cooperative tasks; the 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**. 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 the preemptive threads (`src/thread.rs`, not in the blog series).+scripts, and everything past chapter 12: preemptive threads, user mode,+syscalls, ELF loading and the VFS.  ![boot screenshot](boot.png) @@ -30,7 +35,8 @@ scripts, and the preemptive threads (`src/thread.rs`, not in the blog series). | QEMU (`qemu-system-x86`, `qemu-system-gui`) | 10.0.13 | the emulator we boot in (`-gui` adds the GTK/SDL window) |  `rust-toolchain.toml` pins the project to nightly with the two components, so-`cargo` inside this directory picks them up automatically.+`cargo` inside this directory picks them up automatically. It also installs+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,@@ -41,7 +47,7 @@ Fresh setup on another machine:  ```sh curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh   # rustup-rustup toolchain install nightly -c rust-src -c llvm-tools-preview+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 ```@@ -55,7 +61,8 @@ cargo run                           # build + boot in a QEMU window; serial outp  **Trying the keyboard:** with `cargo run`, click into the QEMU window and type; characters appear on the bottom line of the screen *and* in your terminal (via-serial). Shift works; Enter starts a new line; arrows/F-keys show as e.g.+serial). Press Enter and the `echo` user program reads the line with the+`read` syscall and prints `[echo] you typed: ...` (type `exit` to stop it). Shift works; Enter starts a new line; arrows/F-keys show as e.g. `<ArrowUp>`; Backspace erases the previous character (on the current line). To get the mouse/keyboard back from QEMU press Ctrl+Alt+G; close the window or press Ctrl+C in the terminal to quit. (Over SSH without X, use `cargo run -- -display curses` to see the VGA screen right in your terminal;@@ -72,9 +79,10 @@ cargo run --features headless,stack-overflow -- -display none  # same as the sec                                           # (QEMU's exit status is 33 = success; cargo just reports it as non-zero) cargo run -- -display none                # headless but kernel keeps running; Ctrl+C to stop ./scripts/screenshot.sh [out.png] [text] [expected]-                                          # boot, "type" text via QEMU monitor `sendkey` ("<" = Backspace;-                                          # default "Hello, kernex<l!"), check serial shows the expected-                                          # result ("Hello, kernel!"), save the VGA screen to boot.png+                                          # boot, "type" text via QEMU monitor `sendkey` ("<" = Backspace,+                                          # "|" = Enter; default "Hello, kernex<l!|"), check serial shows the+                                          # expected result ("[echo] you typed: Hello, kernel!"), save the+                                          # VGA screen to boot.png; exits 1 if the text is missing ```  Cargo features (in `Cargo.toml`):@@ -101,38 +109,59 @@ Survived the breakpoint, still running. [heap] Rc count after clone = 2 [heap] Rc count after drop  = 1 [thread] spawned threads A, B, C (status on the top line)-Type something! (keyboard echo, Backspace works)+.[thread A] count 1+.[thread B] count 1+..[vfs] mounted ramfs at /+[vfs] mounted ramfs at /tmp+[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)-.[thread A] count 1-.[thread B] count 1-.......[thread A] count 2+....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+[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 C] busy loop round 1 (count = 2000000)-.. 18 ticks-[thread A] count 3+.....[thread A] count 3+. 18 ticks+.....[thread A] count 4 .[thread B] count 3-.......[thread A] count 4-.[thread B] count 4-.....[thread C] busy loop round 2 (count = 4000000)-..[thread A] count 5-.[thread B] count 5-. 36 ticks-............[thread C] busy loop round 3 (count = 6000000)-...... 54 ticks-..........[thread C] busy loop round 4 (count = 8000000)-........ 72 ticks-....[thread C] busy loop round 5 (count = 10000000)-[thread C] done, exiting-.............. 90 ticks+.[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] 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 [ok] timer works, exiting ```  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.+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.  ## Tests @@ -163,6 +192,8 @@ 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/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]`@@ -183,11 +214,13 @@ x86_64-rust_kernel.json   custom target: no OS, no red zone, soft-float, abort o Cargo.toml                deps, features, test settings, QEMU run/test args src/lib.rs                the kernel library: module list, init(), kprintln!, test framework, exit_qemu src/main.rs               the bootable binary: entry_point!(kernel_main), paging + heap demos, panic handler-src/gdt.rs                GDT + TSS with a separate IST stack for the double-fault handler-src/interrupts.rs         IDT; breakpoint / page fault / double fault handlers; 8259 PIC;+src/gdt.rs                GDT (kernel + user segments) + TSS (double-fault IST stack, rsp0 per thread)+src/interrupts.rs         IDT; breakpoint / page fault / GPF / invalid opcode / double fault handlers+                          (faults from ring 3 kill the process); 8259 PIC;                           timer handler (uptime, ticks, calls the thread scheduler),                           keyboard handler (just queues the scancode)-src/memory.rs             OffsetPageTable setup from CR3, example mapping, BootInfoFrameAllocator+src/memory.rs             OffsetPageTable setup from CR3, example mapping, BootInfoFrameAllocator,+                          global frame allocator, per-process address spaces, user pointer checks src/allocator.rs          heap location/size, init_heap (maps heap pages), #[global_allocator], Locked<A> src/allocator/bump.rs     bump allocator (not active, for learning) src/allocator/fixed_size_block.rs  fixed-size-block allocator (ACTIVE), falls back to linked_list_allocator@@ -195,7 +228,20 @@ src/task/mod.rs           async: Task (pinned boxed future + id), yield_now() src/task/simple_executor.rs  first executor: polls every task in a loop (no wakers) src/task/executor.rs      real executor: task queue + wakers, sleeps with hlt when idle src/task/keyboard.rs      lock-free scancode queue, ScancodeStream, async keyboard echo task-src/thread.rs             preemptive kernel threads: spawn, round-robin schedule(), asm context switch+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/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/ramfs.rs           in-memory filesystem+build.rs                  compiles user/*.rs to ELF files in OUT_DIR+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 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@@ -346,9 +392,185 @@ 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. +## User mode, syscalls and processes++Four 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 |+|---|---|+| `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 |++### Rings in one paragraph++The CPU's *current privilege level* (CPL) is the low 2 bits of the CS register:+0 = kernel, 3 = user. In ring 3 the CPU refuses privileged instructions (`hlt`,+`cli`, writing CR3, `in`/`out`...) and refuses to touch any page whose page-table+entries lack the `USER_ACCESSIBLE` bit. User code can only get back into ring 0+through doors the kernel set up: interrupts/exceptions (via the IDT) and the+`syscall` instruction (via the LSTAR MSR). Every such door switches to a kernel+stack, so a user program can't corrupt kernel state through its own stack pointer.++### GDT and TSS (`src/gdt.rs`)++```+0x00 null+0x08 kernel code (ring 0)       0x10 kernel data (ring 0)+0x18 user data   (ring 3) |3    0x20 user code   (ring 3) |3+0x28 TSS (16 bytes: two slots)+```++The odd order (user *data* before user *code*) is forced by `sysret`: it loads+SS = STAR[63:48] + 8 and CS = STAR[63:48] + 16, so the two user segments must+be adjacent in that order. The TSS has two jobs: IST[0] is the double-fault+stack (stage 2), and **`rsp0`** is the stack the CPU switches to when an+interrupt arrives while in ring 3. Each thread has its own kernel stack, so the+scheduler writes the next thread's stack top into `TSS.rsp0` on every switch+(`gdt::set_kernel_stack`).++### The syscall path (`src/syscall.rs`)++`syscall` was chosen over `int 0x80`: it's what Linux uses on x86_64, it's+faster, and the setup is only four MSRs. The catch is that, unlike an+interrupt, `syscall` does **not** switch stacks -- the entry code must.++Setup (`syscall::init`): `EFER.SCE = 1` (enable), `STAR` = the selectors above,+`LSTAR` = address of `syscall_entry`, `SFMASK = IF|DF|TF` (so we enter with+interrupts off until we're on a kernel stack).++One call, step by step:++1. User code puts the number in `rax`, args in `rdi, rsi, rdx` (Linux ABI; we+   also save `r10, r8, r9`) and executes `syscall`.+2. CPU: `rcx` = user RIP, `r11` = user RFLAGS, CS/SS = kernel, RFLAGS &= ~SFMASK,+   RIP = LSTAR. RSP is still the *user* stack.+3. `syscall_entry` (asm): stash user RSP in a global, load this thread's kernel+   stack top (set by the scheduler), push user RSP, `rcx`, `r11` and the args.+4. Shuffle into the C ABI and call `dispatch(nr, a1..a5)` in Rust with+   interrupts re-enabled (so a long syscall, e.g. a blocking `read`, can be preempted).+5. `dispatch` validates user pointers (`user_slice`: range must be inside the+   user half *and* mapped with `USER_ACCESSIBLE` in the current page table),+   runs the call, and returns a value or `-errno` in `rax`.+6. `cli`, pop everything back, restore user RSP, `sysretq` (RIP = rcx,+   RFLAGS = r11, CPL = 3).++Single CPU, so the "current kernel stack" and "saved user RSP" are plain+globals; SMP kernels use per-CPU data reached with `swapgs`.++| 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 |+| 3 | `close` | fd | |+| 24 | `sched_yield` | | |+| 39 | `getpid` | | |+| 60 | `exit` | code | |++Errors are negative errno values like Linux (`-2` ENOENT, `-9` EBADF, `-14`+EFAULT, `-38` ENOSYS...). Unknown numbers return `-ENOSYS`.++### Address spaces (`src/memory.rs`, `src/process.rs`)++Each process gets a fresh level-4 page table. The kernel's L4 entries are+**copied** into it (so the kernel, heap, physical-memory map and thread stacks+are mapped in every address space and the kernel keeps working after a CR3+switch), and L4 slot 1 is the process's own:++```+0x0000_0000_0020_0000   kernel code (bootloader 0.9 puts it here)        shared, ring 0 only+0x0000_0080_0000_0000   USER_START (L4 slot 1)                           per process+0x0000_0080_0040_0000     program (.text R-X, .rodata R--, .data/.bss RW-)+0x0000_00ff_ffff_b000     user stack (16 KiB, RW-, grows down)+0x0000_0100_0000_0000   USER_END+0x0000_0100_0020_0000   boot stack (L4 slot 2)                           shared+0x0000_0180_0000_0000   all physical memory (phys_mem_offset, slot 3)    shared+0x0000_4444_4444_0000   kernel heap (thread kernel stacks live here)     shared+```++Real kernels put the kernel in the upper half (`0xffff_8000_...`) and give+user space the whole lower half. With bootloader 0.9 the kernel sits low, so+here user space is one 512 GiB slot that the kernel doesn't use. Only user+pages have `USER_ACCESSIBLE`, which is why `evil` faults: the page at+`0x200000` *is* mapped, just not for ring 3 (`PROTECTION_VIOLATION | USER_MODE`).+`.text` is not writable and everything else is `NO_EXECUTE` (when EFER.NXE is on).++A **process** = a kernel thread (`thread.rs`, it already has a kernel stack and+gets preempted) + a CR3 value + a `Process` struct (pid, name, fd table, exit+status). The scheduler, on each switch, sets `TSS.rsp0` and the syscall kernel+stack to the next thread's stack and writes CR3 if it differs. A new process's+thread starts in the kernel, then `iretq`s into ring 3 with a hand-built frame+(SS=user data, RSP=user stack, RFLAGS=IF, CS=user code, RIP=ELF entry).++When ring-3 code faults (page fault with `USER_MODE`, general protection fault+or invalid opcode with CS RPL 3), the handler calls `process::exit_current`+instead of panicking: the process is marked `Killed`, its thread exits, the+scheduler picks someone else. A fault in ring 0 still panics -- that's a kernel bug.++### ELF loading and building user programs (`src/elf.rs`, `build.rs`, `user/`)++`build.rs` compiles each `user/*.rs` with plain `rustc` for the built-in+`x86_64-unknown-none` target (no build-std needed: it ships a precompiled+`core` and uses soft-float, so no SSE state to save). Flags worth knowing:+`-C relocation-model=static` (no dynamic loader), `-C code-model=large`+(the program lives above 4 GiB; the default model assumes 32-bit addresses+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`).++`user/ulib.rs` is a tiny libc-equivalent included by each program: the+`syscall` inline asm, wrappers, `print!/println!`, `_start` (calls `main`,+then `exit` with its return value) and a panic handler (`exit(101)`).++The loader (~160 lines, no crate) checks the header (magic, 64-bit, little+endian, x86_64, `ET_EXEC`), and for every `PT_LOAD` segment: checks it's+inside the user range, maps zeroed frames with flags from the segment+(`W` -> writable, no `X` -> NX), copies `filesz` bytes through the+physical-memory map and leaves the rest (`.bss`) zero.++### VFS (`src/fs/`) and file descriptors++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`.+  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`.+- `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+  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.+- **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.++Keyboard input for `read(0)` goes through a minimal tty line discipline+(`src/tty.rs`): the keyboard task feeds characters in, Backspace edits the+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).+ ## Roadmap -Done: all 12 chapters of the 2nd edition, plus preemptive kernel threads.+Done: all 12 chapters of the 2nd edition, plus preemptive kernel threads,+user mode + syscalls, ELF loading and a VFS with a ramfs.  - [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)@@ -367,26 +589,56 @@ Done: all 12 chapters of the 2nd edition, plus preemptive kernel threads. - [x] Async/await — Task, simple executor, waker-based executor, lock-free async keyboard task (with Backspace). [phil-opp: Async/Await](https://os.phil-opp.com/async-await/), [OSDev: Multitasking Systems](https://wiki.osdev.org/Multitasking_Systems) - [x] Preemptive kernel threads — own stacks, timer-driven round-robin, asm context switch. [OSDev: Brendan's Multi-tasking Tutorial](https://wiki.osdev.org/Brendan%27s_Multi-tasking_Tutorial), [OSDev: Context Switching](https://wiki.osdev.org/Context_Switching) -Next stages (beyond the phil-opp series):--1. **User mode (ring 3) & syscalls** — ring 3 code/data segments in the GDT, set-   `TSS.rsp0` per thread so interrupts from user mode land on the kernel stack,-   enter user mode with `iretq`, then `syscall`/`sysret` (or `int 0x80` first) for a-   few calls (`write`, `exit`, `yield`). Per-process page tables (a new level-4-   table that shares the kernel half). Threads become processes.-   [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)-2. **ELF loading** — parse a static ELF (e.g. with the `xmas-elf` or `goblin` crate),-   map its segments into a fresh address space, jump to the entry point in ring 3.-   [OSDev: ELF](https://wiki.osdev.org/ELF), [OSDev: ELF Tutorial](https://wiki.osdev.org/ELF_Tutorial)-3. **Filesystem** — a VFS layer, a RAM disk / initrd first, then FAT on an ATA/virtio disk.-   [OSDev: File Systems](https://wiki.osdev.org/File_Systems), [OSDev: VFS](https://wiki.osdev.org/VFS),-   [OSDev: Initrd](https://wiki.osdev.org/Initrd), [OSDev: FAT](https://wiki.osdev.org/FAT),-   [OSDev: ATA PIO Mode](https://wiki.osdev.org/ATA_PIO_Mode)-4. **Shell** — line editing on top of the keyboard task, a few built-in commands-   (`help`, `mem`, `uptime`, `ls`, `cat`), later launching user programs.-   [OSDev: Shell](https://wiki.osdev.org/Shell)+- [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)++### Filesystem roadmap (Tyler's plan)++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)+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),+   [The Second Extended File System (Dave Poirier)](https://www.nongnu.org/ext2-doc/ext2.html)+6. **End goals:**+   - **exFAT** — because it works on literally anything (cameras, phones,+     Windows, macOS, Linux), so a kernel that speaks exFAT can share USB sticks+     and SD cards with every other machine. Allocation bitmap + FAT-like cluster+     chains + directory entry sets with checksums.+     [Microsoft exFAT specification](https://learn.microsoft.com/en-us/windows/win32/fileio/exfat-specification)+   - **btrfs** (read-only first) — because most of Tyler's Linux systems will use+     btrfs going forward, so this kernel could read their disks. Much bigger:+     copy-on-write B-trees, the chunk tree (logical -> physical mapping), the+     root/fs trees, checksums, compression (zlib/lzo/zstd). Read-only means+     parsing the superblock, chunk tree and fs tree -- no transactions or+     space allocation. [btrfs on-disk format](https://btrfs.readthedocs.io/en/latest/dev/On-disk-format.html),+     [btrfs docs](https://btrfs.readthedocs.io/)+7. **NTFS** — last, maybe never.++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`.  Small exercises along the way: a *blocked* state + sleep queue for threads, free finished threads' stacks, guard pages under thread stacks, a timer-based

Binary file changed.

@@ -0,0 +1,33 @@+//! Build script: compile the user programs in user/ into ELF executables in+//! OUT_DIR. The kernel embeds them with `include_bytes!` (src/lib.rs).+//!+//! 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).++use std::{env, path::PathBuf, process::Command};++const PROGRAMS: &[&str] = &["hello", "evil", "spin", "echo"];++fn main() {+    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());+    println!("cargo:rerun-if-changed=user");++    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");+    }+}
@@ -1,4 +1,6 @@ # Kernel dev needs nightly features (custom JSON target, build-std).+# x86_64-unknown-none: the target the user programs (user/) are built for. [toolchain] channel = "nightly" components = ["rust-src", "llvm-tools-preview"]+targets = ["x86_64-unknown-none"]
@@ -1,11 +1,13 @@ #!/usr/bin/env bash # Boot the normal (non-headless) kernel with no window, "type" some keys into # it through the QEMU monitor's `sendkey` command, then dump the VGA screen to-# a PNG. Doubles as an automated keyboard-interrupt test: the typed text is-# echoed on the serial port too, so we can grep for it.+# a PNG. Doubles as an automated keyboard + user-mode test: the default types+# a line (with a typo fixed by Backspace) and presses Enter; the `echo` user+# program reads it with read(0) and prints it back, all visible on serial. # # Usage: ./scripts/screenshot.sh [out.png] [text-to-type] [expected-result]-# In text-to-type, "<" means Backspace. The default types a typo and fixes it.+# In text-to-type, "<" means Backspace and "|" means Enter.+# Exits 1 if the expected text doesn't show up on serial. set -euo pipefail cd "$(dirname "$0")/.." @@ -15,8 +17,8 @@ touch target/x86_64-rust_kernel/debug/rust_kernel cargo bootimage IMG=target/x86_64-rust_kernel/debug/bootimage-rust_kernel.bin OUT="${1:-$PWD/boot.png}"-TEXT="${2:-Hello, kernex<l!}"-EXPECT="${3:-Hello, kernel!}"+TEXT="${2:-Hello, kernex<l!|}"+EXPECT="${3:-[echo] you typed: Hello, kernel!}" SOCK=$(mktemp -u /tmp/qemu-mon.XXXXXX) SERIAL_LOG=$(mktemp /tmp/serial.XXXXXX) @@ -31,7 +33,7 @@ import socket, sys, time sock, out, text = sys.argv[1:] # QEMU key names for the characters we might type. names = {'<': 'backspace', ' ': 'spc', ',': 'comma', '.': 'dot', '!': 'shift-1', '?': 'shift-slash',-         '-': 'minus', '\n': 'ret'}+         '-': 'minus', '|': 'ret'} def key(c):     if c in names: return names[c]     if c.isupper(): return 'shift-' + c.lower()@@ -64,6 +66,8 @@ then     echo "[screenshot] keyboard echo OK: typed '$TEXT' -> '$EXPECT'" else     echo "[screenshot] keyboard echo NOT found (expected '$EXPECT')"+    rm -f "$SERIAL_LOG"+    exit 1 fi rm -f "$SERIAL_LOG" echo "[screenshot] saved $OUT"
@@ -0,0 +1,163 @@+//! A tiny ELF64 loader: just enough to load a statically linked user program.+//!+//! An ELF file starts with a header saying what it is (64-bit, little endian,+//! x86_64, executable) and where the *program headers* are. Each program+//! header of type PT_LOAD says: "map `memsz` bytes at virtual address+//! `vaddr`, fill the first `filesz` of them from file offset `offset`, zero+//! the rest (that's .bss), with permissions R/W/X". That's all a loader for a+//! static executable has to do; then jump to `entry`.+//!+//! Every number we read comes from an untrusted file, so all offsets and sizes+//! are bounds-checked. Reference: `man 5 elf`, https://wiki.osdev.org/ELF++use crate::memory::{self, USER_END, USER_START};+use x86_64::structures::paging::{Page, PageTableFlags, PhysFrame, Size4KiB};+use x86_64::VirtAddr;++#[derive(Debug, PartialEq, Eq)]+pub enum ElfError {+    TooShort,+    NotElf,+    Not64BitLittleEndian,+    NotX86_64,+    NotExecutable,+    BadProgramHeader,+    OutsideUserSpace,+    OutOfMemory,+}++const PT_LOAD: u32 = 1;+const PF_X: u32 = 1;+const PF_W: u32 = 2;++fn read_u16(data: &[u8], at: usize) -> Result<u16, ElfError> {+    let bytes = data.get(at..at + 2).ok_or(ElfError::TooShort)?;+    Ok(u16::from_le_bytes(bytes.try_into().unwrap()))+}+fn read_u32(data: &[u8], at: usize) -> Result<u32, ElfError> {+    let bytes = data.get(at..at + 4).ok_or(ElfError::TooShort)?;+    Ok(u32::from_le_bytes(bytes.try_into().unwrap()))+}+fn read_u64(data: &[u8], at: usize) -> Result<u64, ElfError> {+    let bytes = data.get(at..at + 8).ok_or(ElfError::TooShort)?;+    Ok(u64::from_le_bytes(bytes.try_into().unwrap()))+}++/// One PT_LOAD program header.+struct Segment {+    flags: u32,+    offset: u64,+    vaddr: u64,+    filesz: u64,+    memsz: u64,+}++/// Check the ELF header; return (entry point, PT_LOAD segments).+fn parse(data: &[u8]) -> Result<(u64, impl Iterator<Item = Result<Segment, ElfError>> + '_), ElfError> {+    if data.len() < 64 {+        return Err(ElfError::TooShort);+    }+    if &data[0..4] != b"\x7fELF" {+        return Err(ElfError::NotElf);+    }+    if data[4] != 2 || data[5] != 1 {+        return Err(ElfError::Not64BitLittleEndian); // EI_CLASS=ELFCLASS64, EI_DATA=LSB+    }+    if read_u16(data, 18)? != 0x3e {+        return Err(ElfError::NotX86_64); // e_machine = EM_X86_64+    }+    if read_u16(data, 16)? != 2 {+        return Err(ElfError::NotExecutable); // e_type = ET_EXEC (static, non-PIE)+    }+    let entry = read_u64(data, 24)?;+    let phoff = read_u64(data, 32)? as usize;+    let phentsize = read_u16(data, 54)? as usize;+    let phnum = read_u16(data, 56)? as usize;+    if phentsize < 56 {+        return Err(ElfError::BadProgramHeader);+    }++    let segments = (0..phnum).filter_map(move |i| {+        let ph = phoff.checked_add(i.checked_mul(phentsize)?)?;+        let segment = (|| {+            Ok(Segment {+                flags: read_u32(data, ph + 4)?,+                offset: read_u64(data, ph + 8)?,+                vaddr: read_u64(data, ph + 16)?,+                filesz: read_u64(data, ph + 32)?,+                memsz: read_u64(data, ph + 40)?,+            })+        })();+        match read_u32(data, ph) {+            Ok(PT_LOAD) => Some(segment),+            Ok(_) => None, // other kinds (GNU_STACK, ...) we ignore+            Err(e) => Some(Err(e)),+        }+    });+    Ok((entry, segments))+}++/// Load the ELF image `data` into address space `l4`. Returns the entry point.+pub fn load(data: &[u8], l4: PhysFrame) -> Result<u64, ElfError> {+    let (entry, segments) = parse(data)?;+    if !(USER_START..USER_END).contains(&entry) {+        return Err(ElfError::OutsideUserSpace);+    }+    let nx_enabled = memory::nx_enabled();++    for segment in segments {+        let seg = segment?;+        let file_end = seg.offset.checked_add(seg.filesz).ok_or(ElfError::BadProgramHeader)?;+        let mem_end = seg.vaddr.checked_add(seg.memsz).ok_or(ElfError::BadProgramHeader)?;+        if seg.filesz > seg.memsz || file_end > data.len() as u64 {+            return Err(ElfError::BadProgramHeader);+        }+        if seg.vaddr < USER_START || mem_end > USER_END {+            return Err(ElfError::OutsideUserSpace);+        }+        if seg.memsz == 0 {+            continue;+        }++        let mut flags = PageTableFlags::empty();+        if seg.flags & PF_W != 0 {+            flags |= PageTableFlags::WRITABLE;+        }+        if seg.flags & PF_X == 0 && nx_enabled {+            flags |= PageTableFlags::NO_EXECUTE; // data can't be run as code+        }++        // Map every page the segment touches, then copy its file bytes in.+        // We write through the kernel's physical-memory window (the new+        // address space isn't active, so its user addresses mean nothing here).+        let first = Page::<Size4KiB>::containing_address(VirtAddr::new(seg.vaddr));+        let last = Page::<Size4KiB>::containing_address(VirtAddr::new(mem_end - 1));+        for page in Page::range_inclusive(first, last) {+            let frame = memory::map_user_page(l4, page, flags).ok_or(ElfError::OutOfMemory)?;++            // Which part of this page is covered by file bytes?+            let page_start = page.start_address().as_u64();+            let copy_start = page_start.max(seg.vaddr);+            let copy_end = (page_start + 4096).min(seg.vaddr + seg.filesz);+            if copy_start < copy_end {+                let src = &data[(seg.offset + (copy_start - seg.vaddr)) as usize..]+                    [..(copy_end - copy_start) as usize];+                let dst = memory::phys_to_virt(frame.start_address()) + (copy_start - page_start);+                unsafe {+                    core::ptr::copy_nonoverlapping(src.as_ptr(), dst.as_mut_ptr(), src.len());+                }+            }+            // Bytes past filesz (.bss) stay zero: map_user_page zeroes new pages.+        }+    }+    Ok(entry)+}++/// Just validate the headers (used by tests).+pub fn check(data: &[u8]) -> Result<u64, ElfError> {+    let (entry, segments) = parse(data)?;+    for s in segments {+        s?;+    }+    Ok(entry)+}
@@ -0,0 +1,17 @@+//! Error numbers, same values as Linux (`man 3 errno`). Syscalls return+//! `-errno` in rax on failure, like Linux does.++#[derive(Debug, Clone, Copy, PartialEq, Eq)]+#[repr(i64)]+pub enum Errno {+    ENOENT = 2,        // no such file or directory+    EBADF = 9,         // bad file descriptor+    EFAULT = 14,       // bad address (pointer not in user space / not mapped)+    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+    ENAMETOOLONG = 36, // path too long+    ENOSYS = 38,       // no such system call+}
@@ -0,0 +1,49 @@+//! 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`.++use super::{Inode, InodeKind};+use crate::errno::Errno;+use alloc::sync::Arc;+use spin::Mutex;++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>;+}++/// A regular file opened through the VFS: inode + current position.+pub struct InodeFile {+    inode: Arc<dyn Inode>,+    offset: Mutex<usize>,+}++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) })+    }+}++impl File for InodeFile {+    fn read(&self, buf: &mut [u8]) -> Result<usize, Errno> {+        let mut offset = self.offset.lock();+        let n = self.inode.read_at(*offset, buf)?;+        *offset += n;+        Ok(n)+    }++    fn write(&self, buf: &[u8]) -> Result<usize, Errno> {+        let mut offset = self.offset.lock();+        let n = self.inode.write_at(*offset, buf)?;+        *offset += n;+        Ok(n)+    }+}
@@ -0,0 +1,162 @@+//! VFS (virtual file system): the layer every concrete file system plugs into.+//!+//! 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");+//!   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.+//! * [`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+//!   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.++pub mod file;+pub mod ramfs;++use crate::errno::Errno;+use alloc::{string::String, sync::Arc, vec::Vec};+use spin::Mutex;++#[derive(Debug, Clone, Copy, PartialEq, Eq)]+pub enum InodeKind {+    File,+    Directory,+}++/// A file or directory inside a file system.+///+/// Default method bodies return the error a "wrong kind" of inode would give+/// (e.g. `lookup` on a file -> ENOTDIR), so implementations only override+/// what makes sense for them.+pub trait Inode: Send + Sync {+    fn kind(&self) -> InodeKind;+    /// Size in bytes (files) or number of entries (directories).+    fn size(&self) -> usize;+    /// Read bytes starting at `offset`; returns how many were read (0 = EOF).+    fn read_at(&self, _offset: usize, _buf: &mut [u8]) -> Result<usize, Errno> {+        Err(Errno::EISDIR)+    }+    /// Write bytes at `offset` (growing the file if needed).+    fn write_at(&self, _offset: usize, _buf: &[u8]) -> Result<usize, 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)+    }+    /// Create a child of this directory.+    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> {+        Err(Errno::ENOTDIR)+    }+}++/// A mounted file system.+pub trait FileSystem: Send + Sync {+    /// Type name, like the `-t` argument of `mount` ("ramfs", "fat32", ...).+    fn name(&self) -> &'static str;+    fn root(&self) -> Arc<dyn Inode>;+}++struct Mount {+    path: String, // normalized, e.g. "/" or "/tmp"+    fs: Arc<dyn FileSystem>,+}++/// The mount table. Lookups take the lock only briefly (never while doing+/// 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").+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 mut mounts = MOUNTS.lock();+    if mounts.iter().any(|m| m.path == path) {+        return Err(Errno::EEXIST);+    }+    mounts.push(Mount { path, fs });+    Ok(())+}++/// (mount path, fs type) of every mount, for printing.+pub fn mounts() -> Vec<(String, &'static str)> {+    MOUNTS.lock().iter().map(|m| (m.path.clone(), m.fs.name())).collect()+}++/// Does mount point `mount` contain `path`? ("/tmp" contains "/tmp/x" but+/// not "/tmpfoo"; "/" contains everything.)+fn mount_contains(mount: &str, path: &str) -> bool {+    mount == "/"+        || path == mount+        || (path.starts_with(mount) && path.as_bytes().get(mount.len()) == Some(&b'/'))+}++/// 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);+    }+    // 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))+            .max_by_key(|m| m.path.len())+            .ok_or(Errno::ENOENT)?;+        let rest = 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+        }+        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('/');+    let (parent, name) = path.rsplit_once('/').ok_or(Errno::EINVAL)?;+    if name.is_empty() {+        return Err(Errno::EINVAL);+    }+    let parent = lookup(if parent.is_empty() { "/" } else { parent })?;+    parent.create(name, kind)+}++/// Boot-time file system setup: a ramfs as "/", with a few files, and a+/// second, empty ramfs mounted at "/tmp" (to exercise the mount table).+pub fn init() {+    let root = ramfs::RamFs::new();+    let dir = root.root();+    let hello = dir.create("hello.txt", InodeKind::File).unwrap();+    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();+    dir.create("tmp", InodeKind::Directory).unwrap(); // mount point+    mount("/", root).unwrap();+    mount("/tmp", ramfs::RamFs::new()).unwrap();+}
@@ -0,0 +1,98 @@+//! ramfs: a file system that lives entirely on the heap (like Linux's ramfs /+//! tmpfs without the size limit). The simplest possible `FileSystem`: a tree+//! of nodes; files are a `Vec<u8>`, directories a sorted map name -> node.++use super::{FileSystem, Inode, InodeKind};+use crate::errno::Errno;+use alloc::{collections::BTreeMap, string::String, sync::Arc, vec::Vec};+use spin::Mutex;++pub struct RamFs {+    root: Arc<RamInode>,+}++impl RamFs {+    pub fn new() -> Arc<Self> {+        Arc::new(RamFs { root: Arc::new(RamInode::Dir(Mutex::new(BTreeMap::new()))) })+    }+}++impl FileSystem for RamFs {+    fn name(&self) -> &'static str {+        "ramfs"+    }+    fn root(&self) -> Arc<dyn Inode> {+        self.root.clone()+    }+}++pub enum RamInode {+    File(Mutex<Vec<u8>>),+    Dir(Mutex<BTreeMap<String, Arc<RamInode>>>),+}++impl Inode for RamInode {+    fn kind(&self) -> InodeKind {+        match self {+            RamInode::File(_) => InodeKind::File,+            RamInode::Dir(_) => InodeKind::Directory,+        }+    }++    fn size(&self) -> usize {+        match self {+            RamInode::File(data) => data.lock().len(),+            RamInode::Dir(entries) => entries.lock().len(),+        }+    }++    fn read_at(&self, offset: usize, buf: &mut [u8]) -> Result<usize, Errno> {+        let RamInode::File(data) = self else { return Err(Errno::EISDIR) };+        let data = data.lock();+        if offset >= data.len() {+            return Ok(0); // EOF+        }+        let n = buf.len().min(data.len() - offset);+        buf[..n].copy_from_slice(&data[offset..offset + n]);+        Ok(n)+    }++    fn write_at(&self, offset: usize, buf: &[u8]) -> Result<usize, Errno> {+        let RamInode::File(data) = self else { return Err(Errno::EISDIR) };+        let mut data = data.lock();+        let end = offset.checked_add(buf.len()).ok_or(Errno::EINVAL)?;+        if data.len() < end {+            data.resize(end, 0); // writing past the end grows the file (gap = zeros)+        }+        data[offset..end].copy_from_slice(buf);+        Ok(buf.len())+    }++    fn lookup(&self, name: &str) -> Result<Arc<dyn Inode>, Errno> {+        let RamInode::Dir(entries) = self else { return Err(Errno::ENOTDIR) };+        let node = entries.lock().get(name).cloned().ok_or(Errno::ENOENT)?;+        Ok(node)+    }++    fn create(&self, name: &str, kind: InodeKind) -> Result<Arc<dyn Inode>, Errno> {+        let RamInode::Dir(entries) = self else { return Err(Errno::ENOTDIR) };+        if name.is_empty() || name.contains('/') || name.len() > 255 {+            return Err(Errno::EINVAL);+        }+        let mut entries = entries.lock();+        if entries.contains_key(name) {+            return Err(Errno::EEXIST);+        }+        let node = Arc::new(match kind {+            InodeKind::File => RamInode::File(Mutex::new(Vec::new())),+            InodeKind::Directory => RamInode::Dir(Mutex::new(BTreeMap::new())),+        });+        entries.insert(String::from(name), node.clone());+        Ok(node)+    }++    fn list(&self) -> Result<Vec<String>, Errno> {+        let RamInode::Dir(entries) = self else { return Err(Errno::ENOTDIR) };+        Ok(entries.lock().keys().cloned().collect())+    }+}
@@ -1,16 +1,29 @@ //! GDT (Global Descriptor Table) + TSS (Task State Segment). //!-//! In 64-bit mode segmentation is mostly dead, but we still need a GDT for one-//! important reason: it is how the CPU finds the TSS, and the TSS holds the-//! Interrupt Stack Table (IST) -- a list of known-good stacks the CPU can-//! switch to when an exception happens.+//! In 64-bit mode segmentation is mostly dead, but the GDT still matters: //!-//! Why it matters: if the kernel stack overflows, the CPU hits the guard page-//! and raises a page fault. Pushing the exception frame onto the (already-//! overflowed) stack faults again -> double fault -> same problem -> TRIPLE-//! fault, which resets the machine. Giving the double-fault handler its own-//! IST stack breaks that chain.+//! * **Privilege levels.** The low 2 bits of CS are the *current privilege+//!   level* (CPL): ring 0 = kernel, ring 3 = user. The code segment+//!   descriptors in the GDT say which ring they belong to, so to run user code+//!   we need user code/data segments (DPL 3) next to the kernel ones.+//! * **The TSS.** The GDT is how the CPU finds the TSS, which holds:+//!     - `privilege_stack_table[0]` ("RSP0"): the stack the CPU switches to when+//!       an interrupt/exception arrives while running *ring 3* code. User+//!       stacks can't be trusted, so every thread that runs user code has its+//!       own kernel stack, and the scheduler updates RSP0 on every switch.+//!     - the Interrupt Stack Table (IST): known-good stacks for specific+//!       exceptions. The double-fault handler uses one, so a kernel stack+//!       overflow doesn't turn into a triple fault (= reboot).+//!+//! Segment order matters because of `syscall`/`sysret` (see syscall.rs): they+//! don't read the GDT entries' contents but *compute* selectors from the STAR+//! MSR: kernel CS = base, kernel SS = base + 8; user SS = base2 + 8,+//! user CS = base2 + 16. Hence:+//!+//!   0x00 null | 0x08 kernel code | 0x10 kernel data | 0x18 user data |+//!   0x20 user code | 0x28 TSS (16 bytes, two slots) +use core::ptr::{addr_of, addr_of_mut}; use lazy_static::lazy_static; use x86_64::VirtAddr; use x86_64::structures::gdt::{Descriptor, GlobalDescriptorTable, SegmentSelector};@@ -19,49 +32,74 @@ use x86_64::structures::tss::TaskStateSegment; /// Which IST slot (0..=6) the double-fault handler uses. pub const DOUBLE_FAULT_IST_INDEX: u16 = 0; -lazy_static! {-    static ref TSS: TaskStateSegment = {-        let mut tss = TaskStateSegment::new();-        tss.interrupt_stack_table[DOUBLE_FAULT_IST_INDEX as usize] = {-            // No memory allocator yet, so the stack is just a static array.-            // (It has no guard page, so don't do anything recursive in the-            // double-fault handler!)-            const STACK_SIZE: usize = 4096 * 5;-            static mut STACK: [u8; STACK_SIZE] = [0; STACK_SIZE];+/// The TSS. A `static mut` because the scheduler rewrites RSP0 on every+/// context switch (the CPU re-reads it from memory on each ring 3 -> ring 0+/// transition, so writing the field is enough). Only touched with interrupts+/// disabled, on a single CPU.+static mut TSS: TaskStateSegment = TaskStateSegment::new(); -            // Stacks grow downwards on x86, so hand the CPU the *end* address.-            let stack_start = VirtAddr::from_ptr(&raw const STACK);-            stack_start + STACK_SIZE as u64-        };-        tss-    };+/// Stack for the double-fault handler (static: no heap needed. It has no+/// guard page, so don't do anything recursive in the double-fault handler!)+const DOUBLE_FAULT_STACK_SIZE: usize = 4096 * 5;+static mut DOUBLE_FAULT_STACK: [u8; DOUBLE_FAULT_STACK_SIZE] = [0; DOUBLE_FAULT_STACK_SIZE];++/// Selectors (GDT offsets + requested privilege level) of our segments.+pub struct Selectors {+    pub kernel_code: SegmentSelector,+    pub kernel_data: SegmentSelector,+    pub user_data: SegmentSelector, // RPL 3+    pub user_code: SegmentSelector, // RPL 3+    tss: SegmentSelector, }  lazy_static! {-    /// The GDT, plus the selectors (indexes) of the entries we added.     static ref GDT: (GlobalDescriptorTable, Selectors) = {         let mut gdt = GlobalDescriptorTable::new();-        let code_selector = gdt.append(Descriptor::kernel_code_segment());-        let tss_selector = gdt.append(Descriptor::tss_segment(&TSS));-        (gdt, Selectors { code_selector, tss_selector })+        // Order matters for syscall/sysret, see the module comment.+        let kernel_code = gdt.append(Descriptor::kernel_code_segment());+        let kernel_data = gdt.append(Descriptor::kernel_data_segment());+        let user_data = gdt.append(Descriptor::user_data_segment());+        let user_code = gdt.append(Descriptor::user_code_segment());+        // Safety: TSS is a static, so the reference lives forever.+        let tss = gdt.append(Descriptor::tss_segment(unsafe { &*addr_of!(TSS) }));+        (gdt, Selectors { kernel_code, kernel_data, user_data, user_code, tss })     }; } -struct Selectors {-    code_selector: SegmentSelector,-    tss_selector: SegmentSelector,+/// The segment selectors, e.g. to build an `iretq` frame into ring 3.+pub fn selectors() -> &'static Selectors {+    &GDT.1 } -/// Load our GDT, point CS at our code segment and load the TSS.+/// Load our GDT, reload the segment registers and load the TSS. pub fn init() {-    use x86_64::instructions::segmentation::{CS, Segment};+    use x86_64::instructions::segmentation::{CS, DS, ES, SS, Segment};     use x86_64::instructions::tables::load_tss; +    unsafe {+        // Stacks grow downwards on x86, so hand the CPU the *end* address.+        let start = VirtAddr::from_ptr(addr_of!(DOUBLE_FAULT_STACK));+        (*addr_of_mut!(TSS)).interrupt_stack_table[DOUBLE_FAULT_IST_INDEX as usize] =+            start + DOUBLE_FAULT_STACK_SIZE as u64;+    }+     GDT.0.load();+    let s = &GDT.1;+    unsafe {+        // The old selectors point into the bootloader's GDT, which we just+        // replaced, so reload them. (`iretq` checks SS, so it must be valid.)+        CS::set_reg(s.kernel_code);+        SS::set_reg(s.kernel_data);+        DS::set_reg(s.kernel_data);+        ES::set_reg(s.kernel_data);+        load_tss(s.tss);+    }+}++/// Set RSP0: the kernel stack the CPU switches to when an interrupt arrives+/// while user code runs. Called by the scheduler on every context switch.+pub fn set_kernel_stack(stack_top: u64) {     unsafe {-        // The old CS value points into the bootloader's GDT, which we just-        // replaced, so reload it. Then tell the CPU where our TSS is.-        CS::set_reg(GDT.1.code_selector);-        load_tss(GDT.1.tss_selector);+        (*addr_of_mut!(TSS)).privilege_stack_table[0] = VirtAddr::new(stack_top);     } }
@@ -51,6 +51,8 @@ lazy_static! {         let mut idt = InterruptDescriptorTable::new();         idt.breakpoint.set_handler_fn(breakpoint_handler);         idt.page_fault.set_handler_fn(page_fault_handler);+        idt.general_protection_fault.set_handler_fn(general_protection_fault_handler);+        idt.invalid_opcode.set_handler_fn(invalid_opcode_handler);         unsafe {             // Run the double-fault handler on its own known-good stack (IST).             idt.double_fault@@ -78,14 +80,51 @@ extern "x86-interrupt" fn breakpoint_handler(stack_frame: InterruptStackFrame) {     serial_println!("EXCEPTION: BREAKPOINT\n{:#?}", stack_frame); } +/// Did the exception happen in user mode (ring 3)? Then it's the process's+/// fault, not the kernel's: kill the process and keep the kernel running.+/// The CPU puts the interrupted code's CS in the frame; its low 2 bits are+/// the privilege level the code ran at.+fn kill_if_user(stack_frame: &InterruptStackFrame, what: &'static str) {+    use x86_64::PrivilegeLevel;+    if stack_frame.code_segment.rpl() == PrivilegeLevel::Ring3 {+        serial_println!(+            "[kernel] {} in user mode at rip {:#x}",+            what,+            stack_frame.instruction_pointer.as_u64()+        );+        crate::process::exit_current(crate::process::ExitStatus::Killed(what)); // never returns+    }+}++/// General protection fault: e.g. a privileged instruction (`hlt`, `cli`)+/// in ring 3, or a bad segment selector.+extern "x86-interrupt" fn general_protection_fault_handler(+    stack_frame: InterruptStackFrame,+    error_code: u64,+) {+    kill_if_user(&stack_frame, "general protection fault");+    panic!("EXCEPTION: GENERAL PROTECTION FAULT (error {:#x})\n{:#?}", error_code, stack_frame);+}++/// Invalid opcode (`ud2`, or garbage executed as code).+extern "x86-interrupt" fn invalid_opcode_handler(stack_frame: InterruptStackFrame) {+    kill_if_user(&stack_frame, "invalid opcode");+    panic!("EXCEPTION: INVALID OPCODE\n{:#?}", stack_frame);+}+ /// Page fault: accessing unmapped/protected memory. CR2 holds the bad address, /// the error code says why (not present? write? user mode?). A real OS would-/// now load the page from disk or kill the process; we just report and stop.+/// load the page from disk / swap here; we kill a faulting user process, and+/// stop if the kernel itself faulted. extern "x86-interrupt" fn page_fault_handler(     stack_frame: InterruptStackFrame,     error_code: PageFaultErrorCode, ) {     use x86_64::registers::control::Cr2;+    if error_code.contains(PageFaultErrorCode::USER_MODE) {+        serial_println!("[kernel] user page fault: address {:?}, error {:?}", Cr2::read(), error_code);+        kill_if_user(&stack_frame, "page fault");+    }     println!("EXCEPTION: PAGE FAULT at {:?}", Cr2::read());     serial_println!(         "EXCEPTION: PAGE FAULT\nAccessed address: {:?}\nError code: {:?}\n{:#?}",
@@ -22,19 +22,34 @@ extern crate alloc; use core::panic::PanicInfo;  pub mod allocator; // heap: maps heap pages + the #[global_allocator]-pub mod gdt; // GDT + TSS: gives the double-fault handler its own stack+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: page tables, address translation, frame allocator+pub mod memory; // paging, frame allocator, per-process address spaces+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 pub mod task; // cooperative multitasking: async tasks + executor 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`). pub fn init() {     gdt::init();+    syscall::init();     interrupts::init_idt();     unsafe { interrupts::PICS.lock().initialize() };     x86_64::instructions::interrupts::enable();
@@ -14,7 +14,8 @@ use alloc::{boxed::Box, rc::Rc, vec, vec::Vec}; use bootloader::{BootInfo, entry_point}; use core::panic::PanicInfo; use rust_kernel::task::{Task, executor::Executor, keyboard};-use rust_kernel::{allocator, kprintln, memory, println, serial_println, thread, vga_buffer};+use rust_kernel::{allocator, fs, kprintln, memory, println, process, serial_println, thread};+use rust_kernel::{user_programs, vga_buffer}; use x86_64::{VirtAddr, structures::paging::{Page, Translate}};  // `entry_point!` defines the real `_start` for us and checks at compile time@@ -47,12 +48,31 @@ 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) ----------------------+    fs::init();+    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)+    ];+    for (name, elf) in programs {+        if let Err(e) = process::spawn(name, elf) {+            kprintln!("[process] could not start {}: {:?}", name, e);+        }+    }+     // --- Cooperative async tasks (task/) ------------------------------------     // The main thread runs the executor; the keyboard is just another task.     let mut executor = Executor::new();     executor.spawn(Task::new(example_task()));     executor.spawn(Task::new(keyboard::print_keypresses()));-    kprintln!("Type something! (keyboard echo, Backspace works)");+    kprintln!("Type a line + Enter: the echo program (ring 3) reads it");     executor.run(); // never returns: runs tasks, `hlt`s when idle } @@ -184,6 +204,10 @@ fn paging_demo(boot_info: &'static BootInfo) {     kprintln!("[heap] Rc count after clone = {}", Rc::strong_count(&cloned_reference));     core::mem::drop(reference_counted);     kprintln!("[heap] Rc count after drop  = {}", Rc::strong_count(&cloned_reference));++    // From now on the kernel owns the frame allocator globally (processes+    // need frames for their page tables and pages).+    memory::init_frame_allocator(frame_allocator); }  /// Recurse forever until we run off the end of the stack into the guard page.
@@ -7,22 +7,48 @@ //! feature maps all of physical RAM at `physical_memory_offset`, so //!     virtual address = physical_memory_offset + physical address. //! `OffsetPageTable` from the x86_64 crate implements exactly that.+//!+//! Address spaces (for user processes): every process gets its own level-4+//! table. Its entries are copied from the kernel's level-4 table, so the+//! kernel (code, heap, stacks, the physical-memory window) is mapped+//! identically everywhere -- just *without* the USER_ACCESSIBLE flag, so ring+//! 3 code can't touch it. User code and stack go into L4 slot 1+//! (`USER_START..USER_END`), which the kernel doesn't use. Switching CR3 then+//! swaps only the user part. (Linux does the same with the upper half; our+//! bootloader put the kernel in the *lower* half, so we use a free slot.)  use bootloader::bootinfo::{MemoryMap, MemoryRegionType};+use core::sync::atomic::{AtomicU64, Ordering};+use spin::Mutex; use x86_64::{     PhysAddr, VirtAddr,+    registers::control::Cr3,     structures::paging::{         FrameAllocator, Mapper, OffsetPageTable, Page, PageTable, PageTableFlags, PhysFrame,-        Size4KiB,+        Size4KiB, Translate, mapper::TranslateResult,     }, }; +/// User space: level-4 slot 1, i.e. 512 GiB starting at 512 GiB.+pub const USER_START: u64 = 0x0000_0080_0000_0000;+pub const USER_END: u64 = 0x0000_0100_0000_0000; // exclusive+const USER_L4_INDEX: usize = 1;++/// Where all physical memory is mapped (set by `init`).+static PHYS_OFFSET: AtomicU64 = AtomicU64::new(0);+/// Physical address of the kernel's (boot) level-4 table (set by `init`).+static KERNEL_L4: AtomicU64 = AtomicU64::new(0);+/// The frame allocator, once `init_frame_allocator` hands it over.+static FRAME_ALLOCATOR: Mutex<Option<BootInfoFrameAllocator>> = Mutex::new(None);+ /// Create an `OffsetPageTable` for the currently active page tables. /// /// Unsafe: the caller must guarantee that all physical memory really is /// mapped at `physical_memory_offset`, and must call this only once (two /// `&mut` to the same page table would be undefined behaviour). pub unsafe fn init(physical_memory_offset: VirtAddr) -> OffsetPageTable<'static> {+    PHYS_OFFSET.store(physical_memory_offset.as_u64(), Ordering::Relaxed);+    KERNEL_L4.store(Cr3::read().0.start_address().as_u64(), Ordering::Relaxed);     unsafe {         let level_4_table = active_level_4_table(physical_memory_offset);         OffsetPageTable::new(level_4_table, physical_memory_offset)@@ -31,8 +57,6 @@ pub unsafe fn init(physical_memory_offset: VirtAddr) -> OffsetPageTable<'static>  /// Find the active level-4 table via CR3 and return a reference to it. unsafe fn active_level_4_table(physical_memory_offset: VirtAddr) -> &'static mut PageTable {-    use x86_64::registers::control::Cr3;-     let (level_4_table_frame, _flags) = Cr3::read();     let phys = level_4_table_frame.start_address();     let virt = physical_memory_offset + phys.as_u64();@@ -100,3 +124,138 @@ pub fn usable_memory_bytes(memory_map: &MemoryMap) -> u64 {         .map(|r| r.range.end_addr() - r.range.start_addr())         .sum() }++// ---------------------------------------------------------------------------+// Global frame allocator + per-process address spaces+// ---------------------------------------------------------------------------++/// Hand the frame allocator over to the kernel (after the heap is set up),+/// so processes can get page tables and pages later.+pub fn init_frame_allocator(frame_allocator: BootInfoFrameAllocator) {+    *FRAME_ALLOCATOR.lock() = Some(frame_allocator);+}++/// One fresh physical frame, or None if out of memory / not initialized.+pub fn alloc_frame() -> Option<PhysFrame> {+    FRAME_ALLOCATOR.lock().as_mut()?.allocate_frame()+}++/// `FrameAllocator` adapter around the global allocator (for `map_to`, which+/// may need frames for new page tables).+struct GlobalFrameAllocator;++unsafe impl FrameAllocator<Size4KiB> for GlobalFrameAllocator {+    fn allocate_frame(&mut self) -> Option<PhysFrame> {+        alloc_frame()+    }+}++/// Virtual address through which the kernel can reach physical address `phys`.+pub fn phys_to_virt(phys: PhysAddr) -> VirtAddr {+    VirtAddr::new(PHYS_OFFSET.load(Ordering::Relaxed) + phys.as_u64())+}++/// The kernel's level-4 table frame (what kernel threads run with).+pub fn kernel_l4_frame() -> PhysFrame {+    match KERNEL_L4.load(Ordering::Relaxed) {+        0 => Cr3::read().0, // `init` not called yet: we're still on the boot tables+        addr => PhysFrame::containing_address(PhysAddr::new(addr)),+    }+}++/// Does the CPU honour the NO_EXECUTE page flag? (EFER.NXE, which our+/// bootloader turns on. With it off, setting the bit would be an error.)+pub fn nx_enabled() -> bool {+    use x86_64::registers::model_specific::{Efer, EferFlags};+    Efer::read().contains(EferFlags::NO_EXECUTE_ENABLE)+}++/// A frame filled with zeros (new page tables and user pages must not leak+/// old data).+fn alloc_zeroed_frame() -> Option<PhysFrame> {+    let frame = alloc_frame()?;+    let ptr: *mut u8 = phys_to_virt(frame.start_address()).as_mut_ptr();+    unsafe { core::ptr::write_bytes(ptr, 0, 4096) };+    Some(frame)+}++/// Page-table view of the address space whose level-4 table is `l4`.+///+/// Unsafe: the caller must not create two of these for the same table at the+/// same time (both would hand out `&mut` to the same memory).+unsafe fn mapper_for(l4: PhysFrame) -> OffsetPageTable<'static> {+    let table: *mut PageTable = phys_to_virt(l4.start_address()).as_mut_ptr();+    let offset = VirtAddr::new(PHYS_OFFSET.load(Ordering::Relaxed));+    unsafe { OffsetPageTable::new(&mut *table, offset) }+}++/// Create a new address space: a fresh level-4 table that shares all kernel+/// mappings (entries copied from the kernel's table) and has an empty user part.+pub fn new_address_space() -> Option<PhysFrame> {+    let frame = alloc_zeroed_frame()?;+    unsafe {+        let new: &mut PageTable = &mut *phys_to_virt(frame.start_address()).as_mut_ptr();+        let kernel: &PageTable = &*phys_to_virt(kernel_l4_frame().start_address()).as_ptr();+        // The kernel must not use our user slot, or user mappings would end up+        // in page tables shared with the kernel (and every other process).+        assert!(kernel[USER_L4_INDEX].is_unused(), "kernel uses the user L4 slot");+        for i in 0..512 {+            new[i] = kernel[i].clone();+        }+    }+    Some(frame)+}++/// Make sure `page` (in user space) is mapped in address space `l4` with at+/// least `flags` (USER_ACCESSIBLE + PRESENT are added). A new page gets a+/// zeroed frame; an already mapped one keeps its frame and gets the union+/// of old and new flags. Returns the backing frame.+pub fn map_user_page(l4: PhysFrame, page: Page, flags: PageTableFlags) -> Option<PhysFrame> {+    let addr = page.start_address().as_u64();+    assert!((USER_START..USER_END).contains(&addr), "not a user address");+    let flags = flags | PageTableFlags::PRESENT | PageTableFlags::USER_ACCESSIBLE;++    let mut mapper = unsafe { mapper_for(l4) };+    if let TranslateResult::Mapped { frame, flags: old, .. } = mapper.translate(page.start_address()) {+        // Already mapped (two ELF segments sharing a page): widen the flags.+        // Keep NO_EXECUTE only if both want it.+        let mut new = old | flags;+        if !(old & flags).contains(PageTableFlags::NO_EXECUTE) {+            new.remove(PageTableFlags::NO_EXECUTE);+        }+        unsafe { mapper.update_flags(page, new).ok()?.ignore() };+        return Some(PhysFrame::containing_address(frame.start_address()));+    }+    let frame = alloc_zeroed_frame()?;+    // map_to also creates missing L3/L2/L1 tables; since `flags` contains+    // USER_ACCESSIBLE, those parent entries get it too (the CPU checks the+    // user bit at every level). The TLB doesn't matter: this address space+    // may not even be active, and the mapping is new. Hence `ignore`.+    unsafe { mapper.map_to(page, frame, flags, &mut GlobalFrameAllocator).ok()?.ignore() };+    Some(frame)+}++/// Can user code legitimately access `len` bytes at `addr` in the *current*+/// address space? Checks the range is inside user space and every page is+/// mapped USER_ACCESSIBLE (and WRITABLE if `write`). Syscalls call this+/// before touching any pointer a user program hands them; otherwise a program+/// could make the kernel read or overwrite kernel memory for it.+pub fn user_range_ok(addr: u64, len: u64, write: bool) -> bool {+    let Some(end) = addr.checked_add(len) else { return false };+    if addr < USER_START || end > USER_END {+        return false;+    }+    if len == 0 {+        return true;+    }+    let mapper = unsafe { mapper_for(Cr3::read().0) };+    let first = Page::<Size4KiB>::containing_address(VirtAddr::new(addr));+    let last = Page::<Size4KiB>::containing_address(VirtAddr::new(end - 1));+    Page::range_inclusive(first, last).all(|page| match mapper.translate(page.start_address()) {+        TranslateResult::Mapped { flags, .. } => {+            flags.contains(PageTableFlags::USER_ACCESSIBLE)+                && (!write || flags.contains(PageTableFlags::WRITABLE))+        }+        _ => false,+    })+}
@@ -0,0 +1,186 @@+//! Processes = a thread + its own address space + a file descriptor table.+//!+//! Starting one (`spawn`):+//!   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`.+//!+//! 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.+//!+//! 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.++use crate::elf::{self, ElfError};+use crate::errno::Errno;+use crate::fs::file::File;+use crate::memory::{self, USER_END};+use crate::{serial_println, thread, tty::Console};+use alloc::{sync::Arc, vec, vec::Vec};+use core::sync::atomic::{AtomicU64, Ordering};+use spin::Mutex;+use x86_64::VirtAddr;+use x86_64::structures::paging::{Page, PageTableFlags, Size4KiB};++/// The user stack: 16 KiB, ending one (unmapped) page below USER_END.+pub const USER_STACK_TOP: u64 = USER_END - 0x1000;+pub const USER_STACK_SIZE: u64 = 16 * 1024;+/// Max open files per process.+const MAX_FDS: usize = 16;++#[derive(Debug, Clone, Copy, PartialEq, Eq)]+pub enum ExitStatus {+    /// Called exit(code).+    Exited(i64),+    /// Killed by the kernel (e.g. "page fault").+    Killed(&'static str),+}++pub struct Process {+    pub pid: u64,+    pub name: &'static str,+    entry: u64,+    fds: Mutex<Vec<Option<Arc<dyn File>>>>,+    status: Mutex<Option<ExitStatus>>,+}++static NEXT_PID: AtomicU64 = AtomicU64::new(1);++/// 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> {+    let l4 = memory::new_address_space().ok_or(ElfError::OutOfMemory)?;+    let entry = elf::load(elf_image, l4)?;++    let mut stack_flags = PageTableFlags::WRITABLE;+    if memory::nx_enabled() {+        stack_flags |= PageTableFlags::NO_EXECUTE;+    }+    let first = Page::<Size4KiB>::containing_address(VirtAddr::new(USER_STACK_TOP - USER_STACK_SIZE));+    let last = Page::<Size4KiB>::containing_address(VirtAddr::new(USER_STACK_TOP - 1));+    for page in Page::range_inclusive(first, last) {+        memory::map_user_page(l4, page, stack_flags).ok_or(ElfError::OutOfMemory)?;+    }++    let console: Arc<dyn File> = Arc::new(Console);+    let process = Arc::new(Process {+        pid: NEXT_PID.fetch_add(1, Ordering::Relaxed),+        name,+        entry,+        fds: Mutex::new(vec![Some(console.clone()), Some(console.clone()), Some(console)]),+        status: Mutex::new(None),+    });+    serial_println!("[process] started pid {} ({}), entry {:#x}", process.pid, name, entry);+    thread::spawn_with(name, user_thread_entry, l4, Some(process.clone()));+    Ok(process)+}++/// First code of every process thread (still ring 0, but already running+/// with the process's CR3 and kernel stack).+fn user_thread_entry() {+    let process = thread::current_process().expect("process thread without process");+    // 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) }+}++/// 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) -> ! {+    let s = crate::gdt::selectors();+    unsafe {+        core::arch::asm!(+            "push {ss}",+            "push {rsp}",+            "push {rflags}",+            "push {cs}",+            "push {rip}",+            // Don't leak kernel values to user space in registers.+            "xor eax, eax", "xor ebx, ebx", "xor ecx, ecx", "xor edx, edx",+            "xor esi, esi", "xor edi, edi", "xor ebp, ebp",+            "xor r8d, r8d", "xor r9d, r9d", "xor r10d, r10d", "xor r11d, r11d",+            "xor r12d, r12d", "xor r13d, r13d", "xor r14d, r14d", "xor r15d, r15d",+            "iretq",+            ss = in(reg) s.user_data.0 as u64,+            rsp = in(reg) rsp,+            rflags = in(reg) 0x202u64,+            cs = in(reg) s.user_code.0 as u64,+            rip = in(reg) rip,+            options(noreturn)+        );+    }+}++impl Process {+    /// The open file behind `fd`.+    pub fn file(&self, fd: u64) -> Result<Arc<dyn File>, Errno> {+        let fds = self.fds.lock();+        fds.get(fd as usize).and_then(|f| f.clone()).ok_or(Errno::EBADF)+    }++    /// Put `file` into the lowest free fd slot (like Unix).+    pub fn add_file(&self, file: Arc<dyn File>) -> Result<u64, Errno> {+        let mut fds = self.fds.lock();+        if let Some(i) = fds.iter().position(|f| f.is_none()) {+            fds[i] = Some(file);+            return Ok(i as u64);+        }+        if fds.len() >= MAX_FDS {+            return Err(Errno::EMFILE);+        }+        fds.push(Some(file));+        Ok(fds.len() as u64 - 1)+    }++    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(())+            }+            _ => Err(Errno::EBADF),+        }+    }++    pub fn status(&self) -> Option<ExitStatus> {+        *self.status.lock()+    }+}++/// 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);+        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+            ),+        }+    }+    thread::exit_current()+}++/// Block (the calling kernel thread) until `process` has ended.+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+    }+}
@@ -0,0 +1,204 @@+//! System calls via the `syscall` / `sysret` instructions.+//!+//! ABI (like Linux x86_64): number in rax, arguments in rdi, rsi, rdx (r10,+//! r8 for 4th/5th), result in rax; negative results are `-errno`. The+//! `syscall` instruction itself clobbers rcx (return address) and r11 (saved+//! 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                           | |+//!+//! What happens on `syscall` (the CPU does steps 1-3 in one instruction):+//!   1. rcx = user RIP (next instruction), r11 = user RFLAGS+//!   2. RFLAGS &= !SFMASK  (we mask IF -> interrupts off)+//!   3. CS/SS = kernel selectors from STAR, RIP = LSTAR (`syscall_entry`), CPL = 0+//!   ...but RSP is still the *user* stack! The CPU doesn't switch stacks for+//!   `syscall` (unlike interrupts, which use TSS.RSP0). So `syscall_entry`+//!   must do it first: stash the user RSP, load this thread's kernel stack,+//!   save registers, re-enable interrupts and call `syscall_dispatch` in Rust.+//!   `sysretq` reverses it: RIP = rcx, RFLAGS = r11, CPL = 3.+//!+//! Single CPU only: the "current kernel stack" and the user-RSP scratch slot+//! are plain globals. (Multi-core kernels use per-CPU data via `swapgs`.)+//!+//! Why not `int 0x80`? It also works (and is simpler: the CPU switches stacks+//! via TSS.RSP0 for us), but `syscall` is what Linux and every modern x86_64+//! OS use, and it's faster.++use crate::errno::Errno;+use crate::fs::{self, file::InodeFile};+use crate::{memory, process, thread};+use alloc::sync::Arc;+use core::arch::global_asm;+use core::sync::atomic::{AtomicU64, Ordering};++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_SCHED_YIELD: u64 = 24;+pub const SYS_GETPID: u64 = 39;+pub const SYS_EXIT: u64 = 60;++pub const O_CREAT: u64 = 0o100;++/// Kernel stack top of the running thread (set by the scheduler).+#[unsafe(no_mangle)]+static SYSCALL_KERNEL_RSP: AtomicU64 = AtomicU64::new(0);+/// Scratch slot for the user RSP during the first instructions of the entry.+#[unsafe(no_mangle)]+static SYSCALL_USER_RSP: AtomicU64 = AtomicU64::new(0);++/// Called by the scheduler when switching threads.+pub fn set_kernel_stack(stack_top: u64) {+    SYSCALL_KERNEL_RSP.store(stack_top, Ordering::Relaxed);+}++/// Program the MSRs that make `syscall` work.+pub fn init() {+    use x86_64::VirtAddr;+    use x86_64::registers::model_specific::{Efer, EferFlags, LStar, SFMask, Star};+    use x86_64::registers::rflags::RFlags;++    let s = crate::gdt::selectors();+    unsafe {+        // EFER.SCE: enable the syscall/sysret instructions at all.+        Efer::update(|flags| flags.insert(EferFlags::SYSTEM_CALL_EXTENSIONS));+    }+    // STAR: which segments syscall/sysret load (see gdt.rs for the layout rule).+    Star::write(s.user_code, s.user_data, s.kernel_code, s.kernel_data)+        .expect("GDT layout doesn't fit syscall/sysret");+    // LSTAR: where `syscall` jumps to.+    LStar::write(VirtAddr::new(syscall_entry as *const () as u64));+    // SFMASK: RFLAGS bits to clear on entry. IF: no interrupts until we're on+    // the kernel stack. DF: Rust expects the direction flag clear. TF: no+    // single-stepping into the kernel.+    SFMask::write(RFlags::INTERRUPT_FLAG | RFlags::DIRECTION_FLAG | RFlags::TRAP_FLAG);+}++unsafe extern "C" {+    fn syscall_entry();+}++global_asm!(+    ".global syscall_entry",+    "syscall_entry:",+    // Interrupts are off (SFMASK). Switch to this thread's kernel stack.+    "    mov [rip + SYSCALL_USER_RSP], rsp",+    "    mov rsp, [rip + SYSCALL_KERNEL_RSP]",+    "    push qword ptr [rip + SYSCALL_USER_RSP]", // user rsp+    "    push rcx",                                // user rip+    "    push r11",                                // user rflags+    // Preserve the argument registers for the user (Rust may clobber them).+    // rbx, rbp, r12-r15 are callee-saved: the Rust code keeps them intact.+    "    push rdi",+    "    push rsi",+    "    push rdx",+    "    push r8",+    "    push r9",+    "    push r10",+    "    sub rsp, 8", // 10 pushes total -> stack 16-byte aligned for the call+    // syscall_dispatch(nr, a1, a2, a3, a4, a5) in the C calling convention:+    // shuffle rax, rdi, rsi, rdx, r10, r8 -> rdi, rsi, rdx, rcx, r8, r9.+    "    mov r9, r8",+    "    mov r8, r10",+    "    mov rcx, rdx",+    "    mov rdx, rsi",+    "    mov rsi, rdi",+    "    mov rdi, rax",+    "    sti",  // we're on our own stack now: preemption is fine+    "    call {dispatch}", // result in rax+    "    cli",  // no interrupts while we're back on the user stack in ring 0+    "    add rsp, 8",+    "    pop r10",+    "    pop r9",+    "    pop r8",+    "    pop rdx",+    "    pop rsi",+    "    pop rdi",+    "    pop r11",+    "    pop rcx",+    "    pop rsp",+    // (rcx came from the `syscall` itself, so it's a canonical address;+    // sysret with a non-canonical rcx would fault *in ring 0* -- a classic bug.)+    "    sysretq",+    dispatch = sym syscall_dispatch,+);++/// 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 {+    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_SCHED_YIELD => {+            thread::yield_now();+            Ok(0)+        }+        SYS_GETPID => current().map(|p| p.pid),+        SYS_EXIT => process::exit_current(process::ExitStatus::Exited(a1 as i64)),+        _ => Err(Errno::ENOSYS),+    };+    match result {+        Ok(value) => value as i64,+        Err(errno) => -(errno as i64),+    }+}++fn current() -> Result<Arc<process::Process>, Errno> {+    thread::current_process().ok_or(Errno::EINVAL)+}++/// 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 !memory::user_range_ok(ptr, len, false) {+        return Err(Errno::EFAULT);+    }+    Ok(unsafe { core::slice::from_raw_parts(ptr as *const u8, len as usize) })+}++/// 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 !memory::user_range_ok(ptr, len, true) {+        return Err(Errno::EFAULT);+    }+    Ok(unsafe { core::slice::from_raw_parts_mut(ptr as *mut u8, len as usize) })+}++fn sys_read(fd: u64, buf: u64, len: u64) -> Result<u64, Errno> {+    let file = current()?.file(fd)?; // clone the Arc: no lock held while we block+    let buf = user_slice_mut(buf, len)?;+    file.read(buf).map(|n| n as u64)+}++fn sys_write(fd: u64, buf: u64, len: u64) -> Result<u64, Errno> {+    let file = current()?.file(fd)?;+    let buf = user_slice(buf, len)?;+    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 {+        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)?,+        other => other?,+    };+    let file = Arc::new(InodeFile::open(inode)?);+    current()?.add_file(file)+}++fn sys_close(fd: u64) -> Result<u64, Errno> {+    current()?.close(fd).map(|_| 0)+}
@@ -6,7 +6,7 @@ //! (`print_keypresses`), outside interrupt context. That keeps the handler //! short and means it never takes a lock (no deadlock risk). -use crate::{print, serial_print, vga_buffer};+use crate::{print, serial_print}; use conquer_once::spin::OnceCell; use core::{     pin::Pin,@@ -91,17 +91,10 @@ pub async fn print_keypresses() {             && let Some(key) = keyboard.process_keyevent(key_event)         {             match key {-                // Backspace: erase the previous character on screen. On-                // serial, the uart_16550 crate already turns a 0x08 byte into-                // "\x08 \x08" (back, overwrite with space, back) for us.-                DecodedKey::Unicode('\u{8}') => {-                    vga_buffer::backspace();-                    serial_print!("\x08");-                }-                DecodedKey::Unicode(character) => {-                    print!("{}", character);-                    serial_print!("{}", character);-                }+                // Characters (incl. Backspace and Enter) go to the tty's+                // line discipline, which echoes them and buffers the line+                // for user programs reading fd 0.+                DecodedKey::Unicode(character) => crate::tty::input_char(character),                 // Modifiers only change how other keys decode: print nothing.                 DecodedKey::RawKey(                     KeyCode::LShift | KeyCode::RShift | KeyCode::LControl | KeyCode::RControl
@@ -21,15 +21,24 @@ //! So a sleeping thread's whole CPU state is simply sitting on its own stack, //! and all we have to remember per thread is its stack pointer. //!+//! Threads that run user code (processes, see process.rs) additionally carry+//! an address space (CR3) and a kernel stack top. On every switch the+//! scheduler loads the next thread's CR3 and points the TSS's RSP0 and the+//! syscall entry at its kernel stack, so interrupts and syscalls from ring 3+//! land on the right stack.+//! //! 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 //! thread's function. -use alloc::{boxed::Box, vec, vec::Vec};+use crate::process::Process;+use alloc::{boxed::Box, sync::Arc, vec, vec::Vec}; use core::arch::global_asm; use spin::Mutex; 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).@@ -41,6 +50,13 @@ struct Thread {     rsp: u64,     /// The thread's stack. `None` for the boot thread (uses the bootloader's stack).     _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,+    /// Level-4 page table to run with (the kernel's, or a process's own).+    cr3: PhysFrame,+    /// The process this thread belongs to (None = pure kernel thread).+    process: Option<Arc<Process>>,     finished: bool, } @@ -55,6 +71,12 @@ static SCHEDULER: Mutex<Scheduler> = Mutex::new(Scheduler { threads: Vec::new(),  /// Start a new kernel thread running `entry`. Needs the heap. pub fn spawn(name: &'static str, entry: fn()) {+    spawn_with(name, entry, crate::memory::kernel_l4_frame(), None);+}++/// Start a thread for `process`: runs `entry` (in the kernel) with the+/// 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];      // Build the initial stack, top down (stacks grow towards lower addresses):@@ -74,7 +96,15 @@ pub fn spawn(name: &'static str, entry: fn()) {     let rsp = top - 8 * initial.len() as u64;     unsafe { core::ptr::copy_nonoverlapping(initial.as_ptr(), rsp as *mut u64, initial.len()) }; -    let thread = Box::new(Thread { name, rsp, _stack: Some(stack), finished: false });+    let thread = Box::new(Thread {+        name,+        rsp,+        _stack: Some(stack),+        kernel_stack_top: top,+        cr3,+        process,+        finished: false,+    });      // The timer handler also takes this lock, so keep interrupts off while we     // hold it -- otherwise the handler would spin forever on our lock.@@ -88,6 +118,9 @@ pub fn spawn(name: &'static str, entry: fn()) {                 name: "main",                 rsp: 0,                 _stack: None,+                kernel_stack_top: 0,+                cr3: Cr3::read().0,+                process: None,                 finished: false,             }));         }@@ -113,6 +146,20 @@ pub fn schedule() {             _ => return, // nobody else to run         };         s.current = next;++        // Prepare the CPU for the next thread *before* switching stacks.+        let t = &s.threads[next];+        if t.kernel_stack_top != 0 {+            crate::gdt::set_kernel_stack(t.kernel_stack_top); // interrupts from ring 3+            crate::syscall::set_kernel_stack(t.kernel_stack_top); // `syscall` from ring 3+        }+        if Cr3::read().0 != t.cr3 {+            // Switch address space. Safe for the code running right now:+            // all kernel mappings (incl. every kernel stack) are identical in+            // every address space. Writing CR3 also flushes the TLB.+            unsafe { Cr3::write(t.cr3, Cr3::read().1) };+        }+         let old_rsp: *mut u64 = &mut s.threads[current].rsp;         (old_rsp, s.threads[next].rsp)         // The lock is released here, *before* switching: the next thread@@ -121,6 +168,32 @@ pub fn schedule() {     unsafe { switch_context(old_rsp, new_rsp) }; } +/// Give up the rest of this time slice (also callable outside interrupts).+pub fn yield_now() {+    interrupts::without_interrupts(schedule);+}++/// The process the running thread belongs to, if any.+pub fn current_process() -> Option<Arc<Process>> {+    interrupts::without_interrupts(|| {+        let s = SCHEDULER.lock();+        s.threads.get(s.current).and_then(|t| t.process.clone())+    })+}++/// End the running thread: mark it finished and switch away for good.+/// (Its stack is kept: we're still running on it until the switch.)+pub fn exit_current() -> ! {+    interrupts::disable();+    {+        let mut s = SCHEDULER.lock();+        let current = s.current;+        s.threads[current].finished = true;+    }+    schedule(); // never returns: finished threads aren't picked again+    crate::hlt_loop(); // only reached if nothing else exists to run+}+ /// Name of the running thread (for printing). pub fn current_name() -> &'static str {     interrupts::without_interrupts(|| {@@ -189,13 +262,6 @@ extern "C" fn thread_start(entry: usize) -> ! {     let entry: fn() = unsafe { core::mem::transmute(entry) };     entry(); -    // The thread function returned: mark it finished so the scheduler skips-    // it, then idle until we're switched away for good. (Its stack is kept;-    // freeing it while we still run on it would be a bad idea.)-    interrupts::without_interrupts(|| {-        let mut s = SCHEDULER.lock();-        let current = s.current;-        s.threads[current].finished = true;-    });-    crate::hlt_loop();+    // The thread function returned: mark it finished so the scheduler skips it.+    exit_current(); }
@@ -0,0 +1,95 @@+//! The console "tty": keyboard input with a tiny line discipline, and the+//! `File` behind file descriptors 0, 1 and 2.+//!+//! Like a Linux terminal in canonical mode: typed characters are echoed and+//! collected in a line buffer (Backspace edits it), and only when Enter is+//! pressed does the whole line become readable from fd 0. So a user program's+//! `read(0, ...)` gets a complete, already-edited line.++use crate::errno::Errno;+use crate::fs::file::File;+use crate::{print, serial_print, vga_buffer};+use alloc::string::String;+use conquer_once::spin::OnceCell;+use crossbeam_queue::ArrayQueue;+use spin::Mutex;++/// The line being typed (not yet readable).+static LINE: Mutex<String> = Mutex::new(String::new());+/// Finished lines, as bytes, waiting for `read(0, ...)`.+static INPUT: OnceCell<ArrayQueue<u8>> = OnceCell::uninit();++fn input() -> &'static ArrayQueue<u8> {+    INPUT.get_or_init(|| ArrayQueue::new(4096))+}++/// Called by the keyboard task for every typed character.+pub fn input_char(c: char) {+    let mut line = LINE.lock();+    match c {+        '\u{8}' => {+            // Backspace: only erase what's in the current line.+            if line.pop().is_some() {+                vga_buffer::backspace();+                serial_print!("\x08"); // uart_16550 turns this into "\x08 \x08"+            }+        }+        '\n' => {+            print!("\n");+            serial_print!("\n");+            for b in line.bytes().chain(Some(b'\n')) {+                let _ = input().push(b); // queue full: drop (nobody is reading)+            }+            line.clear();+        }+        c => {+            print!("{}", c);+            serial_print!("{}", c);+            line.push(c);+        }+    }+}++/// The console as a file: reading gets keyboard lines, writing prints to the+/// screen and serial port.+pub struct Console;++impl File for Console {+    /// Blocks until a line is available, then returns up to `buf.len()`+    /// bytes of it (stopping after the '\n').+    fn read(&self, buf: &mut [u8]) -> Result<usize, Errno> {+        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;+            }+        }+        Ok(n)+    }++    fn write(&self, buf: &[u8]) -> Result<usize, Errno> {+        // Print valid UTF-8 as is, anything else as '?'.+        for chunk in buf.utf8_chunks() {+            print!("{}", chunk.valid());+            serial_print!("{}", chunk.valid());+            if !chunk.invalid().is_empty() {+                print!("?");+                serial_print!("?");+            }+        }+        Ok(buf.len())+    }+}
@@ -0,0 +1,95 @@+//! 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.+#![no_std]+#![no_main]+#![feature(custom_test_frameworks)]+#![test_runner(rust_kernel::test_runner)]+#![reexport_test_harness_main = "test_main"]++use bootloader::{BootInfo, entry_point};+use core::panic::PanicInfo;+use core::sync::atomic::Ordering;+use rust_kernel::errno::Errno;+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;++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);+    fs::init();++    test_main();+    rust_kernel::hlt_loop();+}++/// 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");+    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);+}++/// 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"));+}++/// 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 start = TICKS.load(Ordering::Relaxed);+    let mut samples_while_spinning = 0;+    while p.status().is_none() {+        // Busy-wait (no hlt): we only get here again if the timer took the+        // CPU away from spin and gave it back to us.+        if TICKS.load(Ordering::Relaxed) > start + 3 {+            samples_while_spinning += 1;+        }+        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!(samples_while_spinning > 0, "never ran while spin was running");+}++#[test_case]+fn elf_loader_rejects_garbage() {+    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");+    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());+}++#[test_case]+fn user_pointers_are_validated() {+    // Kernel addresses, unmapped user addresses and overflowing ranges all fail.+    assert_eq!(syscall::user_slice(0x20_0000, 16).err(), Some(Errno::EFAULT));+    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));+}++#[panic_handler]+fn panic(info: &PanicInfo) -> ! {+    rust_kernel::test_panic_handler(info)+}
@@ -0,0 +1,85 @@+//! VFS + ramfs, used directly from the kernel (no user mode involved).+#![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, 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::{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");+    fs::init();++    test_main();+    rust_kernel::hlt_loop();+}++#[test_case]+fn read_boot_files() {+    let file = InodeFile::open(fs::lookup("/hello.txt").unwrap()).unwrap();+    let mut buf = [0u8; 64];+    let n = file.read(&mut buf).unwrap();+    assert!(buf[..n].starts_with(b"Hello from the ramfs!"));+    assert_eq!(file.read(&mut buf), Ok(0)); // EOF+    assert_eq!(fs::lookup("/etc/motd").unwrap().kind(), InodeKind::File);+    assert_eq!(fs::lookup("/etc").unwrap().kind(), InodeKind::Directory);+    assert_eq!(fs::lookup("//etc/./motd").unwrap().kind(), InodeKind::File);+}++#[test_case]+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));+}++#[test_case]+fn mounts_are_separate_file_systems() {+    // /tmp is its own ramfs: files created there don't show up in "/"'s tmp dir.+    fs::create("/tmp/a.txt", InodeKind::File).unwrap();+    assert!(fs::lookup("/tmp/a.txt").is_ok());+    let root_tmp_dir = fs::lookup("/").unwrap().lookup("tmp").unwrap();+    assert_eq!(root_tmp_dir.list().unwrap().len(), 0);+    assert_eq!(fs::create("/tmp/a.txt", InodeKind::File).err(), Some(Errno::EEXIST));++    // Mount a third one; the longest matching mount point wins.+    fs::create("/tmp/deeper", InodeKind::Directory).unwrap();+    fs::mount("/tmp/deeper", RamFs::new()).unwrap();+    assert_eq!(fs::lookup("/tmp/deeper").unwrap().list().unwrap().len(), 0);+    assert_eq!(fs::mount("/tmp/deeper", RamFs::new()).err(), Some(Errno::EEXIST));+    assert_eq!(fs::mounts().len(), 3);+}++#[test_case]+fn write_grow_and_list() {+    let inode = fs::create("/tmp/grow.bin", InodeKind::File).unwrap();+    inode.write_at(4, b"abc").unwrap(); // gap is filled with zeros+    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")]);+}++#[panic_handler]+fn panic(info: &PanicInfo) -> ! {+    rust_kernel::test_panic_handler(info)+}
@@ -0,0 +1,23 @@+//! Reads lines from the keyboard (fd 0) and echoes them back. "exit" quits.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++fn main() -> i64 {+    println!("[echo] pid {}: type a line and press Enter", ulib::getpid());+    let mut buf = [0u8; 256];+    loop {+        let n = ulib::read(0, &mut buf); // blocks until Enter+        if n <= 0 {+            return 1;+        }+        let line = core::str::from_utf8(&buf[..n as usize]).unwrap_or("?").trim_end();+        println!("[echo] you typed: {}", line);+        if line == "exit" {+            return 0;+        }+    }+}
@@ -0,0 +1,17 @@+//! A misbehaving program: tries to read kernel memory directly. The kernel's+//! pages aren't USER_ACCESSIBLE, so the CPU raises a page fault, and the+//! kernel kills this process (and carries on).+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++fn main() -> i64 {+    let kernel_code = 0x20_0000 as *const u64; // where the kernel is loaded+    println!("[evil] pid {}: reading kernel memory at {:p}...", ulib::getpid(), kernel_code);+    let value = unsafe { core::ptr::read_volatile(kernel_code) };+    println!("[evil] got {:#x} -- this must never be printed!", value);+    1+}
@@ -0,0 +1,57 @@+//! First user program: says hello, uses the VFS through syscalls, and checks+//! that bad syscall arguments get the right errors. Exit code = failed checks.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++fn check(failures: &mut i64, ok: bool, what: &str) {+    if !ok {+        println!("[hello] CHECK FAILED: {}", what);+        *failures += 1;+    }+}++fn main() -> i64 {+    let mut failures = 0;+    println!("Hello from user mode! I'm pid {}.", getpid());++    // Read a file from the ramfs.+    let mut buf = [0u8; 128];+    let fd = open("/hello.txt", 0);+    check(&mut failures, fd >= 3, "open /hello.txt");+    let n = read(fd, &mut buf);+    let text = core::str::from_utf8(&buf[..n.max(0) as usize]).unwrap_or("<not utf-8>");+    print!("[hello] /hello.txt says: {}", text);+    check(&mut failures, text.starts_with("Hello from the ramfs!"), "contents of /hello.txt");+    check(&mut failures, read(fd, &mut buf) == 0, "read at EOF returns 0");+    check(&mut failures, close(fd) == 0, "close");++    // Create a file on the second mount (/tmp), then read it back.+    let fd = open("/tmp/note.txt", O_CREAT);+    check(&mut failures, write(fd, b"written from ring 3") == 19, "write /tmp/note.txt");+    close(fd);+    let fd = open("/tmp/note.txt", 0);+    let n = read(fd, &mut buf);+    check(&mut failures, &buf[..n.max(0) as usize] == b"written from ring 3", "read back /tmp/note.txt");+    close(fd);+    println!("[hello] wrote and read back /tmp/note.txt");++    // Error cases.+    check(&mut failures, open("/does/not/exist", 0) == -ENOENT, "ENOENT");+    check(&mut failures, read(42, &mut buf) == -EBADF, "EBADF");+    check(&mut failures, close(fd) == -EBADF, "double close -> EBADF");+    // Ask the kernel to print *kernel* memory for us (0x200000 = kernel code).+    let r = syscall3(SYS_WRITE, 1, 0x20_0000, 16);+    check(&mut failures, r == -EFAULT, "kernel pointer -> EFAULT");+    check(&mut failures, syscall3(999, 0, 0, 0) == -38, "unknown syscall -> ENOSYS");++    if failures == 0 {+        println!("[hello] all syscall checks passed, exiting with 0");+    }+    failures+}
@@ -0,0 +1,20 @@+//! A CPU hog: busy-loops without ever making a syscall in between. The timer+//! interrupt still preempts it, so everything else keeps running.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++fn main() -> i64 {+    println!("[spin] pid {}: spinning without syscalls...", ulib::getpid());+    let mut count: u64 = 0;+    for round in 1..=3 {+        for _ in 0..30_000_000 {+            count = core::hint::black_box(count + 1);+        }+        println!("[spin] round {} done (count = {})", round, count);+    }+    0+}
@@ -0,0 +1,105 @@+//! A tiny "libc" for user programs: syscall wrappers, print!/println!,+//! the `_start` entry point and a panic handler.+//!+//! Each user program includes it with+//!     #[macro_use] #[path = "ulib.rs"] mod ulib;+//! and defines `fn main() -> i64` (the exit code).+//!+//! User programs are built for the `x86_64-unknown-none` target, which has+//! SSE/floating point disabled. That matters: the kernel doesn't save SSE+//! registers on a context switch.++#![allow(dead_code)]++use core::arch::asm;+use core::fmt;++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_SCHED_YIELD: u64 = 24;+pub const SYS_GETPID: u64 = 39;+pub const SYS_EXIT: u64 = 60;++pub const O_CREAT: u64 = 0o100;+pub const ENOENT: i64 = 2;+pub const EBADF: i64 = 9;+pub const EFAULT: i64 = 14;++/// Raw system call: number in rax, args in rdi/rsi/rdx, result in rax.+/// The `syscall` instruction overwrites rcx and r11.+pub fn syscall3(nr: u64, a1: u64, a2: u64, a3: u64) -> i64 {+    let ret: i64;+    unsafe {+        asm!(+            "syscall",+            inlateout("rax") nr as i64 => ret,+            in("rdi") a1,+            in("rsi") a2,+            in("rdx") a3,+            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)+}+pub fn write(fd: i64, buf: &[u8]) -> i64 {+    syscall3(SYS_WRITE, fd as u64, buf.as_ptr() as u64, buf.len() as u64)+}+pub fn open(path: &str, flags: u64) -> i64 {+    syscall3(SYS_OPEN, path.as_ptr() as u64, path.len() as u64, flags)+}+pub fn close(fd: i64) -> i64 {+    syscall3(SYS_CLOSE, fd as u64, 0, 0)+}+pub fn sched_yield() {+    syscall3(SYS_SCHED_YIELD, 0, 0, 0);+}+pub fn getpid() -> i64 {+    syscall3(SYS_GETPID, 0, 0, 0)+}+pub fn exit(code: i64) -> ! {+    syscall3(SYS_EXIT, code as u64, 0, 0);+    loop {} // not reached+}++pub struct Stdout;++impl fmt::Write for Stdout {+    fn write_str(&mut self, s: &str) -> fmt::Result {+        write(1, s.as_bytes());+        Ok(())+    }+}++macro_rules! print {+    ($($arg:tt)*) => {{+        use core::fmt::Write;+        let _ = write!($crate::ulib::Stdout, $($arg)*);+    }};+}++macro_rules! println {+    ($($arg:tt)*) => {{+        use core::fmt::Write;+        let _ = writeln!($crate::ulib::Stdout, $($arg)*);+    }};+}++/// The kernel jumps here (see process.rs). Run main, exit with its result.+#[unsafe(no_mangle)]+pub extern "C" fn _start() -> ! {+    exit(crate::main())+}++#[panic_handler]+fn panic(info: &core::panic::PanicInfo) -> ! {+    println!("[user panic] {}", info);+    exit(101)+}