foundry
Foundry OS: the operating system built on the Rustbelt kernel
| author | Grok Bot <grokbot@localhost> | 2026-10-31 19:46:15 -0700 |
|---|---|---|
| commit | 55604d879b3a0a8d49b374860be8e8b2f14d2f5c | |
| parent | 8950fae6ee56f66ef0918e92a70b2a4a6d9d493f | |
| tree | browse files at this commit | |
rust/init: Foundry OS /sbin/init for boot contract v2 (rbsh on console + hvc0, respawn, orphan reaping, headless self-test with memory check)
| M | rust/Cargo.lock | +4 -0 |
| M | rust/Cargo.toml | +1 -1 |
| A | rust/init/Cargo.toml | +15 -0 |
| A | rust/init/src/main.rs | +250 -0 |
4 files changed, 270 insertions, 1 deletion
@@ -7,6 +7,10 @@ name = "foundry-coreutils" version = "0.1.0" [[package]]+name = "foundry-init"+version = "0.1.0"++[[package]] name = "foundry-net" version = "0.1.0"
@@ -8,7 +8,7 @@ # in the kernel repo. Turning rbrt into a real library crate is milestone M1b. [workspace] resolver = "3"-members = ["rbsh", "coreutils-rs", "net", "sys"]+members = ["rbsh", "coreutils-rs", "net", "sys", "init"] [workspace.package] version = "0.1.0"
@@ -0,0 +1,15 @@+[package]+name = "foundry-init"+description = "init, Foundry OS's pid 1 under boot contract v2 (/sbin/init on the ext2 root)"+version.workspace = true+edition.workspace = true+license.workspace = true+publish.workspace = true+build = "../rbrt/build.rs"+autobins = false++[[bin]]+name = "init"+path = "src/main.rs"+test = false+bench = false
@@ -0,0 +1,250 @@+//! init -- Foundry OS's first program (pid 1) under boot contract v2.+//!+//! When the kernel finds our ext2 root partition, it mounts it at `/` and+//! starts `/sbin/init` (this program) as process 1, with argv `["init"]`+//! (`["init", "selftest"]` in a headless build) and the environment+//! PATH=/sbin:/bin:/usr/bin:/mnt/disk0/bin, HOME=/, TERM=linux (plus+//! RUSTBELT_HEADLESS=1 when headless). See docs/boot-contract.md.+//!+//! Its jobs:+//!+//! 1. **Start the login shells.** rbsh (`/bin/rbsh`, the shell with drive+//! letters and DOS commands) on the console, and another one on the+//! second console `/dev/hvc0` if QEMU has one.+//! 2. **Respawn them.** Type `exit` in a shell and init starts a new one.+//! (The kernel never restarts pid 1 itself, so init must never exit in+//! normal use: when pid 1 exits, the kernel ends the QEMU run.)+//! 3. **Reap orphans.** When a process exits before its children, the+//! kernel hands those children to pid 1 -- but only if pid 1 is called+//! `init`, which is why this program is named init. A finished child+//! stays a "zombie" until its parent collects its exit status with+//! wait(); init's main loop waits for *any* child, so adopted orphans are+//! collected too.+//! 4. **Headless mode** (`init selftest`, or RUSTBELT_HEADLESS=1), used by+//! `make test`: run `rbsh /scripts/selftest.sh` on the console, wait for+//! it and for every orphan it left behind, report how many memory frames+//! are in use before and after (they must be equal: no leaks), and exit+//! with the self-test's exit code. The kernel turns pid 1's exit(n) into+//! QEMU exit code 33 + 2n, so exit(0) = 33 = "pass".+//!+//! init ignores the keyboard and termination signals (Ctrl+C on the console+//! must not kill pid 1); its children get the default handlers back.+#![no_std]+#![no_main]++extern crate alloc;++#[macro_use]+#[path = "../../rbrt/ulib.rs"]+mod ulib;++use ulib::*;++const SHELL: &str = "/bin/rbsh";+const SELFTEST: &str = "/scripts/selftest.sh";+const HVC0: &str = "/dev/hvc0";+/// Signals init ignores (and its children get back as SIG_DFL).+const IGNORED: [i32; 7] = [SIGHUP, SIGINT, SIGQUIT, SIGTERM, SIGTSTP, SIGTTIN, SIGTTOU];+const CLOCK_MONOTONIC: u64 = 1;++fn now_ms() -> u64 {+ let (s, ns) = clock_gettime(CLOCK_MONOTONIC);+ s * 1000 + ns / 1_000_000+}++/// Start `argv` (argv[0] is the path) in a child process. With `tty`, the+/// child gets a session of its own with that terminal as stdin, stdout,+/// stderr and controlling terminal (what getty does); without, it shares+/// init's console. Returns the child's pid, or -errno.+fn start(path: &str, argv: &[&str], tty: Option<&str>) -> i64 {+ let pid = fork();+ if pid != 0 {+ return pid; // init (or -errno if fork failed)+ }+ // The child.+ for sig in IGNORED {+ sigaction_raw(sig, SIG_DFL, 0, 0);+ }+ if let Some(dev) = tty {+ setsid();+ let fd = open(dev, O_RDWR); // becomes the controlling terminal+ if fd < 0 {+ eprintln!("[init] {}: {}", dev, strerror(fd));+ exit(126);+ }+ for target in 0..3 {+ dup2(fd, target);+ }+ if fd > 2 {+ close(fd);+ }+ println!("\nFoundry OS: shell on {} (type `exit` for a new one)", dev);+ }+ let err = execve(path, argv, &environ());+ eprintln!("[init] {}: {}", path, strerror(err));+ exit(127)+}++/// Print the mount table on one line, e.g. "/ ext2, /tmp ramfs, ...".+fn show_mounts() {+ let mut buf = [0u8; 1024];+ let n = mounts(&mut buf);+ if n > 0 {+ let text = core::str::from_utf8(&buf[..n as usize]).unwrap_or("");+ let list: alloc::vec::Vec<&str> = text.lines().filter(|l| !l.is_empty()).collect();+ println!("[init] mounts: {}", list.join(", "));+ }+}++fn exists(path: &str) -> bool {+ let mut st = Stat::default();+ stat(path, &mut st) == 0+}++/// Headless mode: run the self-test and return its exit code.+fn selftest() -> i64 {+ // Settle first, then count: the frames in use now are the baseline.+ let before = sysinfo();+ let shell = start(SHELL, &["rbsh", SELFTEST], None);+ if shell < 0 {+ eprintln!("[init] can't start {}: {}", SHELL, strerror(shell));+ return 1;+ }+ let mut status = -1;+ let mut orphans = 0u64;+ // Wait for the shell, collecting any adopted orphan that ends first.+ loop {+ match wait4(-1, 0) {+ Ok((pid, st)) if pid == shell => {+ status = exit_code(st);+ break;+ }+ Ok(_) => orphans += 1,+ Err(e) if e == -EINTR => continue,+ Err(e) => {+ eprintln!("[init] wait: {}", strerror(e));+ break;+ }+ }+ }+ // Orphans may still be running (programs the self-test left in the+ // background whose parent has gone). Collect them for up to 5 seconds,+ // until only init itself is left.+ let deadline = now_ms() + 5000;+ loop {+ match wait4(-1, WNOHANG) {+ Ok((0, _)) => {}+ Ok(_) => {+ orphans += 1;+ continue;+ }+ Err(e) if e == -EINTR => continue,+ Err(_) => {} // ECHILD: no children left+ }+ let info = sysinfo();+ if (info.processes <= before.processes && info.threads <= before.threads) || now_ms() > deadline {+ break;+ }+ sleep_ms(10);+ }+ let after = sysinfo();+ if after.processes > before.processes {+ println!("[init] {} process(es) still running after 5 s", after.processes - before.processes);+ }+ println!("[init] self-test finished: exit code {}; init reaped {} adopted orphan(s)", status, orphans);+ println!(+ "[init] memory: {} frames in use before the self-test, {} after it and all its children exited",+ before.frames_in_use, after.frames_in_use+ );+ status+}++/// One terminal with a login shell on it.+struct Seat {+ name: &'static str,+ tty: Option<&'static str>,+ pid: i64,+ quick_deaths: u32,+ started_ms: u64,+}++impl Seat {+ fn spawn(&mut self) {+ self.started_ms = now_ms();+ self.pid = start(SHELL, &["rbsh"], self.tty);+ if self.pid < 0 {+ eprintln!("[init] can't start a shell on {}: {}", self.name, strerror(self.pid));+ }+ }+}++/// Normal mode: keep a shell on every terminal, forever.+fn supervise() -> ! {+ let mut seats = alloc::vec![Seat { name: "console", tty: None, pid: -1, quick_deaths: 0, started_ms: 0 }];+ if exists(HVC0) {+ seats.push(Seat { name: "hvc0", tty: Some(HVC0), pid: -1, quick_deaths: 0, started_ms: 0 });+ }+ for seat in seats.iter_mut() {+ seat.spawn();+ }+ loop {+ let (pid, st) = match wait4(-1, 0) {+ Ok(r) => r,+ Err(e) if e == -EINTR => continue,+ Err(_) => {+ sleep_ms(1000); // no children at all (every shell gave up)+ continue;+ }+ };+ // Not one of our shells: an adopted orphan, now reaped. Nothing else to do.+ let Some(seat) = seats.iter_mut().find(|s| s.pid == pid) else { continue };+ let code = exit_code(st);+ if now_ms() - seat.started_ms < 1000 {+ seat.quick_deaths += 1;+ } else {+ seat.quick_deaths = 0;+ }+ if seat.quick_deaths >= 5 || code == 126 || code == 127 {+ println!("[init] the shell on {} keeps failing (exit code {}); not restarting it", seat.name, code);+ seat.pid = -1;+ continue;+ }+ println!("[init] shell on {} exited (code {}); starting a new one", seat.name, code);+ if seat.quick_deaths > 0 {+ sleep_ms(500); // don't spin if it dies at once+ }+ seat.spawn();+ }+}++fn main() -> i64 {+ let headless = arg(1) == Some("selftest") || getenv("RUSTBELT_HEADLESS") == Some("1");+ for sig in IGNORED {+ sigaction_raw(sig, SIG_IGN, 0, 0);+ }+ let mut buf = [0u8; 6 * 65];+ let [_, _, release, _, _, _] = uname(&mut buf);+ println!(+ "[init] Foundry OS init, pid {}, on Rustbelt {}{}",+ getpid(),+ release,+ if headless { " (headless: running the self-test)" } else { "" }+ );+ if getpid() != 1 {+ eprintln!("[init] warning: I am not pid 1, so orphans won't be handed to me");+ }+ show_mounts();+ if headless {+ return selftest();+ }+ let mut motd = [0u8; 512];+ let fd = open("/etc/motd", 0);+ if fd >= 0 {+ let n = read(fd, &mut motd);+ close(fd);+ if n > 0 {+ write(1, &motd[..n as usize]);+ }+ }+ supervise()+}