feat(apps): permission manager equips apps (manifest+consent+decisions in /system), launches via init w/ file grant; shell unprivileged (/home); install/apps/allow/revoke; revoke stops running app; app-image tool; counter app
This commit is contained in:
@@ -1,69 +1,335 @@
|
||||
use server_framework::Server;
|
||||
use types::id::ProcessId;
|
||||
//! 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 crate::protocol::{PermissionRequest, PermissionResponse};
|
||||
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;
|
||||
|
||||
const MAX_PROCESSES: usize = 64;
|
||||
const NUM_KINDS: usize = 9;
|
||||
use crate::consent::{CapabilityNames, TerminalConsent, version};
|
||||
use crate::policy::{self, Refusal, Store};
|
||||
use crate::store::{self, DiskStore, PATH_BUF};
|
||||
|
||||
pub struct PermissionManager {
|
||||
grants: [[bool; NUM_KINDS]; MAX_PROCESSES],
|
||||
/// 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<CapSlot>,
|
||||
folder: Option<CapSlot>,
|
||||
}
|
||||
|
||||
impl PermissionManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
grants: [[false; NUM_KINDS]; MAX_PROCESSES],
|
||||
}
|
||||
}
|
||||
struct Running {
|
||||
id: AppId,
|
||||
process: CapSlot,
|
||||
granted: Capabilities,
|
||||
/// Its data folder grant, taken back when it dies.
|
||||
data: Option<CapSlot>,
|
||||
}
|
||||
|
||||
fn granted_bits(&self, process: ProcessId) -> u16 {
|
||||
let idx = process.raw() as usize;
|
||||
if idx >= MAX_PROCESSES {
|
||||
return 0;
|
||||
}
|
||||
let mut bits = 0u16;
|
||||
for i in 0..NUM_KINDS {
|
||||
if self.grants[idx][i] {
|
||||
bits |= 1 << i;
|
||||
}
|
||||
}
|
||||
bits
|
||||
pub struct PermissionServer {
|
||||
store: DiskStore,
|
||||
consent: TerminalConsent,
|
||||
/// Init's spawn endpoint.
|
||||
init: CapSlot,
|
||||
display: Option<CapSlot>,
|
||||
/// Bound to the serving thread: deaths of running apps.
|
||||
deaths: CapSlot,
|
||||
staged: Staged,
|
||||
running: [Option<Running>; 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,
|
||||
}
|
||||
}
|
||||
|
||||
impl Server for PermissionManager {
|
||||
type Request = PermissionRequest;
|
||||
type Response = PermissionResponse;
|
||||
const fn code(c: usize) -> [usize; 4] {
|
||||
[c, 0, 0, 0]
|
||||
}
|
||||
|
||||
fn handle(&mut self, request: PermissionRequest) -> PermissionResponse {
|
||||
match request {
|
||||
PermissionRequest::RequestCapability { process, kind } => {
|
||||
let idx = process.raw() as usize;
|
||||
if idx >= MAX_PROCESSES {
|
||||
return PermissionResponse::Denied;
|
||||
}
|
||||
self.grants[idx][kind.index()] = true;
|
||||
PermissionResponse::Granted
|
||||
/// 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, init: CapSlot, display: Option<CapSlot>, 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<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;
|
||||
}
|
||||
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 mut p = [0u8; PATH_BUF];
|
||||
let binary = store::binary_path(&mut p, &plan.app).ok_or(code(reply::FAILED))?;
|
||||
let file = self.store.storage().grant(binary, false).map_err(|_| 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 => store::data_path(&mut p, &plan.app.id).and_then(|d| self.store.storage().grant(d, true).ok()),
|
||||
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.
|
||||
display: self.display.filter(|_| plan.granted.contains(Capability::Display)).and_then(|d| child.give_sending(d, 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);
|
||||
}
|
||||
PermissionRequest::RevokeCapability { process, kind } => {
|
||||
let idx = process.raw() as usize;
|
||||
if idx >= MAX_PROCESSES {
|
||||
return PermissionResponse::Denied;
|
||||
}
|
||||
self.grants[idx][kind.index()] = false;
|
||||
PermissionResponse::Revoked
|
||||
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 });
|
||||
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]
|
||||
}
|
||||
PermissionRequest::CheckCapability { process, kind } => {
|
||||
let idx = process.raw() as usize;
|
||||
if idx >= MAX_PROCESSES {
|
||||
return PermissionResponse::HasCapability(false);
|
||||
}
|
||||
PermissionResponse::HasCapability(self.grants[idx][kind.index()])
|
||||
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);
|
||||
}
|
||||
PermissionRequest::ListGranted { process } => {
|
||||
PermissionResponse::GrantedList(self.granted_bits(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);
|
||||
}
|
||||
syscall::drop_cap(r.process);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user