foundry
Foundry OS: the operating system built on the Rustbelt kernel
| author | Tyler Fowler <tyler@localhost> | 2026-10-30 04:30:07 -0700 |
|---|---|---|
| commit | e493cb833456780edb40c4db7120517b46ef9f0f | |
| parent | 235b98b1bd5539477d1c2b19d811c5f18d4f1ecf | |
| tree | browse 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.
| A | getty.rs | +54 -0 |
| M | ls.rs | +7 -4 |
| A | random.rs | +74 -0 |
| M | sh.rs | +1 -0 |
| A | tile.rs | +173 -0 |
| M | ulib.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",