diff --git a/crates/adapter-riscv/src/boot/loader.rs b/crates/adapter-riscv/src/boot/loader.rs index 70a11bf..e9d3d3b 100644 --- a/crates/adapter-riscv/src/boot/loader.rs +++ b/crates/adapter-riscv/src/boot/loader.rs @@ -2,6 +2,7 @@ use crate::hw::uart::Uart; use crate::hw::virtio_blk::VirtioBlk; use crate::{boot_fs, mmu}; use types::address::VirtualAddress; +use types::budget::Limits; use types::id::{ProcessId, ThreadId}; use types::mapping::PagePerms; @@ -52,14 +53,38 @@ pub fn load_init(uart: &Uart, blk: &mut VirtioBlk) -> LoadedProcess { LoadedProcess { pid, tid } } +/// The servers boot loads, their budgets carved from init's. +pub const STORAGE: Limits = Limits { + pages: 256, + endpoints: 2, + notifications: 2, + // A buffer per client (8) and the disk's DMA. + regions: 12, +}; +/// The framebuffer alone is ~1000 pages at 1280x800. +pub const DISPLAY: Limits = Limits { + pages: 1280, + endpoints: 2, + notifications: 2, + regions: 4, +}; +pub const INPUT: Limits = Limits { + pages: 64, + endpoints: 2, + notifications: 2, + regions: 4, +}; + pub fn load_server( uart: &Uart, blk: &mut VirtioBlk, + init: ProcessId, filename: &[u8], label: &str, + limits: Limits, ) -> LoadedProcess { let pid = kern() - .create_process_internal() + .create_process_internal(init, limits) .unwrap_or_else(|| crate::halt(uart, "process create failed")); let entry = load_elf(uart, blk, pid, filename); let tid = kern() diff --git a/crates/adapter-riscv/src/devices/mod.rs b/crates/adapter-riscv/src/devices/mod.rs index 75143c1..fc7c697 100644 --- a/crates/adapter-riscv/src/devices/mod.rs +++ b/crates/adapter-riscv/src/devices/mod.rs @@ -1,4 +1,4 @@ -// Boot-time userspace setup: the display's page quota and init's BootInfo page. +// Boot-time userspace setup: init's BootInfo page. use crate::hw::uart::Uart; use types::address::VirtualAddress; @@ -6,20 +6,10 @@ use types::bootinfo::{BOOT_INFO_VA, BootDevice, BootInfo, MAGIC, MAX_BOOT_DEVICE use types::id::ProcessId; use types::mapping::PagePerms; -/// The framebuffer alone is ~1000 pages at 1280x800. -const DISPLAY_PAGE_LIMIT: usize = 2048; - fn kern() -> &'static mut kernel::Kernel { unsafe { &mut *crate::kernel_ptr() } } -/// Lets the display server hold a framebuffer. -pub(crate) fn display_quota(uart: &Uart, display: ProcessId) { - kern() - .set_page_limit(display, DISPLAY_PAGE_LIMIT) - .unwrap_or_else(|_| crate::halt(uart, "display quota")); -} - /// Tells init, in a read-only page at BOOT_INFO_VA, which of its slots hold /// each board device's capabilities. pub(crate) fn boot_info( diff --git a/crates/adapter-riscv/src/main.rs b/crates/adapter-riscv/src/main.rs index d754e8d..19f3b54 100644 --- a/crates/adapter-riscv/src/main.rs +++ b/crates/adapter-riscv/src/main.rs @@ -40,15 +40,29 @@ extern "C" fn kmain(hart: usize) -> ! { boot::hardware::probe_block_device(&uart).unwrap_or_else(|| halt(&uart, "no block device")); let init = boot::loader::load_init(&uart, &mut blk); - let storage = boot::loader::load_server(&uart, &mut blk, b"storage.elf", "storage"); - let display = boot::loader::load_server(&uart, &mut blk, b"display.elf", "display"); - let input = boot::loader::load_server(&uart, &mut blk, b"input.elf", "input"); + use boot::loader::{DISPLAY, INPUT, STORAGE, load_server}; + let storage = load_server( + &uart, + &mut blk, + init.pid, + b"storage.elf", + "storage", + STORAGE, + ); + let display = load_server( + &uart, + &mut blk, + init.pid, + b"display.elf", + "display", + DISPLAY, + ); + let input = load_server(&uart, &mut blk, init.pid, b"input.elf", "input", INPUT); let board = board::devices().unwrap_or_else(|| halt(&uart, "bad board table")); kern_mut() .register_board(init.pid, &board) .unwrap_or_else(|_| halt(&uart, "board registration failed")); - devices::display_quota(&uart, display.pid); boot::caps::grant_all(&uart, init.pid, storage.pid, display.pid, input.pid); // Last, so the servers' fixed slots stay where they expect them. boot::caps::grant_console(&uart, &[storage, display, input]); diff --git a/crates/domain/src/budget.rs b/crates/domain/src/budget.rs new file mode 100644 index 0000000..1b0aefd --- /dev/null +++ b/crates/domain/src/budget.rs @@ -0,0 +1,80 @@ +//! What a process may hold: frames and kernel objects. A child's limits +//! are carved from its creator's unused budget and go back when it dies, +//! so the limits of all live processes never add up to more than init got. + +use crate::memory::quota::Quota; +pub use types::budget::Limits; + +#[cfg(test)] +#[path = "budget_tests.rs"] +mod tests; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Budget { + pub pages: Quota, + pub endpoints: Quota, + pub notifications: Quota, + pub regions: Quota, +} + +impl Budget { + pub const EMPTY: Self = Self::new(Limits { + pages: 0, + endpoints: 0, + notifications: 0, + regions: 0, + }); + + pub const fn new(limits: Limits) -> Self { + Self { + pages: Quota::new(limits.pages), + endpoints: Quota::new(limits.endpoints), + notifications: Quota::new(limits.notifications), + regions: Quota::new(limits.regions), + } + } + + pub fn limits(&self) -> Limits { + Limits { + pages: self.pages.limit(), + endpoints: self.endpoints.limit(), + notifications: self.notifications.limit(), + regions: self.regions.limit(), + } + } + + fn parts(&mut self) -> [&mut Quota; 4] { + [ + &mut self.pages, + &mut self.endpoints, + &mut self.notifications, + &mut self.regions, + ] + } + + /// Carves `child` out of the unused budget: all of it or nothing. + pub fn carve(&mut self, child: Limits) -> bool { + let amounts = amounts(child); + let fits = self.parts().iter().zip(amounts).all(|(q, n)| { + q.limit() + .checked_sub(n) + .is_some_and(|rest| rest >= q.used()) + }); + if fits { + for (q, n) in self.parts().into_iter().zip(amounts) { + q.carve(n); + } + } + fits + } + + pub fn refund(&mut self, child: Limits) { + for (q, n) in self.parts().into_iter().zip(amounts(child)) { + q.refund(n); + } + } +} + +fn amounts(l: Limits) -> [usize; 4] { + [l.pages, l.endpoints, l.notifications, l.regions] +} diff --git a/crates/domain/src/budget_tests.rs b/crates/domain/src/budget_tests.rs new file mode 100644 index 0000000..bb4554c --- /dev/null +++ b/crates/domain/src/budget_tests.rs @@ -0,0 +1,35 @@ +use super::{Budget, Limits}; + +const L: Limits = Limits { + pages: 10, + endpoints: 2, + notifications: 2, + regions: 1, +}; + +#[test] +fn a_carve_takes_all_parts_or_none() { + let mut b = Budget::new(L); + b.endpoints.charge(2); + let child = Limits { + pages: 1, + endpoints: 1, + ..Limits::default() + }; + assert!(!b.carve(child)); + assert_eq!(b.limits(), L); + assert!(b.carve(Limits { + endpoints: 0, + ..child + })); + assert_eq!(b.limits().pages, 9); +} + +#[test] +fn a_refund_restores_what_was_carved() { + let mut b = Budget::new(L); + assert!(b.carve(L)); + assert_eq!(b.limits(), Limits::default()); + b.refund(L); + assert_eq!(b.limits(), L); +} diff --git a/crates/domain/src/lib.rs b/crates/domain/src/lib.rs index 6ce9f2c..f88594c 100644 --- a/crates/domain/src/lib.rs +++ b/crates/domain/src/lib.rs @@ -17,6 +17,7 @@ #[cfg(kani)] pub mod proofs; +pub mod budget; pub mod capability; pub mod endpoint; pub mod error; diff --git a/crates/domain/src/memory/quota.rs b/crates/domain/src/memory/quota.rs index b8d1238..0b13fb3 100644 --- a/crates/domain/src/memory/quota.rs +++ b/crates/domain/src/memory/quota.rs @@ -1,5 +1,6 @@ -/// Per-process page budget. Every frame charged to a process (user pages -/// and its page tables) counts against it. +/// A count against a limit: one of a process's budgets (see `budget`). +/// For pages, every frame charged to a process (user pages and its page +/// tables) counts against it. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Quota { used: usize, @@ -7,8 +8,6 @@ pub struct Quota { } impl Quota { - pub const DEFAULT_LIMIT: usize = 1024; - pub const fn new(limit: usize) -> Self { Self { used: 0, limit } } @@ -41,17 +40,19 @@ impl Quota { } } - pub fn set_limit(&mut self, limit: usize) -> bool { - if limit < self.used { - return false; + /// Gives up `amount` of the unused limit, for a child to hold. + pub fn carve(&mut self, amount: usize) -> bool { + match self.limit.checked_sub(amount) { + Some(rest) if rest >= self.used => { + self.limit = rest; + true + } + _ => false, } - self.limit = limit; - true } -} -impl Default for Quota { - fn default() -> Self { - Self::new(Self::DEFAULT_LIMIT) + /// Takes back a limit carved out earlier. + pub fn refund(&mut self, amount: usize) { + self.limit = self.limit.saturating_add(amount); } } diff --git a/crates/domain/src/memory/quota_tests.rs b/crates/domain/src/memory/quota_tests.rs index 4c622ba..d8d0c13 100644 --- a/crates/domain/src/memory/quota_tests.rs +++ b/crates/domain/src/memory/quota_tests.rs @@ -28,10 +28,13 @@ fn uncharge_cannot_underflow() { } #[test] -fn limit_can_be_raised_but_not_below_usage() { +fn only_the_unused_limit_can_be_carved() { let mut q = Quota::new(4); q.charge(3); - assert!(!q.set_limit(2)); - assert!(q.set_limit(10)); - assert!(q.charge(7)); + assert!(!q.carve(2)); + assert_eq!(q.limit(), 4); + assert!(q.carve(1)); + assert!(!q.charge(1)); + q.refund(1); + assert!(q.charge(1)); } diff --git a/crates/domain/src/proofs/budget.rs b/crates/domain/src/proofs/budget.rs new file mode 100644 index 0000000..69190fe --- /dev/null +++ b/crates/domain/src/proofs/budget.rs @@ -0,0 +1,33 @@ +use crate::budget::{Budget, Limits}; + +fn any_limits() -> Limits { + let l = Limits { + pages: kani::any(), + endpoints: kani::any(), + notifications: kani::any(), + regions: kani::any(), + }; + kani::assume(l.pages < 1 << 20 && l.endpoints < 1 << 20); + kani::assume(l.notifications < 1 << 20 && l.regions < 1 << 20); + l +} + +/// A carve never lets parent and child together exceed what the parent +/// had, never strands the parent's usage, and a refund undoes it. +#[kani::proof] +fn carving_conserves_the_budget() { + let (have, child) = (any_limits(), any_limits()); + let used: usize = kani::any(); + let mut b = Budget::new(have); + b.pages.charge(used); + let before = b; + if b.carve(child) { + assert_eq!(b.limits().pages + child.pages, have.pages); + assert_eq!(b.limits().regions + child.regions, have.regions); + assert!(b.pages.used() <= b.pages.limit()); + b.refund(child); + assert_eq!(b, before); + } else { + assert_eq!(b, before); + } +} diff --git a/crates/domain/src/proofs/mod.rs b/crates/domain/src/proofs/mod.rs index 065f2a7..9261426 100644 --- a/crates/domain/src/proofs/mod.rs +++ b/crates/domain/src/proofs/mod.rs @@ -1,3 +1,4 @@ +mod budget; mod capability; mod delegation; mod thread; diff --git a/crates/domain/src/syscall.rs b/crates/domain/src/syscall.rs index 2f55070..baf1661 100644 --- a/crates/domain/src/syscall.rs +++ b/crates/domain/src/syscall.rs @@ -62,9 +62,11 @@ pub enum Syscall { AcknowledgeInterrupt { irq: CapSlot, }, + /// The new process's `limits` are carved from the caller's budget. CreateProcess { create_cap: CapSlot, image: DataRef, + limits: crate::budget::Limits, }, Kill { process: CapSlot, diff --git a/crates/init/src/lib.rs b/crates/init/src/lib.rs index d831eae..7db7514 100644 --- a/crates/init/src/lib.rs +++ b/crates/init/src/lib.rs @@ -7,6 +7,7 @@ mod selftest; use types::address::{EntryPoint, StackPointer, VirtualAddress}; use types::bounded::{CapSlot, PageCount}; +use types::budget::Limits; use types::permission::{Permission, PermissionSet}; use libkos::fs::Storage; use libkos::syscall; @@ -42,7 +43,12 @@ pub(crate) struct LoadedProcess { pub(crate) entry_point: EntryPoint, } -pub(crate) fn load_binary(storage: &mut Storage, scratch: &mut [u8], filename: &[u8]) -> Option { +pub(crate) fn load_binary( + storage: &mut Storage, + scratch: &mut [u8], + filename: &[u8], + limits: Limits, +) -> Option { let file_size = read_file(storage, scratch, filename)?; let elf_data = &scratch[..file_size]; let elf_info = match elf::parse(elf_data) { @@ -50,7 +56,7 @@ pub(crate) fn load_binary(storage: &mut Storage, scratch: &mut [u8], filename: & Err(_) => return None, }; - let process_cap = syscall::create_process(CREATE_CAP)?; + let process_cap = syscall::create_process(CREATE_CAP, limits)?; for segment in elf_info.segments() { let page_start = segment.vaddr & !0xFFF; @@ -102,14 +108,25 @@ pub(crate) fn load_binary(storage: &mut Storage, scratch: &mut [u8], filename: & struct ServerInfo { filename: &'static [u8], name: &'static str, + limits: Limits, } +/// Enough for a small program: its image, stack and a little heap. +pub(crate) const SMALL: Limits = Limits { + pages: 64, + endpoints: 2, + notifications: 2, + regions: 2, +}; + const SERVERS: &[ServerInfo] = &[ - ServerInfo { filename: b"log.elf", name: "log" }, - ServerInfo { filename: b"permis.elf", name: "permis" }, - ServerInfo { filename: b"session.elf", name: "session" }, + ServerInfo { filename: b"log.elf", name: "log", limits: SMALL }, + ServerInfo { filename: b"permis.elf", name: "permis", limits: SMALL }, + ServerInfo { filename: b"session.elf", name: "session", limits: SMALL }, ]; +const SNAKE: Limits = Limits { pages: 128, ..SMALL }; + pub fn run() -> ! { libkos::start::set_console(devices::console()); libkos::println!("init: started (pid={:#x})", syscall::get_process_id().raw()); @@ -142,7 +159,7 @@ pub fn run() -> ! { for server in SERVERS { libkos::print!("init: loading {}... ", server.name); - let loaded = match load_binary(&mut storage, scratch, server.filename) { + let loaded = match load_binary(&mut storage, scratch, server.filename, server.limits) { Some(l) => l, None => { libkos::println!("SKIP"); @@ -166,7 +183,7 @@ pub fn run() -> ! { } libkos::print!("init: loading snake... "); - if let Some(snake) = load_binary(&mut storage, scratch, b"snake.elf") { + if let Some(snake) = load_binary(&mut storage, scratch, b"snake.elf", SNAKE) { // A client only calls: Execute, never Read, so it cannot pose as // the server and take other clients' requests. let client = PermissionSet::empty().with(Permission::Execute); diff --git a/crates/init/src/selftest.rs b/crates/init/src/selftest.rs index 1f045e2..47ddd61 100644 --- a/crates/init/src/selftest.rs +++ b/crates/init/src/selftest.rs @@ -40,7 +40,7 @@ fn start_spinner(storage: &mut Storage, scratch: &mut [u8], page: CapSlot) -> Re for i in 0..=THREADS { word(i).store(0, Ordering::SeqCst); } - let spinner = crate::load_binary(storage, scratch, b"smptest.elf").ok_or("cannot load smptest.elf")?; + let spinner = crate::load_binary(storage, scratch, b"smptest.elf", crate::SMALL).ok_or("cannot load smptest.elf")?; let rw = PermissionSet::empty() .with(Permission::Read) .with(Permission::Write) diff --git a/crates/kernel/src/abi.rs b/crates/kernel/src/abi.rs index 9b64e8b..23321c6 100644 --- a/crates/kernel/src/abi.rs +++ b/crates/kernel/src/abi.rs @@ -94,6 +94,12 @@ pub fn decode(nr: usize, a: &[usize; 6]) -> Option { SYS_CREATE_PROCESS => Syscall::CreateProcess { create_cap: slot(a[0])?, image: DataRef::empty(), + limits: domain::budget::Limits { + pages: a[1], + endpoints: a[2], + notifications: a[3], + regions: a[4], + }, }, SYS_KILL => Syscall::Kill { process: slot(a[0])?, diff --git a/crates/kernel/src/budget.rs b/crates/kernel/src/budget.rs new file mode 100644 index 0000000..34f422f --- /dev/null +++ b/crates/kernel/src/budget.rs @@ -0,0 +1,100 @@ +//! Per-process budgets (see `domain::budget`): carved from the creator, +//! charged as a process takes frames and kernel objects, returned on death. + +use domain::{ + budget::{Budget, Limits}, + capability::CapabilityTable, + error::KernelError, + memory::quota::Quota, +}; +use platform::Platform; +use types::id::ProcessId; + +use crate::Kernel; + +/// Picks the part of a budget an object counts against. +pub(crate) type Part = fn(&mut Budget) -> &mut Quota; + +pub(crate) const ENDPOINTS: Part = |b| &mut b.endpoints; +pub(crate) const NOTIFICATIONS: Part = |b| &mut b.notifications; +pub(crate) const REGIONS: Part = |b| &mut b.regions; + +impl Kernel

{ + pub(crate) fn budget_mut(&mut self, pid: ProcessId) -> Result<&mut Budget, KernelError> { + self.budgets + .get_mut(pid.raw() as usize) + .ok_or(KernelError::InvalidProcess(pid)) + } + + pub fn limits_of(&self, pid: ProcessId) -> Option { + self.processes.get(pid).ok()?; + self.budgets.get(pid.raw() as usize).map(Budget::limits) + } + + /// A new process holding `limits`, carved from `parent`'s unused budget; + /// only boot makes one without a parent. + pub(crate) fn new_process( + &mut self, + parent: Option, + limits: Limits, + ) -> Result { + if let Some(p) = parent { + self.processes.get(p)?; + if !self.budget_mut(p)?.carve(limits) { + return Err(KernelError::OutOfMemory); + } + } + let pid = match self.processes.create(CapabilityTable::new(), None) { + Ok(pid) => pid, + Err(e) => { + if let Some(p) = parent { + self.budget_mut(p)?.refund(limits); + } + return Err(e); + } + }; + *self.budget_mut(pid)? = Budget::new(limits); + if let Some(slot) = self.parents.get_mut(pid.raw() as usize) { + *slot = parent; + } + if let Err(e) = self.create_address_space(pid) { + self.release_budget(pid); + let _ = self.processes.destroy(pid); + return Err(e); + } + Ok(pid) + } + + /// Gives a dead process's limits back to its parent; its children are + /// carved from what it held, so they pass to that parent too. + pub(crate) fn release_budget(&mut self, pid: ProcessId) { + let i = pid.raw() as usize; + let parent = self.parents.get_mut(i).and_then(Option::take); + let limits = self + .budgets + .get_mut(i) + .map(|b| core::mem::replace(b, Budget::EMPTY).limits()); + if let (Some(p), Some(limits)) = (parent, limits) + && let Ok(b) = self.budget_mut(p) + { + b.refund(limits); + } + for slot in self.parents.iter_mut().filter(|s| **s == Some(pid)) { + *slot = parent; + } + } + + pub(crate) fn charge_object(&mut self, pid: ProcessId, part: Part) -> Result<(), KernelError> { + if part(self.budget_mut(pid)?).charge(1) { + Ok(()) + } else { + Err(KernelError::NoFreeSlot) + } + } + + pub(crate) fn uncharge_object(&mut self, pid: ProcessId, part: Part) { + if let Ok(b) = self.budget_mut(pid) { + part(b).uncharge(1); + } + } +} diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index 8bd6af6..0549eec 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -16,6 +16,7 @@ pub mod abi; mod board; +mod budget; mod captree; pub mod interrupt; mod memory; @@ -33,11 +34,12 @@ pub use syscall::{SyscallOutcome, SyscallResult}; mod tests; use domain::{ - capability::{Capability, CapabilityTable}, + budget::{Budget, Limits}, + capability::Capability, endpoint::EndpointTable, error::KernelError, irq::IrqRoutingTable, - memory::{RegionTable, frame::FrameTable, quota::Quota}, + memory::{RegionTable, frame::FrameTable}, notification::NotificationTable, process::{MAX_PROCESSES, ProcessTable}, resource::Resource, @@ -61,7 +63,9 @@ pub struct Kernel { endpoints: EndpointTable, scheduler: MultiCoreScheduler, frames: FrameTable, - quotas: [Quota; MAX_PROCESSES], + budgets: [Budget; MAX_PROCESSES], + /// Whose budget each process was carved from (see budget.rs). + parents: [Option; MAX_PROCESSES], regions: RegionTable, irqs: IrqRoutingTable, notifications: NotificationTable, @@ -85,7 +89,8 @@ impl Kernel

{ endpoints: EndpointTable::new(), scheduler: MultiCoreScheduler::new(num_cores), frames: FrameTable::new(pool_base, pool_pages), - quotas: [Quota::default(); MAX_PROCESSES], + budgets: [Budget::EMPTY; MAX_PROCESSES], + parents: [None; MAX_PROCESSES], regions: RegionTable::new(), irqs: IrqRoutingTable::new(), notifications: NotificationTable::new(), @@ -118,7 +123,8 @@ impl Kernel

{ addr_of_mut!((*slot).endpoints).write(EndpointTable::new()); addr_of_mut!((*slot).scheduler).write(MultiCoreScheduler::new(num_cores)); addr_of_mut!((*slot).frames).write(FrameTable::new(pool_base, pool_pages)); - addr_of_mut!((*slot).quotas).write([Quota::default(); MAX_PROCESSES]); + addr_of_mut!((*slot).budgets).write([Budget::EMPTY; MAX_PROCESSES]); + addr_of_mut!((*slot).parents).write([None; MAX_PROCESSES]); addr_of_mut!((*slot).regions).write(RegionTable::new()); addr_of_mut!((*slot).irqs).write(IrqRoutingTable::new()); addr_of_mut!((*slot).notifications).write(NotificationTable::new()); @@ -135,11 +141,14 @@ impl Kernel

{ priority: Priority, time_slice: TimeSlice, ) -> Result<(ProcessId, ThreadId), KernelError> { - let pid = self.processes.create(CapabilityTable::new(), None)?; - if let Err(e) = self.create_address_space(pid) { - let _ = self.processes.destroy(pid); - return Err(e); - } + // Init holds every frame and every kernel object to start with. + let everything = Limits { + pages: self.frames.free_count(), + endpoints: domain::endpoint::MAX_ENDPOINTS, + notifications: domain::notification::MAX_NOTIFICATIONS, + regions: domain::memory::MAX_REGIONS, + }; + let pid = self.new_process(None, everything)?; let tid = self.threads.spawn(pid, priority, time_slice, None)?; self.processes.get_mut(pid)?.add_thread(tid)?; @@ -211,13 +220,13 @@ impl Kernel

{ self.scheduler.next(core) } - pub fn create_process_internal(&mut self) -> Option { - let pid = self.processes.create(CapabilityTable::new(), None).ok()?; - if self.create_address_space(pid).is_err() { - let _ = self.processes.destroy(pid); - return None; - } - Some(pid) + /// A process for boot code to load, its `limits` carved from `parent`. + pub fn create_process_internal( + &mut self, + parent: ProcessId, + limits: Limits, + ) -> Option { + self.new_process(Some(parent), limits).ok() } pub fn spawn_thread_internal(&mut self, pid: ProcessId) -> Option { @@ -237,7 +246,11 @@ impl Kernel

{ owner: ProcessId, ) -> Result { self.processes.get(owner)?; - self.endpoints.create(owner).ok_or(KernelError::NoFreeSlot) + self.charge_object(owner, budget::ENDPOINTS)?; + self.endpoints + .create(owner) + .ok_or(KernelError::NoFreeSlot) + .inspect_err(|_| self.uncharge_object(owner, budget::ENDPOINTS)) } pub fn grant_capability( diff --git a/crates/kernel/src/memory/mod.rs b/crates/kernel/src/memory/mod.rs index 05dd6f5..6c087ed 100644 --- a/crates/kernel/src/memory/mod.rs +++ b/crates/kernel/src/memory/mod.rs @@ -91,22 +91,13 @@ impl Kernel

{ } fn quota_mut(&mut self, pid: ProcessId) -> Result<&mut Quota, KernelError> { - self.quotas - .get_mut(pid.raw() as usize) - .ok_or(KernelError::InvalidProcess(pid)) + self.budget_mut(pid).map(|b| &mut b.pages) } pub fn page_usage(&self, pid: ProcessId) -> usize { - self.quotas.get(pid.raw() as usize).map_or(0, Quota::used) - } - - pub fn set_page_limit(&mut self, pid: ProcessId, limit: usize) -> Result<(), KernelError> { - self.processes.get(pid)?; - if self.quota_mut(pid)?.set_limit(limit) { - Ok(()) - } else { - Err(KernelError::OutOfMemory) - } + self.budgets + .get(pid.raw() as usize) + .map_or(0, |b| b.pages.used()) } pub fn free_page_count(&self) -> usize { @@ -121,10 +112,11 @@ impl Kernel

{ pa: PhysicalAddress, perms: PagePerms, ) -> Result<(), KernelError> { - let quota = self - .quotas + let quota = &mut self + .budgets .get_mut(pid.raw() as usize) - .ok_or(KernelError::InvalidProcess(pid))?; + .ok_or(KernelError::InvalidProcess(pid))? + .pages; let mut source = ChargedFrames { frames: &mut self.frames, quota, @@ -136,11 +128,11 @@ impl Kernel

{ } pub(crate) fn create_address_space(&mut self, pid: ProcessId) -> Result<(), KernelError> { - let quota = self - .quotas + let quota = &mut self + .budgets .get_mut(pid.raw() as usize) - .ok_or(KernelError::InvalidProcess(pid))?; - *quota = Quota::default(); + .ok_or(KernelError::InvalidProcess(pid))? + .pages; let mut source = ChargedFrames { frames: &mut self.frames, quota, @@ -152,9 +144,6 @@ impl Kernel

{ .map_err(map_error); if created.is_err() { self.frames.free_owned_by(pid); - if let Some(q) = self.quotas.get_mut(pid.raw() as usize) { - *q = Quota::default(); - } } created } @@ -188,8 +177,8 @@ impl Kernel

{ if let Some((pa, _)) = self.user_frame(pid, va) { self.platform.unmap_page(pid, va); self.frames.free(pa); - if let Some(q) = self.quotas.get_mut(pid.raw() as usize) { - q.uncharge(1); + if let Some(b) = self.budgets.get_mut(pid.raw() as usize) { + b.pages.uncharge(1); } } } @@ -200,9 +189,6 @@ impl Kernel

{ self.teardown_regions(pid); self.platform.destroy_address_space(pid); self.frames.free_owned_by(pid); - if let Some(q) = self.quotas.get_mut(pid.raw() as usize) { - *q = Quota::default(); - } if let Some(c) = self.heap_cursors.get_mut(pid.raw() as usize) { *c = 0; } diff --git a/crates/kernel/src/memory/region.rs b/crates/kernel/src/memory/region.rs index 16eb483..21b2271 100644 --- a/crates/kernel/src/memory/region.rs +++ b/crates/kernel/src/memory/region.rs @@ -28,7 +28,9 @@ impl Kernel

{ self.processes.get(owner)?; let id = self.regions.next_free_id().ok_or(KernelError::NoFreeSlot)?; let count = pages.value() as usize; + self.charge_object(owner, crate::budget::REGIONS)?; if !self.quota_mut(owner)?.charge(count) { + self.uncharge_object(owner, crate::budget::REGIONS); return Err(KernelError::OutOfMemory); } let Some(base) = self @@ -36,6 +38,7 @@ impl Kernel

{ .alloc_contiguous(count, |_| FrameUse::Region { region: id }) else { self.quota_mut(owner)?.uncharge(count); + self.uncharge_object(owner, crate::budget::REGIONS); return Err(KernelError::OutOfMemory); }; for i in 0..count { @@ -76,9 +79,11 @@ impl Kernel

{ if overlaps { return Err(KernelError::AddressInUse); } + self.charge_object(owner, crate::budget::REGIONS)?; self.regions .insert(Region::new(owner, RegionBacking::Device, base, pages)) .ok_or(KernelError::NoFreeSlot) + .inspect_err(|_| self.uncharge_object(owner, crate::budget::REGIONS)) } pub fn region_base(&self, id: RegionId) -> Option { diff --git a/crates/kernel/src/proofs/authority.rs b/crates/kernel/src/proofs/authority.rs index 55b702e..664a758 100644 --- a/crates/kernel/src/proofs/authority.rs +++ b/crates/kernel/src/proofs/authority.rs @@ -70,7 +70,13 @@ fn any_slot() -> CapSlot { fn small_world() -> Kernel { let mut k = Kernel::new(KaniPlatform, PhysicalAddress::from_raw(0x8400_0000), 6); let (init, _) = k.boot(Priority::DEFAULT, TimeSlice::DEFAULT).unwrap(); - let b = k.create_process_internal().unwrap(); + let small = domain::budget::Limits { + pages: 2, + endpoints: 1, + notifications: 1, + regions: 1, + }; + let b = k.create_process_internal(init, small).unwrap(); k.spawn_thread_internal(b).unwrap(); let ep = k.create_endpoint(init).unwrap(); let n = k.create_notification(b).unwrap(); diff --git a/crates/kernel/src/syscall/ipc/endpoint.rs b/crates/kernel/src/syscall/ipc/endpoint.rs index a3bd1de..41356ae 100644 --- a/crates/kernel/src/syscall/ipc/endpoint.rs +++ b/crates/kernel/src/syscall/ipc/endpoint.rs @@ -301,6 +301,7 @@ impl Kernel

{ Ok(slot) => SyscallOutcome::Complete(SyscallResult::Slot(slot)), Err(e) => { self.endpoints.destroy(id); + self.uncharge_object(pid, crate::budget::ENDPOINTS); SyscallOutcome::Complete(SyscallResult::Error(e)) } } diff --git a/crates/kernel/src/syscall/ipc/notification.rs b/crates/kernel/src/syscall/ipc/notification.rs index fb9dc54..e4e57d0 100644 --- a/crates/kernel/src/syscall/ipc/notification.rs +++ b/crates/kernel/src/syscall/ipc/notification.rs @@ -38,9 +38,11 @@ impl Kernel

{ pub fn create_notification(&mut self, owner: ProcessId) -> Result { self.processes.get(owner)?; + self.charge_object(owner, crate::budget::NOTIFICATIONS)?; self.notifications .create(owner) .ok_or(KernelError::NoFreeSlot) + .inspect_err(|_| self.uncharge_object(owner, crate::budget::NOTIFICATIONS)) } pub(crate) fn create_notification_cap( @@ -51,6 +53,7 @@ impl Kernel

{ self.grant_cap(pid, Resource::Notification(id), rw_delegate()) .inspect_err(|_| { self.notifications.destroy(id); + self.uncharge_object(pid, crate::budget::NOTIFICATIONS); }) } diff --git a/crates/kernel/src/syscall/lifecycle.rs b/crates/kernel/src/syscall/lifecycle.rs index e74bc85..39a4f17 100644 --- a/crates/kernel/src/syscall/lifecycle.rs +++ b/crates/kernel/src/syscall/lifecycle.rs @@ -41,6 +41,7 @@ impl Kernel

{ caller_pid: types::id::ProcessId, create_cap: types::bounded::CapSlot, _image: types::address::DataRef, + limits: domain::budget::Limits, ) -> SyscallOutcome { if let Err(e) = self.check_cap( caller_pid, @@ -50,19 +51,11 @@ impl Kernel

{ return SyscallOutcome::Complete(SyscallResult::Error(e)); } - let new_pid = match self - .processes - .create(domain::capability::CapabilityTable::new(), None) - { + let new_pid = match self.new_process(Some(caller_pid), limits) { Ok(p) => p, Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), }; - if let Err(e) = self.create_address_space(new_pid) { - let _ = self.processes.destroy(new_pid); - return SyscallOutcome::Complete(SyscallResult::Error(e)); - } - match self.grant_cap( caller_pid, domain::resource::Resource::Process(new_pid), @@ -75,9 +68,7 @@ impl Kernel

{ ) { Ok(slot) => SyscallOutcome::Complete(SyscallResult::Slot(slot)), Err(e) => { - // Rollback: destroy orphaned process - self.teardown_memory(new_pid); - let _ = self.processes.destroy(new_pid); + self.destroy_process(new_pid); SyscallOutcome::Complete(SyscallResult::Error(e)) } } diff --git a/crates/kernel/src/syscall/mod.rs b/crates/kernel/src/syscall/mod.rs index 802345b..8a7b774 100644 --- a/crates/kernel/src/syscall/mod.rs +++ b/crates/kernel/src/syscall/mod.rs @@ -83,9 +83,11 @@ impl Kernel

{ Syscall::SpawnThread { entry, stack, arg } => { self.handle_spawn_thread(pid, entry, stack, arg) } - Syscall::CreateProcess { create_cap, image } => { - self.handle_create_process(pid, create_cap, image) - } + Syscall::CreateProcess { + create_cap, + image, + limits, + } => self.handle_create_process(pid, create_cap, image, limits), Syscall::Kill { process } => self.handle_kill(pid, process), Syscall::Revoke { slot } => self.handle_revoke(pid, slot), Syscall::DropCap { slot } => self.handle_drop_cap(pid, slot), diff --git a/crates/kernel/src/teardown.rs b/crates/kernel/src/teardown.rs index a963c98..174e925 100644 --- a/crates/kernel/src/teardown.rs +++ b/crates/kernel/src/teardown.rs @@ -28,6 +28,7 @@ impl Kernel

{ self.destroy_notifications_owned_by(pid); self.irqs.clear_owned_by(pid); self.teardown_memory(pid); + self.release_budget(pid); let _ = self.processes.destroy(pid); } diff --git a/crates/kernel/src/tests/bench.rs b/crates/kernel/src/tests/bench.rs index da59b0f..258d0a0 100644 --- a/crates/kernel/src/tests/bench.rs +++ b/crates/kernel/src/tests/bench.rs @@ -42,7 +42,7 @@ fn crowded( let parent = k.cap_ref(init, root); let mut pids = std::vec::Vec::new(); for _ in 0..n { - let Some(pid) = k.create_process_internal() else { + let Some(pid) = k.create_process_for_test() else { break; }; while k @@ -173,7 +173,7 @@ fn bench_create_spawn_kill() { let rounds = 2_000; let start = Instant::now(); for _ in 0..rounds { - let pid = k.create_process_internal().unwrap(); + let pid = k.create_process_for_test().unwrap(); k.spawn_thread_internal(pid).unwrap(); k.destroy_process(pid); } diff --git a/crates/kernel/src/tests/board.rs b/crates/kernel/src/tests/board.rs index b67caf8..7d6e7ac 100644 --- a/crates/kernel/src/tests/board.rs +++ b/crates/kernel/src/tests/board.rs @@ -109,7 +109,7 @@ fn halt_needs_execute_on_the_cap() { #[should_panic(expected = "halt")] fn any_holder_of_the_cap_may_halt_not_just_init() { let (mut k, init) = booted(); - let other = k.create_process_internal().unwrap(); + let other = k.create_process_for_test().unwrap(); let tid = k.spawn_thread_for_test(other, Priority::DEFAULT, TimeSlice::DEFAULT); k.grant_cap_for_test( other, @@ -207,7 +207,7 @@ fn granting_needs_a_registered_board() { #[test] fn granting_needs_the_regions_to_be_inits() { let (mut k, init) = kernel_with_process(); - let other = k.create_process_internal().unwrap(); + let other = k.create_process_for_test().unwrap(); k.register_board(other, &board()).unwrap(); assert!(matches!( k.grant_boot_authority(init, &board(), &mut [None; 2]), diff --git a/crates/kernel/src/tests/budget.rs b/crates/kernel/src/tests/budget.rs new file mode 100644 index 0000000..445621d --- /dev/null +++ b/crates/kernel/src/tests/budget.rs @@ -0,0 +1,152 @@ +//! Budgets: carved from the creator, enforced, and returned on death. + +use super::helpers::CHILD_LIMITS; +use super::*; +use crate::{SyscallOutcome, SyscallResult}; +use domain::budget::Limits; +use domain::error::KernelError; +use domain::syscall::Syscall; +use types::address::DataRef; +use types::bounded::{CapSlot, PageCount}; + +fn create(k: &mut Kernel, limits: Limits) -> SyscallResult { + match k.handle_syscall(Syscall::CreateProcess { + create_cap: CapSlot::new(0).unwrap(), + image: DataRef::empty(), + limits, + }) { + SyscallOutcome::Complete(r) => r, + _ => panic!("create process blocked"), + } +} + +fn minus(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, + } +} + +#[test] +fn init_starts_with_the_whole_pool() { + let (k, init) = kernel_with_process(); + let l = k.limits_of(init).unwrap(); + assert_eq!(l.pages, k.free_page_count() + k.page_usage(init)); + assert_eq!(l.endpoints, domain::endpoint::MAX_ENDPOINTS); +} + +#[test] +fn a_child_is_carved_from_its_creator() { + let (mut k, init) = kernel_with_process(); + let before = k.limits_of(init).unwrap(); + let SyscallResult::Slot(_) = create(&mut k, CHILD_LIMITS) else { + panic!("create failed"); + }; + assert_eq!(k.limits_of(init).unwrap(), minus(before, CHILD_LIMITS)); +} + +#[test] +fn a_child_gets_no_more_than_its_creator_has_spare() { + let (mut k, init) = kernel_with_process(); + let before = k.limits_of(init).unwrap(); + // The creator's own pages are in use, so its whole limit is not spare. + let greedy = Limits { + pages: before.pages, + ..CHILD_LIMITS + }; + assert!(matches!( + create(&mut k, greedy), + SyscallResult::Error(KernelError::OutOfMemory) + )); + let greedy = Limits { + endpoints: before.endpoints + 1, + ..CHILD_LIMITS + }; + assert!(matches!( + create(&mut k, greedy), + SyscallResult::Error(KernelError::OutOfMemory) + )); + assert_eq!(k.limits_of(init).unwrap(), before); +} + +#[test] +fn kernel_objects_count_against_the_owner() { + let (mut k, init) = kernel_with_process(); + let one = Limits { + pages: 8, + endpoints: 1, + notifications: 1, + regions: 1, + }; + let child = k.create_process_internal(init, one).unwrap(); + assert!(k.create_endpoint(child).is_ok()); + assert!(matches!( + k.create_endpoint(child), + Err(KernelError::NoFreeSlot) + )); + assert!(k.create_notification(child).is_ok()); + assert!(matches!( + k.create_notification(child), + Err(KernelError::NoFreeSlot) + )); + let page = PageCount::new(1).unwrap(); + assert!(k.create_pool_region(child, page).is_ok()); + assert!(matches!( + k.create_pool_region(child, page), + Err(KernelError::NoFreeSlot) + )); +} + +#[test] +fn a_failed_region_leaves_no_charge_behind() { + let (mut k, init) = kernel_with_process(); + let one = Limits { + pages: 2, + endpoints: 0, + notifications: 0, + regions: 1, + }; + let child = k.create_process_internal(init, one).unwrap(); + // More pages than the child may hold: the region slot stays free. + assert!(matches!( + k.create_pool_region(child, PageCount::new(8).unwrap()), + Err(KernelError::OutOfMemory) + )); + assert!( + k.create_pool_region(child, PageCount::new(1).unwrap()) + .is_ok() + ); +} + +#[test] +fn death_returns_the_budget_to_the_parent() { + let (mut k, init) = kernel_with_process(); + let before = k.limits_of(init).unwrap(); + let child = k.create_process_internal(init, CHILD_LIMITS).unwrap(); + k.create_endpoint(child).unwrap(); + k.destroy_process(child); + assert_eq!(k.limits_of(init).unwrap(), before); +} + +#[test] +fn an_orphan_returns_its_budget_to_the_grandparent() { + let (mut k, init) = kernel_with_process(); + let before = k.limits_of(init).unwrap(); + let a = k.create_process_internal(init, CHILD_LIMITS).unwrap(); + let small = Limits { + pages: 4, + endpoints: 1, + notifications: 1, + regions: 1, + }; + let b = k.create_process_internal(a, small).unwrap(); + k.destroy_process(a); + assert_eq!(k.limits_of(init).unwrap(), minus(before, small)); + // a's slot may be reused; b's budget must still find its way home. + let c = k.create_process_internal(init, CHILD_LIMITS).unwrap(); + k.destroy_process(b); + k.destroy_process(c); + assert_eq!(k.limits_of(init).unwrap(), before); +} diff --git a/crates/kernel/src/tests/capability.rs b/crates/kernel/src/tests/capability.rs index 1a82f25..6dd5fb4 100644 --- a/crates/kernel/src/tests/capability.rs +++ b/crates/kernel/src/tests/capability.rs @@ -28,6 +28,7 @@ fn delegate_with_valid_cap_succeeds() { let target_slot = match k.handle_syscall(Syscall::CreateProcess { create_cap: CapSlot::new(0).unwrap(), image: DataRef::empty(), + limits: super::helpers::CHILD_LIMITS, }) { SyscallOutcome::Complete(SyscallResult::Slot(s)) => s, _ => panic!("expected slot"), diff --git a/crates/kernel/src/tests/captree_model.rs b/crates/kernel/src/tests/captree_model.rs index 373ee13..2c98237 100644 --- a/crates/kernel/src/tests/captree_model.rs +++ b/crates/kernel/src/tests/captree_model.rs @@ -163,6 +163,7 @@ impl World { Syscall::CreateProcess { create_cap: CapSlot::new(0).unwrap(), image: DataRef::empty(), + limits: super::helpers::CHILD_LIMITS, }, ); self.pids[i] = self.k.process_of_cap_for_test(cap); diff --git a/crates/kernel/src/tests/helpers.rs b/crates/kernel/src/tests/helpers.rs index b93878d..412f2e2 100644 --- a/crates/kernel/src/tests/helpers.rs +++ b/crates/kernel/src/tests/helpers.rs @@ -24,7 +24,32 @@ pub fn kernel_with_process() -> (Kernel, ProcessId) { (kernel, pid) } +/// What a test's child processes get: small, so several fit in the pool. +pub const CHILD_LIMITS: domain::budget::Limits = domain::budget::Limits { + pages: 32, + endpoints: 4, + notifications: 4, + regions: 2, +}; + impl Kernel { + /// Shrinks `pid`'s page limit to `limit` (never below its usage). + pub fn cap_pages_for_test(&mut self, pid: ProcessId, limit: usize) { + let pages = &mut self.budgets[pid.raw() as usize].pages; + assert!(pages.carve(pages.limit() - limit)); + } + + /// A process outside any budget tree, as tests used to get by default. + pub fn create_process_for_test(&mut self) -> Option { + let limits = domain::budget::Limits { + pages: 1024, + endpoints: domain::endpoint::MAX_ENDPOINTS, + notifications: domain::notification::MAX_NOTIFICATIONS, + regions: domain::memory::MAX_REGIONS, + }; + self.new_process(None, limits).ok() + } + pub fn spawn_thread_for_test( &mut self, pid: ProcessId, diff --git a/crates/kernel/src/tests/invariants.rs b/crates/kernel/src/tests/invariants.rs index 555ed6f..e410a0d 100644 --- a/crates/kernel/src/tests/invariants.rs +++ b/crates/kernel/src/tests/invariants.rs @@ -66,6 +66,7 @@ impl World { caps[i] = match k.handle_syscall(Syscall::CreateProcess { create_cap: CapSlot::new(0).unwrap(), image: DataRef::empty(), + limits: super::helpers::CHILD_LIMITS, }) { SyscallOutcome::Complete(SyscallResult::Slot(s)) => s, _ => panic!("create process"), @@ -164,6 +165,7 @@ impl World { let fresh = match self.k.handle_syscall(Syscall::CreateProcess { create_cap: CapSlot::new(0).unwrap(), image: DataRef::empty(), + limits: super::helpers::CHILD_LIMITS, }) { SyscallOutcome::Complete(SyscallResult::Slot(s)) => s, _ => panic!("re-create process"), diff --git a/crates/kernel/src/tests/ipc.rs b/crates/kernel/src/tests/ipc.rs index cb1839d..a8c1f76 100644 --- a/crates/kernel/src/tests/ipc.rs +++ b/crates/kernel/src/tests/ipc.rs @@ -42,6 +42,7 @@ impl Ipc { match k.handle_syscall(Syscall::CreateProcess { create_cap: CapSlot::new(0).unwrap(), image: DataRef::empty(), + limits: super::helpers::CHILD_LIMITS, }) { SyscallOutcome::Complete(SyscallResult::Slot(_)) => {} _ => panic!("create process"), @@ -487,7 +488,7 @@ fn a_reply_never_reaches_a_thread_that_reused_the_callers_id() { let mut t = Ipc::new(); let (ta, tb, b, b_recv) = (t.ta, t.tb, t.b, t.b_recv); // A second client process C calls, then dies while B holds the reply. - let c = t.k.create_process_internal().unwrap(); + let c = t.k.create_process_for_test().unwrap(); let tc = t.k.spawn_thread_for_test(c, Priority::DEFAULT, TimeSlice::DEFAULT); let c_send = diff --git a/crates/kernel/src/tests/ipc_invariants.rs b/crates/kernel/src/tests/ipc_invariants.rs index 738b48b..667ece6 100644 --- a/crates/kernel/src/tests/ipc_invariants.rs +++ b/crates/kernel/src/tests/ipc_invariants.rs @@ -64,8 +64,8 @@ impl World { fn new() -> Self { let (mut k, p0) = kernel_with_process(); let t0 = k.current_thread().unwrap(); - let p1 = k.create_process_internal().unwrap(); - let p2 = k.create_process_internal().unwrap(); + let p1 = k.create_process_for_test().unwrap(); + let p2 = k.create_process_for_test().unwrap(); // Endpoints and the notification belong to init, which never dies. let eps = [ k.create_endpoint(p0).unwrap(), @@ -168,7 +168,7 @@ impl World { /// Kills process `pi` and brings it back with fresh threads and caps. fn reincarnate(&mut self, pi: usize) { self.k.destroy_process(self.procs[pi].pid); - self.procs[pi].pid = self.k.create_process_internal().unwrap(); + self.procs[pi].pid = self.k.create_process_for_test().unwrap(); let t0 = self.procs[0].threads[0]; Self::equip( &mut self.k, diff --git a/crates/kernel/src/tests/isolation.rs b/crates/kernel/src/tests/isolation.rs index f432f1c..8cc6a12 100644 --- a/crates/kernel/src/tests/isolation.rs +++ b/crates/kernel/src/tests/isolation.rs @@ -61,6 +61,7 @@ fn two_processes() -> ( let cap_b = match k.handle_syscall(Syscall::CreateProcess { create_cap: CapSlot::new(0).unwrap(), image: DataRef::empty(), + limits: super::helpers::CHILD_LIMITS, }) { SyscallOutcome::Complete(SyscallResult::Slot(s)) => s, _ => panic!("create process"), @@ -219,7 +220,7 @@ fn page_tables_count_against_the_process_quota() { let (mut k, pid) = kernel_with_process(); let used = k.page_usage(pid); // Room for exactly one data page plus the leaf table it needs. - k.set_page_limit(pid, used + 2).unwrap(); + k.cap_pages_for_test(pid, used + 2); assert!(request(&mut k, 1).is_some()); assert!(request(&mut k, 1).is_none(), "quota should be exhausted"); } @@ -231,6 +232,7 @@ fn killing_a_process_returns_its_page_tables_too() { let cap_b = match k.handle_syscall(Syscall::CreateProcess { create_cap: CapSlot::new(0).unwrap(), image: DataRef::empty(), + limits: super::helpers::CHILD_LIMITS, }) { SyscallOutcome::Complete(SyscallResult::Slot(s)) => s, _ => panic!("create process"), @@ -260,7 +262,7 @@ fn map_page_fails_cleanly_when_target_quota_is_full() { let (mut k, pid_a, _, pid_b, _, cap_b) = two_processes(); let src = request(&mut k, 1).unwrap(); let used = k.page_usage(pid_b); - k.set_page_limit(pid_b, used).unwrap(); + k.cap_pages_for_test(pid_b, used); assert!(!map_page(&mut k, cap_b, src, 0x10000)); // the page stays with the caller assert!(k.platform_mut().mapping(pid_a, src).is_some()); diff --git a/crates/kernel/src/tests/lifecycle.rs b/crates/kernel/src/tests/lifecycle.rs index ced7b5a..a1a685b 100644 --- a/crates/kernel/src/tests/lifecycle.rs +++ b/crates/kernel/src/tests/lifecycle.rs @@ -47,6 +47,7 @@ fn create_process_with_cap_succeeds() { let outcome = k.handle_syscall(Syscall::CreateProcess { create_cap: CapSlot::new(0).unwrap(), image: DataRef::empty(), + limits: super::helpers::CHILD_LIMITS, }); assert!(matches!( outcome, @@ -61,6 +62,7 @@ fn create_process_without_cap_fails() { let outcome = k.handle_syscall(Syscall::CreateProcess { create_cap: CapSlot::new(5).unwrap(), image: DataRef::empty(), + limits: super::helpers::CHILD_LIMITS, }); assert!(matches!( outcome, @@ -75,6 +77,7 @@ fn kill_removes_process() { let slot = match k.handle_syscall(Syscall::CreateProcess { create_cap: CapSlot::new(0).unwrap(), image: DataRef::empty(), + limits: super::helpers::CHILD_LIMITS, }) { SyscallOutcome::Complete(SyscallResult::Slot(s)) => s, _ => panic!("expected slot"), diff --git a/crates/kernel/src/tests/lifetimes.rs b/crates/kernel/src/tests/lifetimes.rs index 27040be..f7d3663 100644 --- a/crates/kernel/src/tests/lifetimes.rs +++ b/crates/kernel/src/tests/lifetimes.rs @@ -16,6 +16,7 @@ fn create_process(k: &mut Kernel) -> CapSlot { match k.handle_syscall(Syscall::CreateProcess { create_cap: CapSlot::new(0).unwrap(), image: DataRef::empty(), + limits: super::helpers::CHILD_LIMITS, }) { SyscallOutcome::Complete(SyscallResult::Slot(s)) => s, _ => panic!("create process"), diff --git a/crates/kernel/src/tests/memory.rs b/crates/kernel/src/tests/memory.rs index fed799f..c276f0e 100644 --- a/crates/kernel/src/tests/memory.rs +++ b/crates/kernel/src/tests/memory.rs @@ -70,7 +70,7 @@ fn a_lying_page_table_cannot_hand_out_another_process_frame() { // The kernel finds a page through the platform's page table but only // believes the answer if its own frame table agrees. let (mut k, a) = kernel_with_process(); - let b = k.create_process_internal().unwrap(); + let b = k.create_process_for_test().unwrap(); k.map_fresh_range( b, VirtualAddress::from_raw(0x50000), diff --git a/crates/kernel/src/tests/mod.rs b/crates/kernel/src/tests/mod.rs index 39a26d1..473cab3 100644 --- a/crates/kernel/src/tests/mod.rs +++ b/crates/kernel/src/tests/mod.rs @@ -4,6 +4,7 @@ mod mock_platform; mod abi; mod bench; mod board; +mod budget; mod capability; mod captree_model; mod exit; diff --git a/crates/kernel/src/tests/revocation.rs b/crates/kernel/src/tests/revocation.rs index b20b655..d61ff41 100644 --- a/crates/kernel/src/tests/revocation.rs +++ b/crates/kernel/src/tests/revocation.rs @@ -59,6 +59,7 @@ impl Tree { let create = || Syscall::CreateProcess { create_cap: CapSlot::new(0).unwrap(), image: DataRef::empty(), + limits: super::helpers::CHILD_LIMITS, }; let a_to_b = slot_of(k.handle_syscall(create())); let a_to_c = slot_of(k.handle_syscall(create())); diff --git a/crates/kernel/src/tests/smp.rs b/crates/kernel/src/tests/smp.rs index b76d361..76f2945 100644 --- a/crates/kernel/src/tests/smp.rs +++ b/crates/kernel/src/tests/smp.rs @@ -96,7 +96,7 @@ fn a_busy_core_is_not_kicked_for_an_equal_priority_thread() { #[test] fn killing_a_thread_running_on_another_core_stops_that_core() { let (mut k, init) = smp(2); - let child = k.create_process_internal().unwrap(); + let child = k.create_process_for_test().unwrap(); let t = k.spawn_thread_internal(child).unwrap(); on(&mut k, core(1)); k.platform_mut().take_ipi(core(1)); diff --git a/crates/kernel/src/tests/storm.rs b/crates/kernel/src/tests/storm.rs index ae0e9a4..6bf9993 100644 --- a/crates/kernel/src/tests/storm.rs +++ b/crates/kernel/src/tests/storm.rs @@ -319,7 +319,7 @@ fn new_round(k: &mut Kernel, init: ProcessId, init_thread: ThreadI let _ = k.delete_cap(init, slot); } } - let Some(hub) = k.create_process_internal() else { + let Some(hub) = k.create_process_for_test() else { if std::env::var("STORM_DEBUG").is_ok() { std::eprintln!("no hub: free pages {}", k.free_page_count()); } @@ -346,6 +346,7 @@ fn new_round(k: &mut Kernel, init: ProcessId, init_thread: ThreadI k.handle_syscall(domain::syscall::Syscall::CreateProcess { create_cap: CapSlot::new(0).unwrap(), image: DataRef::empty(), + limits: super::helpers::CHILD_LIMITS, }) { let pid = k.process_of_cap_for_test(cap); diff --git a/crates/libkos/src/syscall/process.rs b/crates/libkos/src/syscall/process.rs index ac6212f..1c0344a 100644 --- a/crates/libkos/src/syscall/process.rs +++ b/crates/libkos/src/syscall/process.rs @@ -3,6 +3,7 @@ use super::{call0, raw, slot_of}; use types::address::{EntryPoint, StackPointer}; use types::bounded::CapSlot; +use types::budget::Limits; use types::id::ProcessId; use types::syscall_abi::*; use types::time::Ticks; @@ -38,10 +39,18 @@ pub fn get_process_id() -> ProcessId { ProcessId::from_raw(call0(SYS_GET_PROCESS_ID).map_or(0, |r| r[1]) as u16) } -pub fn create_process(create_cap: CapSlot) -> Option { +/// The new process's `limits` come out of this process's own budget. +pub fn create_process(create_cap: CapSlot, limits: Limits) -> Option { slot_of(raw( SYS_CREATE_PROCESS, - [create_cap.value() as usize, 0, 0, 0, 0, 0], + [ + create_cap.value() as usize, + limits.pages, + limits.endpoints, + limits.notifications, + limits.regions, + 0, + ], )) .ok() } diff --git a/crates/servers/session-server/src/server.rs b/crates/servers/session-server/src/server.rs index 6ac9eb5..eb6167b 100644 --- a/crates/servers/session-server/src/server.rs +++ b/crates/servers/session-server/src/server.rs @@ -1,10 +1,19 @@ use server_framework::Server; use types::bounded::CapSlot; +use types::budget::Limits; use crate::protocol::{AppId, SessionRequest, SessionResponse}; const MAX_APPS: usize = 16; +/// What each launched app may hold, out of the session server's budget. +const APP_LIMITS: Limits = Limits { + pages: 256, + endpoints: 2, + notifications: 2, + regions: 2, +}; + #[derive(Clone, Copy)] pub struct AppEntry { pub name: &'static str, @@ -77,7 +86,10 @@ impl SessionServer { if self.running_mut(app).is_none_or(|r| r.is_some()) { return SessionResponse::Error; } - let Some(process) = self.create_cap.and_then(libkos::syscall::create_process) else { + let Some(process) = self + .create_cap + .and_then(|c| libkos::syscall::create_process(c, APP_LIMITS)) + else { return SessionResponse::Error; }; if let Some(r) = self.running_mut(app) { diff --git a/crates/types/src/budget.rs b/crates/types/src/budget.rs new file mode 100644 index 0000000..fac80b7 --- /dev/null +++ b/crates/types/src/budget.rs @@ -0,0 +1,10 @@ +//! How much a process may hold, as a creator asks for it in `CreateProcess` +//! (`a1..a4`). The kernel carves it from the creator's own budget. + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct Limits { + pub pages: usize, + pub endpoints: usize, + pub notifications: usize, + pub regions: usize, +} diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index b2a15b3..e9e817c 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -4,6 +4,7 @@ pub mod address; pub mod badge; pub mod bootinfo; pub mod bounded; +pub mod budget; pub mod display; pub mod generation; pub mod id; diff --git a/docs/ipc.md b/docs/ipc.md index d3fa6d1..1cf4366 100644 --- a/docs/ipc.md +++ b/docs/ipc.md @@ -160,6 +160,20 @@ set it stays masked. - If the caller dies, the reply slot is simply stale and `Reply` is a successful no-op. +## Budgets + +Every process has a budget: frames (its pages, page tables and regions' +pages) and how many endpoints, notifications and regions it may own. Init +starts with the whole frame pool and every table's capacity. +`CreateProcess(create, pages, endpoints, notifications, regions)` carves +the child's limits out of the caller's unused budget (`OutOfMemory` if it +does not fit); boot carves the servers it loads out of init's. Over budget, +page requests fail with `OutOfMemory` and object creation with `NoFreeSlot`. +The limits of all live processes never add up to more than init got. + +When a process dies its limits go back to its parent, and its children +pass to that parent, so their budgets return there in turn. + ## Death Destroying an endpoint or notification wakes every thread blocked on it with