RustForge

foundry

Foundry OS: the operating system built on the Rustbelt kernel

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

- GDT: user code/data segments in sysret order; TSS.rsp0 updated per thread
- syscall/sysret entry (STAR/LSTAR/SFMASK, EFER.SCE) with kernel stack switch;
  Linux-numbered read/write/open/close/sched_yield/getpid/exit, -errno results,
  user pointer validation
- per-process L4 table (kernel entries copied, user region = L4 slot 1),
  CR3 switched by the scheduler; process = thread + address space + fd table
- user faults (page fault, GPF, invalid opcode) kill the process, not the kernel
- minimal ELF64 loader; user programs (hello, evil, spin, echo) built by build.rs
- VFS: Inode/FileSystem/File traits, mount table, ramfs on / and /tmp; tty line
  discipline for read(0)
- tests: user_mode, vfs; screenshot.sh drives the echo program; README
Aecho.rs+23 -0
Aevil.rs+17 -0
Ahello.rs+57 -0
Alink.ld+23 -0
Aspin.rs+20 -0
Aulib.rs+105 -0

6 files changed, 245 insertions, 0 deletions

@@ -0,0 +1,23 @@+//! Reads lines from the keyboard (fd 0) and echoes them back. "exit" quits.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++fn main() -> i64 {+    println!("[echo] pid {}: type a line and press Enter", ulib::getpid());+    let mut buf = [0u8; 256];+    loop {+        let n = ulib::read(0, &mut buf); // blocks until Enter+        if n <= 0 {+            return 1;+        }+        let line = core::str::from_utf8(&buf[..n as usize]).unwrap_or("?").trim_end();+        println!("[echo] you typed: {}", line);+        if line == "exit" {+            return 0;+        }+    }+}
@@ -0,0 +1,17 @@+//! A misbehaving program: tries to read kernel memory directly. The kernel's+//! pages aren't USER_ACCESSIBLE, so the CPU raises a page fault, and the+//! kernel kills this process (and carries on).+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++fn main() -> i64 {+    let kernel_code = 0x20_0000 as *const u64; // where the kernel is loaded+    println!("[evil] pid {}: reading kernel memory at {:p}...", ulib::getpid(), kernel_code);+    let value = unsafe { core::ptr::read_volatile(kernel_code) };+    println!("[evil] got {:#x} -- this must never be printed!", value);+    1+}
@@ -0,0 +1,57 @@+//! First user program: says hello, uses the VFS through syscalls, and checks+//! that bad syscall arguments get the right errors. Exit code = failed checks.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++fn check(failures: &mut i64, ok: bool, what: &str) {+    if !ok {+        println!("[hello] CHECK FAILED: {}", what);+        *failures += 1;+    }+}++fn main() -> i64 {+    let mut failures = 0;+    println!("Hello from user mode! I'm pid {}.", getpid());++    // Read a file from the ramfs.+    let mut buf = [0u8; 128];+    let fd = open("/hello.txt", 0);+    check(&mut failures, fd >= 3, "open /hello.txt");+    let n = read(fd, &mut buf);+    let text = core::str::from_utf8(&buf[..n.max(0) as usize]).unwrap_or("<not utf-8>");+    print!("[hello] /hello.txt says: {}", text);+    check(&mut failures, text.starts_with("Hello from the ramfs!"), "contents of /hello.txt");+    check(&mut failures, read(fd, &mut buf) == 0, "read at EOF returns 0");+    check(&mut failures, close(fd) == 0, "close");++    // Create a file on the second mount (/tmp), then read it back.+    let fd = open("/tmp/note.txt", O_CREAT);+    check(&mut failures, write(fd, b"written from ring 3") == 19, "write /tmp/note.txt");+    close(fd);+    let fd = open("/tmp/note.txt", 0);+    let n = read(fd, &mut buf);+    check(&mut failures, &buf[..n.max(0) as usize] == b"written from ring 3", "read back /tmp/note.txt");+    close(fd);+    println!("[hello] wrote and read back /tmp/note.txt");++    // Error cases.+    check(&mut failures, open("/does/not/exist", 0) == -ENOENT, "ENOENT");+    check(&mut failures, read(42, &mut buf) == -EBADF, "EBADF");+    check(&mut failures, close(fd) == -EBADF, "double close -> EBADF");+    // Ask the kernel to print *kernel* memory for us (0x200000 = kernel code).+    let r = syscall3(SYS_WRITE, 1, 0x20_0000, 16);+    check(&mut failures, r == -EFAULT, "kernel pointer -> EFAULT");+    check(&mut failures, syscall3(999, 0, 0, 0) == -38, "unknown syscall -> ENOSYS");++    if failures == 0 {+        println!("[hello] all syscall checks passed, exiting with 0");+    }+    failures+}
@@ -0,0 +1,20 @@+//! A CPU hog: busy-loops without ever making a syscall in between. The timer+//! interrupt still preempts it, so everything else keeps running.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++fn main() -> i64 {+    println!("[spin] pid {}: spinning without syscalls...", ulib::getpid());+    let mut count: u64 = 0;+    for round in 1..=3 {+        for _ in 0..30_000_000 {+            count = core::hint::black_box(count + 1);+        }+        println!("[spin] round {} done (count = {})", round, count);+    }+    0+}
@@ -0,0 +1,105 @@+//! A tiny "libc" for user programs: syscall wrappers, print!/println!,+//! the `_start` entry point and a panic handler.+//!+//! Each user program includes it with+//!     #[macro_use] #[path = "ulib.rs"] mod ulib;+//! and defines `fn main() -> i64` (the exit code).+//!+//! User programs are built for the `x86_64-unknown-none` target, which has+//! SSE/floating point disabled. That matters: the kernel doesn't save SSE+//! registers on a context switch.++#![allow(dead_code)]++use core::arch::asm;+use core::fmt;++pub const SYS_READ: u64 = 0;+pub const SYS_WRITE: u64 = 1;+pub const SYS_OPEN: u64 = 2;+pub const SYS_CLOSE: u64 = 3;+pub const SYS_SCHED_YIELD: u64 = 24;+pub const SYS_GETPID: u64 = 39;+pub const SYS_EXIT: u64 = 60;++pub const O_CREAT: u64 = 0o100;+pub const ENOENT: i64 = 2;+pub const EBADF: i64 = 9;+pub const EFAULT: i64 = 14;++/// Raw system call: number in rax, args in rdi/rsi/rdx, result in rax.+/// The `syscall` instruction overwrites rcx and r11.+pub fn syscall3(nr: u64, a1: u64, a2: u64, a3: u64) -> i64 {+    let ret: i64;+    unsafe {+        asm!(+            "syscall",+            inlateout("rax") nr as i64 => ret,+            in("rdi") a1,+            in("rsi") a2,+            in("rdx") a3,+            lateout("rcx") _,+            lateout("r11") _,+            options(nostack),+        );+    }+    ret+}++pub fn read(fd: i64, buf: &mut [u8]) -> i64 {+    syscall3(SYS_READ, fd as u64, buf.as_mut_ptr() as u64, buf.len() as u64)+}+pub fn write(fd: i64, buf: &[u8]) -> i64 {+    syscall3(SYS_WRITE, fd as u64, buf.as_ptr() as u64, buf.len() as u64)+}+pub fn open(path: &str, flags: u64) -> i64 {+    syscall3(SYS_OPEN, path.as_ptr() as u64, path.len() as u64, flags)+}+pub fn close(fd: i64) -> i64 {+    syscall3(SYS_CLOSE, fd as u64, 0, 0)+}+pub fn sched_yield() {+    syscall3(SYS_SCHED_YIELD, 0, 0, 0);+}+pub fn getpid() -> i64 {+    syscall3(SYS_GETPID, 0, 0, 0)+}+pub fn exit(code: i64) -> ! {+    syscall3(SYS_EXIT, code as u64, 0, 0);+    loop {} // not reached+}++pub struct Stdout;++impl fmt::Write for Stdout {+    fn write_str(&mut self, s: &str) -> fmt::Result {+        write(1, s.as_bytes());+        Ok(())+    }+}++macro_rules! print {+    ($($arg:tt)*) => {{+        use core::fmt::Write;+        let _ = write!($crate::ulib::Stdout, $($arg)*);+    }};+}++macro_rules! println {+    ($($arg:tt)*) => {{+        use core::fmt::Write;+        let _ = writeln!($crate::ulib::Stdout, $($arg)*);+    }};+}++/// The kernel jumps here (see process.rs). Run main, exit with its result.+#[unsafe(no_mangle)]+pub extern "C" fn _start() -> ! {+    exit(crate::main())+}++#[panic_handler]+fn panic(info: &core::panic::PanicInfo) -> ! {+    println!("[user panic] {}", info);+    exit(101)+}