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:
2026-10-27 22:56:44 +01:00
parent 9bc31a9bbe
commit 90e11eb2e8
51 changed files with 1766 additions and 644 deletions

View File

@@ -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);
}
}