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

View File

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

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