RustForge

foundry

Foundry OS: the operating system built on the Rustbelt kernel

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

- PCI bus scan (printed at boot); BlockDevice trait + device registry
- legacy virtio-blk driver (polled virtqueue, bounce buffer); RamDisk for tests
- MBR + GPT parsing, partitions exposed as block devices (vda1...)
- FAT32 via fatfs (git, no_std+alloc) behind a BlockIo adapter; mounted at
  /mnt/disk0; build.rs builds target/disk.img (sfdisk + mkfs.vfat + mtools)
  with text files and the user program ELFs; programs now load from disk
- syscalls: stat, fstat, lseek, pipe, sleep_ms, wait, getcwd, chdir, mkdir,
  rmdir, unlink, getdents64, spawn(path, argv, stdio, trace)
- real blocking: blocked threads, wake_at, WaitQueue; console read, pipes and
  wait block instead of spinning
- 64 KiB thread stacks with a canary, finished stacks freed, 4 MiB heap,
  allocator takes its lock with interrupts off
- demo + child user programs; tests for block, partitions, FAT, spawn/wait
- README: disks section, syscall table, spawn vs fork/exec, shell design
Achild.rs+41 -0
Ademo.rs+152 -0
Mulib.rs+167 -2

3 files changed, 360 insertions, 2 deletions

@@ -0,0 +1,41 @@+//! Helper program that `demo` (and the tests) spawn:+//!   child             -> print my arguments, exit 0+//!   child <number>    -> exit with that number (tests wait() exit codes)+//!   child pipe        -> write a line to stdout (demo reads it via a pipe)+//!   child cat         -> copy stdin to stdout until EOF+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++fn main() -> i64 {+    match arg(1) {+        Some("pipe") => {+            println!("hello through a pipe from pid {}", getpid());+            0+        }+        Some("cat") => {+            let mut buf = [0u8; 64];+            loop {+                let n = read(0, &mut buf);+                if n <= 0 {+                    return n;+                }+                write(1, &buf[..n as usize]);+            }+        }+        Some(number) if number.parse::<i64>().is_ok() => number.parse().unwrap(),+        _ => {+            print!("[child] pid {} got {} args:", getpid(), argc());+            for i in 0..argc() {+                print!(" {:?}", arg(i).unwrap_or("?"));+            }+            println!();+            0+        }+    }+}
@@ -0,0 +1,152 @@+//! Stage 6 demo: this program was itself loaded from the FAT32 disk.+//! It exercises the syscalls the shell (next stage) will need: cwd, reading+//! a file on the FAT disk, stat, lseek, getdents64, mkdir/unlink/rmdir,+//! pipes, and spawn + wait. Exit code = number of failed checks.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++static mut FAILURES: i64 = 0;++fn check(ok: bool, what: &str) {+    if !ok {+        println!("[demo] CHECK FAILED: {}", what);+        unsafe { FAILURES += 1 };+    }+}++fn cwd_is(expected: &str) -> bool {+    let mut buf = [0u8; 64];+    let n = getcwd(&mut buf);+    n >= 0 && &buf[..n as usize] == expected.as_bytes()+}++fn main() -> i64 {+    println!("[demo] pid {} started from /mnt/disk0/bin/demo", getpid());++    // --- current directory --------------------------------------------------+    check(cwd_is("/"), "cwd starts at /");+    check(chdir("/mnt/disk0") == 0, "chdir /mnt/disk0");+    check(cwd_is("/mnt/disk0"), "getcwd after chdir");++    // --- read a file from the FAT disk (relative path!) ---------------------+    let fd = open("readme.txt", 0);+    check(fd >= 3, "open readme.txt");+    let mut buf = [0u8; 256];+    let n = read(fd, &mut buf).max(0) as usize;+    let text = core::str::from_utf8(&buf[..n]).unwrap_or("?");+    print!("[demo] readme.txt from FAT32: {}", text.lines().next().unwrap_or(""));+    println!();+    check(text.starts_with("Hello from the FAT32 disk!"), "readme.txt contents");++    // --- stat / fstat / lseek ------------------------------------------------+    let mut st = Stat::default();+    check(stat("readme.txt", &mut st) == 0 && st.kind == KIND_FILE && st.size == n as u64, "stat readme.txt");+    check(fstat(1, &mut st) == 0 && st.kind == KIND_CONSOLE, "fstat stdout = console");+    check(stat("bin", &mut st) == 0 && st.kind == KIND_DIR, "stat bin is a directory");+    check(lseek(fd, 6, SEEK_SET) == 6, "lseek SEEK_SET");+    let mut word = [0u8; 4];+    check(read(fd, &mut word) == 4 && &word == b"from", "read after lseek");+    check(lseek(fd, 0, SEEK_END) == n as i64, "lseek SEEK_END = size");+    check(lseek(1, 0, SEEK_SET) == -ESPIPE, "can't seek the console");+    close(fd);++    // --- list a directory with getdents64 -----------------------------------+    let dir = open("bin", 0);+    let mut dents = [0u8; 512];+    let len = getdents64(dir, &mut dents);+    check(len > 0, "getdents64 bin");+    let (mut saw_demo, mut saw_child) = (false, false);+    print!("[demo] /mnt/disk0/bin:");+    for_each_dirent(&dents, len.max(0) as usize, |name, d_type| {+        print!(" {}{}", name, if d_type == 4 { "/" } else { "" });+        saw_demo |= name == "demo";+        saw_child |= name == "child";+    });+    println!();+    check(saw_demo && saw_child, "bin contains demo and child");+    check(getdents64(dir, &mut dents) == 0, "getdents64 at end returns 0");+    close(dir);++    // --- relative paths, "..", errors ----------------------------------------+    check(chdir("notes") == 0 && cwd_is("/mnt/disk0/notes"), "chdir notes");+    check(chdir("../..") == 0 && cwd_is("/mnt"), "chdir ../..");+    check(chdir("disk0/readme.txt") == -ENOTDIR, "chdir to a file -> ENOTDIR");+    check(chdir("/nope") == -ENOENT, "chdir to nothing -> ENOENT");++    // --- create and delete (ramfs /tmp and the FAT disk) ---------------------+    for base in ["/tmp", "/mnt/disk0/notes"] {+        let mut path_buf = [0u8; 64];+        let dir = join(&mut path_buf, base, "/newdir");+        check(mkdir(dir) == 0, "mkdir");+        check(mkdir(dir) == -EEXIST, "mkdir again -> EEXIST");+        let mut file_buf = [0u8; 64];+        let file = join(&mut file_buf, dir, "/f.txt");+        let fd = open(file, O_CREAT);+        check(write(fd, b"bytes") == 5, "write new file");+        close(fd);+        check(rmdir(dir) == -ENOTEMPTY, "rmdir non-empty -> ENOTEMPTY");+        check(unlink(dir) == -EISDIR, "unlink a directory -> EISDIR");+        check(unlink(file) == 0, "unlink file");+        check(rmdir(dir) == 0, "rmdir empty dir");+        check(stat(dir, &mut st) == -ENOENT, "gone after rmdir");+    }+    check(rmdir("/tmp") == -EBUSY, "rmdir a mount point -> EBUSY");+    println!("[demo] mkdir/unlink/rmdir work on ramfs and FAT32");++    // --- spawn a child with its stdout connected to a pipe -------------------+    chdir("/mnt/disk0");+    let (r, w) = pipe().unwrap_or((-1, -1));+    let opts = SpawnOptions { stdout: w, ..SpawnOptions::INHERIT };+    let pid = spawn("bin/child", "child\0pipe", &opts);+    check(pid > 0, "spawn child (pipe)");+    close(w); // only the child holds the write end now: EOF when it exits+    let mut got = [0u8; 128];+    let mut total = 0;+    loop {+        let n = read(r, &mut got[total..]); // blocks until data or EOF+        if n <= 0 {+            break;+        }+        total += n as usize;+    }+    close(r);+    let line = core::str::from_utf8(&got[..total]).unwrap_or("?").trim_end();+    println!("[demo] read from pipe: {:?}", line);+    check(line.starts_with("hello through a pipe"), "pipe contents");+    check(wait(pid) == 0, "child (pipe) exit code 0");++    // --- spawn + wait: exit codes ---------------------------------------------+    let pid = spawn("bin/child", "child\0with\0three args", &SpawnOptions::INHERIT);+    check(wait(pid) == 0, "child with args exits 0");+    let pid = spawn("bin/child", "child\042", &SpawnOptions::INHERIT);+    let code = wait(pid);+    println!("[demo] spawned child 42 (pid {}), wait -> exit code {}", pid, code);+    check(code == 42, "wait returns the exit code");+    check(wait(pid) == -ECHILD, "second wait -> ECHILD");+    let pid = spawn("/mnt/disk0/bin/evil", "", &SpawnOptions::INHERIT);+    check(wait(pid) == 139, "killed child -> 139");+    check(spawn("bin/nope", "", &SpawnOptions::INHERIT) == -ENOENT, "spawn missing -> ENOENT");+    check(spawn("readme.txt", "", &SpawnOptions::INHERIT) == -8, "spawn non-ELF -> ENOEXEC");+    check(wait(1) == -ECHILD, "wait for a non-child -> ECHILD");++    sleep_ms(100); // blocks in the kernel (no busy loop)++    let failures = unsafe { FAILURES };+    if failures == 0 {+        println!("[demo] all stage 6 checks passed, exiting with 0");+    }+    failures+}++/// Concatenate two strings into `buf` (no heap in user programs).+fn join<'a>(buf: &'a mut [u8], a: &str, b: &str) -> &'a str {+    buf[..a.len()].copy_from_slice(a.as_bytes());+    buf[a.len()..a.len() + b.len()].copy_from_slice(b.as_bytes());+    core::str::from_utf8(&buf[..a.len() + b.len()]).unwrap()+}
@@ -18,14 +18,65 @@ pub const SYS_READ: u64 = 0; pub const SYS_WRITE: u64 = 1; pub const SYS_OPEN: u64 = 2; pub const SYS_CLOSE: u64 = 3;+pub const SYS_STAT: u64 = 4;+pub const SYS_FSTAT: u64 = 5;+pub const SYS_LSEEK: u64 = 8;+pub const SYS_PIPE: u64 = 22; pub const SYS_SCHED_YIELD: u64 = 24;+pub const SYS_SLEEP_MS: u64 = 35; pub const SYS_GETPID: u64 = 39; pub const SYS_EXIT: u64 = 60;+pub const SYS_WAIT: u64 = 61;+pub const SYS_GETCWD: u64 = 79;+pub const SYS_CHDIR: u64 = 80;+pub const SYS_MKDIR: u64 = 83;+pub const SYS_RMDIR: u64 = 84;+pub const SYS_UNLINK: u64 = 87;+pub const SYS_GETDENTS64: u64 = 217;+pub const SYS_SPAWN: u64 = 500;  pub const O_CREAT: u64 = 0o100;+pub const O_TRUNC: u64 = 0o1000;+pub const SEEK_SET: u64 = 0;+pub const SEEK_CUR: u64 = 1;+pub const SEEK_END: u64 = 2;+pub const SPAWN_TRACE: u64 = 1;+ pub const ENOENT: i64 = 2; pub const EBADF: i64 = 9;+pub const ECHILD: i64 = 10; pub const EFAULT: i64 = 14;+pub const EBUSY: i64 = 16;+pub const EEXIST: i64 = 17;+pub const ENOTDIR: i64 = 20;+pub const EISDIR: i64 = 21;+pub const ESPIPE: i64 = 29;+pub const ENOTEMPTY: i64 = 39;++/// Same layout as the kernel's `fs::file::Stat`.+#[repr(C)]+#[derive(Default, Clone, Copy)]+pub struct Stat {+    pub kind: u64, // KIND_*+    pub size: u64,+}+pub const KIND_FILE: u64 = 1;+pub const KIND_DIR: u64 = 2;+pub const KIND_CONSOLE: u64 = 3;+pub const KIND_PIPE: u64 = 4;++/// Same layout as the kernel's `syscall::SpawnOptions`.+#[repr(C)]+pub struct SpawnOptions {+    pub stdin: i64, // my fd to give the child as its fd 0 (-1 = my fd 0)+    pub stdout: i64,+    pub stderr: i64,+    pub flags: u64, // SPAWN_TRACE+}++impl SpawnOptions {+    pub const INHERIT: SpawnOptions = SpawnOptions { stdin: -1, stdout: -1, stderr: -1, flags: 0 };+}  /// Raw system call: number in rax, args in rdi/rsi/rdx, result in rax. /// The `syscall` instruction overwrites rcx and r11.@@ -46,6 +97,26 @@ pub fn syscall3(nr: u64, a1: u64, a2: u64, a3: u64) -> i64 {     ret } +/// 5-argument version (4th argument goes in r10, 5th in r8).+pub fn syscall5(nr: u64, a1: u64, a2: u64, a3: u64, a4: u64, a5: u64) -> i64 {+    let ret: i64;+    unsafe {+        asm!(+            "syscall",+            inlateout("rax") nr as i64 => ret,+            in("rdi") a1,+            in("rsi") a2,+            in("rdx") a3,+            in("r10") a4,+            in("r8") a5,+            lateout("rcx") _,+            lateout("r11") _,+            options(nostack),+        );+    }+    ret+}+ pub fn read(fd: i64, buf: &mut [u8]) -> i64 {     syscall3(SYS_READ, fd as u64, buf.as_mut_ptr() as u64, buf.len() as u64) }@@ -58,9 +129,29 @@ pub fn open(path: &str, flags: u64) -> i64 { pub fn close(fd: i64) -> i64 {     syscall3(SYS_CLOSE, fd as u64, 0, 0) }+pub fn stat(path: &str, st: &mut Stat) -> i64 {+    syscall3(SYS_STAT, path.as_ptr() as u64, path.len() as u64, st as *mut Stat as u64)+}+pub fn fstat(fd: i64, st: &mut Stat) -> i64 {+    syscall3(SYS_FSTAT, fd as u64, st as *mut Stat as u64, 0)+}+pub fn lseek(fd: i64, offset: i64, whence: u64) -> i64 {+    syscall3(SYS_LSEEK, fd as u64, offset as u64, whence)+}+/// Returns (read end, write end), or the negative errno.+pub fn pipe() -> Result<(i64, i64), i64> {+    let mut fds = [0u32; 2];+    match syscall3(SYS_PIPE, fds.as_mut_ptr() as u64, 0, 0) {+        0 => Ok((fds[0] as i64, fds[1] as i64)),+        e => Err(e),+    }+} pub fn sched_yield() {     syscall3(SYS_SCHED_YIELD, 0, 0, 0); }+pub fn sleep_ms(ms: u64) {+    syscall3(SYS_SLEEP_MS, ms, 0, 0);+} pub fn getpid() -> i64 {     syscall3(SYS_GETPID, 0, 0, 0) }@@ -68,6 +159,75 @@ pub fn exit(code: i64) -> ! {     syscall3(SYS_EXIT, code as u64, 0, 0);     loop {} // not reached }+/// Wait for child `pid` to end; returns its exit code (139 = killed).+pub fn wait(pid: i64) -> i64 {+    syscall3(SYS_WAIT, pid as u64, 0, 0)+}+/// Current directory into `buf`; returns its length (or -errno).+pub fn getcwd(buf: &mut [u8]) -> i64 {+    syscall3(SYS_GETCWD, buf.as_mut_ptr() as u64, buf.len() as u64, 0)+}+pub fn chdir(path: &str) -> i64 {+    syscall3(SYS_CHDIR, path.as_ptr() as u64, path.len() as u64, 0)+}+pub fn mkdir(path: &str) -> i64 {+    syscall3(SYS_MKDIR, path.as_ptr() as u64, path.len() as u64, 0)+}+pub fn rmdir(path: &str) -> i64 {+    syscall3(SYS_RMDIR, path.as_ptr() as u64, path.len() as u64, 0)+}+pub fn unlink(path: &str) -> i64 {+    syscall3(SYS_UNLINK, path.as_ptr() as u64, path.len() as u64, 0)+}+pub fn getdents64(fd: i64, buf: &mut [u8]) -> i64 {+    syscall3(SYS_GETDENTS64, fd as u64, buf.as_mut_ptr() as u64, buf.len() as u64)+}+/// Start program `path` with arguments `argv` (NUL-separated, e.g.+/// "child\0hi"). Returns the child's pid (or -errno).+pub fn spawn(path: &str, argv: &str, opts: &SpawnOptions) -> i64 {+    syscall5(+        SYS_SPAWN,+        path.as_ptr() as u64,+        path.len() as u64,+        argv.as_ptr() as u64,+        argv.len() as u64,+        opts as *const SpawnOptions as u64,+    )+}++/// Walk the `linux_dirent64` records getdents64 put in `buf[..len]`,+/// calling `f(name, d_type)` for each (d_type: 4 = directory, 8 = file).+pub fn for_each_dirent(buf: &[u8], len: usize, mut f: impl FnMut(&str, u8)) {+    let mut pos = 0;+    while pos + 19 < len {+        let reclen = u16::from_le_bytes([buf[pos + 16], buf[pos + 17]]) as usize;+        let d_type = buf[pos + 18];+        let name = &buf[pos + 19..pos + reclen];+        let name = &name[..name.iter().position(|&b| b == 0).unwrap_or(name.len())];+        f(core::str::from_utf8(name).unwrap_or("?"), d_type);+        pos += reclen;+    }+}++// --- program arguments ---------------------------------------------------++/// `argv` as the kernel passes it: (pointer, length) pairs.+static mut ARGC: usize = 0;+static mut ARGV: *const [usize; 2] = core::ptr::null();++/// The program's arguments; `arg(0)` is the program name.+pub fn argc() -> usize {+    unsafe { ARGC }+}+pub fn arg(i: usize) -> Option<&'static str> {+    unsafe {+        if i >= ARGC {+            return None;+        }+        let [ptr, len] = *ARGV.add(i);+        core::str::from_utf8(core::slice::from_raw_parts(ptr as *const u8, len)).ok()+    }+}  pub struct Stdout; @@ -92,9 +252,14 @@ macro_rules! println {     }}; } -/// The kernel jumps here (see process.rs). Run main, exit with its result.+/// The kernel jumps here (see process.rs) with rdi = argc, rsi = argv.+/// Save the arguments, run main, exit with its result. #[unsafe(no_mangle)]-pub extern "C" fn _start() -> ! {+pub extern "C" fn _start(argc: usize, argv: *const [usize; 2]) -> ! {+    unsafe {+        ARGC = argc;+        ARGV = argv;+    }     exit(crate::main()) }