RustForge

foundry

Foundry OS: the operating system built on the Rustbelt kernel

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

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

The shell uses fork + exec with job control: Ctrl+C, kill, fg, bg.
New user programs forktest, cowtest, mmaptest, sigtest and
tests/process.rs (11 tests). README: learner chapter on fork/exec,
copy-on-write, memory maps and signals; roadmap updated. New boot.png.
Acowtest.rs+107 -0
Aforktest.rs+186 -0
Ammaptest.rs+130 -0
Msh.rs+255 -32
Asigtest.rs+189 -0
Mulib.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+    }+}