foundry
Foundry OS: the operating system built on the Rustbelt kernel
| author | Tyler Fowler <tyler@localhost> | 2026-10-30 09:46:08 -0700 |
|---|---|---|
| commit | 5c8fa817a8d6e7db7ede3ec8406defb7bb97444e | |
| parent | e493cb833456780edb40c4db7120517b46ef9f0f | |
| tree | browse files at this commit | |
Stage 14: processes (0.14.0) fork with copy-on-write (refcounted frames, CoW page faults, CR0.WP), vfork and fork-style clone; execve with Linux's argv/envp/auxv stack (AT_PHDR, AT_PAGESZ, AT_ENTRY, AT_RANDOM from virtio-rng, AT_EXECFN...); mmap/munmap/mprotect/brk with VMAs and demand paging (anonymous private and shared, file-backed private); signals: rt_sigaction, rt_sigprocmask, rt_sigreturn with Linux rt_sigframes, kill, pause, default actions incl. stop/continue, SIGSEGV on bad accesses, SIGCHLD, SIGPIPE, EINTR and SA_RESTART; wait4 with Linux status encoding; getppid, exit_group, dup, dup2, dup3, pipe2, fcntl, O_CLOEXEC; process groups and a tty foreground group (Ctrl+C -> SIGINT, Ctrl+Z -> SIGTSTP). Syscalls now return via iretq through a common TrapFrame. The shell uses fork + exec with job control: Ctrl+C, kill, fg, bg. New user programs forktest, cowtest, mmaptest, sigtest and tests/process.rs (11 tests). README: learner chapter on fork/exec, copy-on-write, memory maps and signals; roadmap updated. New boot.png.
| A | cowtest.rs | +107 -0 |
| A | forktest.rs | +186 -0 |
| A | mmaptest.rs | +130 -0 |
| M | sh.rs | +255 -32 |
| A | sigtest.rs | +189 -0 |
| M | ulib.rs | +479 -33 |
6 files changed, 1346 insertions, 65 deletions
@@ -0,0 +1,107 @@+//! cowtest -- checks that fork really gives the child its *own* memory.+//!+//! fork doesn't copy anything up front: parent and child share every page,+//! read-only, and the first write to a page makes the copy (copy-on-write).+//! This checks that the copies are made at the right moments: writes in the+//! child don't show in the parent and vice versa -- for global data, the+//! heap and the stack -- while MAP_SHARED memory *is* shared.+#![no_std]+#![no_main]++extern crate alloc;++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use alloc::vec;+use ulib::*;++/// 64 KiB of global data (16 pages, in .bss).+static mut GLOBAL: [u8; 65536] = [0; 65536];++fn global() -> &'static mut [u8; 65536] {+ unsafe { &mut *core::ptr::addr_of_mut!(GLOBAL) }+}++fn all(buf: &[u8], value: u8) -> bool {+ buf.iter().all(|&b| b == value)+}++fn main() -> i64 {+ let mut c = Checks::new("cowtest");+ global().fill(1);+ let mut heap = vec![2u8; 100_000];+ let mut stack = [3u8; 2000];++ // 1. The child writes; the parent must not see it.+ let pid = fork();+ if pid == 0 {+ let before = all(global(), 1) && all(&heap, 2) && all(&stack, 3);+ global().fill(0xc1);+ heap.fill(0xc2);+ stack.fill(0xc3);+ let after = all(global(), 0xc1) && all(&heap, 0xc2) && all(core::hint::black_box(&stack), 0xc3);+ exit(if before && after { 0 } else { 1 });+ }+ c.check(wait(pid) == 0, "the child starts with the parent's data and can change its copy");+ c.check(all(global(), 1), "the child's writes to global data stay in the child");+ c.check(all(&heap, 2), "... and to the heap");+ c.check(all(core::hint::black_box(&stack), 3), "... and to the stack");++ // 2. The parent writes after fork; the child must keep the old data.+ // A pipe orders things: the child only checks after we wrote.+ let (r, w) = pipe().unwrap_or((-1, -1));+ let pid = fork();+ if pid == 0 {+ close(w);+ let mut b = [0u8];+ read(r, &mut b); // wait for the parent's go+ exit(if all(global(), 1) && all(&heap, 2) { 0 } else { 1 });+ }+ close(r);+ global().fill(0x77);+ heap.fill(0x77);+ write(w, b"!");+ close(w);+ c.check(wait(pid) == 0, "the parent's writes after fork stay in the parent");+ c.check(all(global(), 0x77), "and the parent sees its own writes");++ // 3. A grandchild: three generations share pages, each writes.+ let pid = fork();+ if pid == 0 {+ let pid2 = fork();+ if pid2 == 0 {+ global()[0] = 3;+ exit(if global()[1] == 0x77 { 0 } else { 1 });+ }+ global()[0] = 2;+ let code = wait(pid2);+ exit(if code == 0 && global()[0] == 2 { 0 } else { 1 });+ }+ c.check(wait(pid) == 0 && global()[0] == 0x77, "three generations: each keeps its own copy");++ // 4. MAP_SHARED memory is *not* copied: both see the same pages.+ let shared = mmap(0, 8192, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);+ let private = mmap(0, 8192, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);+ c.check(shared > 0 && private > 0, "mmap shared + private anonymous memory");+ let (sp, pp) = (shared as *mut u8, private as *mut u8);+ unsafe {+ sp.write_volatile(10);+ pp.write_volatile(10);+ }+ let pid = fork();+ if pid == 0 {+ unsafe {+ sp.write_volatile(42);+ sp.add(4096).write_volatile(43); // a page nobody touched before fork+ pp.write_volatile(42);+ }+ exit(0);+ }+ wait(pid);+ let (s0, s1, p0) = unsafe { (sp.read_volatile(), sp.add(4096).read_volatile(), pp.read_volatile()) };+ c.check(s0 == 42 && s1 == 43, "MAP_SHARED: the child's writes show up in the parent");+ c.check(p0 == 10, "MAP_PRIVATE: they don't");+ c.done()+}
@@ -0,0 +1,186 @@+//! forktest -- checks fork, execve, wait4, vfork and fd inheritance.+//!+//! forktest run all the checks (exit code = failed checks)+//! forktest bomb fork until the kernel says no (EAGAIN), then clean+//! up: the process table is bounded, nothing leaks+//! forktest args ... (used by the exec check) verify argv, envp and the+//! auxiliary vector the kernel built for us+#![no_std]+#![no_main]++extern crate alloc;++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use alloc::vec::Vec;+use ulib::*;++const SELF: &str = "/mnt/disk0/bin/forktest";++fn main() -> i64 {+ match arg(1) {+ Some("bomb") => bomb(),+ Some("args") => check_args(),+ _ => all(),+ }+}++fn all() -> i64 {+ let mut c = Checks::new("forktest");+ let me = getpid();++ // 1. The basic dance: fork returns twice.+ let pid = fork();+ if pid == 0 {+ // The child: a new pid, and its parent is the process that forked.+ exit(if getppid() == me && getpid() != me { 7 } else { 1 });+ }+ c.check(pid > 0, "fork returns the child's pid in the parent");+ let r = wait4(pid, 0);+ c.check(matches!(r, Ok((p, s)) if p == pid && wifexited(s) && wexitstatus(s) == 7), "child saw getppid() == parent; wait4 gives its exit code 7");++ // 2. Many children, reaped in any order with wait4(-1).+ let mut sum = 0;+ for i in 1..=10 {+ if fork() == 0 {+ exit(i);+ }+ }+ let mut reaped = 0;+ while let Ok((_, status)) = wait4(-1, 0) {+ sum += wexitstatus(status);+ reaped += 1;+ }+ c.check(reaped == 10 && sum == 55, "10 children, wait4(-1) reaps them all (exit codes add up to 55)");+ c.check(wait4(-1, 0) == Err(-ECHILD), "no children left: ECHILD");++ // 3. execve: the child becomes another program (here: forktest itself+ // in "args" mode, which checks what it was given).+ let pid = fork();+ if pid == 0 {+ let err = execve(SELF, &["forktest", "args", "two words", ""], &["FOO=bar", "EMPTY="]);+ exit(100 + (-err).min(50)); // execve only returns on failure+ }+ let code = wait(pid);+ c.check(code == 0, "execve: argv, envp and auxv arrive the Linux way (see the [forktest args] lines)");+ let pid = fork();+ if pid == 0 {+ let err = execve("/mnt/disk0/bin/no-such-program", &["x"], &[]);+ exit(if err == -ENOENT { 0 } else { 1 });+ }+ c.check(wait(pid) == 0, "execve of a missing file fails with ENOENT (and the caller keeps running)");++ // 4. vfork: the parent waits until the child has exec'd.+ let pid = vfork();+ if pid == 0 {+ execve("/mnt/disk0/bin/child", &["child", "5"], &[]);+ exit(99);+ }+ c.check(wait(pid) == 5, "vfork + execve child 5 -> exit code 5");++ // 5. fds are inherited: the child writes into our pipe via dup2.+ let (r, w) = pipe2(O_CLOEXEC).unwrap_or((-1, -1));+ c.check(fcntl(r, F_GETFD, 0) == FD_CLOEXEC as i64, "pipe2(O_CLOEXEC) sets FD_CLOEXEC");+ let pid = fork();+ if pid == 0 {+ dup2(w, 1); // stdout = the pipe (dup2 clears close-on-exec on fd 1)+ execve("/mnt/disk0/bin/child", &["child", "pipe"], &[]);+ exit(99);+ }+ close(w); // else the read below would never see end-of-file+ let mut out = Vec::new();+ let mut buf = [0u8; 128];+ loop {+ let n = read(r, &mut buf);+ if n <= 0 {+ break;+ }+ out.extend_from_slice(&buf[..n as usize]);+ }+ close(r);+ c.check(wait(pid) == 0 && out.starts_with(b"hello through a pipe"), "fork + dup2 + execve: the child's stdout comes through our pipe");++ // 6. A killed child: the status says which signal.+ let pid = fork();+ if pid == 0 {+ unsafe { core::ptr::read_volatile(0x10 as *const u8) }; // NULL-ish pointer+ exit(0);+ }+ let r = wait4(pid, 0);+ c.check(matches!(r, Ok((_, s)) if wifsignaled(s) && wtermsig(s) == SIGSEGV), "a child that touches address 0x10 dies of SIGSEGV");+ c.done()+}++/// The exec'd side of check 3.+fn check_args() -> i64 {+ let mut c = Checks::new("forktest args");+ c.check(argc() == 4, "argc == 4");+ c.check(arg(0) == Some("forktest") && arg(2) == Some("two words") && arg(3) == Some(""), "argv[0..4] as passed (incl. an empty one)");+ c.check(env(0) == Some("FOO=bar") && env(1) == Some("EMPTY=") && env(2).is_none(), "envp exactly as passed");+ c.check(getenv("FOO") == Some("bar"), "getenv(\"FOO\") == \"bar\"");+ c.check(getauxval(AT_PAGESZ) == Some(4096), "AT_PAGESZ = 4096");+ let entry = getauxval(AT_ENTRY).unwrap_or(0);+ c.check(entry != 0 && entry == ulib_start(), "AT_ENTRY = the address of _start");+ // AT_PHDR points at our own ELF program headers: the first loadable+ // one (type 1 = PT_LOAD) should contain the entry point.+ let (phdr, phent, phnum) = (getauxval(AT_PHDR).unwrap_or(0), getauxval(AT_PHENT).unwrap_or(0), getauxval(AT_PHNUM).unwrap_or(0));+ let mut found = false;+ if phdr != 0 && phent == 56 {+ for i in 0..phnum {+ let h = (phdr + i * phent) as *const u64;+ let (ty, vaddr, memsz) = unsafe { (*(h as *const u32), *h.add(2), *h.add(5)) };+ found |= ty == 1 && (vaddr..vaddr + memsz).contains(&entry);+ }+ }+ c.check(found, "AT_PHDR/AT_PHENT/AT_PHNUM: our program headers, a PT_LOAD contains the entry");+ let random = getauxval(AT_RANDOM).map(|p| unsafe { *(p as *const [u8; 16]) });+ c.check(random.is_some_and(|r| r != [0; 16]), "AT_RANDOM: 16 random bytes");+ let execfn = getauxval(AT_EXECFN).map(|p| unsafe { cstr(p as *const u8) });+ c.check(execfn == Some(SELF), "AT_EXECFN = the path we were run as");+ let sp = &c as *const _ as u64;+ c.check(sp > 0xff_0000_0000 && sp < 0x100_0000_0000, "the stack is at the top of user space");+ c.done()+}++fn ulib_start() -> u64 {+ unsafe extern "C" {+ fn _start();+ }+ _start as usize as u64+}++/// Fork until fork fails. Every child just waits (pause) for a signal.+/// Then kill them all and reap them; the kernel tests check that no memory+/// leaked and that the system still works afterwards.+fn bomb() -> i64 {+ let mut pids = Vec::new();+ let error = loop {+ let pid = fork();+ if pid == 0 {+ loop {+ pause(); // until SIGKILL+ }+ }+ if pid < 0 {+ break pid;+ }+ pids.push(pid);+ if pids.len() > 1000 {+ break 0; // unbounded? stop anyway+ }+ };+ println!("[forktest bomb] {} children, then fork failed with {} ({})", pids.len(), error, strerror(error));+ for &pid in &pids {+ kill(pid, SIGKILL);+ }+ let mut reaped = 0;+ while let Ok((_, status)) = wait4(-1, 0) {+ if wifsignaled(status) && wtermsig(status) == SIGKILL {+ reaped += 1;+ }+ }+ println!("[forktest bomb] killed and reaped {}", reaped);+ if error == -EAGAIN && reaped == pids.len() && !pids.is_empty() { 0 } else { 1 }+}
@@ -0,0 +1,130 @@+//! mmaptest -- checks mmap / munmap / mprotect / brk and demand paging.+//!+//! mmaptest run all the checks (exit code = failed checks)+//! mmaptest crash write to read-only memory without a handler: the+//! default action of SIGSEGV ends the program (139)+//!+//! Faults are caught with a SIGSEGV handler that records what happened+//! (address, SEGV_MAPERR = nothing mapped / SEGV_ACCERR = not allowed) and+//! then *fixes* it (mprotect / mmap the page), so the faulting instruction+//! can run again when the handler returns. Garbage collectors and+//! user-space pagers use the same trick.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use core::sync::atomic::{AtomicU64, Ordering::SeqCst};+use ulib::*;++const PAGE: u64 = 4096;+const SEGV_MAPERR: i32 = 1;+const SEGV_ACCERR: i32 = 2;++static FAULTS: AtomicU64 = AtomicU64::new(0);+static ADDR: AtomicU64 = AtomicU64::new(0);+static CODE: AtomicU64 = AtomicU64::new(0);++extern "C" fn on_segv(sig: i32, info: *const SigInfo, _context: *mut u8) {+ let info = unsafe { &*info };+ FAULTS.fetch_add(1, SeqCst);+ ADDR.store(info.addr_or_pid, SeqCst);+ CODE.store(info.code as u64, SeqCst);+ let page = info.addr_or_pid & !(PAGE - 1);+ // Fix it: make the page readable and writable (mapping it if needed).+ if sig != SIGSEGV || mprotect(page, PAGE, PROT_READ | PROT_WRITE) < 0 {+ mmap(page, PAGE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);+ }+}++fn peek(addr: u64) -> u8 {+ unsafe { (addr as *const u8).read_volatile() }+}+fn poke(addr: u64, value: u8) {+ unsafe { (addr as *mut u8).write_volatile(value) }+}++fn main() -> i64 {+ if arg(1) == Some("crash") {+ let p = mmap(0, PAGE, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) as u64;+ println!("[mmaptest] writing to read-only memory at {:#x}...", p);+ poke(p, 1);+ println!("[mmaptest] not reached");+ return 1;+ }+ let mut c = Checks::new("mmaptest");+ let before = sysinfo().frames_in_use;++ // 1. Anonymous memory: zeroed, readable, writable.+ let a = mmap(0, 3 * PAGE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) as u64;+ c.check(a as i64 > 0 && a % PAGE == 0, "mmap 3 pages of anonymous memory");+ c.check((0..3 * PAGE).all(|i| peek(a + i) == 0), "it starts zeroed");+ for i in 0..3 * PAGE {+ poke(a + i, i as u8);+ }+ c.check((0..3 * PAGE).all(|i| peek(a + i) == i as u8), "it keeps what we write");++ // 2. Demand paging: a big mapping costs (almost) nothing until touched.+ let big = mmap(0, 64 << 20, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) as u64;+ let after_map = sysinfo().frames_in_use;+ poke(big, 1);+ poke(big + (32 << 20), 2);+ poke(big + (64 << 20) - 1, 3);+ let after_touch = sysinfo().frames_in_use;+ c.check(big as i64 > 0 && after_map < before + 8, "mmap 64 MiB: no memory used yet (pages come on first touch)");+ c.check(after_touch >= after_map + 3 && after_touch < after_map + 16, "touching 3 pages of it uses ~3 frames");+ c.check(munmap(big, 64 << 20) == 0, "munmap the 64 MiB");++ // 3. Faults become signals our handler sees.+ c.check(sigaction_info(SIGSEGV, on_segv, 0) == 0, "install a SIGSEGV handler (SA_SIGINFO)");+ c.check(mprotect(a + PAGE, PAGE, PROT_READ) == 0, "mprotect the middle page read-only");+ c.check(peek(a + PAGE + 5) == 5 && FAULTS.load(SeqCst) == 0, "reading it still works");+ poke(a + PAGE + 7, 99);+ c.check(FAULTS.load(SeqCst) == 1 && ADDR.load(SeqCst) == a + PAGE + 7 && CODE.load(SeqCst) == SEGV_ACCERR as u64, "writing it: SIGSEGV, si_addr = that byte, SEGV_ACCERR");+ c.check(peek(a + PAGE + 7) == 99, "the handler's mprotect fixed it and the write was retried");++ c.check(munmap(a + PAGE, PAGE) == 0, "munmap the middle page");+ c.check(peek(a) == 0 && peek(a + 2 * PAGE + 1) == 1, "the pages around the hole are untouched");+ let v = peek(a + PAGE + 7);+ c.check(FAULTS.load(SeqCst) == 2 && CODE.load(SeqCst) == SEGV_MAPERR as u64 && v == 0, "reading the hole: SEGV_MAPERR; the handler maps a fresh (zero) page");+ c.check(mprotect(a, PAGE, PROT_NONE) == 0 && { poke(a, 4); FAULTS.load(SeqCst) == 3 }, "PROT_NONE: even reading faults");+ c.check(munmap(a, 3 * PAGE) == 0, "munmap all of it");++ // 4. A file mapped read-only (private): the same bytes read() gives.+ let fd = open("/mnt/disk0/readme.txt", 0);+ let mut buf = [0u8; 211];+ let n = read(fd, &mut buf);+ let f = mmap(0, 2 * PAGE, PROT_READ, MAP_PRIVATE, fd, 0) as u64;+ c.check(fd >= 0 && n == 211 && f as i64 > 0, "mmap /mnt/disk0/readme.txt");+ c.check((0..211).all(|i| peek(f + i) == buf[i as usize]), "the mapping has the file's bytes");+ c.check(peek(f + 211) == 0 && peek(f + PAGE - 1) == 0, "after the end of the file: zeros");+ let faults = FAULTS.load(SeqCst);+ poke(f, b'J');+ c.check(FAULTS.load(SeqCst) == faults + 1 && peek(f) == b'J', "writing it faults; after mprotect we write our private copy");+ let mut again = [0u8; 1];+ lseek(fd, 0, SEEK_SET);+ read(fd, &mut again);+ c.check(again[0] == b'H', "the file itself is unchanged");+ c.check(mmap(0, PAGE, PROT_READ, MAP_SHARED, fd, 0) < 0, "MAP_SHARED of a file isn't supported (an error, not silent loss)");+ munmap(f, 2 * PAGE);+ close(fd);++ // 5. brk: grow the heap by 1 MiB, use the end, shrink back.+ let start = brk(0);+ let end = brk(start + (1 << 20));+ c.check(end == start + (1 << 20), "brk grows the heap by 1 MiB");+ poke(end - 1, 7);+ c.check(peek(end - 1) == 7, "the new end is usable");+ c.check(brk(start) == start, "brk shrinks it back");++ // 6. Bad requests.+ c.check(mmap(0, 0, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) == -22, "mmap of 0 bytes: EINVAL");+ c.check(mmap(0x1000, PAGE, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) == -22, "MAP_FIXED outside user space: EINVAL");++ sigaction_raw(SIGSEGV, SIG_DFL, 0, 0);+ let leaked = sysinfo().frames_in_use as i64 - before as i64;+ c.check(leaked < 16, "memory use is back to (about) where it started");+ c.done()+}
@@ -5,9 +5,17 @@ //! //! It reads a line, parses it (shparse.rs: words, `|`, `<`, `>`, `>>`, `&`, //! drive letters), then either runs a *builtin* (cd, pwd, echo, ... -- these-//! must change the shell itself, or are just tiny) or *spawns* a program-//! from a `bin` directory, connecting pipes and files to its stdin/stdout.-//! Everything goes through the same syscalls any program could use.+//! must change the shell itself, or are just tiny) or starts a program from+//! a `bin` directory the Unix way: `fork` a copy of the shell, and in the+//! copy connect pipes and files to stdin/stdout with `dup2`, then `execve`+//! the program. Everything goes through the same syscalls any program+//! could use.+//!+//! Job control: every pipeline gets its own *process group*, and while it+//! runs in the foreground the console's foreground group is set to it, so+//! Ctrl+C (SIGINT) and Ctrl+Z (SIGTSTP) reach the program, not the shell+//! (which ignores both). `fg` / `bg` continue a stopped job; `kill` sends+//! any signal. #![no_std] #![no_main]@@ -40,21 +48,27 @@ const HELP: &str = "\ Rustbelt shell commands builtins cd [dir] pwd echo [text] ver drives alias [name=value] history jobs wait [pid] cls / clear help exit [code]+ kill [-SIG] pid|%job ... kill -l fg [%job] bg [%job] programs ls [-l] cat wc grep [-i] [-v] mkdir rm touch cp sleep sysinfo network ifconfig (ip) ping [-c n] host host name (nslookup) fetch [-o file] http://... (wget) httpd [-p port] [-n count] devices random [-n bytes | -d sides [n]] getty [-1] /dev/hvc0 tile [n] | tile -r (bin dirs) hello demo evil spin [rounds] overflow child sh+ tests forktest cowtest mmaptest sigtest run [--trace] prog args run a program (--trace prints its syscalls) start prog args run it in the background (or: prog args &) a | b | c pipes; > file >> file < file redirection+ Ctrl+C interrupt the running program (SIGINT); Ctrl+Z stops it A: is /, B: C: ... are the disks (see `drives`): cd B:, cat B:/readme.txt "; -/// A background job started with `start` or `&`.+/// A background (or stopped) job: one pipeline, one process group. struct Job {- pid: i64,+ pid: i64, // the last program of the pipeline (its exit code counts)+ pgid: i64, // the group: the first program's pid+ pids: Vec<i64>, // programs not reaped yet command: String,+ stopped: bool, } /// One command of a pipeline, after alias, drive letter and `run`/`start`@@ -74,6 +88,7 @@ struct Shell { jobs: Vec<Job>, status: i64, // exit code of the last command exit: Option<i64>, // set by the `exit` builtin+ job_control: bool, // interactive: process groups + the console's foreground } fn main() -> i64 {@@ -84,6 +99,7 @@ fn main() -> i64 { jobs: Vec::new(), status: 0, exit: None,+ job_control: false, }; // Where do commands come from? A script file, or stdin. If stdin is the@@ -91,7 +107,8 @@ fn main() -> i64 { // lines just like from a script. let fd = match arg(1) { Some(path) => {- let fd = open(path, 0);+ // O_CLOEXEC: the programs we fork + exec mustn't inherit it.+ let fd = open(path, O_CLOEXEC); if fd < 0 { eprintln!("sh: {}: {}", path, strerror(fd)); return 127;@@ -106,6 +123,15 @@ fn main() -> i64 { sh.refresh_drives(); if interactive {+ // Job control: be the leader of our own process group, own the+ // console, and ignore the keyboard signals -- they're for the+ // programs we run, not for us.+ sh.job_control = true;+ setpgid(0, 0);+ tcsetpgrp(0, getpgrp());+ for sig in [SIGINT, SIGQUIT, SIGTSTP, SIGTTIN, SIGTTOU] {+ sigaction_raw(sig, SIG_IGN, 0, 0);+ } let disks: Vec<String> = sh.drives.iter().map(|d| format!("{}: = {}", d.letter, d.path)).collect(); println!("Rustbelt shell. Type `help` for commands. Drives: {}", disks.join(", ")); }@@ -150,11 +176,25 @@ impl Shell { } /// Print "[done]" for background jobs that have finished (without- /// blocking: WNOHANG returns -EAGAIN while a job is still running).+ /// blocking: WNOHANG says 0 while a program is still running). fn reap_jobs(&mut self) {- self.jobs.retain(|job| {- let code = try_wait(job.pid);- if code == -EAGAIN {+ self.jobs.retain_mut(|job| {+ let mut code = 0;+ job.pids.retain(|&pid| match wait4(pid, WNOHANG | WUNTRACED) {+ Ok((0, _)) => true, // still running+ Ok((_, status)) if wifstopped(status) => {+ job.stopped = true;+ true+ }+ Ok((_, status)) => {+ if pid == job.pid {+ code = exit_code(status);+ }+ false+ }+ Err(_) => false, // gone already+ });+ if !job.pids.is_empty() { return true; } println!("[done] pid {} ({}), exit code {}", job.pid, job.command, code);@@ -207,6 +247,13 @@ impl Shell { erase(1); } }+ (_, 0x03) => {+ // Ctrl+C while typing: throw the line away (in raw+ // mode the console doesn't make it a signal).+ println!("^C");+ line.clear();+ break 'read;+ } (_, 0x20..=0x7e) => { line.push(b); write(1, &[b]); // echo@@ -307,6 +354,7 @@ impl Shell { let mut opened: Vec<i64> = Vec::new(); // fds to close when done let mut children: Vec<(i64, String)> = Vec::new();+ let mut pgid = 0; // the job's process group = its first program's pid let mut status = 0; let mut prev_pipe = -1; // read end of the pipe from the previous stage let mut last_pid = None; // the last stage, if it's a program@@ -314,7 +362,7 @@ impl Shell { // stdin: `< file`, else the previous pipe, else ours (-1). let mut stdin = prev_pipe; if let Some(file) = &stage.stdin {- stdin = open(file, 0);+ stdin = open(file, O_CLOEXEC); if stdin < 0 { eprintln!("sh: {}: {}", file, strerror(stdin)); status = 1;@@ -327,13 +375,15 @@ impl Shell { let mut stdout = -1; prev_pipe = -1; if i + 1 < stages.len() {- let (r, w) = pipe().unwrap_or((-1, -1));+ // O_CLOEXEC: the programs get these only as their stdin+ // / stdout (dup2), not as extra open fds.+ let (r, w) = pipe2(O_CLOEXEC).unwrap_or((-1, -1)); opened.extend([r, w]); stdout = w; prev_pipe = r; } if let Some((file, append)) = &stage.stdout {- let flags = O_CREAT | if *append { O_APPEND } else { O_TRUNC };+ let flags = O_CREAT | O_CLOEXEC | if *append { O_APPEND } else { O_TRUNC }; stdout = open(file, flags); if stdout < 0 { eprintln!("sh: {}: {}", file, strerror(stdout));@@ -349,8 +399,11 @@ impl Shell { continue; } }- match self.spawn(stage, stdin, stdout) {+ match self.spawn(stage, stdin, stdout, pgid, !background) { Ok(pid) => {+ if pgid == 0 {+ pgid = pid;+ } children.push((pid, stage.argv[0].clone())); if i + 1 == stages.len() { last_pid = Some(pid);@@ -368,23 +421,85 @@ impl Shell { close(fd); } + if children.is_empty() {+ return status;+ }+ let mut job = Job {+ pid: last_pid.unwrap_or(children[children.len() - 1].0),+ pgid,+ pids: children.iter().map(|(pid, _)| *pid).collect(),+ command: line.to_string(),+ stopped: false,+ }; if background {- for (pid, name) in children {+ for (pid, name) in &children { println!("[pid {}] {} started in the background", pid, name);- self.jobs.push(Job { pid, command: line.to_string() }); }+ self.jobs.push(job); return 0; }- for (pid, _) in &children {- let code = wait(*pid);- if Some(*pid) == last_pid {- status = code; // the last stage's exit code is the pipeline's+ match self.wait_job(&mut job) {+ Some(code) if last_pid.is_some() => code,+ Some(_) => status, // the last stage was a builtin+ None => 148, // stopped (128 + SIGTSTP)+ }+ }++ /// Wait for a foreground job: it owns the console until all of its+ /// programs end (-> the exit code of the last one) or it's stopped by+ /// Ctrl+Z (-> None, and it goes on the job list).+ fn wait_job(&mut self, job: &mut Job) -> Option<i64> {+ if self.job_control {+ tcsetpgrp(0, job.pgid);+ }+ let mut code = 0;+ let mut signal = 0;+ while let Some(&next) = job.pids.first() {+ match wait4(next, WUNTRACED) {+ Ok((_, status)) if wifstopped(status) => {+ job.stopped = true;+ break;+ }+ Ok((pid, status)) => {+ job.pids.retain(|&p| p != pid);+ if pid == job.pid {+ code = exit_code(status);+ signal = if wifsignaled(status) { wtermsig(status) } else { 0 };+ }+ }+ Err(e) if e == -EINTR => {}+ Err(_) => break, // no children left (someone else reaped them) } }- if status != 0 && last_pid.is_some() {- println!("[exit code {}]", status);+ if self.job_control {+ tcsetpgrp(0, getpgrp()); // the console is ours again+ }+ if job.stopped {+ println!("\n[stopped] pid {} ({}) -- `fg` to continue", job.pid, job.command);+ self.jobs.push(Job { pids: core::mem::take(&mut job.pids), command: core::mem::take(&mut job.command), ..*job });+ return None;+ }+ match signal {+ 0 if code != 0 => println!("[exit code {}]", code),+ 0 => {}+ SIGINT => println!("[exit code {}] (interrupted: SIGINT)", code),+ sig => println!("[exit code {}] (killed by SIG{})", code, SIGNAL_NAMES.get(sig as usize).unwrap_or(&"?")),+ }+ Some(code)+ }++ /// `%2` (job number), `%` (the latest job) or a pid -> index in jobs.+ fn find_job(&self, spec: Option<&str>) -> Option<usize> {+ match spec {+ None | Some("%") => self.jobs.len().checked_sub(1),+ Some(s) => match s.strip_prefix('%') {+ Some(n) => n.parse::<usize>().ok().filter(|&n| n >= 1 && n <= self.jobs.len()).map(|n| n - 1),+ None => {+ let pid: i64 = s.parse().ok()?;+ self.jobs.iter().position(|j| j.pid == pid || j.pgid == pid)+ }+ }, }- status } /// Find a program: names with a `/` are paths; others are looked up in@@ -397,20 +512,51 @@ impl Shell { self.drives.iter().map(|d| format!("{}/bin/{}", d.path.trim_end_matches('/'), name)).find(|p| stat(p, &mut st) == 0 && st.kind == KIND_FILE) } - fn spawn(&self, stage: &Stage, stdin: i64, stdout: i64) -> Result<i64, i64> {+ /// Start one program: fork, then in the child set up its process+ /// group, stdin/stdout and signals, and execve it.+ fn spawn(&self, stage: &Stage, stdin: i64, stdout: i64, pgid: i64, foreground: bool) -> Result<i64, i64> { let name = &stage.argv[0]; let Some(path) = self.find_program(name) else { eprintln!("sh: {}: command not found (try `help`)", name); return Err(127); };- // spawn takes the arguments as one NUL-separated string.- let argv = stage.argv.join("\0");- let opts = SpawnOptions { stdin, stdout, stderr: -1, flags: if stage.trace { SPAWN_TRACE } else { 0 } };- let pid = spawn(&path, &argv, &opts);+ let pid = fork(); if pid < 0 {- eprintln!("sh: {}: {}", name, strerror(pid));+ eprintln!("sh: {}: fork: {}", name, strerror(pid)); return Err(126); }+ if pid == 0 {+ // --- the child: still the shell's code, in a copy of its memory.+ if self.job_control {+ setpgid(0, pgid); // pgid 0: a new group, named after me+ if foreground {+ tcsetpgrp(0, getpgrp());+ }+ // Ignored signals stay ignored across execve: undo ours.+ for sig in [SIGINT, SIGQUIT, SIGTSTP, SIGTTIN, SIGTTOU] {+ sigaction_raw(sig, SIG_DFL, 0, 0);+ }+ }+ if stdin >= 0 {+ dup2(stdin, 0); // the new fd 0 doesn't close on exec+ }+ if stdout >= 0 {+ dup2(stdout, 1);+ }+ if stage.trace {+ trace_me(true);+ }+ let argv: Vec<&str> = stage.argv.iter().map(|s| s.as_str()).collect();+ let err = execve(&path, &argv, &environ());+ // Only reached if execve failed.+ eprintln!("sh: {}: {}", name, strerror(err));+ exit(126);+ }+ // --- the parent. Set the group here too: whichever of us runs+ // first, the child is in its group before we wait or tcsetpgrp.+ if self.job_control {+ setpgid(pid, if pgid == 0 { pid } else { pgid });+ } Ok(pid) } @@ -493,8 +639,9 @@ impl Shell { } "jobs" => { self.reap_jobs();- for job in &self.jobs {- let _ = writeln!(out, "[pid {}] running {}", job.pid, job.command);+ for (i, job) in self.jobs.iter().enumerate() {+ let state = if job.stopped { "stopped" } else { "running" };+ let _ = writeln!(out, "[{}] [pid {}] {} {}", i + 1, job.pid, state, job.command); } 0 }@@ -506,18 +653,94 @@ impl Shell { if only.is_some_and(|pid| pid != job.pid) { return true; }- code = wait(job.pid);+ for &pid in &job.pids {+ let c = wait(pid);+ if pid == job.pid {+ code = c;+ }+ } let _ = writeln!(out, "[done] pid {} ({}), exit code {}", job.pid, job.command, code); false }); code }+ "kill" => self.kill(&argv[1..], &mut out),+ "fg" | "bg" => {+ let Some(i) = self.find_job(arg1) else {+ eprintln!("{}: no such job", argv[0]);+ return Some(1);+ };+ let mut job = self.jobs.remove(i);+ kill(-job.pgid, SIGCONT); // wake the whole pipeline up+ job.stopped = false;+ if argv[0] == "bg" {+ let _ = writeln!(out, "[pid {}] {} continued in the background", job.pid, job.command);+ self.jobs.push(job);+ 0+ } else {+ let _ = writeln!(out, "{}", job.command);+ self.wait_job(&mut job).unwrap_or(148)+ }+ } _ => return None, }; Some(code) } } +impl Shell {+ /// `kill [-SIG] pid|%job ...` and `kill -l`. Default signal: SIGTERM.+ fn kill(&self, args: &[String], out: &mut Fd) -> i64 {+ let mut sig = SIGTERM;+ let mut args = args;+ match args.first().map(|s| s.as_str()) {+ Some("-l") => {+ for (i, name) in SIGNAL_NAMES.iter().enumerate().skip(1) {+ let _ = write!(out, "{:2}) SIG{:<7}{}", i, name, if i % 5 == 0 { "\n" } else { " " });+ }+ let _ = writeln!(out);+ return 0;+ }+ Some(opt) if opt.starts_with('-') && opt.len() > 1 && !opt[1..].starts_with(|c: char| c == '-') => {+ let name = opt[1..].strip_prefix("s ").unwrap_or(&opt[1..]);+ match signal_number(name) {+ Some(n) => sig = n,+ None => {+ eprintln!("kill: {}: unknown signal (see `kill -l`)", name);+ return 2;+ }+ }+ args = &args[1..];+ }+ _ => {}+ }+ if args.is_empty() {+ eprintln!("usage: kill [-SIGNAL] pid|%job ... (kill -l lists signals)");+ return 2;+ }+ let mut code = 0;+ for target in args {+ // %n = a job's whole process group (negative pid).+ let pid = if target.starts_with('%') {+ // (Without job control the job has no group of its own.)+ let job = |i: usize| if self.job_control { -self.jobs[i].pgid } else { self.jobs[i].pid };+ self.find_job(Some(target)).map(job)+ } else {+ target.parse::<i64>().ok()+ };+ let r = match pid {+ Some(pid) => kill(pid, sig),+ None => -ESRCH,+ };+ if r < 0 {+ eprintln!("kill: {}: {}", target, if r == -ESRCH { "No such process" } else { strerror(r) });+ code = 1;+ }+ }+ code+ }+}+ /// Erase `n` characters before the cursor (the console treats \x08 as /// "back one column and blank it"). fn erase(n: usize) {
@@ -0,0 +1,189 @@+//! sigtest -- checks signals: handlers and sigreturn, masks, default+//! actions, SIGCHLD, stop/continue, SIGPIPE and interrupted system calls.+//!+//! A signal handler is a normal function that the kernel makes the program+//! "call" at a moment it didn't choose: it saves the interrupted registers+//! in a frame on the user stack, runs the handler, and when the handler+//! returns (into ulib's `__restore_rt`), rt_sigreturn puts everything back.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use core::sync::atomic::{AtomicI64, Ordering::SeqCst};+use ulib::*;++static HITS: AtomicI64 = AtomicI64::new(0);+static LAST_SIG: AtomicI64 = AtomicI64::new(0);+static MASK_IN_HANDLER: AtomicI64 = AtomicI64::new(0);+static CHLD_PID: AtomicI64 = AtomicI64::new(0);+static CHLD_STATUS: AtomicI64 = AtomicI64::new(0);+static CHLD_CODE: AtomicI64 = AtomicI64::new(0);++extern "C" fn on_signal(sig: i32) {+ HITS.fetch_add(1, SeqCst);+ LAST_SIG.store(sig as i64, SeqCst);+ // While a handler runs, its own signal is blocked.+ MASK_IN_HANDLER.store(sigprocmask(SIG_BLOCK, 0), SeqCst);+}++extern "C" fn on_child(_sig: i32, info: *const SigInfo, _ctx: *mut u8) {+ let info = unsafe { &*info };+ CHLD_PID.store(info.addr_or_pid as u32 as i64, SeqCst);+ CHLD_STATUS.store(info.status as i64, SeqCst);+ CHLD_CODE.store(info.code as i64, SeqCst);+}++/// Busy work that keeps values in registers across a signal: if sigreturn+/// restored anything wrong, the sum comes out different.+#[inline(never)]+fn compute_with_signal(sig: i32) -> u64 {+ let mut x = core::hint::black_box(0x1234_5678_u64);+ let mut y = core::hint::black_box(0x0bad_cafe_u64);+ for i in 0..1000u64 {+ if i == 500 {+ kill(getpid(), sig); // the handler runs before kill returns+ }+ x = x.wrapping_mul(31).wrapping_add(i);+ y ^= x.rotate_left(7);+ }+ x ^ y+}++/// Fork a child that runs `f` and return its wait4 status.+fn in_child(f: fn()) -> i32 {+ let pid = fork();+ if pid == 0 {+ f();+ exit(0);+ }+ wait4(pid, 0).map(|(_, s)| s).unwrap_or(-1)+}++fn main() -> i64 {+ let mut c = Checks::new("sigtest");+ // 1. A handler round trip, with every register restored.+ sigaction_raw(SIGUSR2, SIG_IGN, 0, 0);+ let without = compute_with_signal(SIGUSR2); // ignored: nothing happens+ c.check(HITS.load(SeqCst) == 0, "SIG_IGN: an ignored signal does nothing");+ c.check(signal(SIGUSR1, on_signal) == 0, "rt_sigaction installs a handler");+ let with = compute_with_signal(SIGUSR1);+ c.check(HITS.load(SeqCst) == 1 && LAST_SIG.load(SeqCst) == SIGUSR1 as i64, "kill(getpid(), SIGUSR1) runs the handler once, with sig = 10");+ c.check(with == without, "after rt_sigreturn the interrupted code computes the same result");+ c.check(MASK_IN_HANDLER.load(SeqCst) as u64 & sigbit(SIGUSR1) != 0, "the signal is blocked while its handler runs");+ c.check(sigprocmask(SIG_BLOCK, 0) as u64 & sigbit(SIGUSR1) == 0, "... and unblocked again after sigreturn");++ // 2. Blocking: a blocked signal waits (pending) until unblocked.+ sigprocmask(SIG_BLOCK, sigbit(SIGUSR1));+ kill(getpid(), SIGUSR1);+ c.check(HITS.load(SeqCst) == 1, "blocked: kill doesn't run the handler yet");+ sigprocmask(SIG_UNBLOCK, sigbit(SIGUSR1));+ c.check(HITS.load(SeqCst) == 2, "unblocking delivers the pending signal");+ c.check(sigaction_raw(SIGKILL, SIG_IGN, 0, 0) == -22, "SIGKILL can't be caught or ignored (EINVAL)");++ // 3. Default actions, seen through wait4's status.+ let s = in_child(|| {+ kill(getpid(), SIGTERM);+ });+ c.check(wifsignaled(s) && wtermsig(s) == SIGTERM, "SIGTERM's default action terminates");+ let s = in_child(|| {+ kill(getpid(), SIGQUIT);+ });+ c.check(wifsignaled(s) && wtermsig(s) == SIGQUIT, "SIGQUIT (\"core\") terminates too (no core files here)");+ let s = in_child(|| {+ kill(getpid(), SIGCHLD); // default: ignore+ kill(getpid(), SIGURG_NUM);+ });+ c.check(wifexited(s) && wexitstatus(s) == 0, "SIGCHLD and SIGURG are ignored by default");+ let s = in_child(|| {+ // Write to a pipe nobody reads: SIGPIPE.+ let (r, w) = pipe().unwrap();+ close(r);+ write(w, b"x");+ });+ c.check(wifsignaled(s) && wtermsig(s) == SIGPIPE, "writing to a pipe without readers: SIGPIPE");+ let s = in_child(|| {+ sigaction_raw(SIGPIPE, SIG_IGN, 0, 0);+ let (r, w) = pipe().unwrap();+ close(r);+ exit(if write(w, b"x") == -32 { 0 } else { 1 });+ });+ c.check(wifexited(s) && wexitstatus(s) == 0, "... with SIGPIPE ignored, write fails with EPIPE");++ // 4. SIGCHLD: the parent hears when a child ends.+ sigaction_info(SIGCHLD, on_child, SA_RESTART);+ let pid = fork();+ if pid == 0 {+ exit(3);+ }+ while CHLD_PID.load(SeqCst) == 0 {+ pause();+ }+ c.check(CHLD_PID.load(SeqCst) == pid && CHLD_STATUS.load(SeqCst) == 3 && CHLD_CODE.load(SeqCst) == 1, "SIGCHLD arrives with si_pid, status 3, CLD_EXITED");+ c.check(wait(pid) == 3, "and the child can still be reaped");+ sigaction_raw(SIGCHLD, SIG_DFL, 0, 0);++ // 5. Stop and continue.+ let pid = fork();+ if pid == 0 {+ loop {+ sleep_ms(10);+ }+ }+ kill(pid, SIGSTOP);+ let r = wait4(pid, WUNTRACED);+ c.check(matches!(r, Ok((p, s)) if p == pid && wifstopped(s) && wstopsig(s) == SIGSTOP), "SIGSTOP stops a child; wait4(WUNTRACED) reports it");+ kill(pid, SIGCONT);+ let r = wait4(pid, WCONTINUED);+ c.check(matches!(r, Ok((_, s)) if wifcontinued(s)), "SIGCONT continues it; wait4(WCONTINUED) reports that");+ kill(pid, SIGKILL);+ let r = wait4(pid, 0);+ c.check(matches!(r, Ok((_, s)) if wifsignaled(s) && wtermsig(s) == SIGKILL), "SIGKILL ends it");++ // 6. Interrupted system calls: a signal during a blocking read.+ for restart in [false, true] {+ sigaction_raw(SIGUSR1, on_signal as usize, if restart { SA_RESTART } else { 0 }, 0);+ let (r, w) = pipe().unwrap();+ let me = getpid();+ let pid = fork();+ if pid == 0 {+ sleep_ms(120);+ kill(me, SIGUSR1); // interrupts the parent's read+ sleep_ms(120);+ write(w, b"y");+ exit(0);+ }+ let hits = HITS.load(SeqCst);+ let mut b = [0u8; 1];+ let n = read(r, &mut b);+ if restart {+ c.check(n == 1 && HITS.load(SeqCst) == hits + 1, "SA_RESTART: the handler runs, then read continues and gets the data");+ } else {+ c.check(n == -EINTR && HITS.load(SeqCst) == hits + 1, "no SA_RESTART: read returns EINTR after the handler");+ }+ wait(pid);+ close(r);+ close(w);+ }++ // 7. kill errors and process groups.+ c.check(kill(99999, SIGTERM) == -ESRCH, "kill of a pid that doesn't exist: ESRCH");+ c.check(kill(getpid(), 99) == -22, "kill with signal 99: EINVAL");+ c.check(kill(getpid(), 0) == 0, "signal 0 only checks that the process exists");+ let pid = fork();+ if pid == 0 {+ setpgid(0, 0);+ loop {+ pause();+ }+ }+ setpgid(pid, pid);+ c.check(kill(-pid, SIGTERM) == 0, "kill(-pgid) signals a whole process group");+ let r = wait4(pid, 0);+ c.check(matches!(r, Ok((_, s)) if wtermsig(s) == SIGTERM), "the group's member got it");+ c.done()+}++const SIGURG_NUM: i32 = 23;
@@ -12,6 +12,8 @@ #![allow(dead_code, unused_macros)] +extern crate alloc;+ use core::alloc::{GlobalAlloc, Layout}; use core::arch::asm; use core::cell::UnsafeCell;@@ -30,7 +32,28 @@ pub const SYS_SCHED_YIELD: u64 = 24; pub const SYS_SLEEP_MS: u64 = 35; pub const SYS_GETPID: u64 = 39; pub const SYS_EXIT: u64 = 60;-pub const SYS_WAIT: u64 = 61;+pub const SYS_WAIT4: u64 = 61;+pub const SYS_MMAP: u64 = 9;+pub const SYS_MPROTECT: u64 = 10;+pub const SYS_MUNMAP: u64 = 11;+pub const SYS_BRK: u64 = 12;+pub const SYS_RT_SIGACTION: u64 = 13;+pub const SYS_RT_SIGPROCMASK: u64 = 14;+pub const SYS_RT_SIGRETURN: u64 = 15;+pub const SYS_DUP: u64 = 32;+pub const SYS_DUP2: u64 = 33;+pub const SYS_PAUSE: u64 = 34;+pub const SYS_FORK: u64 = 57;+pub const SYS_VFORK: u64 = 58;+pub const SYS_EXECVE: u64 = 59;+pub const SYS_KILL: u64 = 62;+pub const SYS_FCNTL: u64 = 72;+pub const SYS_SETPGID: u64 = 109;+pub const SYS_GETPPID: u64 = 110;+pub const SYS_GETPGRP: u64 = 111;+pub const SYS_EXIT_GROUP: u64 = 231;+pub const SYS_PIPE2: u64 = 293;+pub const SYS_TRACE_ME: u64 = 505; pub const SYS_UNAME: u64 = 63; pub const SYS_GETCWD: u64 = 79; pub const SYS_CHDIR: u64 = 80;@@ -65,12 +88,23 @@ pub const FB_FLUSH: u64 = 0x46f0; pub const FB_FILL: u64 = 0x46f1; pub const FB_SET_CONSOLE: u64 = 0x46f2; pub const WNOHANG: u64 = 1;+pub const WUNTRACED: u64 = 2;+pub const WCONTINUED: u64 = 8;+pub const O_CLOEXEC: u64 = 0o2000000;+pub const F_GETFD: u64 = 1;+pub const F_SETFD: u64 = 2;+pub const FD_CLOEXEC: u64 = 1;+pub const TIOCGPGRP: u64 = 0x540F;+pub const TIOCSPGRP: u64 = 0x5410; pub const SEEK_SET: u64 = 0; pub const SEEK_CUR: u64 = 1; pub const SEEK_END: u64 = 2; pub const SPAWN_TRACE: u64 = 1; +pub const EPERM: i64 = 1; pub const ENOENT: i64 = 2;+pub const ESRCH: i64 = 3;+pub const EINTR: i64 = 4; pub const EAGAIN: i64 = 11; pub const EBADF: i64 = 9; pub const ECHILD: i64 = 10;@@ -132,6 +166,11 @@ pub fn syscall3(nr: u64, a1: u64, a2: u64, a3: u64) -> i64 { /// 5-argument version (4th argument goes in r10, 5th in r8). pub fn syscall5(nr: u64, a1: u64, a2: u64, a3: u64, a4: u64, a5: u64) -> i64 {+ syscall6(nr, a1, a2, a3, a4, a5, 0)+}++/// 6-argument version (6th in r9). mmap is the only one that needs it.+pub fn syscall6(nr: u64, a1: u64, a2: u64, a3: u64, a4: u64, a5: u64, a6: u64) -> i64 { let ret: i64; unsafe { asm!(@@ -142,6 +181,7 @@ pub fn syscall5(nr: u64, a1: u64, a2: u64, a3: u64, a4: u64, a5: u64) -> i64 { in("rdx") a3, in("r10") a4, in("r8") a5,+ in("r9") a6, lateout("rcx") _, lateout("r11") _, options(nostack),@@ -173,8 +213,12 @@ pub fn lseek(fd: i64, offset: i64, whence: u64) -> i64 { } /// Returns (read end, write end), or the negative errno. pub fn pipe() -> Result<(i64, i64), i64> {+ pipe2(0)+}+/// pipe with flags (O_CLOEXEC: both ends close on exec).+pub fn pipe2(flags: u64) -> Result<(i64, i64), i64> { let mut fds = [0u32; 2];- match syscall3(SYS_PIPE, fds.as_mut_ptr() as u64, 0, 0) {+ match syscall3(SYS_PIPE2, fds.as_mut_ptr() as u64, flags, 0) { 0 => Ok((fds[0] as i64, fds[1] as i64)), e => Err(e), }@@ -189,17 +233,314 @@ pub fn getpid() -> i64 { syscall3(SYS_GETPID, 0, 0, 0) } pub fn exit(code: i64) -> ! {- syscall3(SYS_EXIT, code as u64, 0, 0);+ syscall3(SYS_EXIT_GROUP, code as u64, 0, 0); loop {} // not reached }-/// Wait for child `pid` to end; returns its exit code (139 = killed).++// --- Processes (fork, exec, wait), stage 14 --------------------------------++/// fork: 0 in the child, the child's pid in the parent (or -errno).+pub fn fork() -> i64 {+ syscall3(SYS_FORK, 0, 0, 0)+}+/// vfork: like fork, but the parent sleeps until the child calls execve or+/// exits. (Here the child gets a copy-on-write copy anyway, so it's safe to+/// do more than exec in it; on Linux it borrows the parent's memory.)+pub fn vfork() -> i64 {+ syscall3(SYS_VFORK, 0, 0, 0)+}+pub fn getppid() -> i64 {+ syscall3(SYS_GETPPID, 0, 0, 0)+}+pub fn getpgrp() -> i64 {+ syscall3(SYS_GETPGRP, 0, 0, 0)+}+/// Put process `pid` (0 = me) into group `pgid` (0 = a new group named+/// after the process).+pub fn setpgid(pid: i64, pgid: i64) -> i64 {+ syscall3(SYS_SETPGID, pid as u64, pgid as u64, 0)+}+/// Which process group gets the console's Ctrl+C.+pub fn tcgetpgrp(fd: i64) -> i64 {+ let mut pgid = 0u32;+ match ioctl(fd, TIOCGPGRP, &mut pgid as *mut u32 as u64) {+ 0 => pgid as i64,+ e => e,+ }+}+pub fn tcsetpgrp(fd: i64, pgid: i64) -> i64 {+ let pgid = pgid as u32;+ ioctl(fd, TIOCSPGRP, &pgid as *const u32 as u64)+}+/// Print my system calls from now on (inherited across fork and exec).+pub fn trace_me(on: bool) -> i64 {+ syscall3(SYS_TRACE_ME, on as u64, 0, 0)+}+pub fn dup(fd: i64) -> i64 {+ syscall3(SYS_DUP, fd as u64, 0, 0)+}+pub fn dup2(old: i64, new: i64) -> i64 {+ syscall3(SYS_DUP2, old as u64, new as u64, 0)+}+pub fn fcntl(fd: i64, cmd: u64, arg: u64) -> i64 {+ syscall3(SYS_FCNTL, fd as u64, cmd, arg)+}++/// A list of C strings (NUL-terminated) plus the NULL-terminated array of+/// pointers to them that execve wants.+pub struct CStrings {+ data: alloc::vec::Vec<u8>,+ ends: alloc::vec::Vec<usize>,+ ptrs: alloc::vec::Vec<u64>,+}++impl CStrings {+ pub fn new<'a>(items: impl IntoIterator<Item = &'a str>) -> CStrings {+ let mut data = alloc::vec::Vec::new();+ let mut ends = alloc::vec::Vec::new();+ for item in items {+ data.extend_from_slice(item.as_bytes());+ data.push(0);+ ends.push(data.len());+ }+ CStrings { data, ends, ptrs: alloc::vec::Vec::new() }+ }+ /// The `char **`: pointers into `data`, then NULL.+ pub fn as_ptr(&mut self) -> *const u64 {+ let base = self.data.as_ptr() as u64;+ let mut start = 0;+ self.ptrs.clear();+ for &end in &self.ends {+ self.ptrs.push(base + start as u64);+ start = end;+ }+ self.ptrs.push(0);+ self.ptrs.as_ptr()+ }+}++/// execve(path, argv, envp): only returns on failure (-errno).+pub fn execve(path: &str, argv: &[&str], envp: &[&str]) -> i64 {+ let mut cpath = alloc::vec::Vec::from(path.as_bytes());+ cpath.push(0);+ let mut args = CStrings::new(argv.iter().copied());+ let mut env = CStrings::new(envp.iter().copied());+ syscall3(SYS_EXECVE, cpath.as_ptr() as u64, args.as_ptr() as u64, env.as_ptr() as u64)+}++/// The same environment I got: for passing on to execve.+pub fn environ() -> alloc::vec::Vec<&'static str> {+ (0..).map_while(env).collect()+}++/// wait4(pid, &status, options): returns (pid, status), or -errno. pid -1 =+/// any child; WNOHANG and nothing finished: pid 0.+pub fn wait4(pid: i64, options: u64) -> Result<(i64, i32), i64> {+ let mut status = 0i32;+ match syscall5(SYS_WAIT4, pid as u64, &mut status as *mut i32 as u64, options, 0, 0) {+ n if n < 0 => Err(n),+ n => Ok((n, status)),+ }+}++// The status macros (C's WIFEXITED etc.): wait4's `status` packs how the+// child ended into one int -- exit code << 8, or the signal number, or+// 0x7f for "stopped".+pub fn wifexited(status: i32) -> bool {+ status & 0x7f == 0+}+pub fn wexitstatus(status: i32) -> i32 {+ (status >> 8) & 0xff+}+pub fn wifsignaled(status: i32) -> bool {+ (status & 0x7f) + 1 >= 2 && status & 0x7f != 0x7f+}+pub fn wtermsig(status: i32) -> i32 {+ status & 0x7f+}+pub fn wcoredump(status: i32) -> bool {+ status & 0x80 != 0+}+pub fn wifstopped(status: i32) -> bool {+ status & 0xff == 0x7f+}+pub fn wstopsig(status: i32) -> i32 {+ wexitstatus(status)+}+pub fn wifcontinued(status: i32) -> bool {+ status == 0xffff+}+/// A shell-style exit code: the code itself, or 128 + the signal number.+pub fn exit_code(status: i32) -> i64 {+ if wifsignaled(status) { 128 + wtermsig(status) as i64 } else { wexitstatus(status) as i64 }+}++/// Wait for child `pid` to end; returns its exit code (139 = killed by+/// SIGSEGV: 128 + 11), or -errno. pub fn wait(pid: i64) -> i64 {- syscall3(SYS_WAIT, pid as u64, 0, 0)+ loop {+ match wait4(pid, 0) {+ Ok((_, status)) => return exit_code(status),+ Err(e) if e == -EINTR => continue,+ Err(e) => return e,+ }+ } } /// Like `wait`, but don't block: -EAGAIN while the child is still running. pub fn try_wait(pid: i64) -> i64 {- syscall3(SYS_WAIT, pid as u64, WNOHANG, 0)+ match wait4(pid, WNOHANG) {+ Ok((0, _)) => -EAGAIN,+ Ok((_, status)) => exit_code(status),+ Err(e) => e,+ }+}++// --- Signals -----------------------------------------------------------------++pub const SIGHUP: i32 = 1;+pub const SIGINT: i32 = 2;+pub const SIGQUIT: i32 = 3;+pub const SIGILL: i32 = 4;+pub const SIGABRT: i32 = 6;+pub const SIGBUS: i32 = 7;+pub const SIGFPE: i32 = 8;+pub const SIGKILL: i32 = 9;+pub const SIGUSR1: i32 = 10;+pub const SIGSEGV: i32 = 11;+pub const SIGUSR2: i32 = 12;+pub const SIGPIPE: i32 = 13;+pub const SIGALRM: i32 = 14;+pub const SIGTERM: i32 = 15;+pub const SIGCHLD: i32 = 17;+pub const SIGCONT: i32 = 18;+pub const SIGSTOP: i32 = 19;+pub const SIGTSTP: i32 = 20;+pub const SIGTTIN: i32 = 21;+pub const SIGTTOU: i32 = 22;++/// Signal names, index = number ("" = unused).+pub const SIGNAL_NAMES: [&str; 32] = [+ "", "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "BUS", "FPE", "KILL", "USR1", "SEGV", "USR2", "PIPE", "ALRM", "TERM",+ "STKFLT", "CHLD", "CONT", "STOP", "TSTP", "TTIN", "TTOU", "URG", "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "IO", "PWR",+ "SYS",+];+/// "INT" or "SIGINT" or "2" -> 2.+pub fn signal_number(name: &str) -> Option<i32> {+ if let Ok(n) = name.parse::<i32>() {+ return (1..64).contains(&n).then_some(n);+ }+ let name = name.strip_prefix("SIG").unwrap_or(name);+ SIGNAL_NAMES.iter().position(|&n| !n.is_empty() && n.eq_ignore_ascii_case(name)).map(|i| i as i32) }++pub const SIG_DFL: usize = 0;+pub const SIG_IGN: usize = 1;+pub const SA_SIGINFO: u64 = 4;+pub const SA_RESTORER: u64 = 0x0400_0000;+pub const SA_RESTART: u64 = 0x1000_0000;+pub const SA_NODEFER: u64 = 0x4000_0000;+pub const SIG_BLOCK: u64 = 0;+pub const SIG_UNBLOCK: u64 = 1;+pub const SIG_SETMASK: u64 = 2;++/// The kernel's `struct sigaction` (not quite libc's: no sa_sigaction+/// union, the mask is 64 bits).+#[repr(C)]+#[derive(Clone, Copy, Default)]+pub struct SigAction {+ pub handler: usize, // fn(i32) or fn(i32, *SigInfo, *ucontext); or SIG_DFL / SIG_IGN+ pub flags: u64,+ pub restorer: usize,+ pub mask: u64,+}++/// The first fields of Linux's `siginfo_t` (it's 128 bytes in all).+#[repr(C)]+pub struct SigInfo {+ pub signo: i32,+ pub errno: i32,+ pub code: i32,+ _pad: i32,+ /// SIGSEGV/SIGBUS: the faulting address. SIGCHLD/kill: the sender's+ /// pid (low 32 bits) and uid (high 32 bits).+ pub addr_or_pid: u64,+ /// SIGCHLD: the exit code (or the signal that ended/stopped the child).+ pub status: i32,+}++// After a handler returns it "returns" into this: the restorer, which asks+// the kernel to put everything back as it was before the signal (from the+// frame the kernel saved on our stack). Linux's libc does exactly this.+core::arch::global_asm!(+ ".global __restore_rt",+ "__restore_rt:",+ "mov rax, 15", // rt_sigreturn+ "syscall",+);+unsafe extern "C" {+ fn __restore_rt();+}++/// Install `handler` for `sig` (plain `fn(i32)`). Returns the old action.+pub fn signal(sig: i32, handler: extern "C" fn(i32)) -> i64 {+ sigaction_raw(sig, handler as usize, 0, 0)+}+/// SA_SIGINFO handler: also gets the SigInfo (fault address, sender pid...).+pub fn sigaction_info(sig: i32, handler: extern "C" fn(i32, *const SigInfo, *mut u8), flags: u64) -> i64 {+ sigaction_raw(sig, handler as usize, flags | SA_SIGINFO, 0)+}+/// Set SIG_DFL / SIG_IGN / a handler address, with flags and a mask of+/// signals to block while it runs.+pub fn sigaction_raw(sig: i32, handler: usize, flags: u64, mask: u64) -> i64 {+ let act = SigAction { handler, flags: flags | SA_RESTORER, restorer: __restore_rt as usize, mask };+ syscall5(SYS_RT_SIGACTION, sig as u64, &act as *const _ as u64, 0, 8, 0)+}+/// Change the blocked set; returns the old one (or -errno).+pub fn sigprocmask(how: u64, set: u64) -> i64 {+ let mut old = 0u64;+ match syscall5(SYS_RT_SIGPROCMASK, how, &set as *const u64 as u64, &mut old as *mut u64 as u64, 8, 0) {+ 0 => old as i64,+ e => e,+ }+}+pub fn sigbit(sig: i32) -> u64 {+ 1 << (sig - 1)+}+/// kill(pid, sig): pid > 0 one process, 0 my group, -pgid a group, -1 all.+pub fn kill(pid: i64, sig: i32) -> i64 {+ syscall3(SYS_KILL, pid as u64, sig as u64, 0)+}+/// Sleep until a signal handler has run (always returns -EINTR).+pub fn pause() -> i64 {+ syscall3(SYS_PAUSE, 0, 0, 0)+}++// --- Memory: mmap and brk ----------------------------------------------------++pub const PROT_NONE: u64 = 0;+pub const PROT_READ: u64 = 1;+pub const PROT_WRITE: u64 = 2;+pub const PROT_EXEC: u64 = 4;+pub const MAP_SHARED: u64 = 0x01;+pub const MAP_PRIVATE: u64 = 0x02;+pub const MAP_FIXED: u64 = 0x10;+pub const MAP_ANONYMOUS: u64 = 0x20;++/// mmap: the address of the new mapping, or -errno. fd -1 = anonymous.+pub fn mmap(addr: u64, len: u64, prot: u64, flags: u64, fd: i64, offset: u64) -> i64 {+ syscall6(SYS_MMAP, addr, len, prot, flags, fd as u64, offset)+}+pub fn munmap(addr: u64, len: u64) -> i64 {+ syscall3(SYS_MUNMAP, addr, len, 0)+}+pub fn mprotect(addr: u64, len: u64, prot: u64) -> i64 {+ syscall3(SYS_MPROTECT, addr, len, prot)+}+/// brk: move the end of the heap; returns the new end (brk(0) = current).+pub fn brk(addr: u64) -> u64 {+ syscall3(SYS_BRK, addr, 0, 0) as u64+}+ /// Console raw mode on/off (keys arrive one by one, no echo). Returns the /// old mode, or -ENOTTY if `fd` isn't the console. pub fn set_raw(fd: i64, raw: bool) -> i64 {@@ -299,23 +640,64 @@ pub fn for_each_dirent(buf: &[u8], len: usize, mut f: impl FnMut(&str, u8)) { } } -// --- program arguments ---------------------------------------------------+// --- program arguments, environment, auxiliary vector -------------------------+//+// The kernel starts a program the way Linux does: rsp points at+// argc, argv[0..argc], NULL, envp[0..], NULL, auxv pairs..., AT_NULL 0+// and the strings themselves sit higher up on the stack. -/// `argv` as the kernel passes it: (pointer, length) pairs. static mut ARGC: usize = 0;-static mut ARGV: *const [usize; 2] = core::ptr::null();+static mut ARGV: *const *const u8 = core::ptr::null();+static mut ENVP: *const *const u8 = core::ptr::null();+static mut AUXV: *const [u64; 2] = core::ptr::null();++/// A C string as &str (up to its NUL).+pub unsafe fn cstr(ptr: *const u8) -> &'static str {+ unsafe {+ let mut len = 0;+ while *ptr.add(len) != 0 {+ len += 1;+ }+ core::str::from_utf8(core::slice::from_raw_parts(ptr, len)).unwrap_or("?")+ }+} /// The program's arguments; `arg(0)` is the program name. pub fn argc() -> usize { unsafe { ARGC } } pub fn arg(i: usize) -> Option<&'static str> {+ unsafe { if i < ARGC { Some(cstr(*ARGV.add(i))) } else { None } }+}+/// The i-th environment string ("NAME=value").+pub fn env(i: usize) -> Option<&'static str> { unsafe {- if i >= ARGC {- return None;+ let p = *ENVP.add(i);+ if p.is_null() { None } else { Some(cstr(p)) }+ }+}+/// getenv("PATH") -> the value after "PATH=".+pub fn getenv(name: &str) -> Option<&'static str> {+ (0..).map_while(env).find_map(|e| e.strip_prefix(name)?.strip_prefix('='))+}+pub const AT_PHDR: u64 = 3;+pub const AT_PHENT: u64 = 4;+pub const AT_PHNUM: u64 = 5;+pub const AT_PAGESZ: u64 = 6;+pub const AT_ENTRY: u64 = 9;+pub const AT_RANDOM: u64 = 25;+pub const AT_EXECFN: u64 = 31;+/// An auxiliary vector entry (AT_PAGESZ -> 4096 ...), if the kernel gave it.+pub fn getauxval(kind: u64) -> Option<u64> {+ unsafe {+ let mut p = AUXV;+ while (*p)[0] != 0 {+ if (*p)[0] == kind {+ return Some((*p)[1]);+ }+ p = p.add(1); }- let [ptr, len] = *ARGV.add(i);- core::str::from_utf8(core::slice::from_raw_parts(ptr as *const u8, len)).ok()+ None } } @@ -365,7 +747,10 @@ pub fn for_each_line(fd: i64, mut f: impl FnMut(&[u8])) -> i64 { /// `ls`, `cat` etc. print errors like "cat: x: No such file or directory". pub fn strerror(errno: i64) -> &'static str { match errno.abs() {+ 1 => "Operation not permitted", 2 => "No such file or directory",+ 3 => "No such process",+ 4 => "Interrupted system call", 5 => "I/O error", 7 => "Argument list too long", 8 => "Exec format error",@@ -641,13 +1026,30 @@ macro_rules! println { }}; } -/// The kernel jumps here (see process.rs) with rdi = argc, rsi = argv.-/// Save the arguments, run main, exit with its result.+// The entry point. The kernel jumps here with rsp at `argc` (see above);+// hand that pointer to Rust (rdi = first argument), keeping rsp 16-byte+// aligned the way a `call` expects.+core::arch::global_asm!(+ ".global _start",+ "_start:",+ "mov rdi, rsp",+ "and rsp, -16",+ "call start_rust",+ "ud2",+);++/// Find argv/envp/auxv on the initial stack, run main, exit with its result. #[unsafe(no_mangle)]-pub extern "C" fn _start(argc: usize, argv: *const [usize; 2]) -> ! {+extern "C" fn start_rust(sp: *const usize) -> ! { unsafe {- ARGC = argc;- ARGV = argv;+ ARGC = *sp;+ ARGV = sp.add(1) as *const *const u8;+ ENVP = ARGV.add(ARGC + 1);+ let mut p = ENVP;+ while !(*p).is_null() {+ p = p.add(1);+ }+ AUXV = p.add(1) as *const [u64; 2]; } exit(crate::main()) }@@ -661,29 +1063,30 @@ fn panic(info: &core::panic::PanicInfo) -> ! { // --- heap ---------------------------------------------------------------- // // A tiny allocator so programs can use `Vec`, `String`, `format!` (with-// `extern crate alloc;`). Memory comes from a 64 KiB array in .bss, which-// the ELF loader maps as zeroed pages -- no `brk`/`mmap` syscall needed.+// `extern crate alloc;`). Memory comes from `brk`: the kernel's "program+// break" is the end of a heap area right after the program, and moving it+// up gives us more (zeroed, demand-paged) memory. // Same idea as the kernel's fixed-size-block allocator: sizes are rounded up-// to a power of two (16 B .. 32 KiB) and freed blocks go on one free list+// to a power of two (16 B .. 1 MiB) and freed blocks go on one free list // per size, to be reused by the next allocation of that size. Programs are // single-threaded, so no lock. -const HEAP_SIZE: usize = 64 * 1024;-const CLASSES: usize = 12; // 16 << 11 = 32 KiB+const CLASSES: usize = 17; // 16 << 16 = 1 MiB -#[repr(C, align(4096))] struct Heap {- memory: UnsafeCell<[u8; HEAP_SIZE]>,- used: UnsafeCell<usize>, // bump pointer: bytes handed out so far- free: UnsafeCell<[usize; CLASSES]>, // free list heads (addresses, 0 = empty)+ start: UnsafeCell<u64>, // where the heap began (0 = not set up yet)+ used: UnsafeCell<u64>, // bump pointer: end of what's handed out+ end: UnsafeCell<u64>, // the current break+ free: UnsafeCell<[usize; CLASSES]>, // free list heads (addresses, 0 = empty) } unsafe impl Sync for Heap {} // single-threaded programs #[global_allocator] static HEAP: Heap = Heap {- memory: UnsafeCell::new([0; HEAP_SIZE]),+ start: UnsafeCell::new(0), used: UnsafeCell::new(0),+ end: UnsafeCell::new(0), free: UnsafeCell::new([0; CLASSES]), }; @@ -707,16 +1110,27 @@ unsafe impl GlobalAlloc for Heap { free[class] = *(block as *const usize); return block; }+ let (used, end) = (&mut *self.used.get(), &mut *self.end.get());+ if *self.start.get() == 0 {+ *self.start.get() = brk(0);+ *used = *self.start.get();+ *end = *used;+ } // Carve a new block. Blocks are aligned to their size, which // covers any alignment up to that size.- let size = 16 << class;- let used = &mut *self.used.get();+ let size = 16u64 << class; let start = (*used).next_multiple_of(size);- if start + size > HEAP_SIZE {- return core::ptr::null_mut();+ if start + size > *end {+ // Grow the heap (at least 64 KiB at a time).+ let want = (start + size).max(*end + 64 * 1024);+ let got = brk(want);+ if got < start + size {+ return core::ptr::null_mut(); // the kernel said no+ }+ *end = got; } *used = start + size;- (self.memory.get() as *mut u8).add(start)+ start as *mut u8 } } @@ -729,3 +1143,35 @@ unsafe impl GlobalAlloc for Heap { } } }++// --- Self-checking test programs ---------------------------------------------++/// Counts failed checks for the test programs (forktest, sigtest, ...):+/// each prints "ok" or "FAILED" per check and exits with the failures.+pub struct Checks {+ pub name: &'static str,+ pub failed: i64,+}++impl Checks {+ pub fn new(name: &'static str) -> Checks {+ Checks { name, failed: 0 }+ }+ pub fn check(&mut self, ok: bool, what: &str) {+ if ok {+ println!("[{}] ok: {}", self.name, what);+ } else {+ println!("[{}] FAILED: {}", self.name, what);+ self.failed += 1;+ }+ }+ /// Print the summary; the result is the exit code.+ pub fn done(self) -> i64 {+ if self.failed == 0 {+ println!("[{}] all checks passed", self.name);+ } else {+ println!("[{}] {} check(s) failed", self.name, self.failed);+ }+ self.failed+ }+}