foundry
Foundry OS: the operating system built on the Rustbelt kernel
| author | Tyler Fowler <tyler@localhost> | 2026-10-30 00:27:17 -0700 |
|---|---|---|
| commit | 431b990267804972598570345cfc0bd999b87de3 | |
| tree | browse 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
| A | echo.rs | +23 -0 |
| A | evil.rs | +17 -0 |
| A | hello.rs | +57 -0 |
| A | link.ld | +23 -0 |
| A | spin.rs | +20 -0 |
| A | ulib.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,23 @@+/* Linker script for user programs: a static executable loaded at 4 MiB into+ user space (USER_START = 0x80_0000_0000, see src/memory.rs). Each part+ starts on its own page so it can get its own page permissions:+ code = read+execute, rodata = read-only, data/bss = read+write.+ The `large` code model (see build.rs) names its sections .ltext, .lrodata,+ .ldata and .lbss, so those must be listed too -- otherwise the linker+ places them wherever it likes (e.g. code in a non-executable segment). */+ENTRY(_start)++PHDRS {+ text PT_LOAD FLAGS(5); /* R-X */+ rodata PT_LOAD FLAGS(4); /* R-- */+ data PT_LOAD FLAGS(6); /* RW- */+}++SECTIONS {+ . = 0x8000400000;+ .text : ALIGN(4K) { *(.text .text.* .ltext .ltext.*) } :text+ .rodata : ALIGN(4K) { *(.rodata .rodata.* .lrodata .lrodata.*) } :rodata+ .data : ALIGN(4K) { *(.data .data.* .ldata .ldata.* .got .got.*) } :data+ .bss : ALIGN(8) { *(.bss .bss.* .lbss .lbss.* COMMON) } :data+ /DISCARD/ : { *(.comment) *(.eh_frame*) *(.note*) }+}
@@ -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)+}