feat(init): spawn service over IPC with per-client allowance refunded on child death; up to 4 death watchers; spawntest on test image

This commit is contained in:
2026-10-27 16:21:08 +01:00
parent 817a38993b
commit 8587661684
23 changed files with 564 additions and 28 deletions

9
Cargo.lock generated
View File

@@ -167,6 +167,7 @@ dependencies = [
"elf", "elf",
"hw-virtio", "hw-virtio",
"libkos", "libkos",
"server-framework",
"types", "types",
] ]
@@ -312,6 +313,14 @@ dependencies = [
"types", "types",
] ]
[[package]]
name = "spawntest"
version = "0.1.0"
dependencies = [
"libkos",
"types",
]
[[package]] [[package]]
name = "storage-qemu" name = "storage-qemu"
version = "0.1.0" version = "0.1.0"

View File

@@ -4,6 +4,7 @@ members = [
"crates/apps/hello", "crates/apps/hello",
"crates/apps/echo", "crates/apps/echo",
"crates/apps/smptest", "crates/apps/smptest",
"crates/apps/spawntest",
"crates/apps/snake", "crates/apps/snake",
"crates/domain", "crates/domain",
"crates/elf", "crates/elf",

View File

@@ -52,7 +52,7 @@ build:
cargo build --target $(RISCV_TARGET) --release \ cargo build --target $(RISCV_TARGET) --release \
-p adapter-riscv -p init -p echo -p snake \ -p adapter-riscv -p init -p echo -p snake \
-p display-qemu -p input-qemu -p storage-qemu \ -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 image: build
scripts/make-disk-image.sh scripts/make-disk-image.sh

View File

@@ -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 }

View File

@@ -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);
}

View File

@@ -93,7 +93,7 @@ pub enum Syscall {
region: CapSlot, region: CapSlot,
}, },
/// Signals `notification` when the process behind `process` dies, by /// 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 { WatchDeath {
process: CapSlot, process: CapSlot,
notification: CapSlot, notification: CapSlot,

View File

@@ -18,6 +18,9 @@ pub struct Target {
pub badge: Badge, 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 { pub struct Timers {
deadlines: [Option<(Ticks, Target)>; MAX_PROCESSES], deadlines: [Option<(Ticks, Target)>; MAX_PROCESSES],
} }

View File

@@ -14,3 +14,4 @@ hw-virtio = { workspace = true }
types = { workspace = true } types = { workspace = true }
libkos = { workspace = true } libkos = { workspace = true }
server-framework = { workspace = true }

View File

@@ -5,12 +5,14 @@ use libkos::device::Device as BoardDevice;
use libkos::fs::Storage; use libkos::fs::Storage;
use libkos::syscall; use libkos::syscall;
use types::address::StackPointer; use types::address::StackPointer;
use types::badge::Badge;
use types::bounded::CapSlot; use types::bounded::CapSlot;
use types::permission::{Permission, PermissionSet}; use types::permission::{Permission, PermissionSet};
use crate::devices::Devices; use crate::devices::Devices;
use crate::load::{self, LoadedProcess}; use crate::load::{self, LoadedProcess};
use crate::manifest::{Device, Program}; use crate::manifest::{Device, Program};
use crate::spawn::Spawner;
const USER_STACK_TOP: StackPointer = StackPointer::from_raw(0x10000); const USER_STACK_TOP: StackPointer = StackPointer::from_raw(0x10000);
const MAX_SERVED: usize = 8; const MAX_SERVED: usize = 8;
@@ -25,11 +27,22 @@ pub(crate) struct Launcher<'a> {
devices: Devices, devices: Devices,
/// The endpoint of every server started so far, by name. /// The endpoint of every server started so far, by name.
served: [Option<(&'static str, CapSlot)>; MAX_SERVED], 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> { impl<'a> Launcher<'a> {
pub(crate) fn new(storage: &'a mut Storage, scratch: &'a mut [u8], devices: Devices) -> Self { pub(crate) fn new(
Self { storage, scratch, devices, served: [None; MAX_SERVED] } storage: &'a mut Storage,
scratch: &'a mut [u8],
devices: Devices,
) -> Result<Self, &'static str> {
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]) { 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")?; 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")?; 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() let console = libkos::start::console()
.filter(|_| p.console) .filter(|_| p.console)
.and_then(|c| syscall::delegate(c, child, perms(&[Permission::Write]))); .and_then(|c| syscall::delegate(c, child, perms(&[Permission::Write])));
@@ -82,6 +102,17 @@ impl<'a> Launcher<'a> {
Ok(note) 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 /// The driver waits for its device before serving, and runs without
/// one (headless) if the board has none. /// one (headless) if the board has none.
fn give(&self, device: Device, ep: CapSlot) -> &'static str { fn give(&self, device: Device, ep: CapSlot) -> &'static str {

View File

@@ -7,6 +7,7 @@ mod launch;
mod load; mod load;
mod manifest; mod manifest;
mod selftest; mod selftest;
mod spawn;
use libkos::fs::Storage; use libkos::fs::Storage;
use libkos::syscall; use libkos::syscall;
@@ -39,21 +40,31 @@ pub fn run() -> ! {
idle(); 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); launch_all(&mut launcher, manifest::SERVERS);
give_storage_the_log(); give_storage_the_log();
// The test image carries the SMP self-test; it runs instead of the // The test image carries the SMP self-test; it and the tests run
// desktop and halts the machine. // instead of the desktop, and the last one halts the machine.
let (storage, scratch) = launcher.storage(); let (storage, scratch) = launcher.storage();
if storage.open(b"smptest.elf").is_ok_and(|h| storage.close(h).is_ok()) { if storage.open(b"smptest.elf").is_ok_and(|h| storage.close(h).is_ok()) {
libkos::println!("init: running the SMP self-test"); libkos::println!("init: running the SMP self-test");
selftest::run(storage, scratch); 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"); libkos::println!("init: all loaded, serving spawns");
idle(); 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]) { fn launch_all(launcher: &mut launch::Launcher, programs: &[manifest::Program]) {

View File

@@ -20,6 +20,10 @@ pub struct Program {
pub uses: &'static [&'static str], pub uses: &'static [&'static str],
/// Takes the console over from init; everyone else prints through it. /// Takes the console over from init; everyone else prints through it.
pub console: bool, pub console: bool,
/// May have init spawn programs, whose limits together stay within this.
pub spawns: Option<Limits>,
/// May halt the machine.
pub system_control: bool,
} }
/// Enough for a small program: its image, stack and a little heap. /// 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 { 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. /// 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), 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. /// The desktop; the test image runs the SMP self-test instead.
pub const APPS: &[Program] = &[Program { pub const APPS: &[Program] = &[Program {
serves: false, serves: false,

View File

@@ -121,12 +121,13 @@ fn timer_is_on_time(events: CapSlot) -> Result<(), &'static str> {
Ok(()) Ok(())
} }
/// Runs the test, reports, and halts the machine. /// Runs the hart checks; on failure reports and halts the machine, else
pub fn run(storage: &mut Storage, scratch: &mut [u8]) -> ! { /// returns so the spawn test can run (and report the whole result).
match run_phases(storage, scratch) { pub fn run(storage: &mut Storage, scratch: &mut [u8]) {
Ok(()) => libkos::println!("SMPTEST PASS"), let Err(why) = run_phases(storage, scratch) else {
Err(why) => libkos::println!("SMPTEST FAIL: {}", why), return;
} };
libkos::println!("SMPTEST FAIL: {}", why);
match crate::devices::system_control() { match crate::devices::system_control() {
Some(control) => syscall::halt(control), Some(control) => syscall::halt(control),
None => { None => {

178
crates/init/src/spawn.rs Normal file
View File

@@ -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<Limits>; MAX_CLIENTS],
children: [Option<Child>; 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<Badge> {
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<Self> {
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<CapSlot>) {
([tag, 0, 0, 0], None)
}
impl Launcher<'_> {
fn spawn(&mut self, badge: usize, r: &Request) -> ([usize; 4], Option<CapSlot>) {
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<CapSlot>) -> ([usize; 4], Option<CapSlot>) {
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);
}
}

View File

@@ -78,7 +78,7 @@ pub struct Kernel<P: Platform> {
/// Where each process's next heap search starts (see memory.rs). /// Where each process's next heap search starts (see memory.rs).
heap_cursors: [usize; MAX_PROCESSES], heap_cursors: [usize; MAX_PROCESSES],
/// Who to signal when each process dies (see syscall/ipc/events.rs). /// Who to signal when each process dies (see syscall/ipc/events.rs).
death_watchers: [Option<domain::timer::Target>; MAX_PROCESSES], death_watchers: [[Option<domain::timer::Target>; domain::timer::MAX_WATCHERS]; MAX_PROCESSES],
timers: domain::timer::Timers, timers: domain::timer::Timers,
} }
@@ -102,7 +102,7 @@ impl<P: Platform> Kernel<P> {
homes: [types::id::CoreId::from_raw(0); MAX_THREADS], homes: [types::id::CoreId::from_raw(0); MAX_THREADS],
tree: domain::capability::tree::CapTree::new(), tree: domain::capability::tree::CapTree::new(),
heap_cursors: [0; MAX_PROCESSES], heap_cursors: [0; MAX_PROCESSES],
death_watchers: [None; MAX_PROCESSES], death_watchers: [[None; domain::timer::MAX_WATCHERS]; MAX_PROCESSES],
timers: domain::timer::Timers::new(), timers: domain::timer::Timers::new(),
} }
} }
@@ -138,7 +138,8 @@ impl<P: Platform> Kernel<P> {
addr_of_mut!((*slot).homes).write([types::id::CoreId::from_raw(0); MAX_THREADS]); 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).tree).write(domain::capability::tree::CapTree::new());
addr_of_mut!((*slot).heap_cursors).write([0; MAX_PROCESSES]); 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()); addr_of_mut!((*slot).timers).write(domain::timer::Timers::new());
} }
} }

View File

@@ -40,22 +40,27 @@ impl<P: Platform> Kernel<P> {
_ => return Err(KernelError::WrongObject), _ => return Err(KernelError::WrongObject),
}; };
let target = self.target(pid, notification)?; let target = self.target(pid, notification)?;
let slot = self let watchers = self
.death_watchers .death_watchers
.get_mut(watched.raw() as usize) .get_mut(watched.raw() as usize)
.ok_or(KernelError::InvalidProcess(watched))?; .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); *slot = Some(target);
Ok(()) 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) { pub(crate) fn report_death(&mut self, pid: ProcessId) {
self.timers.clear(pid); self.timers.clear(pid);
if let Some(target) = self let watchers = self
.death_watchers .death_watchers
.get_mut(pid.raw() as usize) .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); self.fire(target);
} }
} }

View File

@@ -161,3 +161,21 @@ fn notification_id(
_ => panic!("not a notification"), _ => 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);
}

View File

@@ -10,6 +10,7 @@ pub mod fs;
pub mod input; pub mod input;
pub mod io; pub mod io;
mod rt; mod rt;
pub mod spawn;
pub mod start; pub mod start;
pub mod syscall; pub mod syscall;

View File

@@ -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<CapSlot>,
}
pub fn spawn(init: CapSlot, name: &[u8], limits: Limits) -> Result<Spawned, SpawnError> {
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
}
}

View File

@@ -14,6 +14,7 @@ pub mod mapping;
pub mod permission; pub mod permission;
#[cfg(kani)] #[cfg(kani)]
pub mod proofs; pub mod proofs;
pub mod spawn;
pub mod start_arg; pub mod start_arg;
pub mod storage; pub mod storage;
pub mod syscall_abi; pub mod syscall_abi;

110
crates/types/src/spawn.rs Normal file
View File

@@ -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<usize> {
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());
}
}

View File

@@ -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 its log endpoint from init later (`SET_LOG`, accepted only from the
unbadged holder of its endpoint). 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 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. one region, so delegating or revoking it means the same thing everywhere.

View File

@@ -33,6 +33,8 @@ copy session-server "session.elf"
# Only the test image: its presence makes init run the SMP self-test. # Only the test image: its presence makes init run the SMP self-test.
if [ -n "${SMPTEST:-}" ]; then if [ -n "${SMPTEST:-}" ]; then
copy smptest "smptest.elf" copy smptest "smptest.elf"
copy spawntest "spawntst.elf"
copy hello "hello.elf"
fi fi
echo "" echo ""

View File

@@ -1,6 +1,7 @@
#!/bin/bash #!/bin/bash
# Boots IMAGE headless and waits for init's SMP self-test to report; the # Boots IMAGE headless and waits for the self-test (init's SMP checks, then
# machine halts itself when done. Exit 0 only on "SMPTEST PASS". # spawntest) to report; the machine halts itself when done. Exit 0 only on
# "SMPTEST PASS".
set -uo pipefail set -uo pipefail
IMAGE="${1:?usage: test-qemu.sh IMAGE}" 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 -display none -serial "file:${LOG}" -monitor none
status=$? status=$?
grep -a "smptest:\|SMPTEST" "${LOG}" grep -a "smptest:\|spawntest:\|Hello\|SMPTEST" "${LOG}"
if grep -aq "SMPTEST PASS" "${LOG}"; then if grep -aq "SMPTEST PASS" "${LOG}"; then
echo "test-qemu: pass (${SMP} harts)" echo "test-qemu: pass (${SMP} harts)"
rm -f "${LOG}" rm -f "${LOG}"