//! The permission manager over IPC (`types::apps`): the policy, with the //! storage server as its store, the terminal as the owner, and init to //! load what it runs. use alloc::vec; use alloc::vec::Vec; use libkos::syscall::{self, Give}; use libkos::window; use manifest::{AppId, Capabilities, Capability}; use server_framework::{MALFORMED, Server}; use types::address::{ByteOffset, VirtualAddress}; use types::apps::{self as wire, PAGE_SIZE, reply}; use types::badge::Badge; use types::bounded::CapSlot; use types::budget::Limits; use types::mapping::PagePerms; use types::permission::{Permission, PermissionSet}; use types::display::window::role as display_role; use types::start_arg::StartArg; use crate::consent::{CapabilityNames, TerminalConsent, version}; use crate::policy::{self, Refusal, Store}; use crate::store::DiskStore; /// What each app gets to run in, out of the permission manager's allowance. /// 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; const MAX_ARGS: usize = 16; /// The session's desktop, started with it and again if it dies. const HOME: &str = "kos.home"; /// Home's starts in a session at most: one that keeps dying is left down. const MAX_HOMES: u8 = 5; /// A launch that no launcher asked for (Home's): nothing staged is its. const NO_LAUNCHER: usize = usize::MAX; /// Where a client's request page is mapped during its call. const PAGE_VA: VirtualAddress = VirtualAddress::from_raw(0x01A0_0000); /// What a launcher passed for its next launch. #[derive(Default)] struct Staged { badge: usize, output: Option, folder: Option, } struct Running { id: AppId, process: CapSlot, granted: Capabilities, /// Its data folder grant, taken back when it dies. data: Option, /// Its window, closed when it dies. window: Option, /// The management copy of this server's endpoint (`permissions`), /// revoked when it dies. manage: Option, } pub struct PermissionServer { store: DiskStore, consent: TerminalConsent, /// Init's spawn endpoint. init: CapSlot, display: Option, /// Init's session (who is signed in, signing out), for apps granted /// `session`. session: Option, /// This server's own endpoint, which management copies are minted from. endpoint: CapSlot, /// Bound to the serving thread: deaths of running apps. deaths: CapSlot, staged: Staged, running: [Option; MAX_RUNNING], /// Home's starts this session. homes: u8, } fn refused(r: Refusal) -> [usize; 4] { let (code, detail) = match r { Refusal::NoManifest => (reply::NO_MANIFEST, 0), Refusal::BadManifest(e) => (reply::BAD_MANIFEST, line_of(e)), Refusal::AlreadyInstalled => (reply::ALREADY_INSTALLED, 0), Refusal::NotInstalled => (reply::NOT_INSTALLED, 0), Refusal::Unavailable(c) => (reply::UNAVAILABLE, c.bits() as usize), Refusal::Declined => (reply::DECLINED, 0), Refusal::Missing(c) => (reply::MISSING, c.bits() as usize), Refusal::NotRequested => (reply::NOT_REQUESTED, 0), Refusal::SystemOnly(c) => (reply::SYSTEM_ONLY, c.bits() as usize), Refusal::Store => (reply::FAILED, 0), }; [code, detail, 0, 0] } fn line_of(e: manifest::Error) -> usize { use manifest::Error::*; match e { Syntax { line } | Duplicate { line } | Unknown { line } | Invalid { line } => line, Missing(_) | Conflict => 0, } } const fn code(c: usize) -> [usize; 4] { [c, 0, 0, 0] } /// The client's page, copied out; the mapping goes again at once. fn read_page(page: CapSlot) -> Option> { let w = window::map(page, PAGE_VA, PagePerms::ReadWrite).ok(); let out = w.map(|w| { let mut buf = vec![0u8; PAGE_SIZE.min(w.len())]; w.copy_to(ByteOffset::ZERO, &mut buf); buf }); syscall::drop_cap(page); out } /// `text` into the client's page. fn write_page(page: CapSlot, text: &[u8]) -> bool { let w = window::map(page, PAGE_VA, PagePerms::ReadWrite).ok(); let ok = w.is_some_and(|w| text.len() <= w.len() && { w.copy_from(ByteOffset::ZERO, text); true }); syscall::drop_cap(page); ok } /// The NUL-separated parts of a page, up to the first empty one. fn parts(page: &[u8]) -> impl Iterator { page.split(|b| *b == 0).take_while(|p| !p.is_empty()).map_while(|p| core::str::from_utf8(p).ok()) } 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 } } /// What this device has a server for. fn available(&self) -> Capabilities { let mut c = Capabilities::NONE.with(Capability::Storage).with(Capability::Permissions).with(Capability::Launch); if self.display.is_some() { c = c.with(Capability::Display); } if self.session.is_some() { c = c.with(Capability::Session); } c } fn stage(&mut self, badge: usize, op: usize, cap: Option) -> [usize; 4] { if self.staged.badge != badge { self.unstage(); self.staged.badge = badge; } let slot = if op == wire::PASS_OUTPUT { &mut self.staged.output } else { &mut self.staged.folder }; if let Some(old) = core::mem::replace(slot, cap) { syscall::drop_cap(old); } code(reply::OK) } fn unstage(&mut self) { for cap in [self.staged.output.take(), self.staged.folder.take()].into_iter().flatten() { syscall::drop_cap(cap); } } fn launch(&mut self, badge: usize, page: CapSlot) -> ([usize; 4], Option) { let result = self.try_launch(badge, page); self.unstage(); match result { Ok((process, granted)) => ([reply::OK, granted.bits() as usize, 0, 0], Some(process.into())), Err(words) => (words, None), } } fn try_launch(&mut self, badge: usize, page: CapSlot) -> Result<(CapSlot, Capabilities), [usize; 4]> { let page = read_page(page).ok_or(code(reply::FAILED))?; let mut args = [""; MAX_ARGS]; let mut n = 0; for (slot, p) in args.iter_mut().zip(parts(&page)) { *slot = p; n += 1; } self.run(badge, &args[..n]) } /// Starts Home, the session's desktop, unless it runs already or has /// died too often. pub fn start_home(&mut self) { let running = self.running.iter().flatten().any(|r| r.id.as_str() == HOME); if running || self.homes >= MAX_HOMES { return; } self.homes += 1; match self.run(NO_LAUNCHER, &[HOME]) { Ok((process, _)) => { syscall::drop_cap(process); } Err(words) => libkos::println!("permission: home did not start (refused: {})", words[0]), } } /// Runs `args[0]` with `args` for the launcher behind `badge`. fn run(&mut self, badge: usize, args: &[&str]) -> Result<(CapSlot, Capabilities), [usize; 4]> { 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 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 }); let _ = libkos::fs::release(file); let child = spawned.map_err(|e| match e { libkos::spawn::SpawnError::OverAllowance => code(reply::BUSY), _ => code(reply::FAILED), })?; let call = PermissionSet::empty().with(Permission::Execute); let data = match plan.granted.contains(Capability::Storage) { true => self.store.data_grant(&plan.app.id), false => None, }; // Opened here, named from its manifest: the app cannot name its own. let window = match plan.granted.contains(Capability::Display) { true => self.display.and_then(|d| libkos::display::open(d, plan.app.name.as_str().as_bytes())).map(|o| o.window), false => None, }; // Its brand colour too, which the display adapts to the theme, and // what it is: Home, or an app the user may close. if let Some(w) = window { if let Some(accent) = plan.app.accent { libkos::display::set_accent(w, accent.0); } let role = if plan.app.id.as_str() == HOME { display_role::HOME } else { display_role::APP }; libkos::display::set_role(w, role); } // Managing others, or launching them, through a copy of this // endpoint that can do no more (see `MANAGE_BADGE`, `LAUNCH_BADGE`). let bits = [(Capability::Permissions, wire::MANAGE_BADGE), (Capability::Launch, wire::LAUNCH_BADGE)] .iter() .filter(|(c, _)| plan.granted.contains(*c)) .fold(0, |b, (_, bit)| b | bit); let manage = match bits { 0 => None, bits => { let perms = PermissionSet::empty().with(Permission::Execute).with(Permission::Delegate); syscall::mint(self.endpoint, types::badge::Badge::new(bits | index), perms).ok() } }; let session = self.session.filter(|_| plan.granted.contains(Capability::Session)); let staged = (badge == self.staged.badge).then_some(&self.staged); let arg = StartArg { log: staged.and_then(|s| s.output).and_then(|o| child.give(o, call)).or(child.log()), storage: staged.and_then(|s| s.folder).and_then(|f| child.give(f, call)), data: data.and_then(|d| child.give(d, call)), // It subscribes to input by sending the display a notification. // Delegate, to send its notification when it subscribes. display: window.and_then(|w| child.give_sending(w, call)), args: child.set_args(args), console: None, // Delegate, to lend its request page with each call. permissions: manage.and_then(|m| child.give_sending(m, call)), session: session.and_then(|s| child.give(s, call)), }; let watched = self.watch(child.process, index); if !watched || !child.start_with(arg) { let _ = syscall::kill(child.process); if let Some(d) = data { let _ = libkos::fs::release(d); } close_window(window); revoke(manage); syscall::drop_cap(child.process); return Err(code(reply::FAILED)); } self.running[index] = Some(Running { id: plan.app.id, process: child.process, granted: plan.granted, data, window, manage }); Ok((child.process, plan.granted)) } /// Has `process`'s death signal bit `index`. 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 } fn install(&mut self, source: Option) -> [usize; 4] { let Some(source) = source else { return code(reply::FAILED) }; let available = self.available(); let result = policy::install(&mut self.store, &mut self.consent, &source, available); syscall::drop_cap(source); match result { Ok(app) => { let granted = self.store.decision(&app.id).map_or(0, |d| d.granted.bits() as usize); [reply::OK, granted, 0, 0] } Err(r) => refused(r), } } fn list(&mut self, page: CapSlot) -> [usize; 4] { use core::fmt::Write; let mut ids = Vec::new(); self.store.ids(|id| ids.push(id)); let mut text = Text(Vec::new()); for id in ids { let (Some(app), Some(d)) = (self.store.manifest(&id), self.store.decision(&id)) else { continue }; let _ = writeln!(text, "{:<20} {:<10} {:<8} {}", app.id, app.command.as_str(), version(&app), CapabilityNames(d.granted)); } let len = text.0.len().min(PAGE_SIZE); if write_page(page, &text.0[..len]) { [reply::OK, len, 0, 0] } else { code(reply::FAILED) } } /// Every app, for a program to read (`types::apps::APPS`). fn apps(&mut self, page: CapSlot) -> [usize; 4] { use core::fmt::Write; let mut ids = Vec::new(); self.store.ids(|id| ids.push(id)); let mut text = Text(Vec::new()); for id in ids { let (Some(app), Some(d)) = (self.store.manifest(&id), self.store.decision(&id)) else { continue }; let _ = writeln!(text, "{}\t{}\t{}\t{}\t{}", app.id, app.name.as_str(), app.required.bits(), app.optional.bits(), d.granted.bits()); } let len = text.0.len().min(PAGE_SIZE); if write_page(page, &text.0[..len]) { [reply::OK, len, 0, 0] } else { code(reply::FAILED) } } /// The owner changes app `id`'s (in the page) capability `index`. fn change(&mut self, page: CapSlot, index: usize, allow: bool) -> [usize; 4] { let page = read_page(page); let id = page.as_deref().and_then(|p| parts(p).next()).and_then(AppId::new); let (Some(id), Some(cap)) = (id, Capability::from_index(index)) else { return code(reply::NOT_INSTALLED); }; let result = match allow { true => policy::allow(&mut self.store, &id, cap), false => policy::revoke(&mut self.store, &id, cap), }; if let Err(r) = result { return refused(r); } if !allow { // Its access dies at once: a running app that had it stops. for r in self.running.iter().flatten().filter(|r| r.id == id && r.granted.contains(cap)) { let _ = syscall::kill(r.process); } } code(reply::OK) } /// Apps behind `bits` died: their data grants go back. Home comes back. fn reap(&mut self, bits: usize) { let home_died = self.running.iter().enumerate().any(|(i, r)| bits & (1 << i) != 0 && r.as_ref().is_some_and(|r| r.id.as_str() == HOME)); for (i, slot) in self.running.iter_mut().enumerate() { if bits & (1 << i) == 0 { continue; } if let Some(r) = slot.take() { if let Some(d) = r.data { let _ = libkos::fs::release(d); } close_window(r.window); revoke(r.manage); syscall::drop_cap(r.process); } } if home_died { self.start_home(); } } } /// Text built for the client's page. struct Text(Vec); impl core::fmt::Write for Text { fn write_str(&mut self, s: &str) -> core::fmt::Result { self.0.extend_from_slice(s.as_bytes()); Ok(()) } } /// Requests carry their own words; there is nothing to decode up front. pub struct NoRequest; impl server_framework::Decode for NoRequest { fn decode(_: [usize; 4]) -> Option { None } } impl server_framework::Encode for NoRequest { fn encode(&self) -> [usize; 4] { [MALFORMED, 0, 0, 0] } } impl Server for PermissionServer { type Request = NoRequest; type Response = NoRequest; fn handle(&mut self, _: NoRequest) -> NoRequest { NoRequest } fn handle_call(&mut self, badge: usize, words: [usize; 4], cap: Option) -> ([usize; 4], Option) { if !allowed(badge, words[0]) { if let Some(c) = cap { syscall::drop_cap(c); } return ([MALFORMED, 0, 0, 0], None); } match (words[0], cap) { (wire::PASS_OUTPUT | wire::PASS_FOLDER, cap) => (self.stage(badge, words[0], cap), None), (wire::LAUNCH, Some(page)) => self.launch(badge, page), (wire::INSTALL, source) => (self.install(source), None), (wire::LIST, Some(page)) => (self.list(page), None), (wire::APPS, Some(page)) => (self.apps(page), None), (wire::REVOKE, Some(page)) => (self.change(page, words[1], false), None), (wire::ALLOW, Some(page)) => (self.change(page, words[1], true), None), (_, cap) => { if let Some(c) = cap { syscall::drop_cap(c); } ([MALFORMED, 0, 0, 0], None) } } } fn signal(&mut self, bits: usize) { self.reap(bits); } } fn close_window(window: Option) { if let Some(w) = window { libkos::display::close(w); syscall::drop_cap(w); } } /// Whether `op` may come through the copy of this endpoint badged `badge`: /// a management copy (`permissions`) decides, never installs or launches; /// a launcher's (`launch`) lists and launches, passing nothing along. pub fn allowed(badge: usize, op: usize) -> bool { let (manage, launch) = (badge & wire::MANAGE_BADGE != 0, badge & wire::LAUNCH_BADGE != 0); if !manage && !launch { return true; } (manage && matches!(op, wire::LIST | wire::APPS | wire::REVOKE | wire::ALLOW)) || (launch && matches!(op, wire::APPS | wire::LAUNCH)) } fn revoke(cap: Option) { if let Some(c) = cap { let _ = syscall::revoke(c); syscall::drop_cap(c); } } /// What the permission manager was given to reach others. pub struct Ports { /// Init's spawn endpoint. pub init: CapSlot, pub display: Option, pub session: Option, /// Its own endpoint. pub endpoint: CapSlot, }