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,6 @@
[app]
id = "kos.cat"
name = "cat"
version = "1.0.0"
binary = "cat"
author = "K-OS"

View File

@@ -0,0 +1,14 @@
[package]
name = "counter"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "counter"
test = false
bench = false
[dependencies]
types = { workspace = true }
libkos = { workspace = true }

View File

@@ -0,0 +1,10 @@
# Remembers how often it ran, if the owner lets it keep a file.
[app]
id = "kos.counter"
name = "Counter"
version = "1.0.0"
binary = "counter"
author = "K-OS"
[capabilities]
optional = ["storage"]

View File

@@ -0,0 +1,59 @@
//! Counts its runs in its own data folder: the `storage` capability, which
//! it can do without (then it remembers nothing).
#![no_std]
#![forbid(unsafe_code)]
#![no_main]
extern crate libkos;
use libkos::fs::Storage;
use libkos::println;
use types::address::VirtualAddress;
const DATA_BUF_VA: VirtualAddress = VirtualAddress::from_raw(0x0180_0000);
const COUNT: &[u8] = b"count";
/// The number in the file, 0 if there is none yet.
fn read_count(fs: &mut Storage) -> u64 {
let Ok(h) = fs.open(COUNT) else { return 0 };
let mut buf = [0u8; 20];
let n = fs.read(h, &mut buf).unwrap_or(0);
let _ = fs.close(h);
core::str::from_utf8(&buf[..n]).ok().and_then(|s| s.trim().parse().ok()).unwrap_or(0)
}
fn write_count(fs: &mut Storage, count: u64) -> bool {
let mut digits = [0u8; 20];
let mut n = 0;
let mut v = count;
loop {
digits[n] = b'0' + (v % 10) as u8;
n += 1;
v /= 10;
if v == 0 {
break;
}
}
digits[..n].reverse();
let _ = fs.delete(COUNT);
let Ok(h) = fs.create(COUNT) else { return false };
let ok = fs.write(h, &digits[..n]) == Ok(n);
let _ = fs.close(h);
ok
}
fn main() {
let Some(mut fs) = libkos::start::data().and_then(|d| Storage::attach(d, DATA_BUF_VA).ok()) else {
println!("counter: not allowed to keep anything, so this is run 1 as far as it knows");
return;
};
let count = read_count(&mut fs) + 1;
if write_count(&mut fs, count) {
println!("counter: run {} (kept in its own data folder)", count);
} else {
println!("counter: run {}, but it could not save that", count);
}
}
libkos::entry!(main);

View File

@@ -0,0 +1,6 @@
[app]
id = "kos.ls"
name = "ls"
version = "1.0.0"
binary = "ls"
author = "K-OS"

View File

@@ -10,4 +10,5 @@ bench = false
[dependencies]
types = { workspace = true }
manifest = { workspace = true }
libkos = { workspace = true }

View File

@@ -1,8 +1,9 @@
//! The shell: a few built-ins, and anything else is a program on disk.
//! `name args...` runs `/bin/name` through init, handing it the terminal as
//! its output, read-only storage rooted at the current directory, and what
//! its manifest (`/bin/name.manifest`) asks for that the shell can give;
//! then waits for it (Ctrl+C kills it) and takes the storage back.
//! The shell: a few built-ins, and anything else is an app. `name args...`
//! asks the permission manager to run it, passing along the terminal as
//! its output and a read-only grant to the current folder, which is all
//! the shell itself has to give; the app gets the rest of what it may have
//! from the permission manager. Then it waits (Ctrl+C kills it) and takes
//! the grant back. The owner manages apps from here too.
#![no_std]
#![forbid(unsafe_code)]
@@ -10,33 +11,28 @@
extern crate libkos;
use libkos::apps::{Apps, Refused};
use libkos::fs::Storage;
use libkos::spawn::{SpawnError, spawn};
use types::manifest::Needs;
use libkos::{print, println, syscall};
use manifest::{Capabilities, Capability};
use types::address::VirtualAddress;
use types::apps::reply;
use types::bounded::CapSlot;
use types::budget::Limits;
use types::permission::{Permission, PermissionSet};
use types::start_arg::StartArg;
/// Where init's manifest put them: `uses`, then the spawn endpoint.
/// Where init's manifest put them: `uses`, then storage.
const TERMINAL: CapSlot = CapSlot::const_new(0);
const STORAGE: CapSlot = CapSlot::const_new(1);
const DISPLAY: CapSlot = CapSlot::const_new(2);
const INIT: CapSlot = CapSlot::const_new(3);
const PERMISSION: CapSlot = CapSlot::const_new(1);
/// The owner's files, `/home`: the shell's `/`.
const HOME: CapSlot = CapSlot::const_new(2);
/// Bits on the shell's notification: the program died (its death watch is
/// unbadged), or Ctrl+C was typed (the terminal's copy).
const DIED: usize = 1;
const INTERRUPTED: usize = 1 << 1;
/// Longest manifest read.
const MAX_MANIFEST: usize = 512;
/// What each program gets, out of the shell's allowance.
const PROGRAM: Limits = Limits { pages: 128, endpoints: 1, notifications: 1, regions: 1 };
const MAX_ARGS: usize = 16;
/// Where storage's buffer is mapped.
const STORAGE_BUF_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x0180_0000);
const STORAGE_BUF_VA: VirtualAddress = VirtualAddress::from_raw(0x0180_0000);
const REQUEST_PAGE_VA: VirtualAddress = VirtualAddress::from_raw(0x0190_0000);
/// The current directory, as a path on disk: no leading or trailing `/`.
struct Cwd {
@@ -50,7 +46,7 @@ impl Cwd {
}
/// Where `cd to` from here leads: `/` starts at the root, `..` goes up
/// (the shell holds the whole disk, so that is its to resolve).
/// (the shell holds all of its root, so that is its to resolve).
fn join(&self, to: &str) -> Option<Cwd> {
let mut next = match to.starts_with('/') {
true => Cwd {
@@ -98,87 +94,118 @@ impl core::fmt::Display for Cwd {
struct Shell {
fs: Storage,
apps: Apps,
cwd: Cwd,
done: CapSlot,
}
/// `/bin/<name><suffix>` into `out`, if it fits.
fn bin_path<'a>(out: &'a mut [u8], name: &str, suffix: &[u8]) -> Option<&'a [u8]> {
let path = out.get_mut(..4 + name.len() + suffix.len())?;
path[..4].copy_from_slice(b"bin/");
path[4..4 + name.len()].copy_from_slice(name.as_bytes());
path[4 + name.len()..].copy_from_slice(suffix);
Some(path)
}
/// `display, storage`.
struct Names(Capabilities);
/// What `name`'s manifest asks for; none without one. `None` if it asks
/// for something the shell does not know how to give.
fn needs(sh: &mut Shell, name: &str) -> Option<Needs> {
let mut path = [0u8; types::storage::MAX_PATH];
let path = bin_path(&mut path, name, b".manifest")?;
let Ok(file) = sh.fs.open(path) else {
return Some(Needs::default());
};
let mut text = [0u8; MAX_MANIFEST];
let read = sh.fs.read(file, &mut text);
let _ = sh.fs.close(file);
match types::manifest::parse(&text[..read.ok()?]) {
Ok(needs) => Some(needs),
Err(types::manifest::Unknown(what)) => {
println!("shell: {}: its manifest asks for {}", name, core::str::from_utf8(what).unwrap_or("?"));
None
impl core::fmt::Display for Names {
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(())
}
}
/// Runs `/bin/args[0]` with `args` in the current directory, and waits
/// for it to finish or Ctrl+C.
fn names(bits: usize) -> Names {
Names(Capabilities::from_bits(bits as u32))
}
/// Says why the permission manager refused, about `what`.
fn refused(what: &str, r: Refused) {
match r.code {
reply::NOT_INSTALLED => println!("shell: {}: not found", what),
reply::MISSING => println!("shell: {}: cannot run without {}", what, names(r.detail)),
reply::ALREADY_INSTALLED => println!("shell: {}: already installed", what),
reply::UNAVAILABLE => println!("shell: {}: this device has no {}", what, names(r.detail)),
reply::DECLINED => println!("shell: {}: not installed", what),
reply::NO_MANIFEST => println!("shell: {}: no app here (no manifest.toml)", what),
reply::BAD_MANIFEST if r.detail > 0 => println!("shell: {}: bad manifest, line {}", what, r.detail),
reply::BAD_MANIFEST => println!("shell: {}: bad manifest", what),
reply::NOT_REQUESTED => println!("shell: {}: never asked for that", what),
reply::BUSY => println!("shell: no room to run {}", what),
_ => println!("shell: {}: failed", what),
}
}
/// Runs app `args[0]` with `args` in the current folder, and waits for it
/// to finish or Ctrl+C.
fn run_program(sh: &mut Shell, args: &[&str]) {
let mut file = [0u8; types::spawn::MAX_NAME];
let Some(file) = bin_path(&mut file, args[0], b"") else {
println!("shell: {}: name too long", args[0]);
return;
};
let Some(needs) = needs(sh, args[0]) else {
return;
};
let child = match spawn(INIT, file, PROGRAM) {
Ok(c) => c,
Err(SpawnError::NotFound) => return println!("shell: {}: not found", args[0]),
Err(SpawnError::OverAllowance) => return println!("shell: no room to run {}", args[0]),
Err(SpawnError::Failed) => return println!("shell: {}: cannot run", args[0]),
};
let call = PermissionSet::empty().with(Permission::Execute);
let grant = sh.fs.grant(sh.cwd.as_bytes(), false).ok();
let arg = StartArg {
log: child.give(TERMINAL, call),
storage: grant.and_then(|g| child.give(g, call)),
args: child.set_args(args),
// It subscribes to input by sending the display a notification.
display: needs.display.then(|| child.give_sending(DISPLAY, call)).flatten(),
console: None,
};
let folder = sh.fs.grant(sh.cwd.as_bytes(), false).ok();
// A Ctrl+C typed since the last program is not for this one.
let _ = syscall::poll(sh.done);
let watched = syscall::watch_death(child.process, sh.done).is_ok();
if child.start_with(arg) && watched {
while syscall::wait(sh.done).is_ok_and(|bits| {
if bits & INTERRUPTED != 0 {
syscall::kill(child.process);
match sh.apps.launch(args, Some(TERMINAL), folder) {
Ok((process, granted)) => {
if syscall::watch_death(process, sh.done).is_ok() {
while syscall::wait(sh.done).is_ok_and(|bits| {
if bits & INTERRUPTED != 0 {
syscall::kill(process);
}
bits & DIED == 0
}) {}
} else {
let _ = syscall::kill(process);
}
bits & DIED == 0
}) {}
} else {
let _ = syscall::kill(child.process);
println!("shell: {}: cannot start", args[0]);
syscall::drop_cap(process);
// It drew over the terminal and took the keyboard.
if Capabilities::from_bits(granted).contains(Capability::Display) {
libkos::terminal::redraw(TERMINAL);
}
}
Err(r) => refused(args[0], r),
}
syscall::drop_cap(child.process);
if let Some(g) = grant {
if let Some(g) = folder {
let _ = libkos::fs::release(g);
}
// It drew over the terminal and took the keyboard.
if needs.display {
libkos::terminal::redraw(TERMINAL);
}
/// Installs the app in folder `from` (its manifest and binary); the owner
/// is asked on the terminal.
fn install(sh: &mut Shell, from: &str) {
let Some(path) = sh.cwd.join(from) else {
return println!("install: {}: no such folder", from);
};
let Ok(source) = sh.fs.grant(path.as_bytes(), false) else {
return println!("install: {}: no such folder", from);
};
match sh.apps.install(source) {
Ok(0) => println!("installed"),
Ok(bits) => println!("installed, may use: {}", names(bits as usize)),
Err(r) => refused(from, r),
}
let _ = libkos::fs::release(source);
}
fn list(sh: &mut Shell) {
let mut text = [0u8; types::apps::PAGE_SIZE];
match sh.apps.list(&mut text) {
Ok(n) => print!("{}", core::str::from_utf8(&text[..n]).unwrap_or("")),
Err(r) => refused("apps", r),
}
}
/// `allow <app id> <capability>` or `revoke ...`.
fn change(sh: &mut Shell, args: &[&str], allow: bool) {
let verb = if allow { "allow" } else { "revoke" };
let (Some(id), Some(name)) = (args.get(1), args.get(2)) else {
return println!("usage: {} <app id> <capability>", verb);
};
let Some(cap) = Capability::from_name(name) else {
return println!("{}: {}: no such capability", verb, name);
};
let done = match allow {
true => sh.apps.allow(id, cap.index()),
false => sh.apps.revoke(id, cap.index()),
};
if let Err(r) = done {
refused(id, r);
}
}
@@ -201,10 +228,18 @@ fn run(sh: &mut Shell, line: &str) {
None => {}
Some("help") => {
println!("built-ins: help, echo <text>, clear, cd <dir>, pwd");
println!("anything else runs a program from the disk: try ls, cat readme.txt");
println!("programs see the current directory, read-only, and nothing above it");
println!("apps: apps, install <folder>, allow|revoke <app id> <capability>");
println!("anything else runs an app: try ls, cat readme.txt, snake");
println!("apps see the current directory, read-only, and only what you allowed");
println!("Ctrl+C stops a running program; snake quits with q");
}
Some("apps") => list(sh),
Some("install") => match args.get(1) {
Some(from) => install(sh, from),
None => println!("usage: install <folder>"),
},
Some("allow") => change(sh, args, true),
Some("revoke") => change(sh, args, false),
Some("cd") => cd(sh, args.get(1).copied().unwrap_or("/")),
Some("pwd") => println!("{}", sh.cwd),
Some("echo") => println!("{}", Joined(&args[1..])),
@@ -237,15 +272,20 @@ fn main() {
println!("shell: no notification to wait on");
return;
};
let Ok(fs) = Storage::attach(STORAGE, STORAGE_BUF_VA) else {
let Ok(fs) = Storage::attach(HOME, STORAGE_BUF_VA) else {
println!("shell: no storage");
return;
};
let Some(apps) = Apps::connect(PERMISSION, REQUEST_PAGE_VA) else {
println!("shell: no permission manager");
return;
};
if !libkos::terminal::on_interrupt(TERMINAL, done, types::badge::Badge::new(INTERRUPTED)) {
println!("shell: Ctrl+C will not work");
}
let mut sh = Shell {
fs,
apps,
cwd: Cwd {
path: [0; types::storage::MAX_PATH],
len: 0,

View File

@@ -0,0 +1,6 @@
[app]
id = "kos.sleep"
name = "sleep"
version = "1.0.0"
binary = "sleep"
author = "K-OS"

View File

@@ -0,0 +1,9 @@
[app]
id = "kos.snake"
name = "Snake"
version = "1.0.0"
binary = "snake"
author = "K-OS"
[capabilities]
required = ["display"]

View File

@@ -10,4 +10,5 @@ bench = false
[dependencies]
types = { workspace = true }
manifest = { workspace = true }
libkos = { workspace = true }

View File

@@ -9,19 +9,21 @@
extern crate libkos;
use libkos::fs::{FsError, Storage};
use libkos::spawn::{SpawnError, spawn};
use libkos::spawn::{SpawnError, Spawned, spawn};
use libkos::syscall;
use types::bounded::CapSlot;
use types::budget::Limits;
/// Where init put them (see init's manifest: `uses`, the spawn endpoint,
/// then control).
const STORAGE: CapSlot = CapSlot::const_new(0);
const INIT: CapSlot = CapSlot::const_new(1);
const CONTROL: CapSlot = CapSlot::const_new(2);
const PERMISSION: CapSlot = CapSlot::const_new(0);
const STORAGE: CapSlot = CapSlot::const_new(1);
const INIT: CapSlot = CapSlot::const_new(2);
const CONTROL: CapSlot = CapSlot::const_new(3);
/// Where storage's buffers are mapped: the whole disk's, a narrowed one's.
const STORAGE_BUF_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x0180_0000);
const NARROW_BUF_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x0190_0000);
const REQUEST_PAGE_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x01C0_0000);
/// Read back from the disk image by scripts/test-qemu.sh.
const NOTE: &[u8] = b"written by k-os on ext4\n";
@@ -29,10 +31,18 @@ const NOTE: &[u8] = b"written by k-os on ext4\n";
/// the first has died and its limits came back.
const HELLO: Limits = Limits { pages: 80, endpoints: 0, notifications: 0, regions: 0 };
/// Spawns the program at `path`, read through a grant made just for it.
fn spawn_file(fs: &mut Storage, path: &[u8], limits: Limits) -> Result<Spawned, SpawnError> {
let file = fs.grant(path, false).map_err(|_| SpawnError::Failed)?;
let child = spawn(INIT, file, limits);
let _ = libkos::fs::release(file);
child
}
/// Spawns hello, runs it, and waits for it to die; hands it `grant`
/// first, if there is one.
fn run_hello(died: CapSlot, grant: Option<CapSlot>) -> Result<(), &'static str> {
let child = spawn(INIT, b"bin/hello", HELLO).map_err(|_| "init would not spawn hello")?;
fn run_hello(fs: &mut Storage, died: CapSlot, grant: Option<CapSlot>) -> Result<(), &'static str> {
let child = spawn_file(fs, b"bin/hello", HELLO).map_err(|_| "init would not spawn hello")?;
let call = types::permission::PermissionSet::empty().with(types::permission::Permission::Execute);
if grant.is_some_and(|g| child.give(g, call).is_none()) {
return Err("a storage grant could not be passed on");
@@ -78,7 +88,7 @@ fn narrow_storage(fs: &mut Storage, died: CapSlot) -> Result<(), &'static str> {
if widened.and_then(|mut w| w.create(b"x")).is_ok() {
return Err("a grant was widened to write");
}
run_hello(died, Some(grant))?;
run_hello(fs, died, Some(grant))?;
libkos::fs::release(grant).map_err(|_| "release failed")?;
if inner.list_dir(b"", |_| {}).is_ok() {
return Err("a released grant still works");
@@ -86,6 +96,33 @@ fn narrow_storage(fs: &mut Storage, died: CapSlot) -> Result<(), &'static str> {
Ok(())
}
/// An app runs through the permission manager with what it was granted;
/// revoking that stops it, and it cannot run again until allowed.
fn revoke_stops_a_running_app(died: CapSlot) -> Result<(), &'static str> {
use manifest::Capability;
let apps = libkos::apps::Apps::connect(PERMISSION, REQUEST_PAGE_VA).ok_or("no permission manager")?;
let display = Capability::Display.index();
let (snake, granted) = apps.launch(&["snake"], None, None).map_err(|_| "snake would not launch")?;
if granted & manifest::Capabilities::NONE.with(Capability::Display).bits() == 0 {
return Err("snake runs without the display it was granted");
}
syscall::watch_death(snake, died).map_err(|_| "cannot watch snake")?;
apps.revoke("kos.snake", display).map_err(|_| "revoke failed")?;
syscall::wait(died).map_err(|_| "wait failed")?;
syscall::drop_cap(snake);
let refused = apps.launch(&["snake"], None, None);
if !matches!(refused, Err(r) if r.code == types::apps::reply::MISSING) {
return Err("snake ran without the display it requires");
}
apps.allow("kos.snake", display).map_err(|_| "allow failed")?;
let (snake, _) = apps.launch(&["snake"], None, None).map_err(|_| "snake did not run once allowed again")?;
syscall::watch_death(snake, died).map_err(|_| "cannot watch snake")?;
syscall::kill(snake);
syscall::wait(died).map_err(|_| "wait failed")?;
syscall::drop_cap(snake);
Ok(())
}
fn check() -> Result<(), &'static str> {
let mut fs = Storage::attach(STORAGE, STORAGE_BUF_VA).map_err(|_| "cannot attach to storage")?;
write_a_file(&mut fs)?;
@@ -95,21 +132,24 @@ fn check() -> Result<(), &'static str> {
narrow_storage(&mut fs, died)?;
libkos::println!("spawntest: storage grants stay in scope, pass on, release: ok");
run_hello(died, None)?;
run_hello(&mut fs, died, None)?;
libkos::println!("spawntest: spawned program runs and its death is heard: ok");
let greedy = Limits { pages: 200, ..HELLO };
if spawn(INIT, b"bin/hello", greedy).err() != Some(SpawnError::OverAllowance) {
if spawn_file(&mut fs, b"bin/hello", greedy).err() != Some(SpawnError::OverAllowance) {
return Err("init spawned past the allowance");
}
let tiny = Limits { pages: 1, ..HELLO };
if spawn(INIT, b"bin/nothere", tiny).err() != Some(SpawnError::NotFound) {
if spawn_file(&mut fs, b"bin/nothere", tiny).err() != Some(SpawnError::NotFound) {
return Err("a missing program was not reported");
}
libkos::println!("spawntest: allowance and missing files refused: ok");
run_hello(died, None)?;
run_hello(&mut fs, died, None)?;
libkos::println!("spawntest: allowance comes back when a child dies: ok");
revoke_stops_a_running_app(died)?;
libkos::println!("spawntest: revoking stops a running app, allowing lets it run: ok");
Ok(())
}

View File

@@ -44,9 +44,9 @@ impl<'a> Launcher<'a> {
) -> Result<Self, &'static str> {
let spawn_ep = syscall::create_endpoint().ok_or("no spawn endpoint")?;
let deaths = syscall::create_notification().map_err(|_| "no death notification")?;
// Storage was started by boot, before init; programs may use it too.
let mut served = [None; MAX_SERVED];
served[0] = Some(("storage", crate::STORAGE_EP));
// Storage, started by boot, is not among them: it is reached
// through grants (a program's `storage` scope).
let served = [None; MAX_SERVED];
Ok(Self { storage, scratch, devices, served, next_badge: 1, spawn_ep, deaths, spawner: Spawner::new() })
}
@@ -78,20 +78,21 @@ impl<'a> Launcher<'a> {
// and take other clients' requests.
for name in p.uses {
let ep = self.endpoint(name).ok_or("needs a server that is not running")?;
let copy = match *name {
// Storage issues its own badges, each naming a scope.
"storage" => self.storage.grant(b"", true).map_err(|_| "storage refused a grant")?,
_ => {
let badge = Badge::new(self.next_badge);
self.next_badge += 1;
syscall::mint(ep, badge, perms(&[Permission::Execute, Permission::Delegate]))
.map_err(|_| "cannot mint an endpoint")?
}
};
let badge = Badge::new(self.next_badge);
self.next_badge += 1;
let copy = syscall::mint(ep, badge, perms(&[Permission::Execute, Permission::Delegate]))
.map_err(|_| "cannot mint an endpoint")?;
let given = syscall::delegate(copy, child, perms(&[Permission::Execute, Permission::Delegate]));
syscall::drop_cap(copy);
given.ok_or("cannot give an endpoint")?;
}
// Storage issues its own badges, each naming a scope.
if let Some(scope) = p.storage {
let grant = self.storage.grant(scope.path, scope.write).map_err(|_| "storage refused a grant")?;
let given = syscall::delegate(grant, child, perms(&[Permission::Execute, Permission::Delegate]));
syscall::drop_cap(grant);
given.ok_or("cannot give storage")?;
}
if let Some(allowance) = p.spawns {
self.give_spawn(child, allowance)?;
}
@@ -126,7 +127,8 @@ impl<'a> Launcher<'a> {
let badge: Badge = self.spawner.add_client(allowance).ok_or("too many spawn clients")?;
let minted = perms(&[Permission::Execute, Permission::Delegate]);
let copy = syscall::mint(self.spawn_ep, badge, minted).map_err(|_| "cannot mint")?;
let given = syscall::delegate(copy, child, perms(&[Permission::Execute]));
// Delegate, to send the program file's grant with each request.
let given = syscall::delegate(copy, child, perms(&[Permission::Execute, Permission::Delegate]));
syscall::drop_cap(copy);
given.map(|_| ()).ok_or("cannot give the spawn endpoint")
}

View File

@@ -66,7 +66,7 @@ pub fn run() -> ! {
selftest::run(storage, scratch);
launch_all(&mut launcher, manifest::TESTS);
} else {
launch_all(&mut launcher, manifest::APPS);
launch_all(&mut launcher, manifest::DESKTOP);
}
libkos::println!("init: all loaded, serving spawns");

View File

@@ -9,6 +9,14 @@ pub enum Device {
Input,
}
/// A storage grant: a folder (or the whole disk, `b""`) and whether it may
/// be changed.
#[derive(Clone, Copy)]
pub struct Scope {
pub path: &'static [u8],
pub write: bool,
}
pub struct Program {
pub name: &'static str,
pub file: &'static [u8],
@@ -19,6 +27,8 @@ pub struct Program {
/// Earlier programs' endpoints it may call, in its next slots; each
/// program's copy carries its own badge, so a server can tell them apart.
pub uses: &'static [&'static str],
/// Storage it may reach, in the slot after `uses`.
pub storage: Option<Scope>,
/// Takes the console over from init; everyone else prints through it.
pub console: bool,
/// May have init spawn programs, whose limits together stay within this.
@@ -43,6 +53,7 @@ const fn server(name: &'static str, file: &'static [u8], limits: Limits) -> Prog
serves: true,
device: None,
uses: &[],
storage: None,
console: false,
spawns: None,
system_control: false,
@@ -62,7 +73,15 @@ pub const SERVERS: &[Program] = &[
},
// Text on the display, lines from the keyboard (it takes input focus).
Program { uses: &["display"], ..server("terminal", b"bin/terminal", SMALL) },
server("permission", b"bin/permission", SMALL),
// The only one that equips apps (docs/apps.md): it reaches every app's
// files and the owner's decisions, and runs apps through init.
Program {
uses: &["terminal", "display"],
storage: Some(Scope { path: b"", write: true }),
// Room for a few apps at a time.
spawns: Some(Limits { pages: 512, endpoints: 8, notifications: 8, regions: 8 }),
..server("permission", b"bin/permission", Limits { pages: 96, ..SMALL })
},
server("session", b"bin/session", SMALL),
];
@@ -70,18 +89,20 @@ pub const SERVERS: &[Program] = &[
/// spawns through init, then reports and halts.
pub const TESTS: &[Program] = &[Program {
serves: false,
uses: &["storage"],
// The permission manager, to run an app the way the shell does.
uses: &["permission"],
storage: Some(Scope { path: b"", write: true }),
spawns: Some(Limits { pages: 128, endpoints: 2, notifications: 2, regions: 2 }),
system_control: true,
..server("spawntest", b"bin/spawntest", SMALL)
}];
/// The desktop; the test image runs the SMP self-test instead.
pub const APPS: &[Program] = &[Program {
/// The desktop; the test image runs the SMP self-test instead. The shell
/// holds the owner's files and asks the permission manager to run apps;
/// it holds nothing to give them beyond that.
pub const DESKTOP: &[Program] = &[Program {
serves: false,
// The display only to hand on, to programs whose manifest asks for it.
uses: &["terminal", "storage", "display"],
// Room for one program at a time, the way the shell runs them.
spawns: Some(Limits { pages: 256, endpoints: 2, notifications: 2, regions: 2 }),
uses: &["terminal", "permission"],
storage: Some(Scope { path: b"home", write: true }),
..server("shell", b"bin/shell", SMALL)
}];

View File

@@ -8,7 +8,8 @@ use types::badge::Badge;
use types::bounded::CapSlot;
use types::budget::Limits;
use types::permission::{Permission, PermissionSet};
use types::spawn::{MAX_NAME, NONE, reply};
use types::address::VirtualAddress;
use types::spawn::{NONE, reply};
use crate::launch::Launcher;
use crate::load;
@@ -67,15 +68,16 @@ impl Spawner {
}
}
/// Where init maps a spawn request's file grant while it reads it.
const PROGRAM_BUF_VA: VirtualAddress = VirtualAddress::from_raw(0x01A0_0000);
pub(crate) struct Request {
name: [u8; MAX_NAME],
len: usize,
limits: Limits,
}
impl Decode for Request {
fn decode(words: [usize; 4]) -> Option<Self> {
types::spawn::parse(words).map(|(name, len, limits)| Self { name, len, limits })
types::spawn::parse(words).map(|limits| Self { limits })
}
}
@@ -92,7 +94,9 @@ fn refused(tag: usize) -> ([usize; 4], Option<CapSlot>) {
}
impl Launcher<'_> {
fn spawn(&mut self, badge: usize, r: &Request) -> ([usize; 4], Option<CapSlot>) {
/// Loads the file `binary` (a storage grant scoped to it) for `badge`'s
/// client.
fn spawn(&mut self, badge: usize, r: &Request, binary: CapSlot) -> ([usize; 4], Option<CapSlot>) {
let Some(client) = badge.checked_sub(1).filter(|c| *c < MAX_CLIENTS) else {
return refused(reply::ERROR);
};
@@ -105,16 +109,16 @@ impl Launcher<'_> {
let Some(index) = self.spawner.children.iter().position(Option::is_none) else {
return refused(reply::ERROR);
};
let name = &r.name[..r.len];
let (storage, scratch) = self.storage();
match storage.open(name) {
Ok(h) => {
let _ = storage.close(h);
}
Err(_) => return refused(reply::NOT_FOUND),
}
let Some(loaded) = load::load_binary(storage, scratch, name, r.limits) else {
return refused(reply::ERROR);
let Ok(mut file) = libkos::fs::Storage::attach(binary, PROGRAM_BUF_VA) else {
return refused(reply::NOT_FOUND);
};
// The grant is the file: its path inside it is empty.
let found = file.open(b"").and_then(|h| file.close(h)).is_ok();
let (_, scratch) = self.storage();
let loaded = found.then(|| load::load_binary(&mut file, scratch, b"", r.limits)).flatten();
file.detach();
let Some(loaded) = loaded else {
return refused(if found { reply::ERROR } else { reply::NOT_FOUND });
};
if !self.watch(loaded.process_cap, index) {
let _ = syscall::kill(loaded.process_cap);
@@ -165,13 +169,18 @@ impl Server for Launcher<'_> {
Reply(refused(reply::ERROR).0)
}
fn handle_call(&mut self, badge: usize, words: [usize; 4], _cap: Option<CapSlot>) -> ([usize; 4], Option<libkos::syscall::Give>) {
match Request::decode(words) {
Some(r) => {
let (reply, process) = self.spawn(badge, &r);
(reply, process.map(Into::into))
fn handle_call(&mut self, badge: usize, words: [usize; 4], cap: Option<CapSlot>) -> ([usize; 4], Option<libkos::syscall::Give>) {
match (Request::decode(words), cap) {
(Some(r), Some(binary)) => {
let (reply, process) = self.spawn(badge, &r, binary);
// Init's copy of the grant is done with.
syscall::drop_cap(binary);
// The client equips the child, and may pass it on to whoever
// it runs it for.
(reply, process.map(libkos::syscall::Give::keeping_delegate))
}
None => ([MALFORMED, 0, 0, 0], None),
(Some(_), None) => (refused(reply::NOT_FOUND).0, None),
(None, _) => ([MALFORMED, 0, 0, 0], None),
}
}

116
crates/libkos/src/apps.rs Normal file
View File

@@ -0,0 +1,116 @@
//! The permission manager's client (protocol: `types::apps`): run, install
//! and manage apps. Text goes through a request page this process owns and
//! lends with each call.
use crate::syscall::{self, Give, Received};
use crate::window::{self, Window};
use types::address::{ByteOffset, VirtualAddress};
use types::apps::{self as wire, PAGE_SIZE, reply};
use types::bounded::{CapSlot, PageCount};
use types::mapping::PagePerms;
/// What the permission manager refused with: a `types::apps::reply` code
/// and its word (the capability bits it is about, or a manifest line).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Refused {
pub code: usize,
pub detail: usize,
}
pub type Result<T> = core::result::Result<T, Refused>;
const FAILED: Refused = Refused { code: reply::FAILED, detail: 0 };
pub struct Apps {
ep: CapSlot,
page_cap: CapSlot,
page: Window,
}
impl Apps {
/// Talks to the permission manager behind `ep`, with a request page
/// mapped at `va`.
pub fn connect(ep: CapSlot, va: VirtualAddress) -> Option<Self> {
let page_cap = syscall::create_region(PageCount::new(1)?).ok()?;
let page = window::map(page_cap, va, PagePerms::ReadWrite).ok()?;
Some(Self { ep, page_cap, page })
}
fn call(&self, words: [usize; 4], cap: Option<Give>) -> Result<(usize, Option<CapSlot>)> {
let r = syscall::call(self.ep, words, cap).map_err(|_| FAILED)?;
let ([code, detail, _, _], cap) = match r {
Received::Message { words, cap, .. } | Received::Call { words, cap, .. } => (words, cap),
Received::Signal { .. } => return Err(FAILED),
};
match code {
reply::OK => Ok((detail, cap)),
code => Err(Refused { code, detail }),
}
}
/// Lends the request page for one call.
fn with_page(&self, op: usize, word: usize) -> Result<(usize, Option<CapSlot>)> {
self.call([op, word, 0, 0], Some(self.page_cap.into()))
}
/// `parts`, NUL-separated and ending in an empty one, into the page.
fn put(&self, parts: &[&str]) -> Result<()> {
let need: usize = parts.iter().map(|p| p.len() + 1).sum::<usize>() + 1;
if need > PAGE_SIZE || parts.iter().any(|p| p.contains('\0')) {
return Err(FAILED);
}
let mut at = ByteOffset::ZERO;
for p in parts {
self.page.copy_from(at, p.as_bytes());
self.page.write(at + p.len(), 0u8);
at = at + p.len() + 1;
}
self.page.write(at, 0u8);
Ok(())
}
/// Runs `args[0]` with `args`, printing to `output`, working in
/// `folder`; the running process (to wait on and kill) and the bits of
/// what it was granted. Both caps go with Delegate kept, so the
/// permission manager can hand them on.
pub fn launch(&self, args: &[&str], output: Option<CapSlot>, folder: Option<CapSlot>) -> Result<(CapSlot, u32)> {
for (op, cap) in [(wire::PASS_OUTPUT, output), (wire::PASS_FOLDER, folder)] {
if let Some(cap) = cap {
self.call([op, 0, 0, 0], Some(Give::keeping_delegate(cap)))?;
}
}
self.put(args)?;
match self.with_page(wire::LAUNCH, 0)? {
(granted, Some(process)) => Ok((process, granted as u32)),
(_, None) => Err(FAILED),
}
}
/// Installs the app in `source` (a read-only grant to its folder),
/// asking the owner; the bits of what it was granted.
pub fn install(&self, source: CapSlot) -> Result<u32> {
self.call([wire::INSTALL, 0, 0, 0], Some(source.into())).map(|(bits, _)| bits as u32)
}
/// One line per installed app, into `out`; how much.
pub fn list(&self, out: &mut [u8]) -> Result<usize> {
let (len, _) = self.with_page(wire::LIST, 0)?;
let len = len.min(out.len()).min(PAGE_SIZE);
self.page.copy_to(ByteOffset::ZERO, &mut out[..len]);
Ok(len)
}
/// Takes the capability with catalog index `capability` away from app
/// `id` (stopping it if it runs with it).
pub fn revoke(&self, id: &str, capability: usize) -> Result<()> {
self.put(&[id])?;
self.with_page(wire::REVOKE, capability).map(|_| ())
}
/// Gives app `id` the capability with catalog index `capability`, which
/// it asked for.
pub fn allow(&self, id: &str, capability: usize) -> Result<()> {
self.put(&[id])?;
self.with_page(wire::ALLOW, capability).map(|_| ())
}
}

View File

@@ -49,6 +49,8 @@ impl DirEntry {
pub struct Storage {
ep: CapSlot,
buf: &'static mut [u8],
/// What maps `buf`; dropping it unmaps it.
region: CapSlot,
}
impl Storage {
@@ -66,7 +68,12 @@ impl Storage {
};
let mapped = crate::window::map_buffer(region, va).map_err(|_| FsError::Failed)?;
let buf = mapped.get_mut(..size).ok_or(FsError::Failed)?;
Ok(Self { ep, buf })
Ok(Self { ep, buf, region })
}
/// Unmaps the buffer, freeing its address for another connection.
pub fn detach(self) {
syscall::drop_cap(self.region);
}
fn call(&self, words: [usize; 4]) -> Result<[usize; 4]> {

View File

@@ -6,6 +6,9 @@ use core::fmt;
struct Writer {
buf: [u8; BATCH],
len: usize,
/// Where it goes: an endpoint speaking the log protocol, else where
/// `print!` goes.
to: Option<types::bounded::CapSlot>,
}
/// A few whole text messages' worth.
@@ -13,7 +16,10 @@ const BATCH: usize = 5 * types::log::TEXT_BYTES;
impl Writer {
fn flush(&mut self) {
write_bytes(&self.buf[..self.len]);
match self.to {
Some(ep) => write_to(ep, &self.buf[..self.len]),
None => write_bytes(&self.buf[..self.len]),
}
self.len = 0;
}
}
@@ -31,6 +37,22 @@ impl fmt::Write for Writer {
}
}
/// Bytes to `ep`, an endpoint speaking the log protocol (a terminal, the
/// log server), whatever this process's own log is.
pub fn write_to(ep: types::bounded::CapSlot, bytes: &[u8]) {
for chunk in bytes.chunks(types::log::TEXT_BYTES) {
let _ = crate::syscall::call(ep, types::log::text_words(chunk), None);
}
}
/// Formats to `ep` (see `write_to`).
pub fn print_to(ep: types::bounded::CapSlot, args: fmt::Arguments) {
use fmt::Write;
let mut w = Writer { buf: [0; BATCH], len: 0, to: Some(ep) };
w.write_fmt(args).ok();
w.flush();
}
/// Bytes as they are to where `print!` goes: the log endpoint (the log
/// server, or a terminal), else the console; nowhere without either.
pub fn write_bytes(bytes: &[u8]) {
@@ -48,7 +70,7 @@ pub fn write_bytes(bytes: &[u8]) {
pub fn _print(args: fmt::Arguments) {
use fmt::Write;
let mut w = Writer { buf: [0; BATCH], len: 0 };
let mut w = Writer { buf: [0; BATCH], len: 0, to: None };
w.write_fmt(args).ok();
w.flush();
}

View File

@@ -3,6 +3,7 @@
extern crate alloc;
pub mod alloc_impl;
pub mod apps;
pub mod device;
mod arch;
pub mod display;

View File

@@ -1,4 +1,5 @@
//! Running a program through init (protocol: `types::spawn`). Init loads it
//! Running a program through init (protocol: `types::spawn`). The caller
//! hands init a read-only storage grant to the program file; init loads it
//! and gives it a log endpoint; the caller hands it any other capabilities
//! (`delegate` to the process) and starts it.
@@ -25,9 +26,11 @@ pub struct Spawned {
log: Option<CapSlot>,
}
pub fn spawn(init: CapSlot, name: &[u8], limits: Limits) -> Result<Spawned, SpawnError> {
let words = request(name, limits).ok_or(SpawnError::Failed)?;
let reply = syscall::call(init, words, None).map_err(|_| SpawnError::Failed)?;
/// Loads the program `binary` (a read-only storage grant to its file) into
/// a new process, not started.
pub fn spawn(init: CapSlot, binary: CapSlot, limits: Limits) -> Result<Spawned, SpawnError> {
let words = request(limits).ok_or(SpawnError::Failed)?;
let reply = syscall::call(init, words, Some(binary.into())).map_err(|_| SpawnError::Failed)?;
let [tag, entry, log, _] = reply.words();
let cap = match reply {
syscall::Received::Message { cap, .. } | syscall::Received::Call { cap, .. } => cap,

View File

@@ -38,6 +38,12 @@ pub fn storage() -> Option<CapSlot> {
StartArg::decode(arg()).storage
}
/// The app's own data folder (a storage grant, read-write), if it was
/// granted `storage`.
pub fn data() -> Option<CapSlot> {
StartArg::decode(arg()).data
}
/// The display server endpoint this process was given, if any.
pub fn display() -> Option<CapSlot> {
StartArg::decode(arg()).display

View File

@@ -19,6 +19,15 @@ macro_rules! catalog {
}
}
/// Its place in the catalog, as sent in a message word.
pub fn index(self) -> usize {
self as usize
}
pub fn from_index(i: usize) -> Option<Self> {
Capability::ALL.get(i).copied()
}
pub fn name(self) -> &'static str {
match self {
$(Capability::$variant => $name,)*
@@ -65,6 +74,20 @@ pub struct Capabilities(u32);
impl Capabilities {
pub const NONE: Self = Self(0);
/// As sent in a message word.
pub const fn bits(self) -> u32 {
self.0
}
/// From a message word; bits past the catalog are dropped.
pub fn from_bits(bits: u32) -> Self {
Capability::ALL
.iter()
.copied()
.filter(|c| bits & c.bit() != 0)
.collect()
}
pub fn with(self, c: Capability) -> Self {
Self(self.0 | c.bit())
}
@@ -89,6 +112,11 @@ impl Capabilities {
self.0 == 0
}
/// Those in `self` but not in `other`.
pub fn minus(self, other: Self) -> Self {
Self(self.0 & !other.0)
}
/// Whether every capability in `self` is in `other`.
pub fn is_subset(self, other: Self) -> bool {
self.0 & !other.0 == 0

View File

@@ -36,7 +36,8 @@ impl<const N: usize> core::fmt::Debug for Text<N> {
impl<const N: usize> core::fmt::Display for Text<N> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str(self.as_str())
// `pad`, so `{:<20}` and the like line columns up.
f.pad(self.as_str())
}
}
@@ -63,7 +64,7 @@ impl AppId {
impl core::fmt::Display for AppId {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str(self.as_str())
f.pad(self.as_str())
}
}

View File

@@ -10,6 +10,7 @@ bench = false
[dependencies]
types = { workspace = true }
manifest = { workspace = true }
libkos = { workspace = true }
server-framework = { workspace = true }

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

View File

@@ -0,0 +1,7 @@
[package]
name = "app-image"
version = "0.1.0"
edition = "2024"
[dependencies]
manifest = { workspace = true }

View File

@@ -0,0 +1,64 @@
//! Puts apps on a disk image being built (see docs/apps.md), checked by
//! the same strict parser the permission manager uses:
//!
//! app-image install <root> <manifest.toml> <binary>
//! an installed, pre-approved app: /apps/<id>/, its decision granting
//! everything it asks for, and /data/<id>/ if that includes storage
//! app-image stage <folder> <manifest.toml> <binary>
//! an app folder the owner can install later (`install <folder>`)
use manifest::{Capability, Decision, MAX_DECISION_TEXT, Manifest};
use std::path::Path;
use std::process::ExitCode;
use std::{env, fs};
fn read(manifest: &Path) -> Result<(Manifest, String), String> {
let text = fs::read_to_string(manifest).map_err(|e| format!("{}: {e}", manifest.display()))?;
let m = Manifest::parse(&text).map_err(|e| format!("{}: {e:?}", manifest.display()))?;
Ok((m, text))
}
/// The manifest and the binary (under the name the manifest gives it).
fn put_app(folder: &Path, m: &Manifest, text: &str, binary: &Path) -> Result<(), String> {
fs::create_dir_all(folder).map_err(|e| format!("{}: {e}", folder.display()))?;
fs::write(folder.join("manifest.toml"), text).map_err(|e| e.to_string())?;
fs::copy(binary, folder.join(m.binary.as_str()))
.map_err(|e| format!("{}: {e}", binary.display()))?;
Ok(())
}
fn install(root: &Path, manifest: &Path, binary: &Path) -> Result<(), String> {
let (m, text) = read(manifest)?;
put_app(&root.join("apps").join(m.id.as_str()), &m, &text, binary)?;
// Whoever builds the image approves what its apps ask for.
let decision = Decision {
granted: m.requested(),
};
let mut out = [0u8; MAX_DECISION_TEXT];
let n = decision.write(&mut out);
let decisions = root.join("system/permissions");
fs::create_dir_all(&decisions).map_err(|e| e.to_string())?;
fs::write(decisions.join(m.id.as_str()), &out[..n]).map_err(|e| e.to_string())?;
if decision.granted.contains(Capability::Storage) {
fs::create_dir_all(root.join("data").join(m.id.as_str())).map_err(|e| e.to_string())?;
}
Ok(())
}
fn main() -> ExitCode {
let args: Vec<String> = env::args().skip(1).collect();
let done = match args.iter().map(String::as_str).collect::<Vec<_>>().as_slice() {
["install", root, manifest, binary] => install(Path::new(root), Path::new(manifest), Path::new(binary)),
["stage", folder, manifest, binary] => {
read(Path::new(manifest)).and_then(|(m, text)| put_app(Path::new(folder), &m, &text, Path::new(binary)))
}
_ => Err("usage: app-image install <root> <manifest> <binary> | stage <folder> <manifest> <binary>".into()),
};
match done {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("app-image: {e}");
ExitCode::FAILURE
}
}
}

49
crates/types/src/apps.rs Normal file
View File

@@ -0,0 +1,49 @@
//! The permission manager's protocol (docs/apps.md), shared by the server
//! and `libkos::apps`. Every request carries the client's request page (a
//! one-page region) unless it says otherwise; text goes through it,
//! NUL-separated, and answers come back in it.
/// Staged for the next `LAUNCH`, carrying the capability: what the app
/// prints to (an endpoint that speaks the log protocol).
pub const PASS_OUTPUT: usize = 1;
/// Staged for the next `LAUNCH`, carrying the capability: a storage grant
/// the app works in (the launcher's current folder).
pub const PASS_FOLDER: usize = 2;
/// The page holds the command and its arguments. Reply `OK` carries the
/// process (call it to wait and kill), with the granted capabilities' bits
/// in word 1, or an error; what was staged is used up either way.
pub const LAUNCH: usize = 3;
/// Carries a read-only storage grant to the folder holding the app (its
/// `manifest.toml` and binary), instead of the page. The owner is asked.
/// Reply `OK` with the granted capabilities' bits in word 1.
pub const INSTALL: usize = 4;
/// Writes one line per installed app into the page: id, command, version,
/// then what it may use. Reply `OK` with the text's length in word 1.
pub const LIST: usize = 5;
/// The page holds an app id; word 1 the capability's catalog index. The
/// owner takes it away (a running app that had it is stopped) or gives it.
pub const REVOKE: usize = 6;
pub const ALLOW: usize = 7;
/// The request page's size.
pub const PAGE_SIZE: usize = 4096;
pub mod reply {
pub const OK: usize = 0;
pub const NOT_INSTALLED: usize = 1;
/// Word 1: the capabilities it requires and does not get.
pub const MISSING: usize = 2;
pub const ALREADY_INSTALLED: usize = 3;
/// Word 1: the required capabilities this device has no server for.
pub const UNAVAILABLE: usize = 4;
pub const DECLINED: usize = 5;
/// Word 1: the manifest line at fault, 0 for none in particular.
pub const BAD_MANIFEST: usize = 6;
/// The app never asked for that capability.
pub const NOT_REQUESTED: usize = 7;
/// No room: too many apps running, or no spawn allowance left.
pub const BUSY: usize = 8;
pub const FAILED: usize = 9;
/// The offered folder has no `manifest.toml`.
pub const NO_MANIFEST: usize = 10;
}

View File

@@ -2,6 +2,7 @@
#![forbid(unsafe_code)]
pub mod address;
pub mod apps;
pub mod badge;
pub mod bootinfo;
pub mod bounded;
@@ -12,7 +13,6 @@ pub mod id;
pub mod input;
pub mod log;
pub mod macros;
pub mod manifest;
pub mod mapping;
pub mod permission;
#[cfg(kani)]

View File

@@ -1,64 +0,0 @@
//! A program's manifest, `/bin/<name>.manifest`: what it needs beyond the
//! terminal and the storage every program gets. Lines of `needs = a, b`;
//! `#` starts a comment. No manifest means nothing more.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Needs {
/// Draws on the screen itself and takes input focus.
pub display: bool,
}
/// A line or need the parser does not know: the program asks for
/// something nobody can say it may have, so it should not run.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Unknown<'a>(pub &'a [u8]);
pub fn parse(text: &[u8]) -> Result<Needs, Unknown<'_>> {
let mut needs = Needs::default();
for line in text.split(|b| *b == b'\n') {
let line = line
.split(|b| *b == b'#')
.next()
.unwrap_or(&[])
.trim_ascii();
if line.is_empty() {
continue;
}
let mut sides = line.splitn(2, |b| *b == b'=');
let (key, value) = (sides.next().unwrap_or(&[]).trim_ascii(), sides.next());
let (b"needs", Some(value)) = (key, value) else {
return Err(Unknown(line));
};
for need in value
.split(|b| *b == b',')
.map(<[u8]>::trim_ascii)
.filter(|n| !n.is_empty())
{
match need {
b"display" => needs.display = true,
other => return Err(Unknown(other)),
}
}
}
Ok(needs)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn needs_are_read_and_comments_skipped() {
let text = b"# snake\nneeds = display # the screen\n\n";
assert_eq!(parse(text), Ok(Needs { display: true }));
assert_eq!(parse(b""), Ok(Needs::default()));
assert_eq!(parse(b"needs = "), Ok(Needs::default()));
}
#[test]
fn anything_unknown_is_refused() {
assert_eq!(parse(b"needs = display, network"), Err(Unknown(b"network")));
assert_eq!(parse(b"wants = display"), Err(Unknown(b"wants = display")));
assert_eq!(parse(b"display"), Err(Unknown(b"display")));
}
}

View File

@@ -2,15 +2,16 @@
//! init to load a program into a new, not yet running process; init replies
//! with the process capability and the client wires and starts it.
//!
//! Request: `[SPAWN | name_len << 8, packed limits, name bytes 0..8, 8..16]`.
//! Request: `[SPAWN, packed limits, 0, 0]` carrying a read-only storage grant
//! to the program file itself: init reads it through that, so it needs no
//! path, and nothing is loaded its requester could not read.
//! Reply: `[reply::OK, entry point, child's log slot or NONE, 0]` carrying
//! the process capability, or `[error tag, 0, 0, 0]`.
//! the process capability (which the client may pass on), or
//! `[error tag, 0, 0, 0]`.
use crate::budget::Limits;
pub const SPAWN: usize = 0;
/// The longest program file name a spawn request carries.
pub const MAX_NAME: usize = 16;
/// Where init puts the child's stack top (its stack ends at the image).
pub const STACK_TOP: usize = 0x10000;
/// A slot word meaning "none".
@@ -18,6 +19,7 @@ pub const NONE: usize = usize::MAX;
pub mod reply {
pub const OK: usize = 0;
/// The grant names no file init can read.
pub const NOT_FOUND: usize = 1;
/// The limits do not fit in what is left of the client's allowance.
pub const OVER_ALLOWANCE: usize = 2;
@@ -44,32 +46,13 @@ fn unpack(w: usize) -> Limits {
}
}
pub fn request(name: &[u8], limits: Limits) -> Option<[usize; 4]> {
if name.is_empty() || name.len() > MAX_NAME {
return None;
}
let mut bytes = [0u8; MAX_NAME];
bytes[..name.len()].copy_from_slice(name);
let (lo, hi) = bytes.as_chunks::<8>().0.split_at(1);
Some([
SPAWN | name.len() << 8,
pack(limits)?,
u64::from_le_bytes(lo[0]) as usize,
u64::from_le_bytes(hi[0]) as usize,
])
pub fn request(limits: Limits) -> Option<[usize; 4]> {
Some([SPAWN, pack(limits)?, 0, 0])
}
/// The name (and how much of it counts) and limits of a spawn request.
pub fn parse(words: [usize; 4]) -> Option<([u8; MAX_NAME], usize, Limits)> {
let len = words[0] >> 8;
if words[0] & 0xFF != SPAWN || len == 0 || len > MAX_NAME {
return None;
}
let mut name = [0u8; MAX_NAME];
for (b, w) in name.as_chunks_mut::<8>().0.iter_mut().zip(&words[2..]) {
*b = (*w as u64).to_le_bytes();
}
Some((name, len, unpack(words[1])))
/// The limits of a spawn request.
pub fn parse(words: [usize; 4]) -> Option<Limits> {
(words[0] == SPAWN).then(|| unpack(words[1]))
}
#[cfg(test)]
@@ -84,27 +67,18 @@ mod tests {
notifications: 3,
regions: 255,
};
let (name, len, back) = parse(request(b"bin/hello", l).unwrap()).unwrap();
assert_eq!(&name[..len], b"bin/hello");
assert_eq!(back, l);
assert_eq!(parse(request(l).unwrap()), Some(l));
}
#[test]
fn what_cannot_be_said_is_refused() {
let l = Limits::default();
assert!(request(b"", l).is_none());
assert!(request(&[b'a'; 17], l).is_none());
assert!(
request(
b"x",
Limits {
endpoints: 256,
..l
}
)
request(Limits {
endpoints: 256,
..Limits::default()
})
.is_none()
);
assert!(parse([SPAWN | 17 << 8, 0, 0, 0]).is_none());
assert!(parse([1 | 3 << 8, 0, 0, 0]).is_none());
assert!(parse([1, 0, 0, 0]).is_none());
}
}

View File

@@ -9,6 +9,7 @@ const LOG_SHIFT: u32 = 8;
const STORAGE_SHIFT: u32 = 16;
const ARGS_BIT: usize = 1 << 24;
const DISPLAY_SHIFT: u32 = 32;
const DATA_SHIFT: u32 = 40;
/// Where the arguments page sits: the program's arguments, NUL separated,
/// the first its name. Below the stack, which ends at the image.
@@ -24,6 +25,8 @@ pub struct StartArg {
pub args: bool,
/// The display server, for a program that draws on the screen.
pub display: Option<CapSlot>,
/// The app's own data folder, read-write (its `storage` capability).
pub data: Option<CapSlot>,
}
fn field(arg: usize, shift: u32) -> Option<CapSlot> {
@@ -42,6 +45,7 @@ impl StartArg {
| put(self.storage, STORAGE_SHIFT)
| if self.args { ARGS_BIT } else { 0 }
| put(self.display, DISPLAY_SHIFT)
| put(self.data, DATA_SHIFT)
}
pub fn decode(arg: usize) -> Self {
@@ -51,6 +55,7 @@ impl StartArg {
storage: field(arg, STORAGE_SHIFT),
args: arg & ARGS_BIT != 0,
display: field(arg, DISPLAY_SHIFT),
data: field(arg, DATA_SHIFT),
}
}
}
@@ -67,6 +72,7 @@ mod tests {
storage: CapSlot::new(5),
args: true,
display: CapSlot::new(7),
data: CapSlot::new(62),
};
assert_eq!(StartArg::decode(a.encode()), a);
assert_eq!(StartArg::decode(0), StartArg::default());