//! 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::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. const PROGRAM: Limits = Limits { pages: 128, 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; /// 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, } pub struct PermissionServer { store: DiskStore, consent: TerminalConsent, /// Init's spawn endpoint. init: CapSlot, display: Option, /// Bound to the serving thread: deaths of running apps. deaths: CapSlot, staged: Staged, running: [Option; MAX_RUNNING], } 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::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, init: CapSlot, display: Option, deaths: CapSlot) -> Self { Self { store, consent, init, display, deaths, staged: Staged::default(), running: [const { None }; MAX_RUNNING] } } /// What this device has a server for. fn available(&self) -> Capabilities { let base = Capabilities::NONE.with(Capability::Storage); if self.display.is_some() { base.with(Capability::Display) } else { base } } 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; } let args = &args[..n]; 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, 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, }; 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, }; 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); 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 }); 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) } } /// 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. fn reap(&mut self, bits: usize) { 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); syscall::drop_cap(r.process); } } } } /// 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) { 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::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); } }