478 lines
18 KiB
Rust
478 lines
18 KiB
Rust
//! 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<CapSlot>,
|
|
folder: Option<CapSlot>,
|
|
}
|
|
|
|
struct Running {
|
|
id: AppId,
|
|
process: CapSlot,
|
|
granted: Capabilities,
|
|
/// Its data folder grant, taken back when it dies.
|
|
data: Option<CapSlot>,
|
|
/// Its window, closed when it dies.
|
|
window: Option<CapSlot>,
|
|
/// The management copy of this server's endpoint (`permissions`),
|
|
/// revoked when it dies.
|
|
manage: Option<CapSlot>,
|
|
}
|
|
|
|
pub struct PermissionServer {
|
|
store: DiskStore,
|
|
consent: TerminalConsent,
|
|
/// Init's spawn endpoint.
|
|
init: CapSlot,
|
|
display: Option<CapSlot>,
|
|
/// Init's session (who is signed in, signing out), for apps granted
|
|
/// `session`.
|
|
session: Option<CapSlot>,
|
|
/// 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<Running>; 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<Vec<u8>> {
|
|
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<Item = &str> {
|
|
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<CapSlot>) -> [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<Give>) {
|
|
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<CapSlot>) -> [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<u8>);
|
|
|
|
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<Self> {
|
|
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<CapSlot>) -> ([usize; 4], Option<Give>) {
|
|
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<CapSlot>) {
|
|
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<CapSlot>) {
|
|
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<CapSlot>,
|
|
pub session: Option<CapSlot>,
|
|
/// Its own endpoint.
|
|
pub endpoint: CapSlot,
|
|
}
|