From 0de13f124d51d399954259dab5f42d09bea37595 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Wed, 28 Oct 2026 16:22:19 +0100 Subject: [PATCH] fix: pm running apps grow (death-bit bound 60); spawntest stress: 30 files open, apps until BUSY then room comes back; docs: limits and why --- crates/apps/spawntest/src/main.rs | 92 +++++++++++++++++++ .../servers/permission-manager/src/server.rs | 18 +++- docs/ipc.md | 24 +++++ 3 files changed, 130 insertions(+), 4 deletions(-) diff --git a/crates/apps/spawntest/src/main.rs b/crates/apps/spawntest/src/main.rs index 09557db..532f725 100644 --- a/crates/apps/spawntest/src/main.rs +++ b/crates/apps/spawntest/src/main.rs @@ -27,6 +27,9 @@ const STORAGE_BUF_VA: types::address::VirtualAddress = types::address::VirtualAd const NARROW_BUF_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x0190_0000); const REQUEST_PAGE_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x01C0_0000); const HOME_BUF_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x01B0_0000); +const CROWD_PAGE_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x01E0_0000); +/// Apps the old tables held beside Home; the test wants more than this. +const OLD_APP_ROOM: usize = 7; /// Read back from the disk image by scripts/test-qemu.sh. const NOTE: &[u8] = b"written by k-os on ext4\n"; @@ -267,6 +270,89 @@ fn restarted_after_hanging() -> Result<(), &'static str> { Ok(()) } +/// Waits about `ms` on `alarm` (woken early by anything else it carries). +fn nap(alarm: CapSlot, ms: u64) { + let at = syscall::get_time().raw() + ms * types::time::TICKS_PER_MS; + if syscall::set_timer(alarm, types::time::Ticks::from_raw(at)).is_ok() { + let _ = syscall::wait(alarm); + } +} + +/// Apps run until the permission manager says there is no room (busy, not +/// a failure), well past what the old tables held; once they are stopped, +/// the room comes back, all of it. +fn many_apps_and_their_room_comes_back(alarm: CapSlot) -> Result { + use types::apps::reply; + let apps = libkos::apps::Apps::connect(PERMISSION, CROWD_PAGE_VA).ok_or("no permission manager")?; + let mut running = [None; 32]; + let mut n = 0; + let refusal = loop { + match apps.launch(&["sleep", "300"], None, None) { + Ok((p, _)) if n < running.len() => { + running[n] = Some(p); + n += 1; + } + Ok(_) => return Err("more room than the test can hold"), + Err(r) => break r.code, + } + }; + let stop = |running: &mut [Option]| { + for p in running.iter_mut().filter_map(Option::take) { + let _ = syscall::kill(p); + syscall::drop_cap(p); + } + }; + stop(&mut running); + if refusal != reply::BUSY { + return Err("a full permission manager did not say it was busy"); + } + if n <= OLD_APP_ROOM { + return Err("no more apps ran than the old tables held"); + } + // The deaths are its to hear; give it a moment, then all of it again. + let mut again = 0; + for _ in 0..100 { + match apps.launch(&["sleep", "300"], None, None) { + Ok((p, _)) => { + running[again] = Some(p); + again += 1; + if again == n { + break; + } + } + Err(r) if r.code == reply::BUSY => nap(alarm, 50), + Err(_) => { + stop(&mut running); + return Err("launching again failed"); + } + } + } + stop(&mut running); + if again < n { + return Err("the room did not all come back"); + } + Ok(n) +} + +/// More files open at once than the old tables held (8 in ext4). +fn many_files_open_at_once(fs: &mut Storage) -> Result<(), &'static str> { + let mut open = [None; 30]; + let mut result = Ok(()); + for slot in open.iter_mut() { + match fs.open(b"selftest.txt") { + Ok(h) => *slot = Some(h), + Err(_) => { + result = Err("cannot hold 30 files open"); + break; + } + } + } + for h in open.into_iter().flatten() { + let _ = fs.close(h); + } + result +} + /// A server that runs off its stack faults on the guard below it, and is /// restarted like any other that died. fn restarted_after_overflowing() -> Result<(), &'static str> { @@ -316,6 +402,12 @@ fn check() -> Result<(), &'static str> { restarted_after_hanging()?; libkos::println!("spawntest: a hung server is noticed, stopped and restarted: ok"); + many_files_open_at_once(&mut fs)?; + libkos::println!("spawntest: 30 files open at once: ok"); + + let crowd = many_apps_and_their_room_comes_back(died)?; + libkos::println!("spawntest: {} apps at once, then busy, and the room comes back: ok", crowd); + restarted_after_overflowing()?; libkos::println!("spawntest: a server that overflows its stack is caught and restarted: ok"); Ok(()) diff --git a/crates/servers/permission-manager/src/server.rs b/crates/servers/permission-manager/src/server.rs index 3f1b5dc..b2b373a 100644 --- a/crates/servers/permission-manager/src/server.rs +++ b/crates/servers/permission-manager/src/server.rs @@ -26,7 +26,10 @@ use crate::store::DiskStore; /// An app's limits; its pages are what its manifest asks for. const PROGRAM: Limits = Limits { pages: 0, endpoints: 1, notifications: 1, regions: 1 }; /// Apps running at once; a death is a bit each on the notification. -const MAX_RUNNING: usize = 8; +/// Apps running at once, at most: each has a bit of the deaths +/// notification, and the bits from 60 up mark launcher and manager copies. +/// What there is room for is the allowance's to say, well before this. +const MAX_RUNNING: usize = 60; const MAX_ARGS: usize = 16; /// The session's desktop, started with it and again if it dies. const HOME: &str = "kos.home"; @@ -72,7 +75,8 @@ pub struct PermissionServer { /// Bound to the serving thread: deaths of running apps. deaths: CapSlot, staged: Staged, - running: [Option; MAX_RUNNING], + /// By death bit; grown as apps start. + running: Vec>, /// Home's starts this session. homes: u8, } @@ -136,7 +140,7 @@ fn parts(page: &[u8]) -> impl Iterator { impl PermissionServer { pub fn new(store: DiskStore, consent: TerminalConsent, ports: Ports, deaths: CapSlot) -> Self { let Ports { init, display, session, endpoint } = ports; - Self { store, consent, init, display, session, endpoint, deaths, staged: Staged::default(), running: [const { None }; MAX_RUNNING], homes: 0 } + Self { store, consent, init, display, session, endpoint, deaths, staged: Staged::default(), running: Vec::new(), homes: 0 } } /// What this device has a server for. @@ -210,7 +214,10 @@ impl PermissionServer { let command = *args.first().ok_or(code(reply::NOT_INSTALLED))?; let available = self.available(); let plan = policy::plan_launch(&mut self.store, command, available).map_err(refused)?; - let index = self.running.iter().position(Option::is_none).ok_or(code(reply::BUSY))?; + let index = self.running.iter().position(Option::is_none).unwrap_or(self.running.len()); + if index >= MAX_RUNNING { + return Err(code(reply::BUSY)); + } let file = self.store.binary_grant(&plan.app).ok_or(code(reply::FAILED))?; let spawned = libkos::spawn::spawn(self.init, file, Limits { pages: plan.app.memory.pages(), ..PROGRAM }); @@ -278,6 +285,9 @@ impl PermissionServer { syscall::drop_cap(child.process); return Err(code(reply::FAILED)); } + if self.running.len() <= index { + self.running.resize_with(index + 1, || None); + } self.running[index] = Some(Running { id: plan.app.id, process: child.process, granted: plan.granted, data, window, manage }); Ok((child.process, plan.granted)) } diff --git a/docs/ipc.md b/docs/ipc.md index 9976906..49742a2 100644 --- a/docs/ipc.md +++ b/docs/ipc.md @@ -365,6 +365,30 @@ registers are saved when a trap finds them changed and loaded whenever it runs; a thread without them runs with the FPU off, so it can never read another's. The kernel itself never uses the FPU. +## Limits + +What is fixed, and why (the rest grows with the server's memory): + +- **Kernel tables** (a kernel without a heap): processes 64, threads 256 + (4 a process), endpoints 256, notifications 128, regions 256, 255 + capability slots a process (`CapSlot`, a byte with room for + `start_arg`'s plus one). Kani proves the same code with tiny tables. +- **What owns a big resource is counted by it**: a window is a pixel + buffer of the display's (`types::display::window::MAX_WINDOWS`, 8), a + storage connection a buffer of storage's (`types::storage::MAX_CLIENTS`, + 16); those budgets are computed from the counts, in one place, and the + address layouts are checked against them when built. +- **Bits**: init has a death bit per spawned child (16) and per supervised + program; the permission manager one per running app (60); both are + checked when built. +- **Grown, not counted**: storage's handles, open files and grants, the + permission manager's running apps. They stop where the server's memory + or its spawn allowance does, and say so: storage answers `FULL`, init + `STORAGE_BUSY`, the permission manager `BUSY`. + +The self-test pushes past the old counts (30 files open, as many apps as +the allowance holds, a stack overflow), so a regression shows. + ## Budgets Every process has a budget: frames (its pages, page tables and regions'