diff --git a/Cargo.lock b/Cargo.lock index f30cdc3..01b17a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -167,6 +167,7 @@ dependencies = [ "elf", "hw-virtio", "libkos", + "server-framework", "types", ] @@ -312,6 +313,14 @@ dependencies = [ "types", ] +[[package]] +name = "spawntest" +version = "0.1.0" +dependencies = [ + "libkos", + "types", +] + [[package]] name = "storage-qemu" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 2dd7e05..76ed603 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "crates/apps/hello", "crates/apps/echo", "crates/apps/smptest", + "crates/apps/spawntest", "crates/apps/snake", "crates/domain", "crates/elf", diff --git a/Makefile b/Makefile index 10fb754..91a507f 100644 --- a/Makefile +++ b/Makefile @@ -52,7 +52,7 @@ build: cargo build --target $(RISCV_TARGET) --release \ -p adapter-riscv -p init -p echo -p snake \ -p display-qemu -p input-qemu -p storage-qemu \ - -p log-server -p permission-manager -p session-server -p smptest + -p log-server -p permission-manager -p session-server -p smptest -p spawntest -p hello image: build scripts/make-disk-image.sh diff --git a/crates/apps/spawntest/Cargo.toml b/crates/apps/spawntest/Cargo.toml new file mode 100644 index 0000000..74426e5 --- /dev/null +++ b/crates/apps/spawntest/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "spawntest" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "spawntest" +test = false +bench = false + +[dependencies] +types = { workspace = true } +libkos = { workspace = true } diff --git a/crates/apps/spawntest/src/main.rs b/crates/apps/spawntest/src/main.rs new file mode 100644 index 0000000..fc18576 --- /dev/null +++ b/crates/apps/spawntest/src/main.rs @@ -0,0 +1,61 @@ +//! The test image's last check, started by init with a spawn allowance: +//! programs run through init, the allowance holds, and it comes back when +//! a child dies. Reports the whole self-test and halts. + +#![no_std] +#![no_main] + +extern crate libkos; + +use libkos::spawn::{SpawnError, spawn}; +use libkos::syscall; +use types::bounded::CapSlot; +use types::budget::Limits; + +/// Where init put them (see init's manifest: spawn endpoint, then control). +const INIT: CapSlot = CapSlot::const_new(0); +const CONTROL: CapSlot = CapSlot::const_new(1); + +/// Over half the allowance (128 pages), so a second one fits only after +/// the first has died and its limits came back. +const HELLO: Limits = Limits { pages: 80, endpoints: 0, notifications: 0, regions: 0 }; + +/// Spawns hello, runs it, and waits for it to die. +fn run_hello(died: CapSlot) -> Result<(), &'static str> { + let child = spawn(INIT, b"hello.elf", HELLO).map_err(|_| "init would not spawn hello")?; + syscall::watch_death(child.process, died).map_err(|_| "cannot watch hello")?; + if !child.start() { + return Err("cannot start hello"); + } + syscall::wait(died).map_err(|_| "wait failed")?; + Ok(()) +} + +fn check() -> Result<(), &'static str> { + let died = syscall::create_notification().map_err(|_| "no notification")?; + run_hello(died)?; + libkos::println!("spawntest: spawned program runs and its death is heard: ok"); + + let greedy = Limits { pages: 200, ..HELLO }; + if spawn(INIT, b"hello.elf", greedy).err() != Some(SpawnError::OverAllowance) { + return Err("init spawned past the allowance"); + } + let tiny = Limits { pages: 1, ..HELLO }; + if spawn(INIT, b"nothere.elf", tiny).err() != Some(SpawnError::NotFound) { + return Err("a missing program was not reported"); + } + libkos::println!("spawntest: allowance and missing files refused: ok"); + + run_hello(died)?; + libkos::println!("spawntest: allowance comes back when a child dies: ok"); + Ok(()) +} + +#[unsafe(no_mangle)] +extern "C" fn main() { + match check() { + Ok(()) => libkos::println!("SMPTEST PASS"), + Err(why) => libkos::println!("SMPTEST FAIL: {}", why), + } + syscall::halt(CONTROL); +} diff --git a/crates/domain/src/syscall.rs b/crates/domain/src/syscall.rs index 8b3d1b7..3cfcf4d 100644 --- a/crates/domain/src/syscall.rs +++ b/crates/domain/src/syscall.rs @@ -93,7 +93,7 @@ pub enum Syscall { region: CapSlot, }, /// Signals `notification` when the process behind `process` dies, by - /// exit, fault or kill. One watcher per process; a new one replaces it. + /// exit, fault or kill. Up to `timer::MAX_WATCHERS` per process. WatchDeath { process: CapSlot, notification: CapSlot, diff --git a/crates/domain/src/timer.rs b/crates/domain/src/timer.rs index cce8731..b11395a 100644 --- a/crates/domain/src/timer.rs +++ b/crates/domain/src/timer.rs @@ -18,6 +18,9 @@ pub struct Target { pub badge: Badge, } +/// How many may watch one process die: its parent and init, with room. +pub const MAX_WATCHERS: usize = 4; + pub struct Timers { deadlines: [Option<(Ticks, Target)>; MAX_PROCESSES], } diff --git a/crates/init/Cargo.toml b/crates/init/Cargo.toml index aebe84e..21fb140 100644 --- a/crates/init/Cargo.toml +++ b/crates/init/Cargo.toml @@ -14,3 +14,4 @@ hw-virtio = { workspace = true } types = { workspace = true } libkos = { workspace = true } +server-framework = { workspace = true } diff --git a/crates/init/src/launch.rs b/crates/init/src/launch.rs index 7e2e794..d549b96 100644 --- a/crates/init/src/launch.rs +++ b/crates/init/src/launch.rs @@ -5,12 +5,14 @@ use libkos::device::Device as BoardDevice; use libkos::fs::Storage; use libkos::syscall; use types::address::StackPointer; +use types::badge::Badge; use types::bounded::CapSlot; use types::permission::{Permission, PermissionSet}; use crate::devices::Devices; use crate::load::{self, LoadedProcess}; use crate::manifest::{Device, Program}; +use crate::spawn::Spawner; const USER_STACK_TOP: StackPointer = StackPointer::from_raw(0x10000); const MAX_SERVED: usize = 8; @@ -25,11 +27,22 @@ pub(crate) struct Launcher<'a> { devices: Devices, /// The endpoint of every server started so far, by name. served: [Option<(&'static str, CapSlot)>; MAX_SERVED], + /// Init's own endpoint, for the spawn service. + pub(crate) spawn_ep: CapSlot, + /// Bound to init's thread: spawned children's deaths, a bit each. + pub(crate) deaths: CapSlot, + pub(crate) spawner: Spawner, } impl<'a> Launcher<'a> { - pub(crate) fn new(storage: &'a mut Storage, scratch: &'a mut [u8], devices: Devices) -> Self { - Self { storage, scratch, devices, served: [None; MAX_SERVED] } + pub(crate) fn new( + storage: &'a mut Storage, + scratch: &'a mut [u8], + devices: Devices, + ) -> Result { + let spawn_ep = syscall::create_endpoint().ok_or("no spawn endpoint")?; + let deaths = syscall::create_notification().map_err(|_| "no death notification")?; + Ok(Self { storage, scratch, devices, served: [None; MAX_SERVED], spawn_ep, deaths, spawner: Spawner::new() }) } pub(crate) fn storage(&mut self) -> (&mut Storage, &mut [u8]) { @@ -61,6 +74,13 @@ impl<'a> Launcher<'a> { let ep = self.endpoint(name).ok_or("needs a server that is not running")?; syscall::delegate(ep, child, perms(&[Permission::Execute])).ok_or("cannot give an endpoint")?; } + if let Some(allowance) = p.spawns { + self.give_spawn(child, allowance)?; + } + if p.system_control { + let control = crate::devices::system_control().ok_or("no SystemControl to give")?; + syscall::delegate(control, child, perms(&[Permission::Execute])).ok_or("cannot give SystemControl")?; + } let console = libkos::start::console() .filter(|_| p.console) .and_then(|c| syscall::delegate(c, child, perms(&[Permission::Write]))); @@ -82,6 +102,17 @@ impl<'a> Launcher<'a> { Ok(note) } + /// A badged copy of init's endpoint: the badge names the client and so + /// its allowance. + fn give_spawn(&mut self, child: CapSlot, allowance: types::budget::Limits) -> Result<(), &'static str> { + let badge: Badge = self.spawner.add_client(allowance).ok_or("too many spawn clients")?; + let minted = perms(&[Permission::Execute, Permission::Delegate]); + let copy = syscall::mint(self.spawn_ep, badge, minted).map_err(|_| "cannot mint")?; + let given = syscall::delegate(copy, child, perms(&[Permission::Execute])); + syscall::drop_cap(copy); + given.map(|_| ()).ok_or("cannot give the spawn endpoint") + } + /// The driver waits for its device before serving, and runs without /// one (headless) if the board has none. fn give(&self, device: Device, ep: CapSlot) -> &'static str { diff --git a/crates/init/src/lib.rs b/crates/init/src/lib.rs index 628f557..cc5e9df 100644 --- a/crates/init/src/lib.rs +++ b/crates/init/src/lib.rs @@ -7,6 +7,7 @@ mod launch; mod load; mod manifest; mod selftest; +mod spawn; use libkos::fs::Storage; use libkos::syscall; @@ -39,21 +40,31 @@ pub fn run() -> ! { idle(); }; - let mut launcher = launch::Launcher::new(&mut storage, scratch, found); + let mut launcher = match launch::Launcher::new(&mut storage, scratch, found) { + Ok(l) => l, + Err(why) => { + libkos::println!("init: {}", why); + idle(); + } + }; launch_all(&mut launcher, manifest::SERVERS); give_storage_the_log(); - // The test image carries the SMP self-test; it runs instead of the - // desktop and halts the machine. + // The test image carries the SMP self-test; it and the tests run + // instead of the desktop, and the last one halts the machine. let (storage, scratch) = launcher.storage(); if storage.open(b"smptest.elf").is_ok_and(|h| storage.close(h).is_ok()) { libkos::println!("init: running the SMP self-test"); selftest::run(storage, scratch); + launch_all(&mut launcher, manifest::TESTS); + } else { + launch_all(&mut launcher, manifest::APPS); } - launch_all(&mut launcher, manifest::APPS); - libkos::println!("init: all loaded, supervising"); - idle(); + libkos::println!("init: all loaded, serving spawns"); + let _ = syscall::bind_notification(launcher.deaths); + let ep = launcher.spawn_ep; + server_framework::serve(&mut launcher, ep) } fn launch_all(launcher: &mut launch::Launcher, programs: &[manifest::Program]) { diff --git a/crates/init/src/manifest.rs b/crates/init/src/manifest.rs index 4807d48..93c3c5d 100644 --- a/crates/init/src/manifest.rs +++ b/crates/init/src/manifest.rs @@ -20,6 +20,10 @@ pub struct Program { pub uses: &'static [&'static str], /// Takes the console over from init; everyone else prints through it. pub console: bool, + /// May have init spawn programs, whose limits together stay within this. + pub spawns: Option, + /// May halt the machine. + pub system_control: bool, } /// Enough for a small program: its image, stack and a little heap. @@ -31,7 +35,17 @@ pub const SMALL: Limits = Limits { }; const fn server(name: &'static str, file: &'static [u8], limits: Limits) -> Program { - Program { name, file, limits, serves: true, device: None, uses: &[], console: false } + Program { + name, + file, + limits, + serves: true, + device: None, + uses: &[], + console: false, + spawns: None, + system_control: false, + } } /// Started on every boot. The log server comes first so the rest can print. @@ -47,6 +61,15 @@ pub const SERVERS: &[Program] = &[ server("session", b"session.elf", SMALL), ]; +/// Run on the test image after the SMP self-test, instead of the desktop: +/// spawns through init, then reports and halts. +pub const TESTS: &[Program] = &[Program { + serves: false, + spawns: Some(Limits { pages: 128, endpoints: 2, notifications: 2, regions: 2 }), + system_control: true, + ..server("spawntest", b"spawntst.elf", SMALL) +}]; + /// The desktop; the test image runs the SMP self-test instead. pub const APPS: &[Program] = &[Program { serves: false, diff --git a/crates/init/src/selftest.rs b/crates/init/src/selftest.rs index 63b1fcf..f60eb6e 100644 --- a/crates/init/src/selftest.rs +++ b/crates/init/src/selftest.rs @@ -121,12 +121,13 @@ fn timer_is_on_time(events: CapSlot) -> Result<(), &'static str> { Ok(()) } -/// Runs the test, reports, and halts the machine. -pub fn run(storage: &mut Storage, scratch: &mut [u8]) -> ! { - match run_phases(storage, scratch) { - Ok(()) => libkos::println!("SMPTEST PASS"), - Err(why) => libkos::println!("SMPTEST FAIL: {}", why), - } +/// Runs the hart checks; on failure reports and halts the machine, else +/// returns so the spawn test can run (and report the whole result). +pub fn run(storage: &mut Storage, scratch: &mut [u8]) { + let Err(why) = run_phases(storage, scratch) else { + return; + }; + libkos::println!("SMPTEST FAIL: {}", why); match crate::devices::system_control() { Some(control) => syscall::halt(control), None => { diff --git a/crates/init/src/spawn.rs b/crates/init/src/spawn.rs new file mode 100644 index 0000000..e9e4e0e --- /dev/null +++ b/crates/init/src/spawn.rs @@ -0,0 +1,178 @@ +//! Init's spawn service (`types::spawn`): loads a program for a client and +//! hands it the process, unstarted. Each client has an allowance from the +//! manifest; a child's limits come out of it and return when the child dies. + +use libkos::syscall; +use server_framework::{Decode, Encode, MALFORMED, Server}; +use types::badge::Badge; +use types::bounded::CapSlot; +use types::budget::Limits; +use types::permission::{Permission, PermissionSet}; +use types::spawn::{MAX_NAME, NONE, reply}; + +use crate::launch::Launcher; +use crate::load; + +pub(crate) const MAX_CLIENTS: usize = 4; +/// Death signals carry the child's index as a bit. +const MAX_CHILDREN: usize = 8; + +struct Child { + client: usize, + limits: Limits, + process: CapSlot, +} + +pub(crate) struct Spawner { + /// What each client (badge = index + 1) may still give its children. + allowances: [Option; MAX_CLIENTS], + children: [Option; MAX_CHILDREN], +} + +fn fits(want: Limits, left: Limits) -> bool { + want.pages <= left.pages + && want.endpoints <= left.endpoints + && want.notifications <= left.notifications + && want.regions <= left.regions +} + +fn add(a: Limits, b: Limits) -> Limits { + Limits { + pages: a.pages + b.pages, + endpoints: a.endpoints + b.endpoints, + notifications: a.notifications + b.notifications, + regions: a.regions + b.regions, + } +} + +fn sub(a: Limits, b: Limits) -> Limits { + Limits { + pages: a.pages - b.pages, + endpoints: a.endpoints - b.endpoints, + notifications: a.notifications - b.notifications, + regions: a.regions - b.regions, + } +} + +impl Spawner { + pub(crate) const fn new() -> Self { + Self { allowances: [None; MAX_CLIENTS], children: [const { None }; MAX_CHILDREN] } + } + + /// The badge for a new client with `allowance`. + pub(crate) fn add_client(&mut self, allowance: Limits) -> Option { + let i = self.allowances.iter().position(Option::is_none)?; + self.allowances[i] = Some(allowance); + Some(Badge::new(i + 1)) + } +} + +pub(crate) struct Request { + name: [u8; MAX_NAME], + len: usize, + limits: Limits, +} + +impl Decode for Request { + fn decode(words: [usize; 4]) -> Option { + types::spawn::parse(words).map(|(name, len, limits)| Self { name, len, limits }) + } +} + +pub(crate) struct Reply([usize; 4]); + +impl Encode for Reply { + fn encode(&self) -> [usize; 4] { + self.0 + } +} + +fn refused(tag: usize) -> ([usize; 4], Option) { + ([tag, 0, 0, 0], None) +} + +impl Launcher<'_> { + fn spawn(&mut self, badge: usize, r: &Request) -> ([usize; 4], Option) { + let Some(client) = badge.checked_sub(1).filter(|c| *c < MAX_CLIENTS) else { + return refused(reply::ERROR); + }; + let Some(left) = self.spawner.allowances[client] else { + return refused(reply::ERROR); + }; + if !fits(r.limits, left) { + return refused(reply::OVER_ALLOWANCE); + } + let Some(index) = self.spawner.children.iter().position(Option::is_none) else { + return refused(reply::ERROR); + }; + let name = &r.name[..r.len]; + let (storage, scratch) = self.storage(); + match storage.open(name) { + Ok(h) => { + let _ = storage.close(h); + } + Err(_) => return refused(reply::NOT_FOUND), + } + let Some(loaded) = load::load_binary(storage, scratch, name, r.limits) else { + return refused(reply::ERROR); + }; + if !self.watch(loaded.process_cap, index) { + let _ = syscall::kill(loaded.process_cap); + syscall::drop_cap(loaded.process_cap); + return refused(reply::ERROR); + } + let call_only = PermissionSet::empty().with(Permission::Execute); + let log = libkos::start::log().and_then(|l| syscall::delegate(l, loaded.process_cap, call_only)); + self.spawner.allowances[client] = Some(sub(left, r.limits)); + self.spawner.children[index] = Some(Child { client, limits: r.limits, process: loaded.process_cap }); + let log = log.map_or(NONE, |s| s.value() as usize); + ([reply::OK, loaded.entry_point.raw(), log, 0], Some(loaded.process_cap)) + } + + /// Has the child's death signal bit `index` on init's death notification. + fn watch(&self, process: CapSlot, index: usize) -> bool { + let signal = PermissionSet::empty().with(Permission::Write); + let Ok(copy) = syscall::mint(self.deaths, Badge::new(1 << index), signal) else { + return false; + }; + let watched = syscall::watch_death(process, copy).is_ok(); + syscall::drop_cap(copy); + watched + } + + /// Children behind `bits` died: their limits go back to their clients. + fn reap(&mut self, bits: usize) { + for (i, slot) in self.spawner.children.iter_mut().enumerate() { + if bits & (1 << i) == 0 { + continue; + } + if let Some(child) = slot.take() { + if let Some(Some(left)) = self.spawner.allowances.get_mut(child.client) { + *left = add(*left, child.limits); + } + syscall::drop_cap(child.process); + } + } + } +} + +impl Server for Launcher<'_> { + type Request = Request; + type Response = Reply; + + /// Without a badge there is no client, so no allowance. + fn handle(&mut self, _request: Request) -> Reply { + Reply(refused(reply::ERROR).0) + } + + fn handle_call(&mut self, badge: usize, words: [usize; 4], _cap: Option) -> ([usize; 4], Option) { + match Request::decode(words) { + Some(r) => self.spawn(badge, &r), + None => ([MALFORMED, 0, 0, 0], None), + } + } + + fn signal(&mut self, bits: usize) { + self.reap(bits); + } +} diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index 3980a74..af3ff36 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -78,7 +78,7 @@ pub struct Kernel { /// Where each process's next heap search starts (see memory.rs). heap_cursors: [usize; MAX_PROCESSES], /// Who to signal when each process dies (see syscall/ipc/events.rs). - death_watchers: [Option; MAX_PROCESSES], + death_watchers: [[Option; domain::timer::MAX_WATCHERS]; MAX_PROCESSES], timers: domain::timer::Timers, } @@ -102,7 +102,7 @@ impl Kernel

{ homes: [types::id::CoreId::from_raw(0); MAX_THREADS], tree: domain::capability::tree::CapTree::new(), heap_cursors: [0; MAX_PROCESSES], - death_watchers: [None; MAX_PROCESSES], + death_watchers: [[None; domain::timer::MAX_WATCHERS]; MAX_PROCESSES], timers: domain::timer::Timers::new(), } } @@ -138,7 +138,8 @@ impl Kernel

{ addr_of_mut!((*slot).homes).write([types::id::CoreId::from_raw(0); MAX_THREADS]); addr_of_mut!((*slot).tree).write(domain::capability::tree::CapTree::new()); addr_of_mut!((*slot).heap_cursors).write([0; MAX_PROCESSES]); - addr_of_mut!((*slot).death_watchers).write([None; MAX_PROCESSES]); + addr_of_mut!((*slot).death_watchers) + .write([[None; domain::timer::MAX_WATCHERS]; MAX_PROCESSES]); addr_of_mut!((*slot).timers).write(domain::timer::Timers::new()); } } diff --git a/crates/kernel/src/syscall/ipc/events.rs b/crates/kernel/src/syscall/ipc/events.rs index b05915a..2b2d652 100644 --- a/crates/kernel/src/syscall/ipc/events.rs +++ b/crates/kernel/src/syscall/ipc/events.rs @@ -40,22 +40,27 @@ impl Kernel

{ _ => return Err(KernelError::WrongObject), }; let target = self.target(pid, notification)?; - let slot = self + let watchers = self .death_watchers .get_mut(watched.raw() as usize) .ok_or(KernelError::InvalidProcess(watched))?; + let slot = watchers + .iter_mut() + .find(|w| w.is_none_or(|w| w == target)) + .ok_or(KernelError::NoFreeSlot)?; *slot = Some(target); Ok(()) } - /// A process is dying: tell its watcher, forget its own timer. + /// A process is dying: tell its watchers, forget its own timer. pub(crate) fn report_death(&mut self, pid: ProcessId) { self.timers.clear(pid); - if let Some(target) = self + let watchers = self .death_watchers .get_mut(pid.raw() as usize) - .and_then(Option::take) - { + .map(core::mem::take) + .unwrap_or_default(); + for target in watchers.into_iter().flatten() { self.fire(target); } } diff --git a/crates/kernel/src/tests/events.rs b/crates/kernel/src/tests/events.rs index c15c069..e48be5d 100644 --- a/crates/kernel/src/tests/events.rs +++ b/crates/kernel/src/tests/events.rs @@ -161,3 +161,21 @@ fn notification_id( _ => panic!("not a notification"), } } + +#[test] +fn every_watcher_hears_and_there_is_room_for_a_few() { + let (mut k, init, proc_cap, first) = watching(); + let second = k.create_notification_cap(init).unwrap(); + k.watch_death(init, proc_cap, second).unwrap(); + // The same notification again takes no new place. + k.watch_death(init, proc_cap, second).unwrap(); + for _ in 2..domain::timer::MAX_WATCHERS { + let n = k.create_notification_cap(init).unwrap(); + k.watch_death(init, proc_cap, n).unwrap(); + } + let extra = k.create_notification_cap(init).unwrap(); + assert!(k.watch_death(init, proc_cap, extra).is_err()); + k.handle_syscall(Syscall::Kill { process: proc_cap }); + assert_eq!(k.poll(init, first).unwrap(), 1); + assert_eq!(k.poll(init, second).unwrap(), 1); +} diff --git a/crates/libkos/src/lib.rs b/crates/libkos/src/lib.rs index f783b68..9608038 100644 --- a/crates/libkos/src/lib.rs +++ b/crates/libkos/src/lib.rs @@ -10,6 +10,7 @@ pub mod fs; pub mod input; pub mod io; mod rt; +pub mod spawn; pub mod start; pub mod syscall; diff --git a/crates/libkos/src/spawn.rs b/crates/libkos/src/spawn.rs new file mode 100644 index 0000000..75c9a68 --- /dev/null +++ b/crates/libkos/src/spawn.rs @@ -0,0 +1,53 @@ +//! Running a program through init (protocol: `types::spawn`). Init loads it +//! and gives it a log endpoint; the caller hands it any other capabilities +//! (`delegate` to the process) and starts it. + +use crate::syscall; +use types::address::{EntryPoint, StackPointer}; +use types::bounded::CapSlot; +use types::budget::Limits; +use types::spawn::{STACK_TOP, reply, request}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SpawnError { + NotFound, + /// More than what is left of this process's spawn allowance. + OverAllowance, + Failed, +} + +/// A loaded program, not yet running. +pub struct Spawned { + pub process: CapSlot, + entry: EntryPoint, + log: Option, +} + +pub fn spawn(init: CapSlot, name: &[u8], limits: Limits) -> Result { + let words = request(name, limits).ok_or(SpawnError::Failed)?; + let reply = syscall::call(init, words, None).map_err(|_| SpawnError::Failed)?; + let [tag, entry, log, _] = reply.words(); + let cap = match reply { + syscall::Received::Message { cap, .. } | syscall::Received::Call { cap, .. } => cap, + syscall::Received::Signal { .. } => None, + }; + match (tag, cap) { + (reply::OK, Some(process)) => Ok(Spawned { + process, + entry: EntryPoint::from_raw(entry), + log: u8::try_from(log).ok().and_then(CapSlot::new), + }), + (reply::NOT_FOUND, _) => Err(SpawnError::NotFound), + (reply::OVER_ALLOWANCE, _) => Err(SpawnError::OverAllowance), + _ => Err(SpawnError::Failed), + } +} + +impl Spawned { + /// Runs the program, telling it where its log endpoint is. + pub fn start(&self) -> bool { + let arg = types::start_arg::encode(None, self.log); + let stack = StackPointer::from_raw(STACK_TOP); + syscall::spawn_thread_in(self.process, self.entry, stack, arg) != usize::MAX + } +} diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 682f4dd..fc982e2 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -14,6 +14,7 @@ pub mod mapping; pub mod permission; #[cfg(kani)] pub mod proofs; +pub mod spawn; pub mod start_arg; pub mod storage; pub mod syscall_abi; diff --git a/crates/types/src/spawn.rs b/crates/types/src/spawn.rs new file mode 100644 index 0000000..1e2022c --- /dev/null +++ b/crates/types/src/spawn.rs @@ -0,0 +1,110 @@ +//! Init's spawn protocol, shared by init and `libkos::spawn`: a client asks +//! init to load a program into a new, not yet running process; init replies +//! with the process capability and the client wires and starts it. +//! +//! Request: `[SPAWN | name_len << 8, packed limits, name bytes 0..8, 8..16]`. +//! Reply: `[reply::OK, entry point, child's log slot or NONE, 0]` carrying +//! the process capability, or `[error tag, 0, 0, 0]`. + +use crate::budget::Limits; + +pub const SPAWN: usize = 0; +/// Program file names: FAT 8.3, with room to spare. +pub const MAX_NAME: usize = 16; +/// Where init puts the child's stack top (its stack ends at the image). +pub const STACK_TOP: usize = 0x10000; +/// A slot word meaning "none". +pub const NONE: usize = usize::MAX; + +pub mod reply { + pub const OK: usize = 0; + pub const NOT_FOUND: usize = 1; + /// The limits do not fit in what is left of the client's allowance. + pub const OVER_ALLOWANCE: usize = 2; + pub const ERROR: usize = 3; +} + +/// Pages in bits 0..32, endpoints, notifications and regions a byte each +/// above; `None` if a part does not fit. +fn pack(l: Limits) -> Option { + let byte = |v: usize| u8::try_from(v).ok().map(usize::from); + let pages = u32::try_from(l.pages).ok()? as u64; + let rest = (byte(l.endpoints)? | byte(l.notifications)? << 8 | byte(l.regions)? << 16) as u64; + usize::try_from(pages | rest << 32).ok() +} + +fn unpack(w: usize) -> Limits { + let w = w as u64; + let byte = |shift: u32| ((w >> shift) & 0xFF) as usize; + Limits { + pages: (w & 0xFFFF_FFFF) as usize, + endpoints: byte(32), + notifications: byte(40), + regions: byte(48), + } +} + +pub fn request(name: &[u8], limits: Limits) -> Option<[usize; 4]> { + if name.is_empty() || name.len() > MAX_NAME { + return None; + } + let mut bytes = [0u8; MAX_NAME]; + bytes[..name.len()].copy_from_slice(name); + let (lo, hi) = bytes.as_chunks::<8>().0.split_at(1); + Some([ + SPAWN | name.len() << 8, + pack(limits)?, + u64::from_le_bytes(lo[0]) as usize, + u64::from_le_bytes(hi[0]) as usize, + ]) +} + +/// The name (and how much of it counts) and limits of a spawn request. +pub fn parse(words: [usize; 4]) -> Option<([u8; MAX_NAME], usize, Limits)> { + let len = words[0] >> 8; + if words[0] & 0xFF != SPAWN || len == 0 || len > MAX_NAME { + return None; + } + let mut name = [0u8; MAX_NAME]; + for (b, w) in name.as_chunks_mut::<8>().0.iter_mut().zip(&words[2..]) { + *b = (*w as u64).to_le_bytes(); + } + Some((name, len, unpack(words[1]))) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn requests_round_trip() { + let l = Limits { + pages: 4096, + endpoints: 2, + notifications: 3, + regions: 255, + }; + let (name, len, back) = parse(request(b"hello.elf", l).unwrap()).unwrap(); + assert_eq!(&name[..len], b"hello.elf"); + assert_eq!(back, l); + } + + #[test] + fn what_cannot_be_said_is_refused() { + let l = Limits::default(); + assert!(request(b"", l).is_none()); + assert!(request(&[b'a'; 17], l).is_none()); + assert!( + request( + b"x", + Limits { + endpoints: 256, + ..l + } + ) + .is_none() + ); + assert!(parse([SPAWN | 17 << 8, 0, 0, 0]).is_none()); + assert!(parse([1 | 3 << 8, 0, 0, 0]).is_none()); + } +} diff --git a/docs/ipc.md b/docs/ipc.md index 1a8f28c..2faf859 100644 --- a/docs/ipc.md +++ b/docs/ipc.md @@ -84,6 +84,18 @@ and budget listed there. Storage, started before the log server, receives its log endpoint from init later (`SET_LOG`, accepted only from the unbadged holder of its endpoint). +Only init holds `ProcessCreate`. Everyone else runs programs through init's +spawn service (`types::spawn`, client `libkos::spawn`), which init serves +once boot is done. A manifest entry with `spawns` gets a badged copy of +init's endpoint, the badge naming its allowance. `SPAWN(name, limits)` loads +the program into a new, unstarted process, gives it a call-only log +endpoint, and replies with the process capability (Execute and Read, no +Delegate). The caller delegates whatever else the child should hold and +starts it with `SpawnThreadIn`. The child's limits come out of the caller's +allowance (`OVER_ALLOWANCE` if they do not fit) and go back when init hears +the child die (`WatchDeath`, up to four watchers per process, so the caller +can watch too). + Device regions never overlap each other or the frame pool, and each device has exactly one region, so delegating or revoking it means the same thing everywhere. diff --git a/scripts/make-disk-image.sh b/scripts/make-disk-image.sh index 8878d2b..5288829 100755 --- a/scripts/make-disk-image.sh +++ b/scripts/make-disk-image.sh @@ -33,6 +33,8 @@ copy session-server "session.elf" # Only the test image: its presence makes init run the SMP self-test. if [ -n "${SMPTEST:-}" ]; then copy smptest "smptest.elf" + copy spawntest "spawntst.elf" + copy hello "hello.elf" fi echo "" diff --git a/scripts/test-qemu.sh b/scripts/test-qemu.sh index bb44d1b..1d67706 100755 --- a/scripts/test-qemu.sh +++ b/scripts/test-qemu.sh @@ -1,6 +1,7 @@ #!/bin/bash -# Boots IMAGE headless and waits for init's SMP self-test to report; the -# machine halts itself when done. Exit 0 only on "SMPTEST PASS". +# Boots IMAGE headless and waits for the self-test (init's SMP checks, then +# spawntest) to report; the machine halts itself when done. Exit 0 only on +# "SMPTEST PASS". set -uo pipefail IMAGE="${1:?usage: test-qemu.sh IMAGE}" @@ -15,7 +16,7 @@ timeout ${QT:-120} qemu-system-riscv64 -machine virt -smp "${SMP}" -bios default -display none -serial "file:${LOG}" -monitor none status=$? -grep -a "smptest:\|SMPTEST" "${LOG}" +grep -a "smptest:\|spawntest:\|Hello\|SMPTEST" "${LOG}" if grep -aq "SMPTEST PASS" "${LOG}"; then echo "test-qemu: pass (${SMP} harts)" rm -f "${LOG}"