From 90e11eb2e8feaf0078f88b1a2ac6ce21454f9c97 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Tue, 27 Oct 2026 22:56:44 +0100 Subject: [PATCH] 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 --- Cargo.lock | 18 + Cargo.toml | 2 + Makefile | 2 +- crates/apps/cat/manifest.toml | 6 + crates/apps/counter/Cargo.toml | 14 + crates/apps/counter/manifest.toml | 10 + crates/apps/counter/src/main.rs | 59 +++ crates/apps/ls/manifest.toml | 6 + crates/apps/shell/Cargo.toml | 1 + crates/apps/shell/src/main.rs | 218 ++++++----- crates/apps/sleep/manifest.toml | 6 + crates/apps/snake/manifest.toml | 9 + crates/apps/spawntest/Cargo.toml | 1 + crates/apps/spawntest/src/main.rs | 62 ++- crates/init/src/launch.rs | 30 +- crates/init/src/lib.rs | 2 +- crates/init/src/manifest.rs | 37 +- crates/init/src/spawn.rs | 51 ++- crates/libkos/src/apps.rs | 116 ++++++ crates/libkos/src/fs.rs | 9 +- crates/libkos/src/io.rs | 26 +- crates/libkos/src/lib.rs | 1 + crates/libkos/src/spawn.rs | 11 +- crates/libkos/src/start.rs | 6 + crates/manifest/src/capability.rs | 28 ++ crates/manifest/src/fields.rs | 5 +- crates/servers/permission-manager/Cargo.toml | 1 + .../servers/permission-manager/src/consent.rs | 83 ++++ crates/servers/permission-manager/src/lib.rs | 42 +- .../servers/permission-manager/src/policy.rs | 136 +++++++ .../permission-manager/src/protocol.rs | 130 ------ .../servers/permission-manager/src/server.rs | 370 +++++++++++++++--- .../servers/permission-manager/src/store.rs | 196 ++++++++++ .../permission-manager/src/tests/mod.rs | 3 +- .../permission-manager/src/tests/policy.rs | 204 ++++++++++ .../permission-manager/src/tests/protocol.rs | 62 --- .../permission-manager/src/tests/server.rs | 87 ---- crates/tools/app-image/Cargo.toml | 7 + crates/tools/app-image/src/main.rs | 64 +++ crates/types/src/apps.rs | 49 +++ crates/types/src/lib.rs | 2 +- crates/types/src/manifest.rs | 64 --- crates/types/src/spawn.rs | 60 +-- crates/types/src/start_arg.rs | 6 + disk/bin/snake.manifest | 2 - disk/docs/shell.txt | 11 - disk/home/docs/shell.txt | 10 + disk/home/readme.txt | 9 + disk/readme.txt | 8 - docs/ipc.md | 30 +- scripts/make-disk-image.sh | 38 +- 51 files changed, 1766 insertions(+), 644 deletions(-) create mode 100644 crates/apps/cat/manifest.toml create mode 100644 crates/apps/counter/Cargo.toml create mode 100644 crates/apps/counter/manifest.toml create mode 100644 crates/apps/counter/src/main.rs create mode 100644 crates/apps/ls/manifest.toml create mode 100644 crates/apps/sleep/manifest.toml create mode 100644 crates/apps/snake/manifest.toml create mode 100644 crates/libkos/src/apps.rs create mode 100644 crates/servers/permission-manager/src/consent.rs create mode 100644 crates/servers/permission-manager/src/policy.rs delete mode 100644 crates/servers/permission-manager/src/protocol.rs create mode 100644 crates/servers/permission-manager/src/store.rs create mode 100644 crates/servers/permission-manager/src/tests/policy.rs delete mode 100644 crates/servers/permission-manager/src/tests/protocol.rs delete mode 100644 crates/servers/permission-manager/src/tests/server.rs create mode 100644 crates/tools/app-image/Cargo.toml create mode 100644 crates/tools/app-image/src/main.rs create mode 100644 crates/types/src/apps.rs delete mode 100644 crates/types/src/manifest.rs delete mode 100644 disk/bin/snake.manifest delete mode 100644 disk/docs/shell.txt create mode 100644 disk/home/docs/shell.txt create mode 100644 disk/home/readme.txt delete mode 100644 disk/readme.txt diff --git a/Cargo.lock b/Cargo.lock index 3b8906f..62fdf02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/Cargo.toml b/Cargo.toml index cca0f74..fe4b98c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", diff --git a/Makefile b/Makefile index 0718374..47f1b9e 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/crates/apps/cat/manifest.toml b/crates/apps/cat/manifest.toml new file mode 100644 index 0000000..b5a730c --- /dev/null +++ b/crates/apps/cat/manifest.toml @@ -0,0 +1,6 @@ +[app] +id = "kos.cat" +name = "cat" +version = "1.0.0" +binary = "cat" +author = "K-OS" diff --git a/crates/apps/counter/Cargo.toml b/crates/apps/counter/Cargo.toml new file mode 100644 index 0000000..56821c2 --- /dev/null +++ b/crates/apps/counter/Cargo.toml @@ -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 } diff --git a/crates/apps/counter/manifest.toml b/crates/apps/counter/manifest.toml new file mode 100644 index 0000000..611a635 --- /dev/null +++ b/crates/apps/counter/manifest.toml @@ -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"] diff --git a/crates/apps/counter/src/main.rs b/crates/apps/counter/src/main.rs new file mode 100644 index 0000000..0237ec7 --- /dev/null +++ b/crates/apps/counter/src/main.rs @@ -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); diff --git a/crates/apps/ls/manifest.toml b/crates/apps/ls/manifest.toml new file mode 100644 index 0000000..04ddb9b --- /dev/null +++ b/crates/apps/ls/manifest.toml @@ -0,0 +1,6 @@ +[app] +id = "kos.ls" +name = "ls" +version = "1.0.0" +binary = "ls" +author = "K-OS" diff --git a/crates/apps/shell/Cargo.toml b/crates/apps/shell/Cargo.toml index bb83856..0b88a06 100644 --- a/crates/apps/shell/Cargo.toml +++ b/crates/apps/shell/Cargo.toml @@ -10,4 +10,5 @@ bench = false [dependencies] types = { workspace = true } +manifest = { workspace = true } libkos = { workspace = true } diff --git a/crates/apps/shell/src/main.rs b/crates/apps/shell/src/main.rs index 7b2bab0..da26fce 100644 --- a/crates/apps/shell/src/main.rs +++ b/crates/apps/shell/src/main.rs @@ -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 { 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/` 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 { - let mut path = [0u8; types::storage::MAX_PATH]; - let path = bin_path(&mut path, name, b".manifest")?; - let Ok(file) = sh.fs.open(path) else { - return Some(Needs::default()); - }; - let mut text = [0u8; MAX_MANIFEST]; - let read = sh.fs.read(file, &mut text); - let _ = sh.fs.close(file); - match types::manifest::parse(&text[..read.ok()?]) { - Ok(needs) => Some(needs), - Err(types::manifest::Unknown(what)) => { - println!("shell: {}: its manifest asks for {}", name, core::str::from_utf8(what).unwrap_or("?")); - None +impl core::fmt::Display for Names { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + for (i, c) in self.0.iter().enumerate() { + if i > 0 { + f.write_str(", ")?; + } + f.write_str(c.name())?; } + Ok(()) } } -/// Runs `/bin/args[0]` with `args` in the current directory, and waits -/// for it to finish or Ctrl+C. +fn names(bits: usize) -> Names { + Names(Capabilities::from_bits(bits as u32)) +} + +/// Says why the permission manager refused, about `what`. +fn refused(what: &str, r: Refused) { + match r.code { + reply::NOT_INSTALLED => println!("shell: {}: not found", what), + reply::MISSING => println!("shell: {}: cannot run without {}", what, names(r.detail)), + reply::ALREADY_INSTALLED => println!("shell: {}: already installed", what), + reply::UNAVAILABLE => println!("shell: {}: this device has no {}", what, names(r.detail)), + reply::DECLINED => println!("shell: {}: not installed", what), + reply::NO_MANIFEST => println!("shell: {}: no app here (no manifest.toml)", what), + reply::BAD_MANIFEST if r.detail > 0 => println!("shell: {}: bad manifest, line {}", what, r.detail), + reply::BAD_MANIFEST => println!("shell: {}: bad manifest", what), + reply::NOT_REQUESTED => println!("shell: {}: never asked for that", what), + reply::BUSY => println!("shell: no room to run {}", what), + _ => println!("shell: {}: failed", what), + } +} + +/// Runs app `args[0]` with `args` in the current folder, and waits for it +/// to finish or Ctrl+C. fn run_program(sh: &mut Shell, args: &[&str]) { - let mut file = [0u8; types::spawn::MAX_NAME]; - let Some(file) = bin_path(&mut file, args[0], b"") else { - println!("shell: {}: name too long", args[0]); - return; - }; - let Some(needs) = needs(sh, args[0]) else { - return; - }; - let child = match spawn(INIT, file, PROGRAM) { - Ok(c) => c, - Err(SpawnError::NotFound) => return println!("shell: {}: not found", args[0]), - Err(SpawnError::OverAllowance) => return println!("shell: no room to run {}", args[0]), - Err(SpawnError::Failed) => return println!("shell: {}: cannot run", args[0]), - }; - let call = PermissionSet::empty().with(Permission::Execute); - let grant = sh.fs.grant(sh.cwd.as_bytes(), false).ok(); - let arg = StartArg { - log: child.give(TERMINAL, call), - storage: grant.and_then(|g| child.give(g, call)), - args: child.set_args(args), - // It subscribes to input by sending the display a notification. - display: needs.display.then(|| child.give_sending(DISPLAY, call)).flatten(), - console: None, - }; + let folder = sh.fs.grant(sh.cwd.as_bytes(), false).ok(); // A Ctrl+C typed since the last program is not for this one. let _ = syscall::poll(sh.done); - let watched = syscall::watch_death(child.process, sh.done).is_ok(); - if child.start_with(arg) && watched { - while syscall::wait(sh.done).is_ok_and(|bits| { - if bits & INTERRUPTED != 0 { - syscall::kill(child.process); + match sh.apps.launch(args, Some(TERMINAL), folder) { + Ok((process, granted)) => { + if syscall::watch_death(process, sh.done).is_ok() { + while syscall::wait(sh.done).is_ok_and(|bits| { + if bits & INTERRUPTED != 0 { + syscall::kill(process); + } + bits & DIED == 0 + }) {} + } else { + let _ = syscall::kill(process); } - bits & DIED == 0 - }) {} - } else { - let _ = syscall::kill(child.process); - println!("shell: {}: cannot start", args[0]); + syscall::drop_cap(process); + // It drew over the terminal and took the keyboard. + if Capabilities::from_bits(granted).contains(Capability::Display) { + libkos::terminal::redraw(TERMINAL); + } + } + Err(r) => refused(args[0], r), } - syscall::drop_cap(child.process); - if let Some(g) = grant { + if let Some(g) = folder { let _ = libkos::fs::release(g); } - // It drew over the terminal and took the keyboard. - if needs.display { - libkos::terminal::redraw(TERMINAL); +} + +/// Installs the app in folder `from` (its manifest and binary); the owner +/// is asked on the terminal. +fn install(sh: &mut Shell, from: &str) { + let Some(path) = sh.cwd.join(from) else { + return println!("install: {}: no such folder", from); + }; + let Ok(source) = sh.fs.grant(path.as_bytes(), false) else { + return println!("install: {}: no such folder", from); + }; + match sh.apps.install(source) { + Ok(0) => println!("installed"), + Ok(bits) => println!("installed, may use: {}", names(bits as usize)), + Err(r) => refused(from, r), + } + let _ = libkos::fs::release(source); +} + +fn list(sh: &mut Shell) { + let mut text = [0u8; types::apps::PAGE_SIZE]; + match sh.apps.list(&mut text) { + Ok(n) => print!("{}", core::str::from_utf8(&text[..n]).unwrap_or("")), + Err(r) => refused("apps", r), + } +} + +/// `allow ` 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: {} ", 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 , clear, cd , 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 , allow|revoke "); + 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 "), + }, + 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, diff --git a/crates/apps/sleep/manifest.toml b/crates/apps/sleep/manifest.toml new file mode 100644 index 0000000..64ed520 --- /dev/null +++ b/crates/apps/sleep/manifest.toml @@ -0,0 +1,6 @@ +[app] +id = "kos.sleep" +name = "sleep" +version = "1.0.0" +binary = "sleep" +author = "K-OS" diff --git a/crates/apps/snake/manifest.toml b/crates/apps/snake/manifest.toml new file mode 100644 index 0000000..e522472 --- /dev/null +++ b/crates/apps/snake/manifest.toml @@ -0,0 +1,9 @@ +[app] +id = "kos.snake" +name = "Snake" +version = "1.0.0" +binary = "snake" +author = "K-OS" + +[capabilities] +required = ["display"] diff --git a/crates/apps/spawntest/Cargo.toml b/crates/apps/spawntest/Cargo.toml index 74426e5..026ccec 100644 --- a/crates/apps/spawntest/Cargo.toml +++ b/crates/apps/spawntest/Cargo.toml @@ -10,4 +10,5 @@ bench = false [dependencies] types = { workspace = true } +manifest = { workspace = true } libkos = { workspace = true } diff --git a/crates/apps/spawntest/src/main.rs b/crates/apps/spawntest/src/main.rs index d6e780d..7eed431 100644 --- a/crates/apps/spawntest/src/main.rs +++ b/crates/apps/spawntest/src/main.rs @@ -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 { + 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) -> 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) -> 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(()) } diff --git a/crates/init/src/launch.rs b/crates/init/src/launch.rs index 2d22a0b..60b7bfe 100644 --- a/crates/init/src/launch.rs +++ b/crates/init/src/launch.rs @@ -44,9 +44,9 @@ impl<'a> Launcher<'a> { ) -> Result { let spawn_ep = syscall::create_endpoint().ok_or("no spawn endpoint")?; let deaths = syscall::create_notification().map_err(|_| "no death notification")?; - // Storage was started by boot, before init; programs may use it too. - let mut served = [None; MAX_SERVED]; - served[0] = Some(("storage", crate::STORAGE_EP)); + // Storage, started by boot, is not among them: it is reached + // through grants (a program's `storage` scope). + let served = [None; MAX_SERVED]; Ok(Self { storage, scratch, devices, served, next_badge: 1, spawn_ep, deaths, spawner: Spawner::new() }) } @@ -78,20 +78,21 @@ impl<'a> Launcher<'a> { // and take other clients' requests. for name in p.uses { let ep = self.endpoint(name).ok_or("needs a server that is not running")?; - let copy = match *name { - // Storage issues its own badges, each naming a scope. - "storage" => self.storage.grant(b"", true).map_err(|_| "storage refused a grant")?, - _ => { - let badge = Badge::new(self.next_badge); - self.next_badge += 1; - syscall::mint(ep, badge, perms(&[Permission::Execute, Permission::Delegate])) - .map_err(|_| "cannot mint an endpoint")? - } - }; + let badge = Badge::new(self.next_badge); + self.next_badge += 1; + let copy = syscall::mint(ep, badge, perms(&[Permission::Execute, Permission::Delegate])) + .map_err(|_| "cannot mint an endpoint")?; let given = syscall::delegate(copy, child, perms(&[Permission::Execute, Permission::Delegate])); syscall::drop_cap(copy); given.ok_or("cannot give an endpoint")?; } + // Storage issues its own badges, each naming a scope. + if let Some(scope) = p.storage { + let grant = self.storage.grant(scope.path, scope.write).map_err(|_| "storage refused a grant")?; + let given = syscall::delegate(grant, child, perms(&[Permission::Execute, Permission::Delegate])); + syscall::drop_cap(grant); + given.ok_or("cannot give storage")?; + } if let Some(allowance) = p.spawns { self.give_spawn(child, allowance)?; } @@ -126,7 +127,8 @@ impl<'a> Launcher<'a> { let badge: Badge = self.spawner.add_client(allowance).ok_or("too many spawn clients")?; let minted = perms(&[Permission::Execute, Permission::Delegate]); let copy = syscall::mint(self.spawn_ep, badge, minted).map_err(|_| "cannot mint")?; - let given = syscall::delegate(copy, child, perms(&[Permission::Execute])); + // Delegate, to send the program file's grant with each request. + let given = syscall::delegate(copy, child, perms(&[Permission::Execute, Permission::Delegate])); syscall::drop_cap(copy); given.map(|_| ()).ok_or("cannot give the spawn endpoint") } diff --git a/crates/init/src/lib.rs b/crates/init/src/lib.rs index 940d954..4fbf816 100644 --- a/crates/init/src/lib.rs +++ b/crates/init/src/lib.rs @@ -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"); diff --git a/crates/init/src/manifest.rs b/crates/init/src/manifest.rs index 4b6fa17..687776e 100644 --- a/crates/init/src/manifest.rs +++ b/crates/init/src/manifest.rs @@ -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, /// 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) }]; diff --git a/crates/init/src/spawn.rs b/crates/init/src/spawn.rs index 9d3ecfa..371c7b7 100644 --- a/crates/init/src/spawn.rs +++ b/crates/init/src/spawn.rs @@ -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 { - 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) { } impl Launcher<'_> { - fn spawn(&mut self, badge: usize, r: &Request) -> ([usize; 4], Option) { + /// 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) { 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) -> ([usize; 4], Option) { - 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) -> ([usize; 4], Option) { + 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), } } diff --git a/crates/libkos/src/apps.rs b/crates/libkos/src/apps.rs new file mode 100644 index 0000000..cad269b --- /dev/null +++ b/crates/libkos/src/apps.rs @@ -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 = core::result::Result; + +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 { + 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) -> Result<(usize, Option)> { + 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)> { + 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::() + 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, folder: Option) -> 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 { + 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 { + 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(|_| ()) + } +} diff --git a/crates/libkos/src/fs.rs b/crates/libkos/src/fs.rs index 9470dab..48978e8 100644 --- a/crates/libkos/src/fs.rs +++ b/crates/libkos/src/fs.rs @@ -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]> { diff --git a/crates/libkos/src/io.rs b/crates/libkos/src/io.rs index c5b5061..910804c 100644 --- a/crates/libkos/src/io.rs +++ b/crates/libkos/src/io.rs @@ -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, } /// 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(); } diff --git a/crates/libkos/src/lib.rs b/crates/libkos/src/lib.rs index 611f2bb..abc1ce6 100644 --- a/crates/libkos/src/lib.rs +++ b/crates/libkos/src/lib.rs @@ -3,6 +3,7 @@ extern crate alloc; pub mod alloc_impl; +pub mod apps; pub mod device; mod arch; pub mod display; diff --git a/crates/libkos/src/spawn.rs b/crates/libkos/src/spawn.rs index ec0c5f9..5772651 100644 --- a/crates/libkos/src/spawn.rs +++ b/crates/libkos/src/spawn.rs @@ -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, } -pub fn spawn(init: CapSlot, name: &[u8], limits: Limits) -> Result { - 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 { + 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, diff --git a/crates/libkos/src/start.rs b/crates/libkos/src/start.rs index 7b44229..0400aab 100644 --- a/crates/libkos/src/start.rs +++ b/crates/libkos/src/start.rs @@ -38,6 +38,12 @@ pub fn storage() -> Option { StartArg::decode(arg()).storage } +/// The app's own data folder (a storage grant, read-write), if it was +/// granted `storage`. +pub fn data() -> Option { + StartArg::decode(arg()).data +} + /// The display server endpoint this process was given, if any. pub fn display() -> Option { StartArg::decode(arg()).display diff --git a/crates/manifest/src/capability.rs b/crates/manifest/src/capability.rs index 2b395ed..1971229 100644 --- a/crates/manifest/src/capability.rs +++ b/crates/manifest/src/capability.rs @@ -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 { + 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 diff --git a/crates/manifest/src/fields.rs b/crates/manifest/src/fields.rs index 8566a77..5814d05 100644 --- a/crates/manifest/src/fields.rs +++ b/crates/manifest/src/fields.rs @@ -36,7 +36,8 @@ impl core::fmt::Debug for Text { impl core::fmt::Display for Text { 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()) } } diff --git a/crates/servers/permission-manager/Cargo.toml b/crates/servers/permission-manager/Cargo.toml index 1df0fdc..ecca77a 100644 --- a/crates/servers/permission-manager/Cargo.toml +++ b/crates/servers/permission-manager/Cargo.toml @@ -10,6 +10,7 @@ bench = false [dependencies] types = { workspace = true } +manifest = { workspace = true } libkos = { workspace = true } server-framework = { workspace = true } diff --git a/crates/servers/permission-manager/src/consent.rs b/crates/servers/permission-manager/src/consent.rs new file mode 100644 index 0000000..fa5f189 --- /dev/null +++ b/crates/servers/permission-manager/src/consent.rs @@ -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(()) + } +} diff --git a/crates/servers/permission-manager/src/lib.rs b/crates/servers/permission-manager/src/lib.rs index 9af9e4b..1e5c641 100644 --- a/crates/servers/permission-manager/src/lib.rs +++ b/crates/servers/permission-manager/src/lib.rs @@ -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) } diff --git a/crates/servers/permission-manager/src/policy.rs b/crates/servers/permission-manager/src/policy.rs new file mode 100644 index 0000000..cd01cd0 --- /dev/null +++ b/crates/servers/permission-manager/src/policy.rs @@ -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; + /// The installed app run as `command`. + fn find(&mut self, command: &str) -> Option; + fn decision(&mut self, id: &AppId) -> Option; + 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( + store: &mut S, + consent: &mut impl Consent, + source: &S::Source, + available: Capabilities, +) -> Result { + 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 { + 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 { + 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 { + 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) +} diff --git a/crates/servers/permission-manager/src/protocol.rs b/crates/servers/permission-manager/src/protocol.rs deleted file mode 100644 index 541877f..0000000 --- a/crates/servers/permission-manager/src/protocol.rs +++ /dev/null @@ -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 { - 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 { - 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 { - 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, - } - } -} diff --git a/crates/servers/permission-manager/src/server.rs b/crates/servers/permission-manager/src/server.rs index ce7b19a..4a89bab 100644 --- a/crates/servers/permission-manager/src/server.rs +++ b/crates/servers/permission-manager/src/server.rs @@ -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, + folder: Option, } -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, +} - fn granted_bits(&self, process: ProcessId) -> u16 { - let idx = process.raw() as usize; - if idx >= MAX_PROCESSES { - return 0; - } - let mut bits = 0u16; - for i in 0..NUM_KINDS { - if self.grants[idx][i] { - bits |= 1 << i; - } - } - bits +pub struct PermissionServer { + store: DiskStore, + consent: TerminalConsent, + /// Init's spawn endpoint. + init: CapSlot, + display: Option, + /// Bound to the serving thread: deaths of running apps. + deaths: CapSlot, + staged: Staged, + running: [Option; MAX_RUNNING], +} + +fn refused(r: Refusal) -> [usize; 4] { + let (code, detail) = match r { + Refusal::NoManifest => (reply::NO_MANIFEST, 0), + Refusal::BadManifest(e) => (reply::BAD_MANIFEST, line_of(e)), + Refusal::AlreadyInstalled => (reply::ALREADY_INSTALLED, 0), + Refusal::NotInstalled => (reply::NOT_INSTALLED, 0), + Refusal::Unavailable(c) => (reply::UNAVAILABLE, c.bits() as usize), + Refusal::Declined => (reply::DECLINED, 0), + Refusal::Missing(c) => (reply::MISSING, c.bits() as usize), + Refusal::NotRequested => (reply::NOT_REQUESTED, 0), + Refusal::Store => (reply::FAILED, 0), + }; + [code, detail, 0, 0] +} + +fn line_of(e: manifest::Error) -> usize { + use manifest::Error::*; + match e { + Syntax { line } | Duplicate { line } | Unknown { line } | Invalid { line } => line, + Missing(_) | Conflict => 0, } } -impl Server for PermissionManager { - type Request = PermissionRequest; - type Response = PermissionResponse; +const fn code(c: usize) -> [usize; 4] { + [c, 0, 0, 0] +} - fn handle(&mut self, request: PermissionRequest) -> PermissionResponse { - match request { - PermissionRequest::RequestCapability { process, kind } => { - let idx = process.raw() as usize; - if idx >= MAX_PROCESSES { - return PermissionResponse::Denied; - } - self.grants[idx][kind.index()] = true; - PermissionResponse::Granted +/// The client's page, copied out; the mapping goes again at once. +fn read_page(page: CapSlot) -> Option> { + let w = window::map(page, PAGE_VA, PagePerms::ReadWrite).ok(); + let out = w.map(|w| { + let mut buf = vec![0u8; PAGE_SIZE.min(w.len())]; + w.copy_to(ByteOffset::ZERO, &mut buf); + buf + }); + syscall::drop_cap(page); + out +} + +/// `text` into the client's page. +fn write_page(page: CapSlot, text: &[u8]) -> bool { + let w = window::map(page, PAGE_VA, PagePerms::ReadWrite).ok(); + let ok = w.is_some_and(|w| text.len() <= w.len() && { + w.copy_from(ByteOffset::ZERO, text); + true + }); + syscall::drop_cap(page); + ok +} + +/// The NUL-separated parts of a page, up to the first empty one. +fn parts(page: &[u8]) -> impl Iterator { + 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, 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) -> [usize; 4] { + if self.staged.badge != badge { + self.unstage(); + self.staged.badge = badge; + } + let slot = if op == wire::PASS_OUTPUT { &mut self.staged.output } else { &mut self.staged.folder }; + if let Some(old) = core::mem::replace(slot, cap) { + syscall::drop_cap(old); + } + code(reply::OK) + } + + fn unstage(&mut self) { + for cap in [self.staged.output.take(), self.staged.folder.take()].into_iter().flatten() { + syscall::drop_cap(cap); + } + } + + fn launch(&mut self, badge: usize, page: CapSlot) -> ([usize; 4], Option) { + let result = self.try_launch(badge, page); + self.unstage(); + match result { + Ok((process, granted)) => ([reply::OK, granted.bits() as usize, 0, 0], Some(process.into())), + Err(words) => (words, None), + } + } + + fn try_launch(&mut self, badge: usize, page: CapSlot) -> Result<(CapSlot, Capabilities), [usize; 4]> { + let page = read_page(page).ok_or(code(reply::FAILED))?; + let mut args = [""; MAX_ARGS]; + let mut n = 0; + for (slot, p) in args.iter_mut().zip(parts(&page)) { + *slot = p; + n += 1; + } + let args = &args[..n]; + let command = *args.first().ok_or(code(reply::NOT_INSTALLED))?; + let available = self.available(); + let plan = policy::plan_launch(&mut self.store, command, available).map_err(refused)?; + let index = self.running.iter().position(Option::is_none).ok_or(code(reply::BUSY))?; + + let mut p = [0u8; PATH_BUF]; + let binary = store::binary_path(&mut p, &plan.app).ok_or(code(reply::FAILED))?; + let file = self.store.storage().grant(binary, false).map_err(|_| code(reply::FAILED))?; + let spawned = libkos::spawn::spawn(self.init, file, PROGRAM); + let _ = libkos::fs::release(file); + let child = spawned.map_err(|e| match e { + libkos::spawn::SpawnError::OverAllowance => code(reply::BUSY), + _ => code(reply::FAILED), + })?; + + let call = PermissionSet::empty().with(Permission::Execute); + let data = match plan.granted.contains(Capability::Storage) { + true => store::data_path(&mut p, &plan.app.id).and_then(|d| self.store.storage().grant(d, true).ok()), + false => None, + }; + let staged = (badge == self.staged.badge).then_some(&self.staged); + let arg = StartArg { + log: staged.and_then(|s| s.output).and_then(|o| child.give(o, call)).or(child.log()), + storage: staged.and_then(|s| s.folder).and_then(|f| child.give(f, call)), + data: data.and_then(|d| child.give(d, call)), + // It subscribes to input by sending the display a notification. + display: self.display.filter(|_| plan.granted.contains(Capability::Display)).and_then(|d| child.give_sending(d, call)), + args: child.set_args(args), + console: None, + }; + let watched = self.watch(child.process, index); + if !watched || !child.start_with(arg) { + let _ = syscall::kill(child.process); + if let Some(d) = data { + let _ = libkos::fs::release(d); } - PermissionRequest::RevokeCapability { process, kind } => { - let idx = process.raw() as usize; - if idx >= MAX_PROCESSES { - return PermissionResponse::Denied; - } - self.grants[idx][kind.index()] = false; - PermissionResponse::Revoked + syscall::drop_cap(child.process); + return Err(code(reply::FAILED)); + } + self.running[index] = Some(Running { id: plan.app.id, process: child.process, granted: plan.granted, data }); + Ok((child.process, plan.granted)) + } + + /// Has `process`'s death signal bit `index`. + fn watch(&self, process: CapSlot, index: usize) -> bool { + let signal = PermissionSet::empty().with(Permission::Write); + let Ok(copy) = syscall::mint(self.deaths, Badge::new(1 << index), signal) else { + return false; + }; + let watched = syscall::watch_death(process, copy).is_ok(); + syscall::drop_cap(copy); + watched + } + + fn install(&mut self, source: Option) -> [usize; 4] { + let Some(source) = source else { return code(reply::FAILED) }; + let available = self.available(); + let result = policy::install(&mut self.store, &mut self.consent, &source, available); + syscall::drop_cap(source); + match result { + Ok(app) => { + let granted = self.store.decision(&app.id).map_or(0, |d| d.granted.bits() as usize); + [reply::OK, granted, 0, 0] } - PermissionRequest::CheckCapability { process, kind } => { - let idx = process.raw() as usize; - if idx >= MAX_PROCESSES { - return PermissionResponse::HasCapability(false); - } - PermissionResponse::HasCapability(self.grants[idx][kind.index()]) + Err(r) => refused(r), + } + } + + fn list(&mut self, page: CapSlot) -> [usize; 4] { + use core::fmt::Write; + let mut ids = Vec::new(); + self.store.ids(|id| ids.push(id)); + let mut text = Text(Vec::new()); + for id in ids { + let (Some(app), Some(d)) = (self.store.manifest(&id), self.store.decision(&id)) else { continue }; + let _ = writeln!(text, "{:<20} {:<10} {:<8} {}", app.id, app.command.as_str(), version(&app), CapabilityNames(d.granted)); + } + let len = text.0.len().min(PAGE_SIZE); + if write_page(page, &text.0[..len]) { [reply::OK, len, 0, 0] } else { code(reply::FAILED) } + } + + /// The owner changes app `id`'s (in the page) capability `index`. + fn change(&mut self, page: CapSlot, index: usize, allow: bool) -> [usize; 4] { + let page = read_page(page); + let id = page.as_deref().and_then(|p| parts(p).next()).and_then(AppId::new); + let (Some(id), Some(cap)) = (id, Capability::from_index(index)) else { + return code(reply::NOT_INSTALLED); + }; + let result = match allow { + true => policy::allow(&mut self.store, &id, cap), + false => policy::revoke(&mut self.store, &id, cap), + }; + if let Err(r) = result { + return refused(r); + } + if !allow { + // Its access dies at once: a running app that had it stops. + for r in self.running.iter().flatten().filter(|r| r.id == id && r.granted.contains(cap)) { + let _ = syscall::kill(r.process); } - PermissionRequest::ListGranted { process } => { - PermissionResponse::GrantedList(self.granted_bits(process)) + } + code(reply::OK) + } + + /// Apps behind `bits` died: their data grants go back. + fn reap(&mut self, bits: usize) { + for (i, slot) in self.running.iter_mut().enumerate() { + if bits & (1 << i) == 0 { + continue; + } + if let Some(r) = slot.take() { + if let Some(d) = r.data { + let _ = libkos::fs::release(d); + } + syscall::drop_cap(r.process); } } } } + +/// Text built for the client's page. +struct Text(Vec); + +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 { + 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) -> ([usize; 4], Option) { + 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); + } +} diff --git a/crates/servers/permission-manager/src/store.rs b/crates/servers/permission-manager/src/store.rs new file mode 100644 index 0000000..b5e6b46 --- /dev/null +++ b/crates/servers/permission-manager/src/store.rs @@ -0,0 +1,196 @@ +//! The `Store` port on the storage server: apps in `/apps//`, their +//! data in `/data//`, 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(&self, source: CapSlot, f: impl FnOnce(&mut Storage) -> T) -> Option { + 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 { + 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 { + 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 { + 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; diff --git a/crates/servers/permission-manager/src/tests/mod.rs b/crates/servers/permission-manager/src/tests/mod.rs index aa77244..8991200 100644 --- a/crates/servers/permission-manager/src/tests/mod.rs +++ b/crates/servers/permission-manager/src/tests/mod.rs @@ -1,2 +1 @@ -mod protocol; -mod server; +mod policy; diff --git a/crates/servers/permission-manager/src/tests/policy.rs b/crates/servers/permission-manager/src/tests/policy.rs new file mode 100644 index 0000000..c874360 --- /dev/null +++ b/crates/servers/permission-manager/src/tests/policy.rs @@ -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, + decisions: BTreeMap, + data_folders: Vec, + 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 { + self.manifests.get(id.as_str()).copied() + } + fn find(&mut self, command: &str) -> Option { + self.manifests.values().find(|m| m.command.as_str() == command).map(|m| m.id) + } + fn decision(&mut self, id: &AppId) -> Option { + 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, + asked: Vec, +} + +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::>())); + 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])))); +} diff --git a/crates/servers/permission-manager/src/tests/protocol.rs b/crates/servers/permission-manager/src/tests/protocol.rs deleted file mode 100644 index 72dad7c..0000000 --- a/crates/servers/permission-manager/src/tests/protocol.rs +++ /dev/null @@ -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()); -} diff --git a/crates/servers/permission-manager/src/tests/server.rs b/crates/servers/permission-manager/src/tests/server.rs deleted file mode 100644 index c558829..0000000 --- a/crates/servers/permission-manager/src/tests/server.rs +++ /dev/null @@ -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); -} diff --git a/crates/tools/app-image/Cargo.toml b/crates/tools/app-image/Cargo.toml new file mode 100644 index 0000000..27e62de --- /dev/null +++ b/crates/tools/app-image/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "app-image" +version = "0.1.0" +edition = "2024" + +[dependencies] +manifest = { workspace = true } diff --git a/crates/tools/app-image/src/main.rs b/crates/tools/app-image/src/main.rs new file mode 100644 index 0000000..283c5ae --- /dev/null +++ b/crates/tools/app-image/src/main.rs @@ -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 +//! an installed, pre-approved app: /apps//, its decision granting +//! everything it asks for, and /data// if that includes storage +//! app-image stage +//! an app folder the owner can install later (`install `) + +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 = env::args().skip(1).collect(); + let done = match args.iter().map(String::as_str).collect::>().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 | stage ".into()), + }; + match done { + Ok(()) => ExitCode::SUCCESS, + Err(e) => { + eprintln!("app-image: {e}"); + ExitCode::FAILURE + } + } +} diff --git a/crates/types/src/apps.rs b/crates/types/src/apps.rs new file mode 100644 index 0000000..af9f550 --- /dev/null +++ b/crates/types/src/apps.rs @@ -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; +} diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 5d425db..5ee6b8e 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -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)] diff --git a/crates/types/src/manifest.rs b/crates/types/src/manifest.rs deleted file mode 100644 index 43d77a7..0000000 --- a/crates/types/src/manifest.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! A program's manifest, `/bin/.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> { - 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"))); - } -} diff --git a/crates/types/src/spawn.rs b/crates/types/src/spawn.rs index f136dee..49fd4d8 100644 --- a/crates/types/src/spawn.rs +++ b/crates/types/src/spawn.rs @@ -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 { + (words[0] == SPAWN).then(|| unpack(words[1])) } #[cfg(test)] @@ -84,27 +67,18 @@ mod tests { notifications: 3, regions: 255, }; - let (name, len, back) = parse(request(b"bin/hello", l).unwrap()).unwrap(); - assert_eq!(&name[..len], b"bin/hello"); - assert_eq!(back, l); + assert_eq!(parse(request(l).unwrap()), Some(l)); } #[test] fn what_cannot_be_said_is_refused() { - let l = Limits::default(); - assert!(request(b"", l).is_none()); - assert!(request(&[b'a'; 17], l).is_none()); assert!( - request( - b"x", - Limits { - endpoints: 256, - ..l - } - ) + request(Limits { + endpoints: 256, + ..Limits::default() + }) .is_none() ); - assert!(parse([SPAWN | 17 << 8, 0, 0, 0]).is_none()); - assert!(parse([1 | 3 << 8, 0, 0, 0]).is_none()); + assert!(parse([1, 0, 0, 0]).is_none()); } } diff --git a/crates/types/src/start_arg.rs b/crates/types/src/start_arg.rs index 5eded42..aaef977 100644 --- a/crates/types/src/start_arg.rs +++ b/crates/types/src/start_arg.rs @@ -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, + /// The app's own data folder, read-write (its `storage` capability). + pub data: Option, } fn field(arg: usize, shift: u32) -> Option { @@ -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()); diff --git a/disk/bin/snake.manifest b/disk/bin/snake.manifest deleted file mode 100644 index 76f0f83..0000000 --- a/disk/bin/snake.manifest +++ /dev/null @@ -1,2 +0,0 @@ -# What snake needs beyond the terminal and storage. -needs = display diff --git a/disk/docs/shell.txt b/disk/docs/shell.txt deleted file mode 100644 index 26aa4ff..0000000 --- a/disk/docs/shell.txt +++ /dev/null @@ -1,11 +0,0 @@ -The shell runs /bin/ 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/.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. diff --git a/disk/home/docs/shell.txt b/disk/home/docs/shell.txt new file mode 100644 index 0000000..0a37897 --- /dev/null +++ b/disk/home/docs/shell.txt @@ -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 ` 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. diff --git a/disk/home/readme.txt b/disk/home/readme.txt new file mode 100644 index 0000000..cda710e --- /dev/null +++ b/disk/home/readme.txt @@ -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 diff --git a/disk/readme.txt b/disk/readme.txt deleted file mode 100644 index 472d84a..0000000 --- a/disk/readme.txt +++ /dev/null @@ -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 diff --git a/docs/ipc.md b/docs/ipc.md index 28448c3..1603641 100644 --- a/docs/ipc.md +++ b/docs/ipc.md @@ -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 diff --git a/scripts/make-disk-image.sh b/scripts/make-disk-image.sh index ccaed56..bda5975 100755 --- a/scripts/make-disk-image.sh +++ b/scripts/make-disk-image.sh @@ -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