RustForge

foundry

Foundry OS: the operating system built on the Rustbelt kernel

authorTyler Fowler <tyler@localhost>2026-10-30 04:30:07 -0700
commite493cb833456780edb40c4db7120517b46ef9f0f
parent235b98b1bd5539477d1c2b19d811c5f18d4f1ecf
treebrowse files at this commit
Stage 13: virtual drivers (0.13.0)

virtio-rng (getrandom syscall 318, /dev/random, seeds smoltcp and ports),
virtio-console (hvc0, kernel log mirror, getty), virtio-gpu 2D with a
1024x768 framebuffer console (Spleen 8x16 font, VGA text fallback) and
/dev/fb0 (ioctl + write blitting). New user programs: random, getty, tile.
New tests/virtio.rs (9 tests), gpu-check.sh screendump pixel check,
README chapter on virtio, framebuffers and the road to 3D.
Agetty.rs+54 -0
Mls.rs+7 -4
Arandom.rs+74 -0
Msh.rs+1 -0
Atile.rs+173 -0
Mulib.rs+16 -0

6 files changed, 325 insertions, 4 deletions

@@ -0,0 +1,54 @@+//! getty [-1] DEVICE -- run a shell on another terminal, e.g. /dev/hvc0.+//!+//! Real Unix systems run one "getty" per terminal line: it opens the line+//! and starts a login shell on it. Ours skips the login: it opens DEVICE+//! and spawns `sh` with stdin, stdout and stderr all on it. When the shell+//! exits (`exit`) a new one starts, unless `-1` was given.+//!+//! Try it: `getty /dev/hvc0 &` here, then on the host+//! `socat - UNIX-CONNECT:target/hvc0.sock` and type commands. That shell+//! is not "interactive" (no line editing): it reads one line at a time and+//! prints each line after the prompt, like a script.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++const SHELL: &str = "/mnt/disk0/bin/sh";++fn main() -> i64 {+    let (once, dev) = match (arg(1), arg(2)) {+        (Some("-1"), Some(dev)) => (true, dev),+        (Some(dev), None) if dev != "-1" => (false, dev),+        _ => {+            eprintln!("usage: getty [-1] DEVICE   (e.g. getty /dev/hvc0 &)");+            return 2;+        }+    };+    loop {+        let fd = open(dev, 0);+        if fd < 0 {+            eprintln!("getty: {}: {}", dev, strerror(fd));+            return 1;+        }+        let greeting = "\nRustbelt getty: shell on this terminal (type `exit` to end it)\n";+        write(fd, greeting.as_bytes());+        let opts = SpawnOptions { stdin: fd, stdout: fd, stderr: fd, flags: 0 };+        let pid = spawn(SHELL, "sh", &opts);+        if pid < 0 {+            eprintln!("getty: {}: {}", SHELL, strerror(pid));+            close(fd);+            return 1;+        }+        let status = wait(pid);+        close(fd);+        if once {+            return status;+        }+        sleep_ms(500); // don't spin if the shell keeps dying at once+    }+}
@@ -21,6 +21,7 @@ struct Entry {     name: String,     dir: bool,     link: bool,+    dev: bool,           // a device like /dev/fb0 (shown as <DEV>)     size: u64,     mode: u64,           // 0 = no permissions on this file system     target: String,      // symlinks (with -l): where they point@@ -51,7 +52,7 @@ fn main() -> i64 {             continue;         }         if st.kind != KIND_DIR {-            let entry = Entry { name: String::from(*path), dir: false, link: false, size: st.size, mode: st.mode, target: String::new() };+            let entry = Entry { name: String::from(*path), dir: false, link: false, dev: st.kind == KIND_DEVICE, size: st.size, mode: st.mode, target: String::new() };             print_entries(&[entry], long, columns);             continue;         }@@ -85,8 +86,8 @@ fn read_dir(path: &str, long: bool) -> Result<Vec<Entry>, i64> {         }         for_each_dirent(&buf, n as usize, |name, d_type| {             if name != "." && name != ".." {-                let (dir, link) = (d_type == 4, d_type == 10); // DT_DIR, DT_LNK-                entries.push(Entry { name: String::from(name), dir, link, size: 0, mode: 0, target: String::new() });+                let (dir, link, dev) = (d_type == 4, d_type == 10, d_type == 2); // DT_DIR, DT_LNK, DT_CHR+                entries.push(Entry { name: String::from(name), dir, link, dev, size: 0, mode: 0, target: String::new() });             }         });     }@@ -120,6 +121,8 @@ fn print_entries(entries: &[Entry], long: bool, columns: bool) {                 println!("{}     <DIR>  {}/", perms, e.name);             } else if e.link {                 println!("{}            {} -> {}", perms, e.name, e.target);+            } else if e.dev {+                println!("{}     <DEV>  {}", perms, e.name);             } else {                 println!("{}{:>10}  {}", perms, e.size, e.name);             }@@ -153,7 +156,7 @@ fn mode_string(e: &Entry) -> String {     if e.link {         return String::from("lrwxrwxrwx"); // symlink permissions are never used     }-    let mut s = String::from(if e.dir { "d" } else { "-" });+    let mut s = String::from(if e.dir { "d" } else if e.dev { "c" } else { "-" });     for shift in [6, 3, 0] {         let bits = (e.mode >> shift) & 7;         s.push(if bits & 4 != 0 { 'r' } else { '-' });
@@ -0,0 +1,74 @@+//! random -- print random bytes from the kernel's entropy source.+//!+//!   random            16 random bytes as hex (a fresh 128-bit id)+//!   random -n 32      32 bytes as hex+//!   random -d 6 [N]   roll N dice with 6 sides (default 1 die)+//!+//! The bytes come from the `getrandom` system call (Linux number 318),+//! which the kernel fills from the virtio-rng device, i.e. from the host's+//! /dev/urandom. /dev/random and /dev/urandom read the same source.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++fn main() -> i64 {+    match (arg(1), arg(2), arg(3)) {+        (None, ..) => hex(16),+        (Some("-n"), Some(n), None) => match n.parse::<usize>() {+            Ok(n) if n <= 4096 => hex(n),+            _ => usage(),+        },+        (Some("-d"), Some(sides), count) => {+            let sides = sides.parse::<u64>().unwrap_or(0);+            let count = count.map_or(Some(1), |c| c.parse::<u64>().ok()).unwrap_or(0);+            if sides < 2 || count == 0 || count > 100 {+                return usage();+            }+            for i in 0..count {+                print!("{}{}", if i > 0 { " " } else { "" }, roll(sides));+            }+            println!();+            0+        }+        _ => usage(),+    }+}++/// Print `n` random bytes as hex digits.+fn hex(n: usize) -> i64 {+    let mut buf = [0u8; 4096];+    let r = getrandom(&mut buf[..n]);+    if r < 0 {+        eprintln!("random: getrandom: {}", strerror(r));+        return 1;+    }+    for b in &buf[..n] {+        print!("{:02x}", b);+    }+    println!();+    0+}++/// A fair number in 1..=sides. Plain `x % sides` would favour small+/// numbers slightly, so throw away the values from the uneven top part.+fn roll(sides: u64) -> u64 {+    let limit = u64::MAX - u64::MAX % sides;+    loop {+        let mut b = [0u8; 8];+        getrandom(&mut b);+        let x = u64::from_le_bytes(b);+        if x < limit {+            return x % sides + 1;+        }+    }+}++fn usage() -> i64 {+    eprintln!("usage: random | random -n BYTES (max 4096) | random -d SIDES [COUNT]");+    2+}
@@ -43,6 +43,7 @@ Rustbelt shell commands   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   run [--trace] prog args   run a program (--trace prints its syscalls)   start prog args           run it in the background (or: prog args &)
@@ -0,0 +1,173 @@+//! tile [N] -- draw a tiling-window-manager mock-up on /dev/fb0.+//!+//!   tile       4 "windows" in a dwindle layout, the text console in the first+//!   tile 6     1 to 6 windows+//!   tile -r    give the text console the whole screen back+//!+//! A hint of the compositor planned for later (think Hyprland): a gradient+//! wallpaper, windows with gaps between them and a border, the focused one+//! cyan. The "windows" are only coloured rectangles, except the first one,+//! which is where the kernel's text console moves to (FB_SET_CONSOLE), so+//! the shell keeps working inside it.+//!+//! How it talks to the framebuffer (no mmap yet, so no direct pointer):+//!   ioctl(FB_GET_INFO)     width, height, bytes per row, bits per pixel+//!   lseek + write          raw 0x00RRGGBB pixels (the wallpaper rows)+//!   ioctl(FB_FILL)         fill a rectangle with one colour (fast)+//!   ioctl(FB_FLUSH)        send the changed part to the screen+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++extern crate alloc;+use alloc::vec;+use alloc::vec::Vec;+use ulib::*;++const GAP: u32 = 10; // space between windows and around the screen edge+const BORDER: u32 = 3;+const ACTIVE: u32 = 0x33ccff; // focused window border (cyan)+const INACTIVE: u32 = 0x595959; // the other borders (gray)+/// Window contents: soft pastels (the first window is the console).+const COLORS: [u32; 5] = [0x89b4fa, 0xa6e3a1, 0xf9e2af, 0xf38ba8, 0xcba6f7];++#[derive(Clone, Copy)]+struct Rect {+    x: u32,+    y: u32,+    w: u32,+    h: u32,+}++fn main() -> i64 {+    let fd = open("/dev/fb0", 0);+    if fd < 0 {+        eprintln!("tile: /dev/fb0: {} (no virtio-gpu?)", strerror(fd));+        return 1;+    }+    let status = match arg(1) {+        Some("-r") => reset(fd),+        Some(n) => match n.parse::<usize>() {+            Ok(n @ 1..=6) => draw(fd, n),+            _ => {+                eprintln!("usage: tile [1-6] | tile -r");+                2+            }+        },+        None => draw(fd, 4),+    };+    close(fd);+    status+}++fn reset(fd: i64) -> i64 {+    let r = ioctl(fd, FB_SET_CONSOLE, 0);+    if r < 0 {+        eprintln!("tile: {}", strerror(r));+        return 1;+    }+    println!("console: full screen, {}x{} characters", r >> 16, r & 0xffff);+    0+}++fn draw(fd: i64, n: usize) -> i64 {+    let mut info = [0u32; 4];+    if ioctl(fd, FB_GET_INFO, info.as_mut_ptr() as u64) < 0 {+        eprintln!("tile: FB_GET_INFO failed");+        return 1;+    }+    let [width, height, stride, _bpp] = info;+    wallpaper(fd, width, height, stride);++    let screen = Rect { x: GAP, y: GAP, w: width - 2 * GAP, h: height - 2 * GAP };+    let windows = dwindle(screen, n);+    for (i, &win) in windows.iter().enumerate() {+        fill(fd, win, if i == 0 { ACTIVE } else { INACTIVE });+        let inside = shrink(win, BORDER);+        if i > 0 {+            fake_content(fd, inside, COLORS[(i - 1) % COLORS.len()]);+        }+    }+    let flush = [0, 0, width, height];+    ioctl(fd, FB_FLUSH, flush.as_ptr() as u64);++    // Move the console into window 0 (inside its border).+    let c = shrink(windows[0], BORDER);+    let r = ioctl(fd, FB_SET_CONSOLE, [c.x, c.y, c.w, c.h].as_ptr() as u64);+    if r < 0 {+        eprintln!("tile: FB_SET_CONSOLE: {}", strerror(r));+        return 1;+    }+    println!("tile: {} windows on {}x{}, console {}x{}", n, width, height, r >> 16, r & 0xffff);+    0+}++/// Split the screen like a "dwindle" layout: each window takes half of+/// what is left, alternating left|right and top/bottom splits.+fn dwindle(mut area: Rect, n: usize) -> Vec<Rect> {+    let mut out = Vec::new();+    for i in 0..n {+        if i + 1 == n {+            out.push(area); // the last window gets the rest+            break;+        }+        if i % 2 == 0 {+            let w = (area.w - GAP) / 2;+            out.push(Rect { w, ..area });+            area = Rect { x: area.x + w + GAP, w: area.w - w - GAP, ..area };+        } else {+            let h = (area.h - GAP) / 2;+            out.push(Rect { h, ..area });+            area = Rect { y: area.y + h + GAP, h: area.h - h - GAP, ..area };+        }+    }+    out+}++/// A vertical gradient, written 8 rows (32 KiB) at a time with lseek ++/// write. (Programs only have a 64 KiB heap until mmap exists.)+fn wallpaper(fd: i64, width: u32, height: u32, stride: u32) {+    const ROWS: u32 = 8;+    let mut buf = vec![0u8; (stride * ROWS) as usize];+    for y0 in (0..height).step_by(ROWS as usize) {+        let rows = ROWS.min(height - y0);+        for dy in 0..rows {+            let color = gradient(y0 + dy, height).to_le_bytes();+            let row = &mut buf[(dy * stride) as usize..][..(width * 4) as usize];+            row.chunks_exact_mut(4).for_each(|px| px.copy_from_slice(&color));+        }+        lseek(fd, (y0 * stride) as i64, SEEK_SET);+        write(fd, &buf[..(rows * stride) as usize]);+    }+}++/// Dark blue at the top to purple at the bottom.+fn gradient(y: u32, height: u32) -> u32 {+    let mix = |a: u32, b: u32| a + (b - a) * y / height;+    mix(0x1e, 0x50) << 16 | mix(0x1e, 0x2a) << 8 | mix(0x2e, 0x78)+}++/// Fill a window with `color` and draw some darker bars, like lines of text.+fn fake_content(fd: i64, r: Rect, color: u32) {+    fill(fd, r, color);+    let dark = (color >> 1) & 0x7f7f7f; // every channel halved+    let mut y = r.y + 16;+    let mut line = 0;+    while y + 6 < r.y + r.h - 8 {+        let w = (r.w - 32) * [9, 6, 8, 4, 7][line % 5] / 10;+        fill(fd, Rect { x: r.x + 16, y, w, h: 6 }, dark);+        y += 16;+        line += 1;+    }+}++fn shrink(r: Rect, by: u32) -> Rect {+    Rect { x: r.x + by, y: r.y + by, w: r.w - 2 * by, h: r.h - 2 * by }+}++fn fill(fd: i64, r: Rect, color: u32) {+    ioctl(fd, FB_FILL, [r.x, r.y, r.w, r.h, color].as_ptr() as u64);+}
@@ -53,11 +53,17 @@ pub const SYS_SETSOCKOPT: u64 = 54; pub const SYS_CLOCK_GETTIME: u64 = 228; pub const SYS_RESOLVE: u64 = 503; pub const SYS_NETINFO: u64 = 504;+pub const SYS_GETRANDOM: u64 = 318;  pub const O_CREAT: u64 = 0o100; pub const O_TRUNC: u64 = 0o1000; pub const O_APPEND: u64 = 0o2000; pub const TTY_SET_RAW: u64 = 1;+// /dev/fb0 ioctls (see the table in the kernel's syscall.rs).+pub const FB_GET_INFO: u64 = 0x4600;+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 SEEK_SET: u64 = 0; pub const SEEK_CUR: u64 = 1;@@ -90,6 +96,7 @@ pub const KIND_DIR: u64 = 2; pub const KIND_CONSOLE: u64 = 3; pub const KIND_PIPE: u64 = 4; pub const KIND_SYMLINK: u64 = 5;+pub const KIND_DEVICE: u64 = 7; // /dev/null, /dev/fb0, ...  /// Same layout as the kernel's `syscall::SpawnOptions`. #[repr(C)]@@ -208,6 +215,14 @@ pub fn uname(buf: &mut [u8; 6 * 65]) -> [&str; 6] {         core::str::from_utf8(&field[..len]).unwrap_or("?")     }) }+/// Fill `buf` with random bytes from the kernel (virtio-rng). Linux 318.+pub fn getrandom(buf: &mut [u8]) -> i64 {+    syscall3(SYS_GETRANDOM, buf.as_mut_ptr() as u64, buf.len() as u64, 0)+}+/// A raw ioctl: what `request` means depends on the device.+pub fn ioctl(fd: i64, request: u64, arg: u64) -> i64 {+    syscall3(SYS_IOCTL, fd as u64, request, arg)+} /// The mount table as text ("path fstype" lines); returns the length. pub fn mounts(buf: &mut [u8]) -> i64 {     syscall3(SYS_MOUNTS, buf.as_mut_ptr() as u64, buf.len() as u64, 0)@@ -360,6 +375,7 @@ pub fn strerror(errno: i64) -> &'static str {         12 => "Out of memory",         14 => "Bad address",         16 => "Device or resource busy",+        19 => "No such device",         17 => "File exists",         20 => "Not a directory",         21 => "Is a directory",