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

@@ -0,0 +1,83 @@
//! The `Consent` port on the terminal: a yes/no question, answered by the
//! owner typing. (Not yet a trusted path: an app could draw a look-alike
//! prompt, though it cannot answer one.)
use libkos::io::print_to;
use manifest::{Capabilities, Capability, Manifest};
use types::bounded::CapSlot;
use crate::policy::Consent;
pub struct TerminalConsent {
pub terminal: CapSlot,
}
impl TerminalConsent {
/// Only `y` or `yes` is yes.
fn answer(&self) -> bool {
let mut line = [0u8; 16];
let n = libkos::terminal::read_line(self.terminal, &mut line);
matches!(core::str::from_utf8(&line[..n]).map(str::trim), Ok("y" | "Y" | "yes"))
}
}
/// `display, storage`.
pub struct CapabilityNames(pub Capabilities);
impl core::fmt::Display for CapabilityNames {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
for (i, c) in self.0.iter().enumerate() {
if i > 0 {
f.write_str(", ")?;
}
f.write_str(c.name())?;
}
Ok(())
}
}
impl Consent for TerminalConsent {
fn approve_required(&mut self, app: &Manifest, required: Capabilities) -> bool {
print_to(
self.terminal,
format_args!("Install {} ({} {})? It needs: {}. [y/N] ", app.name, app.id, version(app), CapabilityNames(required)),
);
self.answer()
}
fn approve_optional(&mut self, app: &Manifest, cap: Capability) -> bool {
print_to(self.terminal, format_args!("Let {} use {} (it can do without)? [y/N] ", app.name, cap.name()));
self.answer()
}
}
/// `1.0.0`.
pub fn version(app: &Manifest) -> impl core::fmt::Display {
struct V(manifest::Version);
impl core::fmt::Display for V {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
let mut v = [0u8; 18];
let mut w = Buf { buf: &mut v, len: 0 };
let _ = core::fmt::write(&mut w, format_args!("{}.{}.{}", self.0.major, self.0.minor, self.0.patch));
let len = w.len;
f.pad(core::str::from_utf8(&v[..len]).unwrap_or(""))
}
}
V(app.version)
}
/// Formats into a small buffer, so the whole can then be padded.
struct Buf<'a> {
buf: &'a mut [u8],
len: usize,
}
impl core::fmt::Write for Buf<'_> {
fn write_str(&mut self, s: &str) -> core::fmt::Result {
let end = self.len + s.len();
self.buf.get_mut(self.len..end).ok_or(core::fmt::Error)?.copy_from_slice(s.as_bytes());
self.len = end;
Ok(())
}
}

View File

@@ -1,15 +1,49 @@
//! The permission manager (docs/apps.md): the only one that grants an app
//! what its manifest asks for, as far as the owner allows. Its logic
//! (`policy`) is pure; `store`, `consent` and `server` are its adapters.
#![no_std]
#![forbid(unsafe_code)]
pub mod protocol;
extern crate alloc;
pub mod consent;
pub mod policy;
pub mod server;
pub mod store;
#[cfg(test)]
#[path = "tests/mod.rs"]
mod tests;
use libkos::fs::Storage;
use libkos::syscall;
use types::address::VirtualAddress;
use types::bounded::CapSlot;
/// Where init's manifest put them: its own endpoint, then `uses`, then
/// storage, then the spawn endpoint.
const EP: CapSlot = CapSlot::const_new(0);
const TERMINAL: CapSlot = CapSlot::const_new(1);
const DISPLAY: CapSlot = CapSlot::const_new(2);
const STORAGE: CapSlot = CapSlot::const_new(3);
const INIT: CapSlot = CapSlot::const_new(4);
const STORAGE_BUF_VA: VirtualAddress = VirtualAddress::from_raw(0x0180_0000);
const SOURCE_BUF_VA: VirtualAddress = VirtualAddress::from_raw(0x0190_0000);
pub fn run() -> ! {
let mut srv = server::PermissionManager::new();
let ep = types::bounded::CapSlot::new(0).unwrap();
server_framework::serve(&mut srv, ep)
let Ok(fs) = Storage::attach(STORAGE, STORAGE_BUF_VA) else {
libkos::println!("permission: no storage, exiting");
syscall::exit();
};
let Some(deaths) = syscall::create_notification().ok().filter(|n| syscall::bind_notification(*n).is_ok()) else {
libkos::println!("permission: no notification, exiting");
syscall::exit();
};
let store = store::DiskStore::new(fs, SOURCE_BUF_VA);
let consent = consent::TerminalConsent { terminal: TERMINAL };
let mut srv = server::PermissionServer::new(store, consent, INIT, Some(DISPLAY), deaths);
libkos::println!("permission: serving");
server_framework::serve(&mut srv, EP)
}

View File

@@ -0,0 +1,136 @@
//! What the permission manager decides, with nothing but its two ports:
//! the owner (`Consent`) and where apps and decisions live (`Store`).
//! No kernel, no IPC, no hardware: tested on the host.
use manifest::{AppId, Capabilities, Capability, Decision, Manifest};
/// Asks the owner. Answers are final: there is no "ask again later".
pub trait Consent {
/// May `app` have everything it cannot work without? No means it is
/// not installed.
fn approve_required(&mut self, app: &Manifest, required: Capabilities) -> bool;
/// May `app` have `cap`, which it can do without?
fn approve_optional(&mut self, app: &Manifest, cap: Capability) -> bool;
}
/// Installed apps, their files and the owner's decisions.
pub trait Store {
/// An app folder offered for installation.
type Source;
/// The manifest in `source`, as text.
fn source_manifest<'a>(&mut self, source: &Self::Source, buf: &'a mut [u8]) -> Option<&'a str>;
fn is_installed(&mut self, id: &AppId) -> bool;
/// Copies `source`'s binary and manifest into the app's own folder,
/// and makes its data folder if it is to have one.
fn install(&mut self, source: &Self::Source, app: &Manifest, data_folder: bool) -> bool;
fn manifest(&mut self, id: &AppId) -> Option<Manifest>;
/// The installed app run as `command`.
fn find(&mut self, command: &str) -> Option<AppId>;
fn decision(&mut self, id: &AppId) -> Option<Decision>;
fn record(&mut self, id: &AppId, decision: &Decision) -> bool;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Refusal {
/// The offered folder holds no manifest it can read.
NoManifest,
BadManifest(manifest::Error),
AlreadyInstalled,
NotInstalled,
/// This device has no server for these.
Unavailable(Capabilities),
/// The owner said no to what it requires.
Declined,
/// It does not hold these, and cannot run without them.
Missing(Capabilities),
/// It never asked for that capability.
NotRequested,
/// Reading or writing the store failed.
Store,
}
/// Longest manifest read.
pub const MAX_MANIFEST: usize = 2048;
/// Checks the app in `source` and asks the owner: required capabilities
/// all together, then each optional one this device has. Installs it and
/// records the decision; returns its manifest.
pub fn install<S: Store>(
store: &mut S,
consent: &mut impl Consent,
source: &S::Source,
available: Capabilities,
) -> Result<Manifest, Refusal> {
let mut buf = [0u8; MAX_MANIFEST];
let text = store.source_manifest(source, &mut buf).ok_or(Refusal::NoManifest)?;
let app = Manifest::parse(text).map_err(Refusal::BadManifest)?;
if store.is_installed(&app.id) {
return Err(Refusal::AlreadyInstalled);
}
let lacking = app.required.minus(available);
if !lacking.is_empty() {
return Err(Refusal::Unavailable(lacking));
}
if !app.required.is_empty() && !consent.approve_required(&app, app.required) {
return Err(Refusal::Declined);
}
let mut granted = app.required;
for cap in app.optional.intersect(available).iter() {
if consent.approve_optional(&app, cap) {
granted = granted.with(cap);
}
}
let decision = Decision { granted };
if !store.install(source, &app, granted.contains(Capability::Storage)) || !store.record(&app.id, &decision) {
return Err(Refusal::Store);
}
Ok(app)
}
/// An app about to run and what it gets.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Launch {
pub app: Manifest,
pub granted: Capabilities,
}
/// What the app run as `command` gets: what it asked for, the owner
/// granted and this device has. Refused if that leaves out anything it
/// requires (a decision changed since install, or a server is gone).
pub fn plan_launch(store: &mut impl Store, command: &str, available: Capabilities) -> Result<Launch, Refusal> {
let id = store.find(command).ok_or(Refusal::NotInstalled)?;
let app = store.manifest(&id).ok_or(Refusal::NotInstalled)?;
let decision = store.decision(&id).ok_or(Refusal::Store)?;
let granted = decision.granted.intersect(app.requested()).intersect(available);
let missing = app.required.minus(granted);
if !missing.is_empty() {
return Err(Refusal::Missing(missing));
}
Ok(Launch { app, granted })
}
/// The owner takes `cap` away from `id`; the new decision.
pub fn revoke(store: &mut impl Store, id: &AppId, cap: Capability) -> Result<Decision, Refusal> {
let decision = store.decision(id).ok_or(Refusal::NotInstalled)?;
let changed = Decision { granted: decision.granted.without(cap) };
if !store.record(id, &changed) {
return Err(Refusal::Store);
}
Ok(changed)
}
/// The owner gives `id` a capability it asked for (one withdrawn, or an
/// optional one declined at install); the new decision.
pub fn allow(store: &mut impl Store, id: &AppId, cap: Capability) -> Result<Decision, Refusal> {
let app = store.manifest(id).ok_or(Refusal::NotInstalled)?;
if !app.requested().contains(cap) {
return Err(Refusal::NotRequested);
}
let decision = store.decision(id).ok_or(Refusal::Store)?;
let changed = Decision { granted: decision.granted.with(cap) };
if !store.record(id, &changed) {
return Err(Refusal::Store);
}
Ok(changed)
}

View File

@@ -1,130 +0,0 @@
use server_framework::{Decode, Encode};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CapabilityKind {
Display,
Input,
AudioPlayback,
AudioCapture,
Camera,
Storage,
Network,
Gps,
Bluetooth,
}
impl CapabilityKind {
pub fn index(self) -> usize {
match self {
Self::Display => 0,
Self::Input => 1,
Self::AudioPlayback => 2,
Self::AudioCapture => 3,
Self::Camera => 4,
Self::Storage => 5,
Self::Network => 6,
Self::Gps => 7,
Self::Bluetooth => 8,
}
}
pub fn from_index(v: usize) -> Option<Self> {
match v {
0 => Some(Self::Display),
1 => Some(Self::Input),
2 => Some(Self::AudioPlayback),
3 => Some(Self::AudioCapture),
4 => Some(Self::Camera),
5 => Some(Self::Storage),
6 => Some(Self::Network),
7 => Some(Self::Gps),
8 => Some(Self::Bluetooth),
_ => None,
}
}
}
#[derive(Debug, PartialEq)]
pub enum PermissionRequest {
RequestCapability { process: types::id::ProcessId, kind: CapabilityKind },
RevokeCapability { process: types::id::ProcessId, kind: CapabilityKind },
CheckCapability { process: types::id::ProcessId, kind: CapabilityKind },
ListGranted { process: types::id::ProcessId },
}
#[derive(Debug, PartialEq)]
pub enum PermissionResponse {
Granted,
Denied,
Revoked,
HasCapability(bool),
GrantedList(u16),
}
impl Encode for PermissionRequest {
fn encode(&self) -> [usize; 4] {
match self {
PermissionRequest::RequestCapability { process, kind } => {
[0, process.raw() as usize, kind.index(), 0]
}
PermissionRequest::RevokeCapability { process, kind } => {
[1, process.raw() as usize, kind.index(), 0]
}
PermissionRequest::CheckCapability { process, kind } => {
[2, process.raw() as usize, kind.index(), 0]
}
PermissionRequest::ListGranted { process } => {
[3, process.raw() as usize, 0, 0]
}
}
}
}
impl Decode for PermissionRequest {
fn decode(words: [usize; 4]) -> Option<Self> {
match words[0] {
0 => Some(PermissionRequest::RequestCapability {
process: types::id::ProcessId::from_raw(words[1] as u16),
kind: CapabilityKind::from_index(words[2])?,
}),
1 => Some(PermissionRequest::RevokeCapability {
process: types::id::ProcessId::from_raw(words[1] as u16),
kind: CapabilityKind::from_index(words[2])?,
}),
2 => Some(PermissionRequest::CheckCapability {
process: types::id::ProcessId::from_raw(words[1] as u16),
kind: CapabilityKind::from_index(words[2])?,
}),
3 => Some(PermissionRequest::ListGranted {
process: types::id::ProcessId::from_raw(words[1] as u16),
}),
_ => None,
}
}
}
impl Encode for PermissionResponse {
fn encode(&self) -> [usize; 4] {
match self {
PermissionResponse::Granted => [0, 0, 0, 0],
PermissionResponse::Denied => [1, 0, 0, 0],
PermissionResponse::Revoked => [2, 0, 0, 0],
PermissionResponse::HasCapability(has) => [3, *has as usize, 0, 0],
PermissionResponse::GrantedList(bits) => [4, *bits as usize, 0, 0],
}
}
}
impl Decode for PermissionResponse {
fn decode(words: [usize; 4]) -> Option<Self> {
match words[0] {
0 => Some(PermissionResponse::Granted),
1 => Some(PermissionResponse::Denied),
2 => Some(PermissionResponse::Revoked),
3 => Some(PermissionResponse::HasCapability(words[1] != 0)),
4 => Some(PermissionResponse::GrantedList(words[1] as u16)),
_ => None,
}
}
}

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

View File

@@ -0,0 +1,196 @@
//! The `Store` port on the storage server: apps in `/apps/<id>/`, their
//! data in `/data/<id>/`, the owner's decisions in `/system/permissions/`.
use libkos::fs::Storage;
use manifest::{AppId, Decision, MAX_DECISION_TEXT, Manifest};
use types::address::VirtualAddress;
use types::bounded::CapSlot;
use crate::policy::{MAX_MANIFEST, Store};
const APPS: &str = "apps";
const DATA: &str = "data";
const DECISIONS: &str = "system/permissions";
const MANIFEST: &str = "manifest.toml";
/// Apps `find` looks through.
pub const MAX_APPS: usize = 32;
const LONGEST_PATH: usize = 160;
const COPY_CHUNK: usize = 4096;
/// `parts` joined with `/`, in `buf`.
fn path<'a>(buf: &'a mut [u8; LONGEST_PATH], parts: &[&str]) -> Option<&'a [u8]> {
let mut len = 0;
for (i, p) in parts.iter().enumerate() {
let sep = usize::from(i > 0);
let end = len + sep + p.len();
let room = buf.get_mut(len..end)?;
room[..sep].copy_from_slice(&b"/"[..sep]);
room[sep..].copy_from_slice(p.as_bytes());
len = end;
}
Some(&buf[..len])
}
/// All of the file at `path` into `buf`, as text.
fn read_text<'a>(fs: &mut Storage, path: &[u8], buf: &'a mut [u8]) -> Option<&'a str> {
let h = fs.open(path).ok()?;
let size = fs.file_size(h).ok().filter(|s| *s <= buf.len());
let read = size.and_then(|s| fs.read(h, &mut buf[..s]).ok());
let _ = fs.close(h);
let n = read.filter(|r| Some(*r) == size)?;
core::str::from_utf8(&buf[..n]).ok()
}
/// Copies the file at `from` (in `src`) to `to` (in `dst`), replacing
/// what an earlier, failed attempt left there.
fn copy(src: &mut Storage, from: &[u8], dst: &mut Storage, to: &[u8]) -> bool {
let Ok(input) = src.open(from) else { return false };
let _ = dst.delete(to);
let Ok(output) = dst.create(to) else {
let _ = src.close(input);
return false;
};
let mut chunk = [0u8; COPY_CHUNK];
let ok = loop {
match src.read(input, &mut chunk) {
Ok(0) => break true,
Ok(n) if dst.write(output, &chunk[..n]) == Ok(n) => {}
_ => break false,
}
};
let _ = src.close(input);
let _ = dst.close(output);
ok
}
pub struct DiskStore {
fs: Storage,
/// Where an offered app folder's grant is mapped while it is read.
source_va: VirtualAddress,
}
impl DiskStore {
pub fn new(fs: Storage, source_va: VirtualAddress) -> Self {
Self { fs, source_va }
}
/// Storage itself, for grants on the app's behalf.
pub fn storage(&mut self) -> &mut Storage {
&mut self.fs
}
/// The ids of the installed apps (folder names in `/apps`).
pub fn ids(&mut self, mut each: impl FnMut(AppId)) {
let _ = self.fs.list_dir(APPS.as_bytes(), |e| {
if let Some(id) = core::str::from_utf8(e.name()).ok().and_then(AppId::new) {
each(id);
}
});
}
fn with_source<T>(&self, source: CapSlot, f: impl FnOnce(&mut Storage) -> T) -> Option<T> {
let mut src = Storage::attach(source, self.source_va).ok()?;
let out = f(&mut src);
src.detach();
Some(out)
}
}
impl Store for DiskStore {
/// A read-only storage grant to the offered app's folder.
type Source = CapSlot;
fn source_manifest<'a>(&mut self, source: &CapSlot, buf: &'a mut [u8]) -> Option<&'a str> {
let mut text = [0u8; MAX_MANIFEST];
let n = self.with_source(*source, |src| read_text(src, MANIFEST.as_bytes(), &mut text).map(str::len))??;
buf.get_mut(..n)?.copy_from_slice(&text[..n]);
core::str::from_utf8(&buf[..n]).ok()
}
fn is_installed(&mut self, id: &AppId) -> bool {
let mut p = [0u8; LONGEST_PATH];
let Some(p) = path(&mut p, &[APPS, id.as_str(), MANIFEST]) else { return false };
self.fs.open(p).and_then(|h| self.fs.close(h)).is_ok()
}
fn install(&mut self, source: &CapSlot, app: &Manifest, data_folder: bool) -> bool {
let id = app.id.as_str();
let (mut folder, mut to_manifest, mut to_binary) = ([0u8; LONGEST_PATH], [0u8; LONGEST_PATH], [0u8; LONGEST_PATH]);
let (Some(folder), Some(to_manifest), Some(to_binary)) = (
path(&mut folder, &[APPS, id]),
path(&mut to_manifest, &[APPS, id, MANIFEST]),
path(&mut to_binary, &[APPS, id, app.binary.as_str()]),
) else {
return false;
};
// There already if an earlier attempt failed part way; an app
// counts as installed only once its manifest is in.
let _ = self.fs.mkdir(folder);
let fs = &mut self.fs;
let copied = Storage::attach(*source, self.source_va).ok().map(|mut src| {
// The binary first: an app folder with a manifest is installed.
let ok = copy(&mut src, app.binary.as_str().as_bytes(), fs, to_binary)
&& copy(&mut src, MANIFEST.as_bytes(), fs, to_manifest);
src.detach();
ok
});
if copied != Some(true) {
return false;
}
let mut data = [0u8; LONGEST_PATH];
// Kept from an earlier install if there is one: the app's data.
!data_folder || path(&mut data, &[DATA, id]).is_some_and(|d| self.fs.mkdir(d).is_ok() || self.fs.list_dir(d, |_| {}).is_ok())
}
fn manifest(&mut self, id: &AppId) -> Option<Manifest> {
let mut p = [0u8; LONGEST_PATH];
let p = path(&mut p, &[APPS, id.as_str(), MANIFEST])?;
let mut text = [0u8; MAX_MANIFEST];
let m = Manifest::parse(read_text(&mut self.fs, p, &mut text)?).ok()?;
// A manifest in the wrong folder is no app.
(m.id == *id).then_some(m)
}
fn find(&mut self, command: &str) -> Option<AppId> {
let mut ids = [None; MAX_APPS];
let mut n = 0;
self.ids(|id| {
if let Some(slot) = ids.get_mut(n) {
*slot = Some(id);
n += 1;
}
});
ids.into_iter().flatten().find(|id| self.manifest(id).is_some_and(|m| m.command.as_str() == command))
}
fn decision(&mut self, id: &AppId) -> Option<Decision> {
let mut p = [0u8; LONGEST_PATH];
let p = path(&mut p, &[DECISIONS, id.as_str()])?;
let mut text = [0u8; MAX_DECISION_TEXT];
Decision::parse(read_text(&mut self.fs, p, &mut text)?).ok()
}
fn record(&mut self, id: &AppId, decision: &Decision) -> bool {
let mut p = [0u8; LONGEST_PATH];
let Some(p) = path(&mut p, &[DECISIONS, id.as_str()]) else { return false };
let mut text = [0u8; MAX_DECISION_TEXT];
let n = decision.write(&mut text);
// Replaced whole: there is no truncate.
let _ = self.fs.delete(p);
let Ok(h) = self.fs.create(p) else { return false };
let written = self.fs.write(h, &text[..n]) == Ok(n);
let _ = self.fs.close(h);
written
}
}
/// The path of app `id`'s binary and its data folder, for grants.
pub fn binary_path<'a>(buf: &'a mut [u8; LONGEST_PATH], app: &Manifest) -> Option<&'a [u8]> {
path(buf, &[APPS, app.id.as_str(), app.binary.as_str()])
}
pub fn data_path<'a>(buf: &'a mut [u8; LONGEST_PATH], id: &AppId) -> Option<&'a [u8]> {
path(buf, &[DATA, id.as_str()])
}
pub const PATH_BUF: usize = LONGEST_PATH;

View File

@@ -1,2 +1 @@
mod protocol;
mod server;
mod policy;

View File

@@ -0,0 +1,204 @@
extern crate std;
use crate::policy::*;
use manifest::{AppId, Capabilities, Capability, Decision, Manifest};
use std::collections::BTreeMap;
use std::string::{String, ToString};
use std::vec::Vec;
/// Installed apps in memory; a source is the manifest text itself.
#[derive(Default)]
struct MemoryStore {
manifests: BTreeMap<String, Manifest>,
decisions: BTreeMap<String, Decision>,
data_folders: Vec<String>,
fail_writes: bool,
}
impl Store for MemoryStore {
type Source = String;
fn source_manifest<'a>(&mut self, source: &String, buf: &'a mut [u8]) -> Option<&'a str> {
if source.is_empty() {
return None;
}
let n = source.len().min(buf.len());
buf[..n].copy_from_slice(&source.as_bytes()[..n]);
core::str::from_utf8(&buf[..n]).ok()
}
fn is_installed(&mut self, id: &AppId) -> bool {
self.manifests.contains_key(id.as_str())
}
fn install(&mut self, _source: &String, app: &Manifest, data_folder: bool) -> bool {
if self.fail_writes {
return false;
}
self.manifests.insert(app.id.as_str().to_string(), *app);
if data_folder {
self.data_folders.push(app.id.as_str().to_string());
}
true
}
fn manifest(&mut self, id: &AppId) -> Option<Manifest> {
self.manifests.get(id.as_str()).copied()
}
fn find(&mut self, command: &str) -> Option<AppId> {
self.manifests.values().find(|m| m.command.as_str() == command).map(|m| m.id)
}
fn decision(&mut self, id: &AppId) -> Option<Decision> {
self.decisions.get(id.as_str()).copied()
}
fn record(&mut self, id: &AppId, decision: &Decision) -> bool {
if self.fail_writes {
return false;
}
self.decisions.insert(id.as_str().to_string(), *decision);
true
}
}
/// An owner who answers from a script, and remembers what was asked.
struct Owner {
required: bool,
optional: Vec<Capability>,
asked: Vec<String>,
}
impl Owner {
fn says(required: bool, optional: &[Capability]) -> Self {
Self { required, optional: optional.to_vec(), asked: Vec::new() }
}
}
impl Consent for Owner {
fn approve_required(&mut self, _app: &Manifest, required: Capabilities) -> bool {
self.asked.push(std::format!("required {:?}", required.iter().collect::<Vec<_>>()));
self.required
}
fn approve_optional(&mut self, _app: &Manifest, cap: Capability) -> bool {
self.asked.push(std::format!("optional {}", cap.name()));
self.optional.contains(&cap)
}
}
fn app(id: &str, command: &str, required: &str, optional: &str) -> String {
std::format!(
"[app]\nid = \"{id}\"\nname = \"App\"\nversion = \"1.0.0\"\nbinary = \"{command}\"\n[capabilities]\nrequired = [{required}]\noptional = [{optional}]\n"
)
}
const DISPLAY: Capability = Capability::Display;
const STORAGE: Capability = Capability::Storage;
const NETWORK: Capability = Capability::Network;
fn device() -> Capabilities {
Capabilities::NONE.with(DISPLAY).with(STORAGE)
}
fn set(caps: &[Capability]) -> Capabilities {
caps.iter().copied().collect()
}
fn id(s: &str) -> AppId {
AppId::new(s).unwrap()
}
#[test]
fn installing_asks_once_for_the_required_then_each_optional() {
let mut store = MemoryStore::default();
let mut owner = Owner::says(true, &[STORAGE]);
let src = app("kos.snake", "snake", "\"display\"", "\"storage\"");
let m = install(&mut store, &mut owner, &src, device()).unwrap();
assert_eq!(owner.asked, ["required [Display]", "optional storage"]);
assert_eq!(store.decision(&m.id), Some(Decision { granted: set(&[DISPLAY, STORAGE]) }));
assert_eq!(store.data_folders, ["kos.snake"], "storage granted: it gets a data folder");
}
#[test]
fn an_app_needing_nothing_is_installed_without_asking() {
let mut store = MemoryStore::default();
let mut owner = Owner::says(false, &[]);
install(&mut store, &mut owner, &app("kos.cat", "cat", "", ""), device()).unwrap();
assert!(owner.asked.is_empty());
assert!(store.data_folders.is_empty(), "no storage, no data folder");
}
#[test]
fn declining_the_required_installs_nothing() {
let mut store = MemoryStore::default();
let mut owner = Owner::says(false, &[]);
let src = app("kos.snake", "snake", "\"display\"", "");
assert_eq!(install(&mut store, &mut owner, &src, device()), Err(Refusal::Declined));
assert!(store.manifests.is_empty() && store.decisions.is_empty());
}
#[test]
fn what_the_device_lacks_is_refused_or_never_asked() {
let mut store = MemoryStore::default();
let mut owner = Owner::says(true, &[NETWORK]);
let needs_network = app("kos.chat", "chat", "\"network\"", "");
assert_eq!(install(&mut store, &mut owner, &needs_network, device()), Err(Refusal::Unavailable(set(&[NETWORK]))));
assert!(owner.asked.is_empty(), "nothing asked for an app that cannot run here");
let wants_network = app("kos.notes", "notes", "", "\"network\"");
install(&mut store, &mut owner, &wants_network, device()).unwrap();
assert!(owner.asked.is_empty(), "an optional capability the device lacks is not offered");
assert_eq!(store.decision(&id("kos.notes")), Some(Decision::default()));
}
#[test]
fn bad_or_duplicate_apps_are_refused() {
let mut store = MemoryStore::default();
let mut owner = Owner::says(true, &[]);
assert!(matches!(install(&mut store, &mut owner, &"[app]\n".to_string(), device()), Err(Refusal::BadManifest(_))));
assert_eq!(install(&mut store, &mut owner, &String::new(), device()), Err(Refusal::NoManifest));
let src = app("kos.cat", "cat", "", "");
install(&mut store, &mut owner, &src, device()).unwrap();
assert_eq!(install(&mut store, &mut owner, &src, device()), Err(Refusal::AlreadyInstalled));
}
#[test]
fn a_failing_store_is_reported() {
let mut store = MemoryStore { fail_writes: true, ..MemoryStore::default() };
let mut owner = Owner::says(true, &[]);
assert_eq!(install(&mut store, &mut owner, &app("kos.cat", "cat", "", ""), device()), Err(Refusal::Store));
}
#[test]
fn a_launch_gets_what_was_asked_granted_and_present() {
let mut store = MemoryStore::default();
let mut owner = Owner::says(true, &[STORAGE]);
install(&mut store, &mut owner, &app("kos.snake", "snake", "\"display\"", "\"storage\""), device()).unwrap();
let plan = plan_launch(&mut store, "snake", device()).unwrap();
assert_eq!(plan.granted, set(&[DISPLAY, STORAGE]));
// A decision listing more than the app asked for grants no more.
store.decisions.insert("kos.snake".to_string(), Decision { granted: set(&[DISPLAY, STORAGE, NETWORK]) });
let plan = plan_launch(&mut store, "snake", device().with(NETWORK)).unwrap();
assert_eq!(plan.granted, set(&[DISPLAY, STORAGE]));
assert_eq!(plan_launch(&mut store, "nope", device()), Err(Refusal::NotInstalled));
}
#[test]
fn revoking_and_allowing_change_the_next_launch() {
let mut store = MemoryStore::default();
let mut owner = Owner::says(true, &[]);
install(&mut store, &mut owner, &app("kos.snake", "snake", "\"display\"", "\"storage\""), device()).unwrap();
assert_eq!(plan_launch(&mut store, "snake", device()).unwrap().granted, set(&[DISPLAY]));
allow(&mut store, &id("kos.snake"), STORAGE).unwrap();
assert_eq!(plan_launch(&mut store, "snake", device()).unwrap().granted, set(&[DISPLAY, STORAGE]));
assert_eq!(allow(&mut store, &id("kos.snake"), NETWORK), Err(Refusal::NotRequested), "only what it asked for");
revoke(&mut store, &id("kos.snake"), DISPLAY).unwrap();
assert_eq!(plan_launch(&mut store, "snake", device()), Err(Refusal::Missing(set(&[DISPLAY]))), "a required one gone: it cannot run");
assert_eq!(revoke(&mut store, &id("kos.nope"), DISPLAY), Err(Refusal::NotInstalled));
}
#[test]
fn a_server_gone_since_install_stops_an_app_that_requires_it() {
let mut store = MemoryStore::default();
let mut owner = Owner::says(true, &[]);
install(&mut store, &mut owner, &app("kos.snake", "snake", "\"display\"", ""), device()).unwrap();
let headless = Capabilities::NONE.with(STORAGE);
assert_eq!(plan_launch(&mut store, "snake", headless), Err(Refusal::Missing(set(&[DISPLAY]))));
}

View File

@@ -1,62 +0,0 @@
use crate::protocol::*;
use server_framework::{Decode, Encode};
use types::id::ProcessId;
fn pid(v: u16) -> ProcessId { ProcessId::from_raw(v) }
#[test]
fn request_capability_roundtrip() {
let req = PermissionRequest::RequestCapability { process: pid(5), kind: CapabilityKind::Camera };
assert_eq!(PermissionRequest::decode(req.encode()), Some(req));
}
#[test]
fn revoke_capability_roundtrip() {
let req = PermissionRequest::RevokeCapability { process: pid(3), kind: CapabilityKind::Network };
assert_eq!(PermissionRequest::decode(req.encode()), Some(req));
}
#[test]
fn check_capability_roundtrip() {
let req = PermissionRequest::CheckCapability { process: pid(1), kind: CapabilityKind::Storage };
assert_eq!(PermissionRequest::decode(req.encode()), Some(req));
}
#[test]
fn list_granted_roundtrip() {
let req = PermissionRequest::ListGranted { process: pid(7) };
assert_eq!(PermissionRequest::decode(req.encode()), Some(req));
}
#[test]
fn all_kinds_roundtrip() {
let kinds = [
CapabilityKind::Display, CapabilityKind::Input, CapabilityKind::AudioPlayback,
CapabilityKind::AudioCapture, CapabilityKind::Camera, CapabilityKind::Storage,
CapabilityKind::Network, CapabilityKind::Gps, CapabilityKind::Bluetooth,
];
for kind in kinds {
let req = PermissionRequest::RequestCapability { process: pid(0), kind };
assert_eq!(PermissionRequest::decode(req.encode()), Some(req));
}
}
#[test]
fn response_roundtrips() {
let responses = [
PermissionResponse::Granted,
PermissionResponse::Denied,
PermissionResponse::Revoked,
PermissionResponse::HasCapability(true),
PermissionResponse::HasCapability(false),
PermissionResponse::GrantedList(0b101),
];
for resp in responses {
assert_eq!(PermissionResponse::decode(resp.encode()), Some(resp));
}
}
#[test]
fn invalid_request_returns_none() {
assert!(PermissionRequest::decode([99, 0, 0, 0]).is_none());
}

View File

@@ -1,87 +0,0 @@
use crate::protocol::*;
use crate::server::PermissionManager;
use server_framework::Server;
use types::id::ProcessId;
fn pid(v: u16) -> ProcessId { ProcessId::from_raw(v) }
#[test]
fn new_manager_has_no_grants() {
let mut mgr = PermissionManager::new();
let resp = mgr.handle(PermissionRequest::CheckCapability {
process: pid(0),
kind: CapabilityKind::Camera,
});
assert_eq!(resp, PermissionResponse::HasCapability(false));
}
#[test]
fn grant_then_check() {
let mut mgr = PermissionManager::new();
mgr.handle(PermissionRequest::RequestCapability {
process: pid(1),
kind: CapabilityKind::Network,
});
let resp = mgr.handle(PermissionRequest::CheckCapability {
process: pid(1),
kind: CapabilityKind::Network,
});
assert_eq!(resp, PermissionResponse::HasCapability(true));
}
#[test]
fn grant_does_not_affect_other_process() {
let mut mgr = PermissionManager::new();
mgr.handle(PermissionRequest::RequestCapability {
process: pid(1),
kind: CapabilityKind::Camera,
});
let resp = mgr.handle(PermissionRequest::CheckCapability {
process: pid(2),
kind: CapabilityKind::Camera,
});
assert_eq!(resp, PermissionResponse::HasCapability(false));
}
#[test]
fn revoke_removes_grant() {
let mut mgr = PermissionManager::new();
mgr.handle(PermissionRequest::RequestCapability {
process: pid(0),
kind: CapabilityKind::Storage,
});
mgr.handle(PermissionRequest::RevokeCapability {
process: pid(0),
kind: CapabilityKind::Storage,
});
let resp = mgr.handle(PermissionRequest::CheckCapability {
process: pid(0),
kind: CapabilityKind::Storage,
});
assert_eq!(resp, PermissionResponse::HasCapability(false));
}
#[test]
fn list_granted_returns_bitfield() {
let mut mgr = PermissionManager::new();
mgr.handle(PermissionRequest::RequestCapability {
process: pid(0),
kind: CapabilityKind::Display,
});
mgr.handle(PermissionRequest::RequestCapability {
process: pid(0),
kind: CapabilityKind::Network,
});
let resp = mgr.handle(PermissionRequest::ListGranted { process: pid(0) });
assert_eq!(resp, PermissionResponse::GrantedList(0b1000001));
}
#[test]
fn out_of_range_process_denied() {
let mut mgr = PermissionManager::new();
let resp = mgr.handle(PermissionRequest::RequestCapability {
process: pid(999),
kind: CapabilityKind::Camera,
});
assert_eq!(resp, PermissionResponse::Denied);
}