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

18
Cargo.lock generated
View File

@@ -21,6 +21,13 @@ dependencies = [
name = "alarm-server"
version = "0.1.0"
[[package]]
name = "app-image"
version = "0.1.0"
dependencies = [
"manifest",
]
[[package]]
name = "audio-capture-server"
version = "0.1.0"
@@ -57,6 +64,14 @@ version = "0.1.0"
name = "contacts-server"
version = "0.1.0"
[[package]]
name = "counter"
version = "0.1.0"
dependencies = [
"libkos",
"types",
]
[[package]]
name = "display-qemu"
version = "0.1.0"
@@ -265,6 +280,7 @@ name = "permission-manager"
version = "0.1.0"
dependencies = [
"libkos",
"manifest",
"server-framework",
"types",
]
@@ -321,6 +337,7 @@ name = "shell"
version = "0.1.0"
dependencies = [
"libkos",
"manifest",
"types",
]
@@ -355,6 +372,7 @@ name = "spawntest"
version = "0.1.0"
dependencies = [
"libkos",
"manifest",
"types",
]

View File

@@ -4,6 +4,7 @@ members = [
"crates/apps/hello",
"crates/apps/shell",
"crates/apps/cat",
"crates/apps/counter",
"crates/apps/ls",
"crates/apps/echo",
"crates/apps/sleep",
@@ -13,6 +14,7 @@ members = [
"crates/domain",
"crates/elf",
"crates/manifest",
"crates/tools/app-image",
"crates/init",
"crates/kernel",
"crates/platform",

View File

@@ -55,7 +55,7 @@ build:
-p init -p echo -p sleep -p snake \
-p display-qemu -p input-qemu -p storage-qemu \
-p log-server -p permission-manager -p session-server -p smptest -p spawntest -p hello \
-p terminal-server -p shell -p cat -p ls
-p terminal-server -p shell -p cat -p ls -p counter
cargo build --target $(RISCV_TARGET) --release -p adapter-riscv
image: build

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 {
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(child.process);
syscall::kill(process);
}
bits & DIED == 0
}) {}
} else {
let _ = syscall::kill(child.process);
println!("shell: {}: cannot start", args[0]);
let _ = syscall::kill(process);
}
syscall::drop_cap(child.process);
if let Some(g) = grant {
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),
}
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 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>,
}
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,
}
}
fn granted_bits(&self, process: ProcessId) -> u16 {
let idx = process.raw() as usize;
if idx >= MAX_PROCESSES {
return 0;
const fn code(c: usize) -> [usize; 4] {
[c, 0, 0, 0]
}
let mut bits = 0u16;
for i in 0..NUM_KINDS {
if self.grants[idx][i] {
bits |= 1 << i;
/// 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
}
bits
/// 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);
}
}
impl Server for PermissionManager {
type Request = PermissionRequest;
type Response = PermissionResponse;
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 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;
fn try_launch(&mut self, badge: usize, page: CapSlot) -> Result<(CapSlot, Capabilities), [usize; 4]> {
let page = read_page(page).ok_or(code(reply::FAILED))?;
let mut args = [""; MAX_ARGS];
let mut n = 0;
for (slot, p) in args.iter_mut().zip(parts(&page)) {
*slot = p;
n += 1;
}
self.grants[idx][kind.index()] = true;
PermissionResponse::Granted
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;
syscall::drop_cap(child.process);
return Err(code(reply::FAILED));
}
self.grants[idx][kind.index()] = false;
PermissionResponse::Revoked
self.running[index] = Some(Running { id: plan.app.id, process: child.process, granted: plan.granted, data });
Ok((child.process, plan.granted))
}
PermissionRequest::CheckCapability { process, kind } => {
let idx = process.raw() as usize;
if idx >= MAX_PROCESSES {
return PermissionResponse::HasCapability(false);
/// 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
}
PermissionResponse::HasCapability(self.grants[idx][kind.index()])
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::ListGranted { process } => {
PermissionResponse::GrantedList(self.granted_bits(process))
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);
}
}
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 {
request(Limits {
endpoints: 256,
..l
}
)
..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());

View File

@@ -1,2 +0,0 @@
# What snake needs beyond the terminal and storage.
needs = display

View File

@@ -1,11 +0,0 @@
The shell runs /bin/<name> by asking init to load it (init alone may
create processes), then gives the program the terminal as its output
and a read-only storage grant to the current directory, writes its
arguments onto a page it moves into the program, starts it and waits
for it to die. Then it takes the grant back. Ctrl+C kills it.
A program that needs more says so in /bin/<name>.manifest, e.g.
`needs = display` for snake, which then gets the screen and keyboard
until it quits. A need the shell does not know, it refuses to run.
Built-ins: help, echo, clear, cd, pwd.

10
disk/home/docs/shell.txt Normal file
View File

@@ -0,0 +1,10 @@
The shell runs an app by asking the permission manager, passing along
the terminal as its output and a read-only grant to the current folder:
all the shell has to give. The permission manager adds what the app's
manifest asks for and you allowed, and runs it. Ctrl+C stops it.
You decide what each app may use: `install <folder>` asks you, `apps`
shows what each may use, `allow` and `revoke` change it (revoking from
a running app stops it).
Built-ins: help, echo, clear, cd, pwd, apps, install, allow, revoke.

9
disk/home/readme.txt Normal file
View File

@@ -0,0 +1,9 @@
Welcome to k-os.
Every program here is an app that runs with only the capabilities it
was given: this file reached your screen because the shell handed `cat`
the terminal and read-only access to this folder, and nothing else.
Try: ls cd docs cat shell.txt cat ../readme.txt
apps snake (q quits) sleep 30, then Ctrl+C
install inbox/counter, then counter, counter

View File

@@ -1,8 +0,0 @@
Welcome to k-os.
Every program here runs with only the capabilities it was given:
this file reached your screen because the shell handed `cat` the
terminal and read-only access to this directory, and nothing else.
Try: ls cd docs cat shell.txt cat ../readme.txt
ls bin snake (q quits) sleep 30, then Ctrl+C

View File

@@ -122,24 +122,26 @@ kept for the next one, except Ctrl+C, which signals the notification its
first `ON_INTERRUPT` caller gave. `REDRAW` repaints the screen and takes
input focus back.
The desktop is the shell, given the terminal, storage, the display and a
spawn allowance. Programs live in `/bin`, without extensions. A command
that is not a built-in runs `/bin/name`: the shell spawns it, gives it a
call-only copy of the terminal (as its log, so its output) and a read-only
storage grant to the shell's current directory (`cd`, `pwd`), writes its
arguments page, starts it, waits for its death (Ctrl+C kills it) and
releases the grant. `/bin/name.manifest` (`types::manifest`) asks for more:
`needs = display` gets the display (`StartArg::display`), with which the
program takes focus; the shell has the terminal redraw once it is gone. A
need the shell does not know, it refuses to run.
The desktop is the shell, given the terminal, the permission manager and
`/home` (its `/`). Everything the owner runs is an app (docs/apps.md): a
command that is not a built-in asks the permission manager (`types::apps`,
client `libkos::apps`) to run it, passing the terminal as its output and a
read-only grant to the current folder (`cd`, `pwd`). The permission manager
adds what the app's manifest asks for and the owner allowed (the display,
its own data folder), runs it, and hands back the process; the shell waits
for its death (Ctrl+C kills it), releases the grant, and has the terminal
redraw if the app had the display. `install`, `apps`, `allow` and `revoke`
manage apps.
Only init holds `ProcessCreate`. Everyone else runs programs through init's
spawn service (`types::spawn`, client `libkos::spawn`), which init serves
once boot is done. A manifest entry with `spawns` gets a badged copy of
init's endpoint, the badge naming its allowance. `SPAWN(name, limits)` loads
the program into a new, unstarted process, gives it a call-only log
endpoint, and replies with the process capability (Execute and Read, no
Delegate). The caller delegates whatever else the child should hold and
init's endpoint, the badge naming its allowance. `SPAWN(limits)` carries a
read-only storage grant to the program file itself; init loads it through
that grant (it needs no path, and loads nothing its requester could not
read) into a new, unstarted process, gives it a call-only log endpoint, and
replies with the process capability (Execute and Read, Delegate kept, so
the caller may pass it on to whoever it runs the program for). The caller delegates whatever else the child should hold and
starts it with `SpawnThreadIn`. The child's limits come out of the caller's
allowance (`OVER_ALLOWANCE` if they do not fit) and go back when init hears
the child die (`WatchDeath`, up to four watchers per process, so the caller

View File

@@ -1,6 +1,6 @@
#!/bin/bash
# Builds the k-os disk: one ext4 filesystem holding every program init may
# start, in /bin. Init and storage themselves are packed into the kernel image.
# start, in /bin, and the apps (docs/apps.md). Init and storage themselves are packed into the kernel image.
set -euo pipefail
IMG="${IMG:-boot.img}"
@@ -21,9 +21,24 @@ copy() {
fi
}
mkdir -p "${ROOT}/bin"
copy echo echo
copy snake snake
# An app, installed and pre-approved by whoever builds the image, or (staged)
# a folder the owner may install. See docs/apps.md.
APP_IMAGE=(cargo run -q --release -p app-image --)
app() {
local crate="$1"
if [ -f "${BUILD_DIR}/${crate}" ]; then
"${APP_IMAGE[@]}" install "${ROOT}" "crates/apps/${crate}/manifest.toml" "${BUILD_DIR}/${crate}"
fi
}
staged() {
local crate="$1" folder="$2"
if [ -f "${BUILD_DIR}/${crate}" ]; then
"${APP_IMAGE[@]}" stage "${ROOT}/home/${folder}" "crates/apps/${crate}/manifest.toml" "${BUILD_DIR}/${crate}"
fi
}
mkdir -p "${ROOT}/bin" "${ROOT}/apps" "${ROOT}/data" "${ROOT}/system/permissions" "${ROOT}/home"
# System servers and the shell: init starts these.
copy display-qemu display
copy input-qemu input
copy log-server log
@@ -31,11 +46,14 @@ copy permission-manager permission
copy session-server session
copy terminal-server terminal
copy shell shell
copy cat cat
copy ls ls
copy sleep sleep
# Files that ship on the disk, programs' manifests among them.
cp -r disk/. "${ROOT}/"
# Apps.
app cat
app ls
app sleep
app snake
staged counter inbox/counter
# The owner's files.
cp -r disk/home/. "${ROOT}/home/"
# Only the test image: its presence makes init run the self-test.
if [ -n "${SMPTEST:-}" ]; then
copy smptest smptest
@@ -46,4 +64,4 @@ fi
rm -f "${IMG}"
mkfs.ext4 -q -F -b 4096 -L k-os -O "${FEATURES}" -d "${ROOT}" "${IMG}" "${SIZE}"
echo "=== ${IMG}: ext4, ${SIZE} ==="
ls -1 "${ROOT}" "${ROOT}/bin"
find "${ROOT}" -mindepth 1 | sed "s|${ROOT}/||" | sort