feat(apps): permission manager equips apps (manifest+consent+decisions in /system), launches via init w/ file grant; shell unprivileged (/home); install/apps/allow/revoke; revoke stops running app; app-image tool; counter app
This commit is contained in:
18
Cargo.lock
generated
18
Cargo.lock
generated
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
2
Makefile
2
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
|
||||
|
||||
6
crates/apps/cat/manifest.toml
Normal file
6
crates/apps/cat/manifest.toml
Normal file
@@ -0,0 +1,6 @@
|
||||
[app]
|
||||
id = "kos.cat"
|
||||
name = "cat"
|
||||
version = "1.0.0"
|
||||
binary = "cat"
|
||||
author = "K-OS"
|
||||
14
crates/apps/counter/Cargo.toml
Normal file
14
crates/apps/counter/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "counter"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
name = "counter"
|
||||
test = false
|
||||
bench = false
|
||||
|
||||
[dependencies]
|
||||
types = { workspace = true }
|
||||
|
||||
libkos = { workspace = true }
|
||||
10
crates/apps/counter/manifest.toml
Normal file
10
crates/apps/counter/manifest.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
# Remembers how often it ran, if the owner lets it keep a file.
|
||||
[app]
|
||||
id = "kos.counter"
|
||||
name = "Counter"
|
||||
version = "1.0.0"
|
||||
binary = "counter"
|
||||
author = "K-OS"
|
||||
|
||||
[capabilities]
|
||||
optional = ["storage"]
|
||||
59
crates/apps/counter/src/main.rs
Normal file
59
crates/apps/counter/src/main.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
//! Counts its runs in its own data folder: the `storage` capability, which
|
||||
//! it can do without (then it remembers nothing).
|
||||
|
||||
#![no_std]
|
||||
#![forbid(unsafe_code)]
|
||||
#![no_main]
|
||||
|
||||
extern crate libkos;
|
||||
|
||||
use libkos::fs::Storage;
|
||||
use libkos::println;
|
||||
use types::address::VirtualAddress;
|
||||
|
||||
const DATA_BUF_VA: VirtualAddress = VirtualAddress::from_raw(0x0180_0000);
|
||||
const COUNT: &[u8] = b"count";
|
||||
|
||||
/// The number in the file, 0 if there is none yet.
|
||||
fn read_count(fs: &mut Storage) -> u64 {
|
||||
let Ok(h) = fs.open(COUNT) else { return 0 };
|
||||
let mut buf = [0u8; 20];
|
||||
let n = fs.read(h, &mut buf).unwrap_or(0);
|
||||
let _ = fs.close(h);
|
||||
core::str::from_utf8(&buf[..n]).ok().and_then(|s| s.trim().parse().ok()).unwrap_or(0)
|
||||
}
|
||||
|
||||
fn write_count(fs: &mut Storage, count: u64) -> bool {
|
||||
let mut digits = [0u8; 20];
|
||||
let mut n = 0;
|
||||
let mut v = count;
|
||||
loop {
|
||||
digits[n] = b'0' + (v % 10) as u8;
|
||||
n += 1;
|
||||
v /= 10;
|
||||
if v == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
digits[..n].reverse();
|
||||
let _ = fs.delete(COUNT);
|
||||
let Ok(h) = fs.create(COUNT) else { return false };
|
||||
let ok = fs.write(h, &digits[..n]) == Ok(n);
|
||||
let _ = fs.close(h);
|
||||
ok
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let Some(mut fs) = libkos::start::data().and_then(|d| Storage::attach(d, DATA_BUF_VA).ok()) else {
|
||||
println!("counter: not allowed to keep anything, so this is run 1 as far as it knows");
|
||||
return;
|
||||
};
|
||||
let count = read_count(&mut fs) + 1;
|
||||
if write_count(&mut fs, count) {
|
||||
println!("counter: run {} (kept in its own data folder)", count);
|
||||
} else {
|
||||
println!("counter: run {}, but it could not save that", count);
|
||||
}
|
||||
}
|
||||
|
||||
libkos::entry!(main);
|
||||
6
crates/apps/ls/manifest.toml
Normal file
6
crates/apps/ls/manifest.toml
Normal file
@@ -0,0 +1,6 @@
|
||||
[app]
|
||||
id = "kos.ls"
|
||||
name = "ls"
|
||||
version = "1.0.0"
|
||||
binary = "ls"
|
||||
author = "K-OS"
|
||||
@@ -10,4 +10,5 @@ bench = false
|
||||
|
||||
[dependencies]
|
||||
types = { workspace = true }
|
||||
manifest = { workspace = true }
|
||||
libkos = { workspace = true }
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
//! The shell: a few built-ins, and anything else is a program on disk.
|
||||
//! `name args...` runs `/bin/name` through init, handing it the terminal as
|
||||
//! its output, read-only storage rooted at the current directory, and what
|
||||
//! its manifest (`/bin/name.manifest`) asks for that the shell can give;
|
||||
//! then waits for it (Ctrl+C kills it) and takes the storage back.
|
||||
//! The shell: a few built-ins, and anything else is an app. `name args...`
|
||||
//! asks the permission manager to run it, passing along the terminal as
|
||||
//! its output and a read-only grant to the current folder, which is all
|
||||
//! the shell itself has to give; the app gets the rest of what it may have
|
||||
//! from the permission manager. Then it waits (Ctrl+C kills it) and takes
|
||||
//! the grant back. The owner manages apps from here too.
|
||||
|
||||
#![no_std]
|
||||
#![forbid(unsafe_code)]
|
||||
@@ -10,33 +11,28 @@
|
||||
|
||||
extern crate libkos;
|
||||
|
||||
use libkos::apps::{Apps, Refused};
|
||||
use libkos::fs::Storage;
|
||||
use libkos::spawn::{SpawnError, spawn};
|
||||
use types::manifest::Needs;
|
||||
use libkos::{print, println, syscall};
|
||||
use manifest::{Capabilities, Capability};
|
||||
use types::address::VirtualAddress;
|
||||
use types::apps::reply;
|
||||
use types::bounded::CapSlot;
|
||||
use types::budget::Limits;
|
||||
use types::permission::{Permission, PermissionSet};
|
||||
use types::start_arg::StartArg;
|
||||
|
||||
/// Where init's manifest put them: `uses`, then the spawn endpoint.
|
||||
/// Where init's manifest put them: `uses`, then storage.
|
||||
const TERMINAL: CapSlot = CapSlot::const_new(0);
|
||||
const STORAGE: CapSlot = CapSlot::const_new(1);
|
||||
const DISPLAY: CapSlot = CapSlot::const_new(2);
|
||||
const INIT: CapSlot = CapSlot::const_new(3);
|
||||
const PERMISSION: CapSlot = CapSlot::const_new(1);
|
||||
/// The owner's files, `/home`: the shell's `/`.
|
||||
const HOME: CapSlot = CapSlot::const_new(2);
|
||||
|
||||
/// Bits on the shell's notification: the program died (its death watch is
|
||||
/// unbadged), or Ctrl+C was typed (the terminal's copy).
|
||||
const DIED: usize = 1;
|
||||
const INTERRUPTED: usize = 1 << 1;
|
||||
/// Longest manifest read.
|
||||
const MAX_MANIFEST: usize = 512;
|
||||
|
||||
/// What each program gets, out of the shell's allowance.
|
||||
const PROGRAM: Limits = Limits { pages: 128, endpoints: 1, notifications: 1, regions: 1 };
|
||||
const MAX_ARGS: usize = 16;
|
||||
/// Where storage's buffer is mapped.
|
||||
const STORAGE_BUF_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x0180_0000);
|
||||
const STORAGE_BUF_VA: VirtualAddress = VirtualAddress::from_raw(0x0180_0000);
|
||||
const REQUEST_PAGE_VA: VirtualAddress = VirtualAddress::from_raw(0x0190_0000);
|
||||
|
||||
/// The current directory, as a path on disk: no leading or trailing `/`.
|
||||
struct Cwd {
|
||||
@@ -50,7 +46,7 @@ impl Cwd {
|
||||
}
|
||||
|
||||
/// Where `cd to` from here leads: `/` starts at the root, `..` goes up
|
||||
/// (the shell holds the whole disk, so that is its to resolve).
|
||||
/// (the shell holds all of its root, so that is its to resolve).
|
||||
fn join(&self, to: &str) -> Option<Cwd> {
|
||||
let mut next = match to.starts_with('/') {
|
||||
true => Cwd {
|
||||
@@ -98,87 +94,118 @@ impl core::fmt::Display for Cwd {
|
||||
|
||||
struct Shell {
|
||||
fs: Storage,
|
||||
apps: Apps,
|
||||
cwd: Cwd,
|
||||
done: CapSlot,
|
||||
}
|
||||
|
||||
/// `/bin/<name><suffix>` into `out`, if it fits.
|
||||
fn bin_path<'a>(out: &'a mut [u8], name: &str, suffix: &[u8]) -> Option<&'a [u8]> {
|
||||
let path = out.get_mut(..4 + name.len() + suffix.len())?;
|
||||
path[..4].copy_from_slice(b"bin/");
|
||||
path[4..4 + name.len()].copy_from_slice(name.as_bytes());
|
||||
path[4 + name.len()..].copy_from_slice(suffix);
|
||||
Some(path)
|
||||
}
|
||||
/// `display, storage`.
|
||||
struct Names(Capabilities);
|
||||
|
||||
/// What `name`'s manifest asks for; none without one. `None` if it asks
|
||||
/// for something the shell does not know how to give.
|
||||
fn needs(sh: &mut Shell, name: &str) -> Option<Needs> {
|
||||
let mut path = [0u8; types::storage::MAX_PATH];
|
||||
let path = bin_path(&mut path, name, b".manifest")?;
|
||||
let Ok(file) = sh.fs.open(path) else {
|
||||
return Some(Needs::default());
|
||||
};
|
||||
let mut text = [0u8; MAX_MANIFEST];
|
||||
let read = sh.fs.read(file, &mut text);
|
||||
let _ = sh.fs.close(file);
|
||||
match types::manifest::parse(&text[..read.ok()?]) {
|
||||
Ok(needs) => Some(needs),
|
||||
Err(types::manifest::Unknown(what)) => {
|
||||
println!("shell: {}: its manifest asks for {}", name, core::str::from_utf8(what).unwrap_or("?"));
|
||||
None
|
||||
impl core::fmt::Display for Names {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
for (i, c) in self.0.iter().enumerate() {
|
||||
if i > 0 {
|
||||
f.write_str(", ")?;
|
||||
}
|
||||
f.write_str(c.name())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs `/bin/args[0]` with `args` in the current directory, and waits
|
||||
/// for it to finish or Ctrl+C.
|
||||
fn names(bits: usize) -> Names {
|
||||
Names(Capabilities::from_bits(bits as u32))
|
||||
}
|
||||
|
||||
/// Says why the permission manager refused, about `what`.
|
||||
fn refused(what: &str, r: Refused) {
|
||||
match r.code {
|
||||
reply::NOT_INSTALLED => println!("shell: {}: not found", what),
|
||||
reply::MISSING => println!("shell: {}: cannot run without {}", what, names(r.detail)),
|
||||
reply::ALREADY_INSTALLED => println!("shell: {}: already installed", what),
|
||||
reply::UNAVAILABLE => println!("shell: {}: this device has no {}", what, names(r.detail)),
|
||||
reply::DECLINED => println!("shell: {}: not installed", what),
|
||||
reply::NO_MANIFEST => println!("shell: {}: no app here (no manifest.toml)", what),
|
||||
reply::BAD_MANIFEST if r.detail > 0 => println!("shell: {}: bad manifest, line {}", what, r.detail),
|
||||
reply::BAD_MANIFEST => println!("shell: {}: bad manifest", what),
|
||||
reply::NOT_REQUESTED => println!("shell: {}: never asked for that", what),
|
||||
reply::BUSY => println!("shell: no room to run {}", what),
|
||||
_ => println!("shell: {}: failed", what),
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs app `args[0]` with `args` in the current folder, and waits for it
|
||||
/// to finish or Ctrl+C.
|
||||
fn run_program(sh: &mut Shell, args: &[&str]) {
|
||||
let mut file = [0u8; types::spawn::MAX_NAME];
|
||||
let Some(file) = bin_path(&mut file, args[0], b"") else {
|
||||
println!("shell: {}: name too long", args[0]);
|
||||
return;
|
||||
};
|
||||
let Some(needs) = needs(sh, args[0]) else {
|
||||
return;
|
||||
};
|
||||
let child = match spawn(INIT, file, PROGRAM) {
|
||||
Ok(c) => c,
|
||||
Err(SpawnError::NotFound) => return println!("shell: {}: not found", args[0]),
|
||||
Err(SpawnError::OverAllowance) => return println!("shell: no room to run {}", args[0]),
|
||||
Err(SpawnError::Failed) => return println!("shell: {}: cannot run", args[0]),
|
||||
};
|
||||
let call = PermissionSet::empty().with(Permission::Execute);
|
||||
let grant = sh.fs.grant(sh.cwd.as_bytes(), false).ok();
|
||||
let arg = StartArg {
|
||||
log: child.give(TERMINAL, call),
|
||||
storage: grant.and_then(|g| child.give(g, call)),
|
||||
args: child.set_args(args),
|
||||
// It subscribes to input by sending the display a notification.
|
||||
display: needs.display.then(|| child.give_sending(DISPLAY, call)).flatten(),
|
||||
console: None,
|
||||
};
|
||||
let folder = sh.fs.grant(sh.cwd.as_bytes(), false).ok();
|
||||
// A Ctrl+C typed since the last program is not for this one.
|
||||
let _ = syscall::poll(sh.done);
|
||||
let watched = syscall::watch_death(child.process, sh.done).is_ok();
|
||||
if child.start_with(arg) && watched {
|
||||
match sh.apps.launch(args, Some(TERMINAL), folder) {
|
||||
Ok((process, granted)) => {
|
||||
if syscall::watch_death(process, sh.done).is_ok() {
|
||||
while syscall::wait(sh.done).is_ok_and(|bits| {
|
||||
if bits & INTERRUPTED != 0 {
|
||||
syscall::kill(child.process);
|
||||
syscall::kill(process);
|
||||
}
|
||||
bits & DIED == 0
|
||||
}) {}
|
||||
} else {
|
||||
let _ = syscall::kill(child.process);
|
||||
println!("shell: {}: cannot start", args[0]);
|
||||
let _ = syscall::kill(process);
|
||||
}
|
||||
syscall::drop_cap(child.process);
|
||||
if let Some(g) = grant {
|
||||
syscall::drop_cap(process);
|
||||
// It drew over the terminal and took the keyboard.
|
||||
if Capabilities::from_bits(granted).contains(Capability::Display) {
|
||||
libkos::terminal::redraw(TERMINAL);
|
||||
}
|
||||
}
|
||||
Err(r) => refused(args[0], r),
|
||||
}
|
||||
if let Some(g) = folder {
|
||||
let _ = libkos::fs::release(g);
|
||||
}
|
||||
// It drew over the terminal and took the keyboard.
|
||||
if needs.display {
|
||||
libkos::terminal::redraw(TERMINAL);
|
||||
}
|
||||
|
||||
/// Installs the app in folder `from` (its manifest and binary); the owner
|
||||
/// is asked on the terminal.
|
||||
fn install(sh: &mut Shell, from: &str) {
|
||||
let Some(path) = sh.cwd.join(from) else {
|
||||
return println!("install: {}: no such folder", from);
|
||||
};
|
||||
let Ok(source) = sh.fs.grant(path.as_bytes(), false) else {
|
||||
return println!("install: {}: no such folder", from);
|
||||
};
|
||||
match sh.apps.install(source) {
|
||||
Ok(0) => println!("installed"),
|
||||
Ok(bits) => println!("installed, may use: {}", names(bits as usize)),
|
||||
Err(r) => refused(from, r),
|
||||
}
|
||||
let _ = libkos::fs::release(source);
|
||||
}
|
||||
|
||||
fn list(sh: &mut Shell) {
|
||||
let mut text = [0u8; types::apps::PAGE_SIZE];
|
||||
match sh.apps.list(&mut text) {
|
||||
Ok(n) => print!("{}", core::str::from_utf8(&text[..n]).unwrap_or("")),
|
||||
Err(r) => refused("apps", r),
|
||||
}
|
||||
}
|
||||
|
||||
/// `allow <app id> <capability>` or `revoke ...`.
|
||||
fn change(sh: &mut Shell, args: &[&str], allow: bool) {
|
||||
let verb = if allow { "allow" } else { "revoke" };
|
||||
let (Some(id), Some(name)) = (args.get(1), args.get(2)) else {
|
||||
return println!("usage: {} <app id> <capability>", verb);
|
||||
};
|
||||
let Some(cap) = Capability::from_name(name) else {
|
||||
return println!("{}: {}: no such capability", verb, name);
|
||||
};
|
||||
let done = match allow {
|
||||
true => sh.apps.allow(id, cap.index()),
|
||||
false => sh.apps.revoke(id, cap.index()),
|
||||
};
|
||||
if let Err(r) = done {
|
||||
refused(id, r);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,10 +228,18 @@ fn run(sh: &mut Shell, line: &str) {
|
||||
None => {}
|
||||
Some("help") => {
|
||||
println!("built-ins: help, echo <text>, clear, cd <dir>, pwd");
|
||||
println!("anything else runs a program from the disk: try ls, cat readme.txt");
|
||||
println!("programs see the current directory, read-only, and nothing above it");
|
||||
println!("apps: apps, install <folder>, allow|revoke <app id> <capability>");
|
||||
println!("anything else runs an app: try ls, cat readme.txt, snake");
|
||||
println!("apps see the current directory, read-only, and only what you allowed");
|
||||
println!("Ctrl+C stops a running program; snake quits with q");
|
||||
}
|
||||
Some("apps") => list(sh),
|
||||
Some("install") => match args.get(1) {
|
||||
Some(from) => install(sh, from),
|
||||
None => println!("usage: install <folder>"),
|
||||
},
|
||||
Some("allow") => change(sh, args, true),
|
||||
Some("revoke") => change(sh, args, false),
|
||||
Some("cd") => cd(sh, args.get(1).copied().unwrap_or("/")),
|
||||
Some("pwd") => println!("{}", sh.cwd),
|
||||
Some("echo") => println!("{}", Joined(&args[1..])),
|
||||
@@ -237,15 +272,20 @@ fn main() {
|
||||
println!("shell: no notification to wait on");
|
||||
return;
|
||||
};
|
||||
let Ok(fs) = Storage::attach(STORAGE, STORAGE_BUF_VA) else {
|
||||
let Ok(fs) = Storage::attach(HOME, STORAGE_BUF_VA) else {
|
||||
println!("shell: no storage");
|
||||
return;
|
||||
};
|
||||
let Some(apps) = Apps::connect(PERMISSION, REQUEST_PAGE_VA) else {
|
||||
println!("shell: no permission manager");
|
||||
return;
|
||||
};
|
||||
if !libkos::terminal::on_interrupt(TERMINAL, done, types::badge::Badge::new(INTERRUPTED)) {
|
||||
println!("shell: Ctrl+C will not work");
|
||||
}
|
||||
let mut sh = Shell {
|
||||
fs,
|
||||
apps,
|
||||
cwd: Cwd {
|
||||
path: [0; types::storage::MAX_PATH],
|
||||
len: 0,
|
||||
|
||||
6
crates/apps/sleep/manifest.toml
Normal file
6
crates/apps/sleep/manifest.toml
Normal file
@@ -0,0 +1,6 @@
|
||||
[app]
|
||||
id = "kos.sleep"
|
||||
name = "sleep"
|
||||
version = "1.0.0"
|
||||
binary = "sleep"
|
||||
author = "K-OS"
|
||||
9
crates/apps/snake/manifest.toml
Normal file
9
crates/apps/snake/manifest.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[app]
|
||||
id = "kos.snake"
|
||||
name = "Snake"
|
||||
version = "1.0.0"
|
||||
binary = "snake"
|
||||
author = "K-OS"
|
||||
|
||||
[capabilities]
|
||||
required = ["display"]
|
||||
@@ -10,4 +10,5 @@ bench = false
|
||||
|
||||
[dependencies]
|
||||
types = { workspace = true }
|
||||
manifest = { workspace = true }
|
||||
libkos = { workspace = true }
|
||||
|
||||
@@ -9,19 +9,21 @@
|
||||
extern crate libkos;
|
||||
|
||||
use libkos::fs::{FsError, Storage};
|
||||
use libkos::spawn::{SpawnError, spawn};
|
||||
use libkos::spawn::{SpawnError, Spawned, spawn};
|
||||
use libkos::syscall;
|
||||
use types::bounded::CapSlot;
|
||||
use types::budget::Limits;
|
||||
|
||||
/// Where init put them (see init's manifest: `uses`, the spawn endpoint,
|
||||
/// then control).
|
||||
const STORAGE: CapSlot = CapSlot::const_new(0);
|
||||
const INIT: CapSlot = CapSlot::const_new(1);
|
||||
const CONTROL: CapSlot = CapSlot::const_new(2);
|
||||
const PERMISSION: CapSlot = CapSlot::const_new(0);
|
||||
const STORAGE: CapSlot = CapSlot::const_new(1);
|
||||
const INIT: CapSlot = CapSlot::const_new(2);
|
||||
const CONTROL: CapSlot = CapSlot::const_new(3);
|
||||
/// Where storage's buffers are mapped: the whole disk's, a narrowed one's.
|
||||
const STORAGE_BUF_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x0180_0000);
|
||||
const NARROW_BUF_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x0190_0000);
|
||||
const REQUEST_PAGE_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x01C0_0000);
|
||||
/// Read back from the disk image by scripts/test-qemu.sh.
|
||||
const NOTE: &[u8] = b"written by k-os on ext4\n";
|
||||
|
||||
@@ -29,10 +31,18 @@ const NOTE: &[u8] = b"written by k-os on ext4\n";
|
||||
/// the first has died and its limits came back.
|
||||
const HELLO: Limits = Limits { pages: 80, endpoints: 0, notifications: 0, regions: 0 };
|
||||
|
||||
/// Spawns the program at `path`, read through a grant made just for it.
|
||||
fn spawn_file(fs: &mut Storage, path: &[u8], limits: Limits) -> Result<Spawned, SpawnError> {
|
||||
let file = fs.grant(path, false).map_err(|_| SpawnError::Failed)?;
|
||||
let child = spawn(INIT, file, limits);
|
||||
let _ = libkos::fs::release(file);
|
||||
child
|
||||
}
|
||||
|
||||
/// Spawns hello, runs it, and waits for it to die; hands it `grant`
|
||||
/// first, if there is one.
|
||||
fn run_hello(died: CapSlot, grant: Option<CapSlot>) -> Result<(), &'static str> {
|
||||
let child = spawn(INIT, b"bin/hello", HELLO).map_err(|_| "init would not spawn hello")?;
|
||||
fn run_hello(fs: &mut Storage, died: CapSlot, grant: Option<CapSlot>) -> Result<(), &'static str> {
|
||||
let child = spawn_file(fs, b"bin/hello", HELLO).map_err(|_| "init would not spawn hello")?;
|
||||
let call = types::permission::PermissionSet::empty().with(types::permission::Permission::Execute);
|
||||
if grant.is_some_and(|g| child.give(g, call).is_none()) {
|
||||
return Err("a storage grant could not be passed on");
|
||||
@@ -78,7 +88,7 @@ fn narrow_storage(fs: &mut Storage, died: CapSlot) -> Result<(), &'static str> {
|
||||
if widened.and_then(|mut w| w.create(b"x")).is_ok() {
|
||||
return Err("a grant was widened to write");
|
||||
}
|
||||
run_hello(died, Some(grant))?;
|
||||
run_hello(fs, died, Some(grant))?;
|
||||
libkos::fs::release(grant).map_err(|_| "release failed")?;
|
||||
if inner.list_dir(b"", |_| {}).is_ok() {
|
||||
return Err("a released grant still works");
|
||||
@@ -86,6 +96,33 @@ fn narrow_storage(fs: &mut Storage, died: CapSlot) -> Result<(), &'static str> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// An app runs through the permission manager with what it was granted;
|
||||
/// revoking that stops it, and it cannot run again until allowed.
|
||||
fn revoke_stops_a_running_app(died: CapSlot) -> Result<(), &'static str> {
|
||||
use manifest::Capability;
|
||||
let apps = libkos::apps::Apps::connect(PERMISSION, REQUEST_PAGE_VA).ok_or("no permission manager")?;
|
||||
let display = Capability::Display.index();
|
||||
let (snake, granted) = apps.launch(&["snake"], None, None).map_err(|_| "snake would not launch")?;
|
||||
if granted & manifest::Capabilities::NONE.with(Capability::Display).bits() == 0 {
|
||||
return Err("snake runs without the display it was granted");
|
||||
}
|
||||
syscall::watch_death(snake, died).map_err(|_| "cannot watch snake")?;
|
||||
apps.revoke("kos.snake", display).map_err(|_| "revoke failed")?;
|
||||
syscall::wait(died).map_err(|_| "wait failed")?;
|
||||
syscall::drop_cap(snake);
|
||||
let refused = apps.launch(&["snake"], None, None);
|
||||
if !matches!(refused, Err(r) if r.code == types::apps::reply::MISSING) {
|
||||
return Err("snake ran without the display it requires");
|
||||
}
|
||||
apps.allow("kos.snake", display).map_err(|_| "allow failed")?;
|
||||
let (snake, _) = apps.launch(&["snake"], None, None).map_err(|_| "snake did not run once allowed again")?;
|
||||
syscall::watch_death(snake, died).map_err(|_| "cannot watch snake")?;
|
||||
syscall::kill(snake);
|
||||
syscall::wait(died).map_err(|_| "wait failed")?;
|
||||
syscall::drop_cap(snake);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check() -> Result<(), &'static str> {
|
||||
let mut fs = Storage::attach(STORAGE, STORAGE_BUF_VA).map_err(|_| "cannot attach to storage")?;
|
||||
write_a_file(&mut fs)?;
|
||||
@@ -95,21 +132,24 @@ fn check() -> Result<(), &'static str> {
|
||||
narrow_storage(&mut fs, died)?;
|
||||
libkos::println!("spawntest: storage grants stay in scope, pass on, release: ok");
|
||||
|
||||
run_hello(died, None)?;
|
||||
run_hello(&mut fs, died, None)?;
|
||||
libkos::println!("spawntest: spawned program runs and its death is heard: ok");
|
||||
|
||||
let greedy = Limits { pages: 200, ..HELLO };
|
||||
if spawn(INIT, b"bin/hello", greedy).err() != Some(SpawnError::OverAllowance) {
|
||||
if spawn_file(&mut fs, b"bin/hello", greedy).err() != Some(SpawnError::OverAllowance) {
|
||||
return Err("init spawned past the allowance");
|
||||
}
|
||||
let tiny = Limits { pages: 1, ..HELLO };
|
||||
if spawn(INIT, b"bin/nothere", tiny).err() != Some(SpawnError::NotFound) {
|
||||
if spawn_file(&mut fs, b"bin/nothere", tiny).err() != Some(SpawnError::NotFound) {
|
||||
return Err("a missing program was not reported");
|
||||
}
|
||||
libkos::println!("spawntest: allowance and missing files refused: ok");
|
||||
|
||||
run_hello(died, None)?;
|
||||
run_hello(&mut fs, died, None)?;
|
||||
libkos::println!("spawntest: allowance comes back when a child dies: ok");
|
||||
|
||||
revoke_stops_a_running_app(died)?;
|
||||
libkos::println!("spawntest: revoking stops a running app, allowing lets it run: ok");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -44,9 +44,9 @@ impl<'a> Launcher<'a> {
|
||||
) -> Result<Self, &'static str> {
|
||||
let spawn_ep = syscall::create_endpoint().ok_or("no spawn endpoint")?;
|
||||
let deaths = syscall::create_notification().map_err(|_| "no death notification")?;
|
||||
// Storage was started by boot, before init; programs may use it too.
|
||||
let mut served = [None; MAX_SERVED];
|
||||
served[0] = Some(("storage", crate::STORAGE_EP));
|
||||
// Storage, started by boot, is not among them: it is reached
|
||||
// through grants (a program's `storage` scope).
|
||||
let served = [None; MAX_SERVED];
|
||||
Ok(Self { storage, scratch, devices, served, next_badge: 1, spawn_ep, deaths, spawner: Spawner::new() })
|
||||
}
|
||||
|
||||
@@ -78,20 +78,21 @@ impl<'a> Launcher<'a> {
|
||||
// and take other clients' requests.
|
||||
for name in p.uses {
|
||||
let ep = self.endpoint(name).ok_or("needs a server that is not running")?;
|
||||
let copy = match *name {
|
||||
// Storage issues its own badges, each naming a scope.
|
||||
"storage" => self.storage.grant(b"", true).map_err(|_| "storage refused a grant")?,
|
||||
_ => {
|
||||
let badge = Badge::new(self.next_badge);
|
||||
self.next_badge += 1;
|
||||
syscall::mint(ep, badge, perms(&[Permission::Execute, Permission::Delegate]))
|
||||
.map_err(|_| "cannot mint an endpoint")?
|
||||
}
|
||||
};
|
||||
let copy = syscall::mint(ep, badge, perms(&[Permission::Execute, Permission::Delegate]))
|
||||
.map_err(|_| "cannot mint an endpoint")?;
|
||||
let given = syscall::delegate(copy, child, perms(&[Permission::Execute, Permission::Delegate]));
|
||||
syscall::drop_cap(copy);
|
||||
given.ok_or("cannot give an endpoint")?;
|
||||
}
|
||||
// Storage issues its own badges, each naming a scope.
|
||||
if let Some(scope) = p.storage {
|
||||
let grant = self.storage.grant(scope.path, scope.write).map_err(|_| "storage refused a grant")?;
|
||||
let given = syscall::delegate(grant, child, perms(&[Permission::Execute, Permission::Delegate]));
|
||||
syscall::drop_cap(grant);
|
||||
given.ok_or("cannot give storage")?;
|
||||
}
|
||||
if let Some(allowance) = p.spawns {
|
||||
self.give_spawn(child, allowance)?;
|
||||
}
|
||||
@@ -126,7 +127,8 @@ impl<'a> Launcher<'a> {
|
||||
let badge: Badge = self.spawner.add_client(allowance).ok_or("too many spawn clients")?;
|
||||
let minted = perms(&[Permission::Execute, Permission::Delegate]);
|
||||
let copy = syscall::mint(self.spawn_ep, badge, minted).map_err(|_| "cannot mint")?;
|
||||
let given = syscall::delegate(copy, child, perms(&[Permission::Execute]));
|
||||
// Delegate, to send the program file's grant with each request.
|
||||
let given = syscall::delegate(copy, child, perms(&[Permission::Execute, Permission::Delegate]));
|
||||
syscall::drop_cap(copy);
|
||||
given.map(|_| ()).ok_or("cannot give the spawn endpoint")
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -9,6 +9,14 @@ pub enum Device {
|
||||
Input,
|
||||
}
|
||||
|
||||
/// A storage grant: a folder (or the whole disk, `b""`) and whether it may
|
||||
/// be changed.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Scope {
|
||||
pub path: &'static [u8],
|
||||
pub write: bool,
|
||||
}
|
||||
|
||||
pub struct Program {
|
||||
pub name: &'static str,
|
||||
pub file: &'static [u8],
|
||||
@@ -19,6 +27,8 @@ pub struct Program {
|
||||
/// Earlier programs' endpoints it may call, in its next slots; each
|
||||
/// program's copy carries its own badge, so a server can tell them apart.
|
||||
pub uses: &'static [&'static str],
|
||||
/// Storage it may reach, in the slot after `uses`.
|
||||
pub storage: Option<Scope>,
|
||||
/// Takes the console over from init; everyone else prints through it.
|
||||
pub console: bool,
|
||||
/// May have init spawn programs, whose limits together stay within this.
|
||||
@@ -43,6 +53,7 @@ const fn server(name: &'static str, file: &'static [u8], limits: Limits) -> Prog
|
||||
serves: true,
|
||||
device: None,
|
||||
uses: &[],
|
||||
storage: None,
|
||||
console: false,
|
||||
spawns: None,
|
||||
system_control: false,
|
||||
@@ -62,7 +73,15 @@ pub const SERVERS: &[Program] = &[
|
||||
},
|
||||
// Text on the display, lines from the keyboard (it takes input focus).
|
||||
Program { uses: &["display"], ..server("terminal", b"bin/terminal", SMALL) },
|
||||
server("permission", b"bin/permission", SMALL),
|
||||
// The only one that equips apps (docs/apps.md): it reaches every app's
|
||||
// files and the owner's decisions, and runs apps through init.
|
||||
Program {
|
||||
uses: &["terminal", "display"],
|
||||
storage: Some(Scope { path: b"", write: true }),
|
||||
// Room for a few apps at a time.
|
||||
spawns: Some(Limits { pages: 512, endpoints: 8, notifications: 8, regions: 8 }),
|
||||
..server("permission", b"bin/permission", Limits { pages: 96, ..SMALL })
|
||||
},
|
||||
server("session", b"bin/session", SMALL),
|
||||
];
|
||||
|
||||
@@ -70,18 +89,20 @@ pub const SERVERS: &[Program] = &[
|
||||
/// spawns through init, then reports and halts.
|
||||
pub const TESTS: &[Program] = &[Program {
|
||||
serves: false,
|
||||
uses: &["storage"],
|
||||
// The permission manager, to run an app the way the shell does.
|
||||
uses: &["permission"],
|
||||
storage: Some(Scope { path: b"", write: true }),
|
||||
spawns: Some(Limits { pages: 128, endpoints: 2, notifications: 2, regions: 2 }),
|
||||
system_control: true,
|
||||
..server("spawntest", b"bin/spawntest", SMALL)
|
||||
}];
|
||||
|
||||
/// The desktop; the test image runs the SMP self-test instead.
|
||||
pub const APPS: &[Program] = &[Program {
|
||||
/// The desktop; the test image runs the SMP self-test instead. The shell
|
||||
/// holds the owner's files and asks the permission manager to run apps;
|
||||
/// it holds nothing to give them beyond that.
|
||||
pub const DESKTOP: &[Program] = &[Program {
|
||||
serves: false,
|
||||
// The display only to hand on, to programs whose manifest asks for it.
|
||||
uses: &["terminal", "storage", "display"],
|
||||
// Room for one program at a time, the way the shell runs them.
|
||||
spawns: Some(Limits { pages: 256, endpoints: 2, notifications: 2, regions: 2 }),
|
||||
uses: &["terminal", "permission"],
|
||||
storage: Some(Scope { path: b"home", write: true }),
|
||||
..server("shell", b"bin/shell", SMALL)
|
||||
}];
|
||||
|
||||
@@ -8,7 +8,8 @@ use types::badge::Badge;
|
||||
use types::bounded::CapSlot;
|
||||
use types::budget::Limits;
|
||||
use types::permission::{Permission, PermissionSet};
|
||||
use types::spawn::{MAX_NAME, NONE, reply};
|
||||
use types::address::VirtualAddress;
|
||||
use types::spawn::{NONE, reply};
|
||||
|
||||
use crate::launch::Launcher;
|
||||
use crate::load;
|
||||
@@ -67,15 +68,16 @@ impl Spawner {
|
||||
}
|
||||
}
|
||||
|
||||
/// Where init maps a spawn request's file grant while it reads it.
|
||||
const PROGRAM_BUF_VA: VirtualAddress = VirtualAddress::from_raw(0x01A0_0000);
|
||||
|
||||
pub(crate) struct Request {
|
||||
name: [u8; MAX_NAME],
|
||||
len: usize,
|
||||
limits: Limits,
|
||||
}
|
||||
|
||||
impl Decode for Request {
|
||||
fn decode(words: [usize; 4]) -> Option<Self> {
|
||||
types::spawn::parse(words).map(|(name, len, limits)| Self { name, len, limits })
|
||||
types::spawn::parse(words).map(|limits| Self { limits })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +94,9 @@ fn refused(tag: usize) -> ([usize; 4], Option<CapSlot>) {
|
||||
}
|
||||
|
||||
impl Launcher<'_> {
|
||||
fn spawn(&mut self, badge: usize, r: &Request) -> ([usize; 4], Option<CapSlot>) {
|
||||
/// Loads the file `binary` (a storage grant scoped to it) for `badge`'s
|
||||
/// client.
|
||||
fn spawn(&mut self, badge: usize, r: &Request, binary: CapSlot) -> ([usize; 4], Option<CapSlot>) {
|
||||
let Some(client) = badge.checked_sub(1).filter(|c| *c < MAX_CLIENTS) else {
|
||||
return refused(reply::ERROR);
|
||||
};
|
||||
@@ -105,16 +109,16 @@ impl Launcher<'_> {
|
||||
let Some(index) = self.spawner.children.iter().position(Option::is_none) else {
|
||||
return refused(reply::ERROR);
|
||||
};
|
||||
let name = &r.name[..r.len];
|
||||
let (storage, scratch) = self.storage();
|
||||
match storage.open(name) {
|
||||
Ok(h) => {
|
||||
let _ = storage.close(h);
|
||||
}
|
||||
Err(_) => return refused(reply::NOT_FOUND),
|
||||
}
|
||||
let Some(loaded) = load::load_binary(storage, scratch, name, r.limits) else {
|
||||
return refused(reply::ERROR);
|
||||
let Ok(mut file) = libkos::fs::Storage::attach(binary, PROGRAM_BUF_VA) else {
|
||||
return refused(reply::NOT_FOUND);
|
||||
};
|
||||
// The grant is the file: its path inside it is empty.
|
||||
let found = file.open(b"").and_then(|h| file.close(h)).is_ok();
|
||||
let (_, scratch) = self.storage();
|
||||
let loaded = found.then(|| load::load_binary(&mut file, scratch, b"", r.limits)).flatten();
|
||||
file.detach();
|
||||
let Some(loaded) = loaded else {
|
||||
return refused(if found { reply::ERROR } else { reply::NOT_FOUND });
|
||||
};
|
||||
if !self.watch(loaded.process_cap, index) {
|
||||
let _ = syscall::kill(loaded.process_cap);
|
||||
@@ -165,13 +169,18 @@ impl Server for Launcher<'_> {
|
||||
Reply(refused(reply::ERROR).0)
|
||||
}
|
||||
|
||||
fn handle_call(&mut self, badge: usize, words: [usize; 4], _cap: Option<CapSlot>) -> ([usize; 4], Option<libkos::syscall::Give>) {
|
||||
match Request::decode(words) {
|
||||
Some(r) => {
|
||||
let (reply, process) = self.spawn(badge, &r);
|
||||
(reply, process.map(Into::into))
|
||||
fn handle_call(&mut self, badge: usize, words: [usize; 4], cap: Option<CapSlot>) -> ([usize; 4], Option<libkos::syscall::Give>) {
|
||||
match (Request::decode(words), cap) {
|
||||
(Some(r), Some(binary)) => {
|
||||
let (reply, process) = self.spawn(badge, &r, binary);
|
||||
// Init's copy of the grant is done with.
|
||||
syscall::drop_cap(binary);
|
||||
// The client equips the child, and may pass it on to whoever
|
||||
// it runs it for.
|
||||
(reply, process.map(libkos::syscall::Give::keeping_delegate))
|
||||
}
|
||||
None => ([MALFORMED, 0, 0, 0], None),
|
||||
(Some(_), None) => (refused(reply::NOT_FOUND).0, None),
|
||||
(None, _) => ([MALFORMED, 0, 0, 0], None),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
116
crates/libkos/src/apps.rs
Normal file
116
crates/libkos/src/apps.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
//! The permission manager's client (protocol: `types::apps`): run, install
|
||||
//! and manage apps. Text goes through a request page this process owns and
|
||||
//! lends with each call.
|
||||
|
||||
use crate::syscall::{self, Give, Received};
|
||||
use crate::window::{self, Window};
|
||||
use types::address::{ByteOffset, VirtualAddress};
|
||||
use types::apps::{self as wire, PAGE_SIZE, reply};
|
||||
use types::bounded::{CapSlot, PageCount};
|
||||
use types::mapping::PagePerms;
|
||||
|
||||
/// What the permission manager refused with: a `types::apps::reply` code
|
||||
/// and its word (the capability bits it is about, or a manifest line).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Refused {
|
||||
pub code: usize,
|
||||
pub detail: usize,
|
||||
}
|
||||
|
||||
pub type Result<T> = core::result::Result<T, Refused>;
|
||||
|
||||
const FAILED: Refused = Refused { code: reply::FAILED, detail: 0 };
|
||||
|
||||
pub struct Apps {
|
||||
ep: CapSlot,
|
||||
page_cap: CapSlot,
|
||||
page: Window,
|
||||
}
|
||||
|
||||
impl Apps {
|
||||
/// Talks to the permission manager behind `ep`, with a request page
|
||||
/// mapped at `va`.
|
||||
pub fn connect(ep: CapSlot, va: VirtualAddress) -> Option<Self> {
|
||||
let page_cap = syscall::create_region(PageCount::new(1)?).ok()?;
|
||||
let page = window::map(page_cap, va, PagePerms::ReadWrite).ok()?;
|
||||
Some(Self { ep, page_cap, page })
|
||||
}
|
||||
|
||||
fn call(&self, words: [usize; 4], cap: Option<Give>) -> Result<(usize, Option<CapSlot>)> {
|
||||
let r = syscall::call(self.ep, words, cap).map_err(|_| FAILED)?;
|
||||
let ([code, detail, _, _], cap) = match r {
|
||||
Received::Message { words, cap, .. } | Received::Call { words, cap, .. } => (words, cap),
|
||||
Received::Signal { .. } => return Err(FAILED),
|
||||
};
|
||||
match code {
|
||||
reply::OK => Ok((detail, cap)),
|
||||
code => Err(Refused { code, detail }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Lends the request page for one call.
|
||||
fn with_page(&self, op: usize, word: usize) -> Result<(usize, Option<CapSlot>)> {
|
||||
self.call([op, word, 0, 0], Some(self.page_cap.into()))
|
||||
}
|
||||
|
||||
/// `parts`, NUL-separated and ending in an empty one, into the page.
|
||||
fn put(&self, parts: &[&str]) -> Result<()> {
|
||||
let need: usize = parts.iter().map(|p| p.len() + 1).sum::<usize>() + 1;
|
||||
if need > PAGE_SIZE || parts.iter().any(|p| p.contains('\0')) {
|
||||
return Err(FAILED);
|
||||
}
|
||||
let mut at = ByteOffset::ZERO;
|
||||
for p in parts {
|
||||
self.page.copy_from(at, p.as_bytes());
|
||||
self.page.write(at + p.len(), 0u8);
|
||||
at = at + p.len() + 1;
|
||||
}
|
||||
self.page.write(at, 0u8);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runs `args[0]` with `args`, printing to `output`, working in
|
||||
/// `folder`; the running process (to wait on and kill) and the bits of
|
||||
/// what it was granted. Both caps go with Delegate kept, so the
|
||||
/// permission manager can hand them on.
|
||||
pub fn launch(&self, args: &[&str], output: Option<CapSlot>, folder: Option<CapSlot>) -> Result<(CapSlot, u32)> {
|
||||
for (op, cap) in [(wire::PASS_OUTPUT, output), (wire::PASS_FOLDER, folder)] {
|
||||
if let Some(cap) = cap {
|
||||
self.call([op, 0, 0, 0], Some(Give::keeping_delegate(cap)))?;
|
||||
}
|
||||
}
|
||||
self.put(args)?;
|
||||
match self.with_page(wire::LAUNCH, 0)? {
|
||||
(granted, Some(process)) => Ok((process, granted as u32)),
|
||||
(_, None) => Err(FAILED),
|
||||
}
|
||||
}
|
||||
|
||||
/// Installs the app in `source` (a read-only grant to its folder),
|
||||
/// asking the owner; the bits of what it was granted.
|
||||
pub fn install(&self, source: CapSlot) -> Result<u32> {
|
||||
self.call([wire::INSTALL, 0, 0, 0], Some(source.into())).map(|(bits, _)| bits as u32)
|
||||
}
|
||||
|
||||
/// One line per installed app, into `out`; how much.
|
||||
pub fn list(&self, out: &mut [u8]) -> Result<usize> {
|
||||
let (len, _) = self.with_page(wire::LIST, 0)?;
|
||||
let len = len.min(out.len()).min(PAGE_SIZE);
|
||||
self.page.copy_to(ByteOffset::ZERO, &mut out[..len]);
|
||||
Ok(len)
|
||||
}
|
||||
|
||||
/// Takes the capability with catalog index `capability` away from app
|
||||
/// `id` (stopping it if it runs with it).
|
||||
pub fn revoke(&self, id: &str, capability: usize) -> Result<()> {
|
||||
self.put(&[id])?;
|
||||
self.with_page(wire::REVOKE, capability).map(|_| ())
|
||||
}
|
||||
|
||||
/// Gives app `id` the capability with catalog index `capability`, which
|
||||
/// it asked for.
|
||||
pub fn allow(&self, id: &str, capability: usize) -> Result<()> {
|
||||
self.put(&[id])?;
|
||||
self.with_page(wire::ALLOW, capability).map(|_| ())
|
||||
}
|
||||
}
|
||||
@@ -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]> {
|
||||
|
||||
@@ -6,6 +6,9 @@ use core::fmt;
|
||||
struct Writer {
|
||||
buf: [u8; BATCH],
|
||||
len: usize,
|
||||
/// Where it goes: an endpoint speaking the log protocol, else where
|
||||
/// `print!` goes.
|
||||
to: Option<types::bounded::CapSlot>,
|
||||
}
|
||||
|
||||
/// A few whole text messages' worth.
|
||||
@@ -13,7 +16,10 @@ const BATCH: usize = 5 * types::log::TEXT_BYTES;
|
||||
|
||||
impl Writer {
|
||||
fn flush(&mut self) {
|
||||
write_bytes(&self.buf[..self.len]);
|
||||
match self.to {
|
||||
Some(ep) => write_to(ep, &self.buf[..self.len]),
|
||||
None => write_bytes(&self.buf[..self.len]),
|
||||
}
|
||||
self.len = 0;
|
||||
}
|
||||
}
|
||||
@@ -31,6 +37,22 @@ impl fmt::Write for Writer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Bytes to `ep`, an endpoint speaking the log protocol (a terminal, the
|
||||
/// log server), whatever this process's own log is.
|
||||
pub fn write_to(ep: types::bounded::CapSlot, bytes: &[u8]) {
|
||||
for chunk in bytes.chunks(types::log::TEXT_BYTES) {
|
||||
let _ = crate::syscall::call(ep, types::log::text_words(chunk), None);
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats to `ep` (see `write_to`).
|
||||
pub fn print_to(ep: types::bounded::CapSlot, args: fmt::Arguments) {
|
||||
use fmt::Write;
|
||||
let mut w = Writer { buf: [0; BATCH], len: 0, to: Some(ep) };
|
||||
w.write_fmt(args).ok();
|
||||
w.flush();
|
||||
}
|
||||
|
||||
/// Bytes as they are to where `print!` goes: the log endpoint (the log
|
||||
/// server, or a terminal), else the console; nowhere without either.
|
||||
pub fn write_bytes(bytes: &[u8]) {
|
||||
@@ -48,7 +70,7 @@ pub fn write_bytes(bytes: &[u8]) {
|
||||
|
||||
pub fn _print(args: fmt::Arguments) {
|
||||
use fmt::Write;
|
||||
let mut w = Writer { buf: [0; BATCH], len: 0 };
|
||||
let mut w = Writer { buf: [0; BATCH], len: 0, to: None };
|
||||
w.write_fmt(args).ok();
|
||||
w.flush();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
extern crate alloc;
|
||||
|
||||
pub mod alloc_impl;
|
||||
pub mod apps;
|
||||
pub mod device;
|
||||
mod arch;
|
||||
pub mod display;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! Running a program through init (protocol: `types::spawn`). Init loads it
|
||||
//! Running a program through init (protocol: `types::spawn`). The caller
|
||||
//! hands init a read-only storage grant to the program file; init loads it
|
||||
//! and gives it a log endpoint; the caller hands it any other capabilities
|
||||
//! (`delegate` to the process) and starts it.
|
||||
|
||||
@@ -25,9 +26,11 @@ pub struct Spawned {
|
||||
log: Option<CapSlot>,
|
||||
}
|
||||
|
||||
pub fn spawn(init: CapSlot, name: &[u8], limits: Limits) -> Result<Spawned, SpawnError> {
|
||||
let words = request(name, limits).ok_or(SpawnError::Failed)?;
|
||||
let reply = syscall::call(init, words, None).map_err(|_| SpawnError::Failed)?;
|
||||
/// Loads the program `binary` (a read-only storage grant to its file) into
|
||||
/// a new process, not started.
|
||||
pub fn spawn(init: CapSlot, binary: CapSlot, limits: Limits) -> Result<Spawned, SpawnError> {
|
||||
let words = request(limits).ok_or(SpawnError::Failed)?;
|
||||
let reply = syscall::call(init, words, Some(binary.into())).map_err(|_| SpawnError::Failed)?;
|
||||
let [tag, entry, log, _] = reply.words();
|
||||
let cap = match reply {
|
||||
syscall::Received::Message { cap, .. } | syscall::Received::Call { cap, .. } => cap,
|
||||
|
||||
@@ -38,6 +38,12 @@ pub fn storage() -> Option<CapSlot> {
|
||||
StartArg::decode(arg()).storage
|
||||
}
|
||||
|
||||
/// The app's own data folder (a storage grant, read-write), if it was
|
||||
/// granted `storage`.
|
||||
pub fn data() -> Option<CapSlot> {
|
||||
StartArg::decode(arg()).data
|
||||
}
|
||||
|
||||
/// The display server endpoint this process was given, if any.
|
||||
pub fn display() -> Option<CapSlot> {
|
||||
StartArg::decode(arg()).display
|
||||
|
||||
@@ -19,6 +19,15 @@ macro_rules! catalog {
|
||||
}
|
||||
}
|
||||
|
||||
/// Its place in the catalog, as sent in a message word.
|
||||
pub fn index(self) -> usize {
|
||||
self as usize
|
||||
}
|
||||
|
||||
pub fn from_index(i: usize) -> Option<Self> {
|
||||
Capability::ALL.get(i).copied()
|
||||
}
|
||||
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
$(Capability::$variant => $name,)*
|
||||
@@ -65,6 +74,20 @@ pub struct Capabilities(u32);
|
||||
impl Capabilities {
|
||||
pub const NONE: Self = Self(0);
|
||||
|
||||
/// As sent in a message word.
|
||||
pub const fn bits(self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// From a message word; bits past the catalog are dropped.
|
||||
pub fn from_bits(bits: u32) -> Self {
|
||||
Capability::ALL
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|c| bits & c.bit() != 0)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn with(self, c: Capability) -> Self {
|
||||
Self(self.0 | c.bit())
|
||||
}
|
||||
@@ -89,6 +112,11 @@ impl Capabilities {
|
||||
self.0 == 0
|
||||
}
|
||||
|
||||
/// Those in `self` but not in `other`.
|
||||
pub fn minus(self, other: Self) -> Self {
|
||||
Self(self.0 & !other.0)
|
||||
}
|
||||
|
||||
/// Whether every capability in `self` is in `other`.
|
||||
pub fn is_subset(self, other: Self) -> bool {
|
||||
self.0 & !other.0 == 0
|
||||
|
||||
@@ -36,7 +36,8 @@ impl<const N: usize> core::fmt::Debug for Text<N> {
|
||||
|
||||
impl<const N: usize> core::fmt::Display for Text<N> {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
// `pad`, so `{:<20}` and the like line columns up.
|
||||
f.pad(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +64,7 @@ impl AppId {
|
||||
|
||||
impl core::fmt::Display for AppId {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
f.pad(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ bench = false
|
||||
|
||||
[dependencies]
|
||||
types = { workspace = true }
|
||||
manifest = { workspace = true }
|
||||
|
||||
libkos = { workspace = true }
|
||||
server-framework = { workspace = true }
|
||||
|
||||
83
crates/servers/permission-manager/src/consent.rs
Normal file
83
crates/servers/permission-manager/src/consent.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
//! The `Consent` port on the terminal: a yes/no question, answered by the
|
||||
//! owner typing. (Not yet a trusted path: an app could draw a look-alike
|
||||
//! prompt, though it cannot answer one.)
|
||||
|
||||
use libkos::io::print_to;
|
||||
use manifest::{Capabilities, Capability, Manifest};
|
||||
use types::bounded::CapSlot;
|
||||
|
||||
use crate::policy::Consent;
|
||||
|
||||
pub struct TerminalConsent {
|
||||
pub terminal: CapSlot,
|
||||
}
|
||||
|
||||
impl TerminalConsent {
|
||||
/// Only `y` or `yes` is yes.
|
||||
fn answer(&self) -> bool {
|
||||
let mut line = [0u8; 16];
|
||||
let n = libkos::terminal::read_line(self.terminal, &mut line);
|
||||
matches!(core::str::from_utf8(&line[..n]).map(str::trim), Ok("y" | "Y" | "yes"))
|
||||
}
|
||||
}
|
||||
|
||||
/// `display, storage`.
|
||||
pub struct CapabilityNames(pub Capabilities);
|
||||
|
||||
impl core::fmt::Display for CapabilityNames {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
for (i, c) in self.0.iter().enumerate() {
|
||||
if i > 0 {
|
||||
f.write_str(", ")?;
|
||||
}
|
||||
f.write_str(c.name())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Consent for TerminalConsent {
|
||||
fn approve_required(&mut self, app: &Manifest, required: Capabilities) -> bool {
|
||||
print_to(
|
||||
self.terminal,
|
||||
format_args!("Install {} ({} {})? It needs: {}. [y/N] ", app.name, app.id, version(app), CapabilityNames(required)),
|
||||
);
|
||||
self.answer()
|
||||
}
|
||||
|
||||
fn approve_optional(&mut self, app: &Manifest, cap: Capability) -> bool {
|
||||
print_to(self.terminal, format_args!("Let {} use {} (it can do without)? [y/N] ", app.name, cap.name()));
|
||||
self.answer()
|
||||
}
|
||||
}
|
||||
|
||||
/// `1.0.0`.
|
||||
pub fn version(app: &Manifest) -> impl core::fmt::Display {
|
||||
struct V(manifest::Version);
|
||||
impl core::fmt::Display for V {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
let mut v = [0u8; 18];
|
||||
let mut w = Buf { buf: &mut v, len: 0 };
|
||||
let _ = core::fmt::write(&mut w, format_args!("{}.{}.{}", self.0.major, self.0.minor, self.0.patch));
|
||||
let len = w.len;
|
||||
f.pad(core::str::from_utf8(&v[..len]).unwrap_or(""))
|
||||
}
|
||||
}
|
||||
V(app.version)
|
||||
}
|
||||
|
||||
|
||||
/// Formats into a small buffer, so the whole can then be padded.
|
||||
struct Buf<'a> {
|
||||
buf: &'a mut [u8],
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl core::fmt::Write for Buf<'_> {
|
||||
fn write_str(&mut self, s: &str) -> core::fmt::Result {
|
||||
let end = self.len + s.len();
|
||||
self.buf.get_mut(self.len..end).ok_or(core::fmt::Error)?.copy_from_slice(s.as_bytes());
|
||||
self.len = end;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
136
crates/servers/permission-manager/src/policy.rs
Normal file
136
crates/servers/permission-manager/src/policy.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
//! What the permission manager decides, with nothing but its two ports:
|
||||
//! the owner (`Consent`) and where apps and decisions live (`Store`).
|
||||
//! No kernel, no IPC, no hardware: tested on the host.
|
||||
|
||||
use manifest::{AppId, Capabilities, Capability, Decision, Manifest};
|
||||
|
||||
/// Asks the owner. Answers are final: there is no "ask again later".
|
||||
pub trait Consent {
|
||||
/// May `app` have everything it cannot work without? No means it is
|
||||
/// not installed.
|
||||
fn approve_required(&mut self, app: &Manifest, required: Capabilities) -> bool;
|
||||
/// May `app` have `cap`, which it can do without?
|
||||
fn approve_optional(&mut self, app: &Manifest, cap: Capability) -> bool;
|
||||
}
|
||||
|
||||
/// Installed apps, their files and the owner's decisions.
|
||||
pub trait Store {
|
||||
/// An app folder offered for installation.
|
||||
type Source;
|
||||
|
||||
/// The manifest in `source`, as text.
|
||||
fn source_manifest<'a>(&mut self, source: &Self::Source, buf: &'a mut [u8]) -> Option<&'a str>;
|
||||
fn is_installed(&mut self, id: &AppId) -> bool;
|
||||
/// Copies `source`'s binary and manifest into the app's own folder,
|
||||
/// and makes its data folder if it is to have one.
|
||||
fn install(&mut self, source: &Self::Source, app: &Manifest, data_folder: bool) -> bool;
|
||||
fn manifest(&mut self, id: &AppId) -> Option<Manifest>;
|
||||
/// The installed app run as `command`.
|
||||
fn find(&mut self, command: &str) -> Option<AppId>;
|
||||
fn decision(&mut self, id: &AppId) -> Option<Decision>;
|
||||
fn record(&mut self, id: &AppId, decision: &Decision) -> bool;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Refusal {
|
||||
/// The offered folder holds no manifest it can read.
|
||||
NoManifest,
|
||||
BadManifest(manifest::Error),
|
||||
AlreadyInstalled,
|
||||
NotInstalled,
|
||||
/// This device has no server for these.
|
||||
Unavailable(Capabilities),
|
||||
/// The owner said no to what it requires.
|
||||
Declined,
|
||||
/// It does not hold these, and cannot run without them.
|
||||
Missing(Capabilities),
|
||||
/// It never asked for that capability.
|
||||
NotRequested,
|
||||
/// Reading or writing the store failed.
|
||||
Store,
|
||||
}
|
||||
|
||||
/// Longest manifest read.
|
||||
pub const MAX_MANIFEST: usize = 2048;
|
||||
|
||||
/// Checks the app in `source` and asks the owner: required capabilities
|
||||
/// all together, then each optional one this device has. Installs it and
|
||||
/// records the decision; returns its manifest.
|
||||
pub fn install<S: Store>(
|
||||
store: &mut S,
|
||||
consent: &mut impl Consent,
|
||||
source: &S::Source,
|
||||
available: Capabilities,
|
||||
) -> Result<Manifest, Refusal> {
|
||||
let mut buf = [0u8; MAX_MANIFEST];
|
||||
let text = store.source_manifest(source, &mut buf).ok_or(Refusal::NoManifest)?;
|
||||
let app = Manifest::parse(text).map_err(Refusal::BadManifest)?;
|
||||
if store.is_installed(&app.id) {
|
||||
return Err(Refusal::AlreadyInstalled);
|
||||
}
|
||||
let lacking = app.required.minus(available);
|
||||
if !lacking.is_empty() {
|
||||
return Err(Refusal::Unavailable(lacking));
|
||||
}
|
||||
if !app.required.is_empty() && !consent.approve_required(&app, app.required) {
|
||||
return Err(Refusal::Declined);
|
||||
}
|
||||
let mut granted = app.required;
|
||||
for cap in app.optional.intersect(available).iter() {
|
||||
if consent.approve_optional(&app, cap) {
|
||||
granted = granted.with(cap);
|
||||
}
|
||||
}
|
||||
let decision = Decision { granted };
|
||||
if !store.install(source, &app, granted.contains(Capability::Storage)) || !store.record(&app.id, &decision) {
|
||||
return Err(Refusal::Store);
|
||||
}
|
||||
Ok(app)
|
||||
}
|
||||
|
||||
/// An app about to run and what it gets.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Launch {
|
||||
pub app: Manifest,
|
||||
pub granted: Capabilities,
|
||||
}
|
||||
|
||||
/// What the app run as `command` gets: what it asked for, the owner
|
||||
/// granted and this device has. Refused if that leaves out anything it
|
||||
/// requires (a decision changed since install, or a server is gone).
|
||||
pub fn plan_launch(store: &mut impl Store, command: &str, available: Capabilities) -> Result<Launch, Refusal> {
|
||||
let id = store.find(command).ok_or(Refusal::NotInstalled)?;
|
||||
let app = store.manifest(&id).ok_or(Refusal::NotInstalled)?;
|
||||
let decision = store.decision(&id).ok_or(Refusal::Store)?;
|
||||
let granted = decision.granted.intersect(app.requested()).intersect(available);
|
||||
let missing = app.required.minus(granted);
|
||||
if !missing.is_empty() {
|
||||
return Err(Refusal::Missing(missing));
|
||||
}
|
||||
Ok(Launch { app, granted })
|
||||
}
|
||||
|
||||
/// The owner takes `cap` away from `id`; the new decision.
|
||||
pub fn revoke(store: &mut impl Store, id: &AppId, cap: Capability) -> Result<Decision, Refusal> {
|
||||
let decision = store.decision(id).ok_or(Refusal::NotInstalled)?;
|
||||
let changed = Decision { granted: decision.granted.without(cap) };
|
||||
if !store.record(id, &changed) {
|
||||
return Err(Refusal::Store);
|
||||
}
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
/// The owner gives `id` a capability it asked for (one withdrawn, or an
|
||||
/// optional one declined at install); the new decision.
|
||||
pub fn allow(store: &mut impl Store, id: &AppId, cap: Capability) -> Result<Decision, Refusal> {
|
||||
let app = store.manifest(id).ok_or(Refusal::NotInstalled)?;
|
||||
if !app.requested().contains(cap) {
|
||||
return Err(Refusal::NotRequested);
|
||||
}
|
||||
let decision = store.decision(id).ok_or(Refusal::Store)?;
|
||||
let changed = Decision { granted: decision.granted.with(cap) };
|
||||
if !store.record(id, &changed) {
|
||||
return Err(Refusal::Store);
|
||||
}
|
||||
Ok(changed)
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
use server_framework::{Decode, Encode};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CapabilityKind {
|
||||
Display,
|
||||
Input,
|
||||
AudioPlayback,
|
||||
AudioCapture,
|
||||
Camera,
|
||||
Storage,
|
||||
Network,
|
||||
Gps,
|
||||
Bluetooth,
|
||||
}
|
||||
|
||||
impl CapabilityKind {
|
||||
pub fn index(self) -> usize {
|
||||
match self {
|
||||
Self::Display => 0,
|
||||
Self::Input => 1,
|
||||
Self::AudioPlayback => 2,
|
||||
Self::AudioCapture => 3,
|
||||
Self::Camera => 4,
|
||||
Self::Storage => 5,
|
||||
Self::Network => 6,
|
||||
Self::Gps => 7,
|
||||
Self::Bluetooth => 8,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_index(v: usize) -> Option<Self> {
|
||||
match v {
|
||||
0 => Some(Self::Display),
|
||||
1 => Some(Self::Input),
|
||||
2 => Some(Self::AudioPlayback),
|
||||
3 => Some(Self::AudioCapture),
|
||||
4 => Some(Self::Camera),
|
||||
5 => Some(Self::Storage),
|
||||
6 => Some(Self::Network),
|
||||
7 => Some(Self::Gps),
|
||||
8 => Some(Self::Bluetooth),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum PermissionRequest {
|
||||
RequestCapability { process: types::id::ProcessId, kind: CapabilityKind },
|
||||
RevokeCapability { process: types::id::ProcessId, kind: CapabilityKind },
|
||||
CheckCapability { process: types::id::ProcessId, kind: CapabilityKind },
|
||||
ListGranted { process: types::id::ProcessId },
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum PermissionResponse {
|
||||
Granted,
|
||||
Denied,
|
||||
Revoked,
|
||||
HasCapability(bool),
|
||||
GrantedList(u16),
|
||||
}
|
||||
|
||||
|
||||
impl Encode for PermissionRequest {
|
||||
fn encode(&self) -> [usize; 4] {
|
||||
match self {
|
||||
PermissionRequest::RequestCapability { process, kind } => {
|
||||
[0, process.raw() as usize, kind.index(), 0]
|
||||
}
|
||||
PermissionRequest::RevokeCapability { process, kind } => {
|
||||
[1, process.raw() as usize, kind.index(), 0]
|
||||
}
|
||||
PermissionRequest::CheckCapability { process, kind } => {
|
||||
[2, process.raw() as usize, kind.index(), 0]
|
||||
}
|
||||
PermissionRequest::ListGranted { process } => {
|
||||
[3, process.raw() as usize, 0, 0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode for PermissionRequest {
|
||||
fn decode(words: [usize; 4]) -> Option<Self> {
|
||||
match words[0] {
|
||||
0 => Some(PermissionRequest::RequestCapability {
|
||||
process: types::id::ProcessId::from_raw(words[1] as u16),
|
||||
kind: CapabilityKind::from_index(words[2])?,
|
||||
}),
|
||||
1 => Some(PermissionRequest::RevokeCapability {
|
||||
process: types::id::ProcessId::from_raw(words[1] as u16),
|
||||
kind: CapabilityKind::from_index(words[2])?,
|
||||
}),
|
||||
2 => Some(PermissionRequest::CheckCapability {
|
||||
process: types::id::ProcessId::from_raw(words[1] as u16),
|
||||
kind: CapabilityKind::from_index(words[2])?,
|
||||
}),
|
||||
3 => Some(PermissionRequest::ListGranted {
|
||||
process: types::id::ProcessId::from_raw(words[1] as u16),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode for PermissionResponse {
|
||||
fn encode(&self) -> [usize; 4] {
|
||||
match self {
|
||||
PermissionResponse::Granted => [0, 0, 0, 0],
|
||||
PermissionResponse::Denied => [1, 0, 0, 0],
|
||||
PermissionResponse::Revoked => [2, 0, 0, 0],
|
||||
PermissionResponse::HasCapability(has) => [3, *has as usize, 0, 0],
|
||||
PermissionResponse::GrantedList(bits) => [4, *bits as usize, 0, 0],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode for PermissionResponse {
|
||||
fn decode(words: [usize; 4]) -> Option<Self> {
|
||||
match words[0] {
|
||||
0 => Some(PermissionResponse::Granted),
|
||||
1 => Some(PermissionResponse::Denied),
|
||||
2 => Some(PermissionResponse::Revoked),
|
||||
3 => Some(PermissionResponse::HasCapability(words[1] != 0)),
|
||||
4 => Some(PermissionResponse::GrantedList(words[1] as u16)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +1,335 @@
|
||||
use server_framework::Server;
|
||||
use types::id::ProcessId;
|
||||
//! The permission manager over IPC (`types::apps`): the policy, with the
|
||||
//! storage server as its store, the terminal as the owner, and init to
|
||||
//! load what it runs.
|
||||
|
||||
use crate::protocol::{PermissionRequest, PermissionResponse};
|
||||
use alloc::vec;
|
||||
use alloc::vec::Vec;
|
||||
use libkos::syscall::{self, Give};
|
||||
use libkos::window;
|
||||
use manifest::{AppId, Capabilities, Capability};
|
||||
use server_framework::{MALFORMED, Server};
|
||||
use types::address::{ByteOffset, VirtualAddress};
|
||||
use types::apps::{self as wire, PAGE_SIZE, reply};
|
||||
use types::badge::Badge;
|
||||
use types::bounded::CapSlot;
|
||||
use types::budget::Limits;
|
||||
use types::mapping::PagePerms;
|
||||
use types::permission::{Permission, PermissionSet};
|
||||
use types::start_arg::StartArg;
|
||||
|
||||
const MAX_PROCESSES: usize = 64;
|
||||
const NUM_KINDS: usize = 9;
|
||||
use crate::consent::{CapabilityNames, TerminalConsent, version};
|
||||
use crate::policy::{self, Refusal, Store};
|
||||
use crate::store::{self, DiskStore, PATH_BUF};
|
||||
|
||||
pub struct PermissionManager {
|
||||
grants: [[bool; NUM_KINDS]; MAX_PROCESSES],
|
||||
/// What each app gets to run in, out of the permission manager's allowance.
|
||||
const PROGRAM: Limits = Limits { pages: 128, endpoints: 1, notifications: 1, regions: 1 };
|
||||
/// Apps running at once; a death is a bit each on the notification.
|
||||
const MAX_RUNNING: usize = 8;
|
||||
const MAX_ARGS: usize = 16;
|
||||
/// Where a client's request page is mapped during its call.
|
||||
const PAGE_VA: VirtualAddress = VirtualAddress::from_raw(0x01A0_0000);
|
||||
|
||||
/// What a launcher passed for its next launch.
|
||||
#[derive(Default)]
|
||||
struct Staged {
|
||||
badge: usize,
|
||||
output: Option<CapSlot>,
|
||||
folder: Option<CapSlot>,
|
||||
}
|
||||
|
||||
impl PermissionManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
grants: [[false; NUM_KINDS]; MAX_PROCESSES],
|
||||
}
|
||||
}
|
||||
struct Running {
|
||||
id: AppId,
|
||||
process: CapSlot,
|
||||
granted: Capabilities,
|
||||
/// Its data folder grant, taken back when it dies.
|
||||
data: Option<CapSlot>,
|
||||
}
|
||||
|
||||
fn granted_bits(&self, process: ProcessId) -> u16 {
|
||||
let idx = process.raw() as usize;
|
||||
if idx >= MAX_PROCESSES {
|
||||
return 0;
|
||||
}
|
||||
let mut bits = 0u16;
|
||||
for i in 0..NUM_KINDS {
|
||||
if self.grants[idx][i] {
|
||||
bits |= 1 << i;
|
||||
}
|
||||
}
|
||||
bits
|
||||
pub struct PermissionServer {
|
||||
store: DiskStore,
|
||||
consent: TerminalConsent,
|
||||
/// Init's spawn endpoint.
|
||||
init: CapSlot,
|
||||
display: Option<CapSlot>,
|
||||
/// Bound to the serving thread: deaths of running apps.
|
||||
deaths: CapSlot,
|
||||
staged: Staged,
|
||||
running: [Option<Running>; MAX_RUNNING],
|
||||
}
|
||||
|
||||
fn refused(r: Refusal) -> [usize; 4] {
|
||||
let (code, detail) = match r {
|
||||
Refusal::NoManifest => (reply::NO_MANIFEST, 0),
|
||||
Refusal::BadManifest(e) => (reply::BAD_MANIFEST, line_of(e)),
|
||||
Refusal::AlreadyInstalled => (reply::ALREADY_INSTALLED, 0),
|
||||
Refusal::NotInstalled => (reply::NOT_INSTALLED, 0),
|
||||
Refusal::Unavailable(c) => (reply::UNAVAILABLE, c.bits() as usize),
|
||||
Refusal::Declined => (reply::DECLINED, 0),
|
||||
Refusal::Missing(c) => (reply::MISSING, c.bits() as usize),
|
||||
Refusal::NotRequested => (reply::NOT_REQUESTED, 0),
|
||||
Refusal::Store => (reply::FAILED, 0),
|
||||
};
|
||||
[code, detail, 0, 0]
|
||||
}
|
||||
|
||||
fn line_of(e: manifest::Error) -> usize {
|
||||
use manifest::Error::*;
|
||||
match e {
|
||||
Syntax { line } | Duplicate { line } | Unknown { line } | Invalid { line } => line,
|
||||
Missing(_) | Conflict => 0,
|
||||
}
|
||||
}
|
||||
|
||||
impl Server for PermissionManager {
|
||||
type Request = PermissionRequest;
|
||||
type Response = PermissionResponse;
|
||||
const fn code(c: usize) -> [usize; 4] {
|
||||
[c, 0, 0, 0]
|
||||
}
|
||||
|
||||
fn handle(&mut self, request: PermissionRequest) -> PermissionResponse {
|
||||
match request {
|
||||
PermissionRequest::RequestCapability { process, kind } => {
|
||||
let idx = process.raw() as usize;
|
||||
if idx >= MAX_PROCESSES {
|
||||
return PermissionResponse::Denied;
|
||||
/// The client's page, copied out; the mapping goes again at once.
|
||||
fn read_page(page: CapSlot) -> Option<Vec<u8>> {
|
||||
let w = window::map(page, PAGE_VA, PagePerms::ReadWrite).ok();
|
||||
let out = w.map(|w| {
|
||||
let mut buf = vec![0u8; PAGE_SIZE.min(w.len())];
|
||||
w.copy_to(ByteOffset::ZERO, &mut buf);
|
||||
buf
|
||||
});
|
||||
syscall::drop_cap(page);
|
||||
out
|
||||
}
|
||||
|
||||
/// `text` into the client's page.
|
||||
fn write_page(page: CapSlot, text: &[u8]) -> bool {
|
||||
let w = window::map(page, PAGE_VA, PagePerms::ReadWrite).ok();
|
||||
let ok = w.is_some_and(|w| text.len() <= w.len() && {
|
||||
w.copy_from(ByteOffset::ZERO, text);
|
||||
true
|
||||
});
|
||||
syscall::drop_cap(page);
|
||||
ok
|
||||
}
|
||||
|
||||
/// The NUL-separated parts of a page, up to the first empty one.
|
||||
fn parts(page: &[u8]) -> impl Iterator<Item = &str> {
|
||||
page.split(|b| *b == 0).take_while(|p| !p.is_empty()).map_while(|p| core::str::from_utf8(p).ok())
|
||||
}
|
||||
|
||||
impl PermissionServer {
|
||||
pub fn new(store: DiskStore, consent: TerminalConsent, init: CapSlot, display: Option<CapSlot>, deaths: CapSlot) -> Self {
|
||||
Self { store, consent, init, display, deaths, staged: Staged::default(), running: [const { None }; MAX_RUNNING] }
|
||||
}
|
||||
self.grants[idx][kind.index()] = true;
|
||||
PermissionResponse::Granted
|
||||
|
||||
/// 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 }
|
||||
}
|
||||
PermissionRequest::RevokeCapability { process, kind } => {
|
||||
let idx = process.raw() as usize;
|
||||
if idx >= MAX_PROCESSES {
|
||||
return PermissionResponse::Denied;
|
||||
|
||||
fn stage(&mut self, badge: usize, op: usize, cap: Option<CapSlot>) -> [usize; 4] {
|
||||
if self.staged.badge != badge {
|
||||
self.unstage();
|
||||
self.staged.badge = badge;
|
||||
}
|
||||
self.grants[idx][kind.index()] = false;
|
||||
PermissionResponse::Revoked
|
||||
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);
|
||||
}
|
||||
PermissionRequest::CheckCapability { process, kind } => {
|
||||
let idx = process.raw() as usize;
|
||||
if idx >= MAX_PROCESSES {
|
||||
return PermissionResponse::HasCapability(false);
|
||||
code(reply::OK)
|
||||
}
|
||||
PermissionResponse::HasCapability(self.grants[idx][kind.index()])
|
||||
|
||||
fn unstage(&mut self) {
|
||||
for cap in [self.staged.output.take(), self.staged.folder.take()].into_iter().flatten() {
|
||||
syscall::drop_cap(cap);
|
||||
}
|
||||
PermissionRequest::ListGranted { process } => {
|
||||
PermissionResponse::GrantedList(self.granted_bits(process))
|
||||
}
|
||||
|
||||
fn launch(&mut self, badge: usize, page: CapSlot) -> ([usize; 4], Option<Give>) {
|
||||
let result = self.try_launch(badge, page);
|
||||
self.unstage();
|
||||
match result {
|
||||
Ok((process, granted)) => ([reply::OK, granted.bits() as usize, 0, 0], Some(process.into())),
|
||||
Err(words) => (words, None),
|
||||
}
|
||||
}
|
||||
|
||||
fn try_launch(&mut self, badge: usize, page: CapSlot) -> Result<(CapSlot, Capabilities), [usize; 4]> {
|
||||
let page = read_page(page).ok_or(code(reply::FAILED))?;
|
||||
let mut args = [""; MAX_ARGS];
|
||||
let mut n = 0;
|
||||
for (slot, p) in args.iter_mut().zip(parts(&page)) {
|
||||
*slot = p;
|
||||
n += 1;
|
||||
}
|
||||
let args = &args[..n];
|
||||
let command = *args.first().ok_or(code(reply::NOT_INSTALLED))?;
|
||||
let available = self.available();
|
||||
let plan = policy::plan_launch(&mut self.store, command, available).map_err(refused)?;
|
||||
let index = self.running.iter().position(Option::is_none).ok_or(code(reply::BUSY))?;
|
||||
|
||||
let mut p = [0u8; PATH_BUF];
|
||||
let binary = store::binary_path(&mut p, &plan.app).ok_or(code(reply::FAILED))?;
|
||||
let file = self.store.storage().grant(binary, false).map_err(|_| code(reply::FAILED))?;
|
||||
let spawned = libkos::spawn::spawn(self.init, file, PROGRAM);
|
||||
let _ = libkos::fs::release(file);
|
||||
let child = spawned.map_err(|e| match e {
|
||||
libkos::spawn::SpawnError::OverAllowance => code(reply::BUSY),
|
||||
_ => code(reply::FAILED),
|
||||
})?;
|
||||
|
||||
let call = PermissionSet::empty().with(Permission::Execute);
|
||||
let data = match plan.granted.contains(Capability::Storage) {
|
||||
true => store::data_path(&mut p, &plan.app.id).and_then(|d| self.store.storage().grant(d, true).ok()),
|
||||
false => None,
|
||||
};
|
||||
let staged = (badge == self.staged.badge).then_some(&self.staged);
|
||||
let arg = StartArg {
|
||||
log: staged.and_then(|s| s.output).and_then(|o| child.give(o, call)).or(child.log()),
|
||||
storage: staged.and_then(|s| s.folder).and_then(|f| child.give(f, call)),
|
||||
data: data.and_then(|d| child.give(d, call)),
|
||||
// It subscribes to input by sending the display a notification.
|
||||
display: self.display.filter(|_| plan.granted.contains(Capability::Display)).and_then(|d| child.give_sending(d, call)),
|
||||
args: child.set_args(args),
|
||||
console: None,
|
||||
};
|
||||
let watched = self.watch(child.process, index);
|
||||
if !watched || !child.start_with(arg) {
|
||||
let _ = syscall::kill(child.process);
|
||||
if let Some(d) = data {
|
||||
let _ = libkos::fs::release(d);
|
||||
}
|
||||
syscall::drop_cap(child.process);
|
||||
return Err(code(reply::FAILED));
|
||||
}
|
||||
self.running[index] = Some(Running { id: plan.app.id, process: child.process, granted: plan.granted, data });
|
||||
Ok((child.process, plan.granted))
|
||||
}
|
||||
|
||||
/// Has `process`'s death signal bit `index`.
|
||||
fn watch(&self, process: CapSlot, index: usize) -> bool {
|
||||
let signal = PermissionSet::empty().with(Permission::Write);
|
||||
let Ok(copy) = syscall::mint(self.deaths, Badge::new(1 << index), signal) else {
|
||||
return false;
|
||||
};
|
||||
let watched = syscall::watch_death(process, copy).is_ok();
|
||||
syscall::drop_cap(copy);
|
||||
watched
|
||||
}
|
||||
|
||||
fn install(&mut self, source: Option<CapSlot>) -> [usize; 4] {
|
||||
let Some(source) = source else { return code(reply::FAILED) };
|
||||
let available = self.available();
|
||||
let result = policy::install(&mut self.store, &mut self.consent, &source, available);
|
||||
syscall::drop_cap(source);
|
||||
match result {
|
||||
Ok(app) => {
|
||||
let granted = self.store.decision(&app.id).map_or(0, |d| d.granted.bits() as usize);
|
||||
[reply::OK, granted, 0, 0]
|
||||
}
|
||||
Err(r) => refused(r),
|
||||
}
|
||||
}
|
||||
|
||||
fn list(&mut self, page: CapSlot) -> [usize; 4] {
|
||||
use core::fmt::Write;
|
||||
let mut ids = Vec::new();
|
||||
self.store.ids(|id| ids.push(id));
|
||||
let mut text = Text(Vec::new());
|
||||
for id in ids {
|
||||
let (Some(app), Some(d)) = (self.store.manifest(&id), self.store.decision(&id)) else { continue };
|
||||
let _ = writeln!(text, "{:<20} {:<10} {:<8} {}", app.id, app.command.as_str(), version(&app), CapabilityNames(d.granted));
|
||||
}
|
||||
let len = text.0.len().min(PAGE_SIZE);
|
||||
if write_page(page, &text.0[..len]) { [reply::OK, len, 0, 0] } else { code(reply::FAILED) }
|
||||
}
|
||||
|
||||
/// The owner changes app `id`'s (in the page) capability `index`.
|
||||
fn change(&mut self, page: CapSlot, index: usize, allow: bool) -> [usize; 4] {
|
||||
let page = read_page(page);
|
||||
let id = page.as_deref().and_then(|p| parts(p).next()).and_then(AppId::new);
|
||||
let (Some(id), Some(cap)) = (id, Capability::from_index(index)) else {
|
||||
return code(reply::NOT_INSTALLED);
|
||||
};
|
||||
let result = match allow {
|
||||
true => policy::allow(&mut self.store, &id, cap),
|
||||
false => policy::revoke(&mut self.store, &id, cap),
|
||||
};
|
||||
if let Err(r) = result {
|
||||
return refused(r);
|
||||
}
|
||||
if !allow {
|
||||
// Its access dies at once: a running app that had it stops.
|
||||
for r in self.running.iter().flatten().filter(|r| r.id == id && r.granted.contains(cap)) {
|
||||
let _ = syscall::kill(r.process);
|
||||
}
|
||||
}
|
||||
code(reply::OK)
|
||||
}
|
||||
|
||||
/// Apps behind `bits` died: their data grants go back.
|
||||
fn reap(&mut self, bits: usize) {
|
||||
for (i, slot) in self.running.iter_mut().enumerate() {
|
||||
if bits & (1 << i) == 0 {
|
||||
continue;
|
||||
}
|
||||
if let Some(r) = slot.take() {
|
||||
if let Some(d) = r.data {
|
||||
let _ = libkos::fs::release(d);
|
||||
}
|
||||
syscall::drop_cap(r.process);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Text built for the client's page.
|
||||
struct Text(Vec<u8>);
|
||||
|
||||
impl core::fmt::Write for Text {
|
||||
fn write_str(&mut self, s: &str) -> core::fmt::Result {
|
||||
self.0.extend_from_slice(s.as_bytes());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Requests carry their own words; there is nothing to decode up front.
|
||||
pub struct NoRequest;
|
||||
|
||||
impl server_framework::Decode for NoRequest {
|
||||
fn decode(_: [usize; 4]) -> Option<Self> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl server_framework::Encode for NoRequest {
|
||||
fn encode(&self) -> [usize; 4] {
|
||||
[MALFORMED, 0, 0, 0]
|
||||
}
|
||||
}
|
||||
|
||||
impl Server for PermissionServer {
|
||||
type Request = NoRequest;
|
||||
type Response = NoRequest;
|
||||
|
||||
fn handle(&mut self, _: NoRequest) -> NoRequest {
|
||||
NoRequest
|
||||
}
|
||||
|
||||
fn handle_call(&mut self, badge: usize, words: [usize; 4], cap: Option<CapSlot>) -> ([usize; 4], Option<Give>) {
|
||||
match (words[0], cap) {
|
||||
(wire::PASS_OUTPUT | wire::PASS_FOLDER, cap) => (self.stage(badge, words[0], cap), None),
|
||||
(wire::LAUNCH, Some(page)) => self.launch(badge, page),
|
||||
(wire::INSTALL, source) => (self.install(source), None),
|
||||
(wire::LIST, Some(page)) => (self.list(page), None),
|
||||
(wire::REVOKE, Some(page)) => (self.change(page, words[1], false), None),
|
||||
(wire::ALLOW, Some(page)) => (self.change(page, words[1], true), None),
|
||||
(_, cap) => {
|
||||
if let Some(c) = cap {
|
||||
syscall::drop_cap(c);
|
||||
}
|
||||
([MALFORMED, 0, 0, 0], None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn signal(&mut self, bits: usize) {
|
||||
self.reap(bits);
|
||||
}
|
||||
}
|
||||
|
||||
196
crates/servers/permission-manager/src/store.rs
Normal file
196
crates/servers/permission-manager/src/store.rs
Normal file
@@ -0,0 +1,196 @@
|
||||
//! The `Store` port on the storage server: apps in `/apps/<id>/`, their
|
||||
//! data in `/data/<id>/`, the owner's decisions in `/system/permissions/`.
|
||||
|
||||
use libkos::fs::Storage;
|
||||
use manifest::{AppId, Decision, MAX_DECISION_TEXT, Manifest};
|
||||
use types::address::VirtualAddress;
|
||||
use types::bounded::CapSlot;
|
||||
|
||||
use crate::policy::{MAX_MANIFEST, Store};
|
||||
|
||||
const APPS: &str = "apps";
|
||||
const DATA: &str = "data";
|
||||
const DECISIONS: &str = "system/permissions";
|
||||
const MANIFEST: &str = "manifest.toml";
|
||||
/// Apps `find` looks through.
|
||||
pub const MAX_APPS: usize = 32;
|
||||
const LONGEST_PATH: usize = 160;
|
||||
const COPY_CHUNK: usize = 4096;
|
||||
|
||||
/// `parts` joined with `/`, in `buf`.
|
||||
fn path<'a>(buf: &'a mut [u8; LONGEST_PATH], parts: &[&str]) -> Option<&'a [u8]> {
|
||||
let mut len = 0;
|
||||
for (i, p) in parts.iter().enumerate() {
|
||||
let sep = usize::from(i > 0);
|
||||
let end = len + sep + p.len();
|
||||
let room = buf.get_mut(len..end)?;
|
||||
room[..sep].copy_from_slice(&b"/"[..sep]);
|
||||
room[sep..].copy_from_slice(p.as_bytes());
|
||||
len = end;
|
||||
}
|
||||
Some(&buf[..len])
|
||||
}
|
||||
|
||||
/// All of the file at `path` into `buf`, as text.
|
||||
fn read_text<'a>(fs: &mut Storage, path: &[u8], buf: &'a mut [u8]) -> Option<&'a str> {
|
||||
let h = fs.open(path).ok()?;
|
||||
let size = fs.file_size(h).ok().filter(|s| *s <= buf.len());
|
||||
let read = size.and_then(|s| fs.read(h, &mut buf[..s]).ok());
|
||||
let _ = fs.close(h);
|
||||
let n = read.filter(|r| Some(*r) == size)?;
|
||||
core::str::from_utf8(&buf[..n]).ok()
|
||||
}
|
||||
|
||||
/// Copies the file at `from` (in `src`) to `to` (in `dst`), replacing
|
||||
/// what an earlier, failed attempt left there.
|
||||
fn copy(src: &mut Storage, from: &[u8], dst: &mut Storage, to: &[u8]) -> bool {
|
||||
let Ok(input) = src.open(from) else { return false };
|
||||
let _ = dst.delete(to);
|
||||
let Ok(output) = dst.create(to) else {
|
||||
let _ = src.close(input);
|
||||
return false;
|
||||
};
|
||||
let mut chunk = [0u8; COPY_CHUNK];
|
||||
let ok = loop {
|
||||
match src.read(input, &mut chunk) {
|
||||
Ok(0) => break true,
|
||||
Ok(n) if dst.write(output, &chunk[..n]) == Ok(n) => {}
|
||||
_ => break false,
|
||||
}
|
||||
};
|
||||
let _ = src.close(input);
|
||||
let _ = dst.close(output);
|
||||
ok
|
||||
}
|
||||
|
||||
pub struct DiskStore {
|
||||
fs: Storage,
|
||||
/// Where an offered app folder's grant is mapped while it is read.
|
||||
source_va: VirtualAddress,
|
||||
}
|
||||
|
||||
impl DiskStore {
|
||||
pub fn new(fs: Storage, source_va: VirtualAddress) -> Self {
|
||||
Self { fs, source_va }
|
||||
}
|
||||
|
||||
/// Storage itself, for grants on the app's behalf.
|
||||
pub fn storage(&mut self) -> &mut Storage {
|
||||
&mut self.fs
|
||||
}
|
||||
|
||||
/// The ids of the installed apps (folder names in `/apps`).
|
||||
pub fn ids(&mut self, mut each: impl FnMut(AppId)) {
|
||||
let _ = self.fs.list_dir(APPS.as_bytes(), |e| {
|
||||
if let Some(id) = core::str::from_utf8(e.name()).ok().and_then(AppId::new) {
|
||||
each(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn with_source<T>(&self, source: CapSlot, f: impl FnOnce(&mut Storage) -> T) -> Option<T> {
|
||||
let mut src = Storage::attach(source, self.source_va).ok()?;
|
||||
let out = f(&mut src);
|
||||
src.detach();
|
||||
Some(out)
|
||||
}
|
||||
}
|
||||
|
||||
impl Store for DiskStore {
|
||||
/// A read-only storage grant to the offered app's folder.
|
||||
type Source = CapSlot;
|
||||
|
||||
fn source_manifest<'a>(&mut self, source: &CapSlot, buf: &'a mut [u8]) -> Option<&'a str> {
|
||||
let mut text = [0u8; MAX_MANIFEST];
|
||||
let n = self.with_source(*source, |src| read_text(src, MANIFEST.as_bytes(), &mut text).map(str::len))??;
|
||||
buf.get_mut(..n)?.copy_from_slice(&text[..n]);
|
||||
core::str::from_utf8(&buf[..n]).ok()
|
||||
}
|
||||
|
||||
fn is_installed(&mut self, id: &AppId) -> bool {
|
||||
let mut p = [0u8; LONGEST_PATH];
|
||||
let Some(p) = path(&mut p, &[APPS, id.as_str(), MANIFEST]) else { return false };
|
||||
self.fs.open(p).and_then(|h| self.fs.close(h)).is_ok()
|
||||
}
|
||||
|
||||
fn install(&mut self, source: &CapSlot, app: &Manifest, data_folder: bool) -> bool {
|
||||
let id = app.id.as_str();
|
||||
let (mut folder, mut to_manifest, mut to_binary) = ([0u8; LONGEST_PATH], [0u8; LONGEST_PATH], [0u8; LONGEST_PATH]);
|
||||
let (Some(folder), Some(to_manifest), Some(to_binary)) = (
|
||||
path(&mut folder, &[APPS, id]),
|
||||
path(&mut to_manifest, &[APPS, id, MANIFEST]),
|
||||
path(&mut to_binary, &[APPS, id, app.binary.as_str()]),
|
||||
) else {
|
||||
return false;
|
||||
};
|
||||
// There already if an earlier attempt failed part way; an app
|
||||
// counts as installed only once its manifest is in.
|
||||
let _ = self.fs.mkdir(folder);
|
||||
let fs = &mut self.fs;
|
||||
let copied = Storage::attach(*source, self.source_va).ok().map(|mut src| {
|
||||
// The binary first: an app folder with a manifest is installed.
|
||||
let ok = copy(&mut src, app.binary.as_str().as_bytes(), fs, to_binary)
|
||||
&& copy(&mut src, MANIFEST.as_bytes(), fs, to_manifest);
|
||||
src.detach();
|
||||
ok
|
||||
});
|
||||
if copied != Some(true) {
|
||||
return false;
|
||||
}
|
||||
let mut data = [0u8; LONGEST_PATH];
|
||||
// Kept from an earlier install if there is one: the app's data.
|
||||
!data_folder || path(&mut data, &[DATA, id]).is_some_and(|d| self.fs.mkdir(d).is_ok() || self.fs.list_dir(d, |_| {}).is_ok())
|
||||
}
|
||||
|
||||
fn manifest(&mut self, id: &AppId) -> Option<Manifest> {
|
||||
let mut p = [0u8; LONGEST_PATH];
|
||||
let p = path(&mut p, &[APPS, id.as_str(), MANIFEST])?;
|
||||
let mut text = [0u8; MAX_MANIFEST];
|
||||
let m = Manifest::parse(read_text(&mut self.fs, p, &mut text)?).ok()?;
|
||||
// A manifest in the wrong folder is no app.
|
||||
(m.id == *id).then_some(m)
|
||||
}
|
||||
|
||||
fn find(&mut self, command: &str) -> Option<AppId> {
|
||||
let mut ids = [None; MAX_APPS];
|
||||
let mut n = 0;
|
||||
self.ids(|id| {
|
||||
if let Some(slot) = ids.get_mut(n) {
|
||||
*slot = Some(id);
|
||||
n += 1;
|
||||
}
|
||||
});
|
||||
ids.into_iter().flatten().find(|id| self.manifest(id).is_some_and(|m| m.command.as_str() == command))
|
||||
}
|
||||
|
||||
fn decision(&mut self, id: &AppId) -> Option<Decision> {
|
||||
let mut p = [0u8; LONGEST_PATH];
|
||||
let p = path(&mut p, &[DECISIONS, id.as_str()])?;
|
||||
let mut text = [0u8; MAX_DECISION_TEXT];
|
||||
Decision::parse(read_text(&mut self.fs, p, &mut text)?).ok()
|
||||
}
|
||||
|
||||
fn record(&mut self, id: &AppId, decision: &Decision) -> bool {
|
||||
let mut p = [0u8; LONGEST_PATH];
|
||||
let Some(p) = path(&mut p, &[DECISIONS, id.as_str()]) else { return false };
|
||||
let mut text = [0u8; MAX_DECISION_TEXT];
|
||||
let n = decision.write(&mut text);
|
||||
// Replaced whole: there is no truncate.
|
||||
let _ = self.fs.delete(p);
|
||||
let Ok(h) = self.fs.create(p) else { return false };
|
||||
let written = self.fs.write(h, &text[..n]) == Ok(n);
|
||||
let _ = self.fs.close(h);
|
||||
written
|
||||
}
|
||||
}
|
||||
|
||||
/// The path of app `id`'s binary and its data folder, for grants.
|
||||
pub fn binary_path<'a>(buf: &'a mut [u8; LONGEST_PATH], app: &Manifest) -> Option<&'a [u8]> {
|
||||
path(buf, &[APPS, app.id.as_str(), app.binary.as_str()])
|
||||
}
|
||||
|
||||
pub fn data_path<'a>(buf: &'a mut [u8; LONGEST_PATH], id: &AppId) -> Option<&'a [u8]> {
|
||||
path(buf, &[DATA, id.as_str()])
|
||||
}
|
||||
|
||||
pub const PATH_BUF: usize = LONGEST_PATH;
|
||||
@@ -1,2 +1 @@
|
||||
mod protocol;
|
||||
mod server;
|
||||
mod policy;
|
||||
|
||||
204
crates/servers/permission-manager/src/tests/policy.rs
Normal file
204
crates/servers/permission-manager/src/tests/policy.rs
Normal file
@@ -0,0 +1,204 @@
|
||||
extern crate std;
|
||||
|
||||
use crate::policy::*;
|
||||
use manifest::{AppId, Capabilities, Capability, Decision, Manifest};
|
||||
use std::collections::BTreeMap;
|
||||
use std::string::{String, ToString};
|
||||
use std::vec::Vec;
|
||||
|
||||
/// Installed apps in memory; a source is the manifest text itself.
|
||||
#[derive(Default)]
|
||||
struct MemoryStore {
|
||||
manifests: BTreeMap<String, Manifest>,
|
||||
decisions: BTreeMap<String, Decision>,
|
||||
data_folders: Vec<String>,
|
||||
fail_writes: bool,
|
||||
}
|
||||
|
||||
impl Store for MemoryStore {
|
||||
type Source = String;
|
||||
|
||||
fn source_manifest<'a>(&mut self, source: &String, buf: &'a mut [u8]) -> Option<&'a str> {
|
||||
if source.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let n = source.len().min(buf.len());
|
||||
buf[..n].copy_from_slice(&source.as_bytes()[..n]);
|
||||
core::str::from_utf8(&buf[..n]).ok()
|
||||
}
|
||||
fn is_installed(&mut self, id: &AppId) -> bool {
|
||||
self.manifests.contains_key(id.as_str())
|
||||
}
|
||||
fn install(&mut self, _source: &String, app: &Manifest, data_folder: bool) -> bool {
|
||||
if self.fail_writes {
|
||||
return false;
|
||||
}
|
||||
self.manifests.insert(app.id.as_str().to_string(), *app);
|
||||
if data_folder {
|
||||
self.data_folders.push(app.id.as_str().to_string());
|
||||
}
|
||||
true
|
||||
}
|
||||
fn manifest(&mut self, id: &AppId) -> Option<Manifest> {
|
||||
self.manifests.get(id.as_str()).copied()
|
||||
}
|
||||
fn find(&mut self, command: &str) -> Option<AppId> {
|
||||
self.manifests.values().find(|m| m.command.as_str() == command).map(|m| m.id)
|
||||
}
|
||||
fn decision(&mut self, id: &AppId) -> Option<Decision> {
|
||||
self.decisions.get(id.as_str()).copied()
|
||||
}
|
||||
fn record(&mut self, id: &AppId, decision: &Decision) -> bool {
|
||||
if self.fail_writes {
|
||||
return false;
|
||||
}
|
||||
self.decisions.insert(id.as_str().to_string(), *decision);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// An owner who answers from a script, and remembers what was asked.
|
||||
struct Owner {
|
||||
required: bool,
|
||||
optional: Vec<Capability>,
|
||||
asked: Vec<String>,
|
||||
}
|
||||
|
||||
impl Owner {
|
||||
fn says(required: bool, optional: &[Capability]) -> Self {
|
||||
Self { required, optional: optional.to_vec(), asked: Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Consent for Owner {
|
||||
fn approve_required(&mut self, _app: &Manifest, required: Capabilities) -> bool {
|
||||
self.asked.push(std::format!("required {:?}", required.iter().collect::<Vec<_>>()));
|
||||
self.required
|
||||
}
|
||||
fn approve_optional(&mut self, _app: &Manifest, cap: Capability) -> bool {
|
||||
self.asked.push(std::format!("optional {}", cap.name()));
|
||||
self.optional.contains(&cap)
|
||||
}
|
||||
}
|
||||
|
||||
fn app(id: &str, command: &str, required: &str, optional: &str) -> String {
|
||||
std::format!(
|
||||
"[app]\nid = \"{id}\"\nname = \"App\"\nversion = \"1.0.0\"\nbinary = \"{command}\"\n[capabilities]\nrequired = [{required}]\noptional = [{optional}]\n"
|
||||
)
|
||||
}
|
||||
|
||||
const DISPLAY: Capability = Capability::Display;
|
||||
const STORAGE: Capability = Capability::Storage;
|
||||
const NETWORK: Capability = Capability::Network;
|
||||
|
||||
fn device() -> Capabilities {
|
||||
Capabilities::NONE.with(DISPLAY).with(STORAGE)
|
||||
}
|
||||
|
||||
fn set(caps: &[Capability]) -> Capabilities {
|
||||
caps.iter().copied().collect()
|
||||
}
|
||||
|
||||
fn id(s: &str) -> AppId {
|
||||
AppId::new(s).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn installing_asks_once_for_the_required_then_each_optional() {
|
||||
let mut store = MemoryStore::default();
|
||||
let mut owner = Owner::says(true, &[STORAGE]);
|
||||
let src = app("kos.snake", "snake", "\"display\"", "\"storage\"");
|
||||
let m = install(&mut store, &mut owner, &src, device()).unwrap();
|
||||
assert_eq!(owner.asked, ["required [Display]", "optional storage"]);
|
||||
assert_eq!(store.decision(&m.id), Some(Decision { granted: set(&[DISPLAY, STORAGE]) }));
|
||||
assert_eq!(store.data_folders, ["kos.snake"], "storage granted: it gets a data folder");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_app_needing_nothing_is_installed_without_asking() {
|
||||
let mut store = MemoryStore::default();
|
||||
let mut owner = Owner::says(false, &[]);
|
||||
install(&mut store, &mut owner, &app("kos.cat", "cat", "", ""), device()).unwrap();
|
||||
assert!(owner.asked.is_empty());
|
||||
assert!(store.data_folders.is_empty(), "no storage, no data folder");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declining_the_required_installs_nothing() {
|
||||
let mut store = MemoryStore::default();
|
||||
let mut owner = Owner::says(false, &[]);
|
||||
let src = app("kos.snake", "snake", "\"display\"", "");
|
||||
assert_eq!(install(&mut store, &mut owner, &src, device()), Err(Refusal::Declined));
|
||||
assert!(store.manifests.is_empty() && store.decisions.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_the_device_lacks_is_refused_or_never_asked() {
|
||||
let mut store = MemoryStore::default();
|
||||
let mut owner = Owner::says(true, &[NETWORK]);
|
||||
let needs_network = app("kos.chat", "chat", "\"network\"", "");
|
||||
assert_eq!(install(&mut store, &mut owner, &needs_network, device()), Err(Refusal::Unavailable(set(&[NETWORK]))));
|
||||
assert!(owner.asked.is_empty(), "nothing asked for an app that cannot run here");
|
||||
|
||||
let wants_network = app("kos.notes", "notes", "", "\"network\"");
|
||||
install(&mut store, &mut owner, &wants_network, device()).unwrap();
|
||||
assert!(owner.asked.is_empty(), "an optional capability the device lacks is not offered");
|
||||
assert_eq!(store.decision(&id("kos.notes")), Some(Decision::default()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_or_duplicate_apps_are_refused() {
|
||||
let mut store = MemoryStore::default();
|
||||
let mut owner = Owner::says(true, &[]);
|
||||
assert!(matches!(install(&mut store, &mut owner, &"[app]\n".to_string(), device()), Err(Refusal::BadManifest(_))));
|
||||
assert_eq!(install(&mut store, &mut owner, &String::new(), device()), Err(Refusal::NoManifest));
|
||||
let src = app("kos.cat", "cat", "", "");
|
||||
install(&mut store, &mut owner, &src, device()).unwrap();
|
||||
assert_eq!(install(&mut store, &mut owner, &src, device()), Err(Refusal::AlreadyInstalled));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failing_store_is_reported() {
|
||||
let mut store = MemoryStore { fail_writes: true, ..MemoryStore::default() };
|
||||
let mut owner = Owner::says(true, &[]);
|
||||
assert_eq!(install(&mut store, &mut owner, &app("kos.cat", "cat", "", ""), device()), Err(Refusal::Store));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_launch_gets_what_was_asked_granted_and_present() {
|
||||
let mut store = MemoryStore::default();
|
||||
let mut owner = Owner::says(true, &[STORAGE]);
|
||||
install(&mut store, &mut owner, &app("kos.snake", "snake", "\"display\"", "\"storage\""), device()).unwrap();
|
||||
let plan = plan_launch(&mut store, "snake", device()).unwrap();
|
||||
assert_eq!(plan.granted, set(&[DISPLAY, STORAGE]));
|
||||
// A decision listing more than the app asked for grants no more.
|
||||
store.decisions.insert("kos.snake".to_string(), Decision { granted: set(&[DISPLAY, STORAGE, NETWORK]) });
|
||||
let plan = plan_launch(&mut store, "snake", device().with(NETWORK)).unwrap();
|
||||
assert_eq!(plan.granted, set(&[DISPLAY, STORAGE]));
|
||||
assert_eq!(plan_launch(&mut store, "nope", device()), Err(Refusal::NotInstalled));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revoking_and_allowing_change_the_next_launch() {
|
||||
let mut store = MemoryStore::default();
|
||||
let mut owner = Owner::says(true, &[]);
|
||||
install(&mut store, &mut owner, &app("kos.snake", "snake", "\"display\"", "\"storage\""), device()).unwrap();
|
||||
assert_eq!(plan_launch(&mut store, "snake", device()).unwrap().granted, set(&[DISPLAY]));
|
||||
|
||||
allow(&mut store, &id("kos.snake"), STORAGE).unwrap();
|
||||
assert_eq!(plan_launch(&mut store, "snake", device()).unwrap().granted, set(&[DISPLAY, STORAGE]));
|
||||
assert_eq!(allow(&mut store, &id("kos.snake"), NETWORK), Err(Refusal::NotRequested), "only what it asked for");
|
||||
|
||||
revoke(&mut store, &id("kos.snake"), DISPLAY).unwrap();
|
||||
assert_eq!(plan_launch(&mut store, "snake", device()), Err(Refusal::Missing(set(&[DISPLAY]))), "a required one gone: it cannot run");
|
||||
assert_eq!(revoke(&mut store, &id("kos.nope"), DISPLAY), Err(Refusal::NotInstalled));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_server_gone_since_install_stops_an_app_that_requires_it() {
|
||||
let mut store = MemoryStore::default();
|
||||
let mut owner = Owner::says(true, &[]);
|
||||
install(&mut store, &mut owner, &app("kos.snake", "snake", "\"display\"", ""), device()).unwrap();
|
||||
let headless = Capabilities::NONE.with(STORAGE);
|
||||
assert_eq!(plan_launch(&mut store, "snake", headless), Err(Refusal::Missing(set(&[DISPLAY]))));
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
7
crates/tools/app-image/Cargo.toml
Normal file
7
crates/tools/app-image/Cargo.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "app-image"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
manifest = { workspace = true }
|
||||
64
crates/tools/app-image/src/main.rs
Normal file
64
crates/tools/app-image/src/main.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
//! Puts apps on a disk image being built (see docs/apps.md), checked by
|
||||
//! the same strict parser the permission manager uses:
|
||||
//!
|
||||
//! app-image install <root> <manifest.toml> <binary>
|
||||
//! an installed, pre-approved app: /apps/<id>/, its decision granting
|
||||
//! everything it asks for, and /data/<id>/ if that includes storage
|
||||
//! app-image stage <folder> <manifest.toml> <binary>
|
||||
//! an app folder the owner can install later (`install <folder>`)
|
||||
|
||||
use manifest::{Capability, Decision, MAX_DECISION_TEXT, Manifest};
|
||||
use std::path::Path;
|
||||
use std::process::ExitCode;
|
||||
use std::{env, fs};
|
||||
|
||||
fn read(manifest: &Path) -> Result<(Manifest, String), String> {
|
||||
let text = fs::read_to_string(manifest).map_err(|e| format!("{}: {e}", manifest.display()))?;
|
||||
let m = Manifest::parse(&text).map_err(|e| format!("{}: {e:?}", manifest.display()))?;
|
||||
Ok((m, text))
|
||||
}
|
||||
|
||||
/// The manifest and the binary (under the name the manifest gives it).
|
||||
fn put_app(folder: &Path, m: &Manifest, text: &str, binary: &Path) -> Result<(), String> {
|
||||
fs::create_dir_all(folder).map_err(|e| format!("{}: {e}", folder.display()))?;
|
||||
fs::write(folder.join("manifest.toml"), text).map_err(|e| e.to_string())?;
|
||||
fs::copy(binary, folder.join(m.binary.as_str()))
|
||||
.map_err(|e| format!("{}: {e}", binary.display()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn install(root: &Path, manifest: &Path, binary: &Path) -> Result<(), String> {
|
||||
let (m, text) = read(manifest)?;
|
||||
put_app(&root.join("apps").join(m.id.as_str()), &m, &text, binary)?;
|
||||
// Whoever builds the image approves what its apps ask for.
|
||||
let decision = Decision {
|
||||
granted: m.requested(),
|
||||
};
|
||||
let mut out = [0u8; MAX_DECISION_TEXT];
|
||||
let n = decision.write(&mut out);
|
||||
let decisions = root.join("system/permissions");
|
||||
fs::create_dir_all(&decisions).map_err(|e| e.to_string())?;
|
||||
fs::write(decisions.join(m.id.as_str()), &out[..n]).map_err(|e| e.to_string())?;
|
||||
if decision.granted.contains(Capability::Storage) {
|
||||
fs::create_dir_all(root.join("data").join(m.id.as_str())).map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args: Vec<String> = env::args().skip(1).collect();
|
||||
let done = match args.iter().map(String::as_str).collect::<Vec<_>>().as_slice() {
|
||||
["install", root, manifest, binary] => install(Path::new(root), Path::new(manifest), Path::new(binary)),
|
||||
["stage", folder, manifest, binary] => {
|
||||
read(Path::new(manifest)).and_then(|(m, text)| put_app(Path::new(folder), &m, &text, Path::new(binary)))
|
||||
}
|
||||
_ => Err("usage: app-image install <root> <manifest> <binary> | stage <folder> <manifest> <binary>".into()),
|
||||
};
|
||||
match done {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(e) => {
|
||||
eprintln!("app-image: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
49
crates/types/src/apps.rs
Normal file
49
crates/types/src/apps.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
//! The permission manager's protocol (docs/apps.md), shared by the server
|
||||
//! and `libkos::apps`. Every request carries the client's request page (a
|
||||
//! one-page region) unless it says otherwise; text goes through it,
|
||||
//! NUL-separated, and answers come back in it.
|
||||
|
||||
/// Staged for the next `LAUNCH`, carrying the capability: what the app
|
||||
/// prints to (an endpoint that speaks the log protocol).
|
||||
pub const PASS_OUTPUT: usize = 1;
|
||||
/// Staged for the next `LAUNCH`, carrying the capability: a storage grant
|
||||
/// the app works in (the launcher's current folder).
|
||||
pub const PASS_FOLDER: usize = 2;
|
||||
/// The page holds the command and its arguments. Reply `OK` carries the
|
||||
/// process (call it to wait and kill), with the granted capabilities' bits
|
||||
/// in word 1, or an error; what was staged is used up either way.
|
||||
pub const LAUNCH: usize = 3;
|
||||
/// Carries a read-only storage grant to the folder holding the app (its
|
||||
/// `manifest.toml` and binary), instead of the page. The owner is asked.
|
||||
/// Reply `OK` with the granted capabilities' bits in word 1.
|
||||
pub const INSTALL: usize = 4;
|
||||
/// Writes one line per installed app into the page: id, command, version,
|
||||
/// then what it may use. Reply `OK` with the text's length in word 1.
|
||||
pub const LIST: usize = 5;
|
||||
/// The page holds an app id; word 1 the capability's catalog index. The
|
||||
/// owner takes it away (a running app that had it is stopped) or gives it.
|
||||
pub const REVOKE: usize = 6;
|
||||
pub const ALLOW: usize = 7;
|
||||
|
||||
/// The request page's size.
|
||||
pub const PAGE_SIZE: usize = 4096;
|
||||
|
||||
pub mod reply {
|
||||
pub const OK: usize = 0;
|
||||
pub const NOT_INSTALLED: usize = 1;
|
||||
/// Word 1: the capabilities it requires and does not get.
|
||||
pub const MISSING: usize = 2;
|
||||
pub const ALREADY_INSTALLED: usize = 3;
|
||||
/// Word 1: the required capabilities this device has no server for.
|
||||
pub const UNAVAILABLE: usize = 4;
|
||||
pub const DECLINED: usize = 5;
|
||||
/// Word 1: the manifest line at fault, 0 for none in particular.
|
||||
pub const BAD_MANIFEST: usize = 6;
|
||||
/// The app never asked for that capability.
|
||||
pub const NOT_REQUESTED: usize = 7;
|
||||
/// No room: too many apps running, or no spawn allowance left.
|
||||
pub const BUSY: usize = 8;
|
||||
pub const FAILED: usize = 9;
|
||||
/// The offered folder has no `manifest.toml`.
|
||||
pub const NO_MANIFEST: usize = 10;
|
||||
}
|
||||
@@ -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)]
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
//! A program's manifest, `/bin/<name>.manifest`: what it needs beyond the
|
||||
//! terminal and the storage every program gets. Lines of `needs = a, b`;
|
||||
//! `#` starts a comment. No manifest means nothing more.
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct Needs {
|
||||
/// Draws on the screen itself and takes input focus.
|
||||
pub display: bool,
|
||||
}
|
||||
|
||||
/// A line or need the parser does not know: the program asks for
|
||||
/// something nobody can say it may have, so it should not run.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Unknown<'a>(pub &'a [u8]);
|
||||
|
||||
pub fn parse(text: &[u8]) -> Result<Needs, Unknown<'_>> {
|
||||
let mut needs = Needs::default();
|
||||
for line in text.split(|b| *b == b'\n') {
|
||||
let line = line
|
||||
.split(|b| *b == b'#')
|
||||
.next()
|
||||
.unwrap_or(&[])
|
||||
.trim_ascii();
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let mut sides = line.splitn(2, |b| *b == b'=');
|
||||
let (key, value) = (sides.next().unwrap_or(&[]).trim_ascii(), sides.next());
|
||||
let (b"needs", Some(value)) = (key, value) else {
|
||||
return Err(Unknown(line));
|
||||
};
|
||||
for need in value
|
||||
.split(|b| *b == b',')
|
||||
.map(<[u8]>::trim_ascii)
|
||||
.filter(|n| !n.is_empty())
|
||||
{
|
||||
match need {
|
||||
b"display" => needs.display = true,
|
||||
other => return Err(Unknown(other)),
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(needs)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn needs_are_read_and_comments_skipped() {
|
||||
let text = b"# snake\nneeds = display # the screen\n\n";
|
||||
assert_eq!(parse(text), Ok(Needs { display: true }));
|
||||
assert_eq!(parse(b""), Ok(Needs::default()));
|
||||
assert_eq!(parse(b"needs = "), Ok(Needs::default()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anything_unknown_is_refused() {
|
||||
assert_eq!(parse(b"needs = display, network"), Err(Unknown(b"network")));
|
||||
assert_eq!(parse(b"wants = display"), Err(Unknown(b"wants = display")));
|
||||
assert_eq!(parse(b"display"), Err(Unknown(b"display")));
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,16 @@
|
||||
//! init to load a program into a new, not yet running process; init replies
|
||||
//! with the process capability and the client wires and starts it.
|
||||
//!
|
||||
//! Request: `[SPAWN | name_len << 8, packed limits, name bytes 0..8, 8..16]`.
|
||||
//! Request: `[SPAWN, packed limits, 0, 0]` carrying a read-only storage grant
|
||||
//! to the program file itself: init reads it through that, so it needs no
|
||||
//! path, and nothing is loaded its requester could not read.
|
||||
//! Reply: `[reply::OK, entry point, child's log slot or NONE, 0]` carrying
|
||||
//! the process capability, or `[error tag, 0, 0, 0]`.
|
||||
//! the process capability (which the client may pass on), or
|
||||
//! `[error tag, 0, 0, 0]`.
|
||||
|
||||
use crate::budget::Limits;
|
||||
|
||||
pub const SPAWN: usize = 0;
|
||||
/// The longest program file name a spawn request carries.
|
||||
pub const MAX_NAME: usize = 16;
|
||||
/// Where init puts the child's stack top (its stack ends at the image).
|
||||
pub const STACK_TOP: usize = 0x10000;
|
||||
/// A slot word meaning "none".
|
||||
@@ -18,6 +19,7 @@ pub const NONE: usize = usize::MAX;
|
||||
|
||||
pub mod reply {
|
||||
pub const OK: usize = 0;
|
||||
/// The grant names no file init can read.
|
||||
pub const NOT_FOUND: usize = 1;
|
||||
/// The limits do not fit in what is left of the client's allowance.
|
||||
pub const OVER_ALLOWANCE: usize = 2;
|
||||
@@ -44,32 +46,13 @@ fn unpack(w: usize) -> Limits {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request(name: &[u8], limits: Limits) -> Option<[usize; 4]> {
|
||||
if name.is_empty() || name.len() > MAX_NAME {
|
||||
return None;
|
||||
}
|
||||
let mut bytes = [0u8; MAX_NAME];
|
||||
bytes[..name.len()].copy_from_slice(name);
|
||||
let (lo, hi) = bytes.as_chunks::<8>().0.split_at(1);
|
||||
Some([
|
||||
SPAWN | name.len() << 8,
|
||||
pack(limits)?,
|
||||
u64::from_le_bytes(lo[0]) as usize,
|
||||
u64::from_le_bytes(hi[0]) as usize,
|
||||
])
|
||||
pub fn request(limits: Limits) -> Option<[usize; 4]> {
|
||||
Some([SPAWN, pack(limits)?, 0, 0])
|
||||
}
|
||||
|
||||
/// The name (and how much of it counts) and limits of a spawn request.
|
||||
pub fn parse(words: [usize; 4]) -> Option<([u8; MAX_NAME], usize, Limits)> {
|
||||
let len = words[0] >> 8;
|
||||
if words[0] & 0xFF != SPAWN || len == 0 || len > MAX_NAME {
|
||||
return None;
|
||||
}
|
||||
let mut name = [0u8; MAX_NAME];
|
||||
for (b, w) in name.as_chunks_mut::<8>().0.iter_mut().zip(&words[2..]) {
|
||||
*b = (*w as u64).to_le_bytes();
|
||||
}
|
||||
Some((name, len, unpack(words[1])))
|
||||
/// The limits of a spawn request.
|
||||
pub fn parse(words: [usize; 4]) -> Option<Limits> {
|
||||
(words[0] == SPAWN).then(|| unpack(words[1]))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -84,27 +67,18 @@ mod tests {
|
||||
notifications: 3,
|
||||
regions: 255,
|
||||
};
|
||||
let (name, len, back) = parse(request(b"bin/hello", l).unwrap()).unwrap();
|
||||
assert_eq!(&name[..len], b"bin/hello");
|
||||
assert_eq!(back, l);
|
||||
assert_eq!(parse(request(l).unwrap()), Some(l));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_cannot_be_said_is_refused() {
|
||||
let l = Limits::default();
|
||||
assert!(request(b"", l).is_none());
|
||||
assert!(request(&[b'a'; 17], l).is_none());
|
||||
assert!(
|
||||
request(
|
||||
b"x",
|
||||
Limits {
|
||||
request(Limits {
|
||||
endpoints: 256,
|
||||
..l
|
||||
}
|
||||
)
|
||||
..Limits::default()
|
||||
})
|
||||
.is_none()
|
||||
);
|
||||
assert!(parse([SPAWN | 17 << 8, 0, 0, 0]).is_none());
|
||||
assert!(parse([1 | 3 << 8, 0, 0, 0]).is_none());
|
||||
assert!(parse([1, 0, 0, 0]).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ const LOG_SHIFT: u32 = 8;
|
||||
const STORAGE_SHIFT: u32 = 16;
|
||||
const ARGS_BIT: usize = 1 << 24;
|
||||
const DISPLAY_SHIFT: u32 = 32;
|
||||
const DATA_SHIFT: u32 = 40;
|
||||
|
||||
/// Where the arguments page sits: the program's arguments, NUL separated,
|
||||
/// the first its name. Below the stack, which ends at the image.
|
||||
@@ -24,6 +25,8 @@ pub struct StartArg {
|
||||
pub args: bool,
|
||||
/// The display server, for a program that draws on the screen.
|
||||
pub display: Option<CapSlot>,
|
||||
/// The app's own data folder, read-write (its `storage` capability).
|
||||
pub data: Option<CapSlot>,
|
||||
}
|
||||
|
||||
fn field(arg: usize, shift: u32) -> Option<CapSlot> {
|
||||
@@ -42,6 +45,7 @@ impl StartArg {
|
||||
| put(self.storage, STORAGE_SHIFT)
|
||||
| if self.args { ARGS_BIT } else { 0 }
|
||||
| put(self.display, DISPLAY_SHIFT)
|
||||
| put(self.data, DATA_SHIFT)
|
||||
}
|
||||
|
||||
pub fn decode(arg: usize) -> Self {
|
||||
@@ -51,6 +55,7 @@ impl StartArg {
|
||||
storage: field(arg, STORAGE_SHIFT),
|
||||
args: arg & ARGS_BIT != 0,
|
||||
display: field(arg, DISPLAY_SHIFT),
|
||||
data: field(arg, DATA_SHIFT),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,6 +72,7 @@ mod tests {
|
||||
storage: CapSlot::new(5),
|
||||
args: true,
|
||||
display: CapSlot::new(7),
|
||||
data: CapSlot::new(62),
|
||||
};
|
||||
assert_eq!(StartArg::decode(a.encode()), a);
|
||||
assert_eq!(StartArg::decode(0), StartArg::default());
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
# What snake needs beyond the terminal and storage.
|
||||
needs = display
|
||||
@@ -1,11 +0,0 @@
|
||||
The shell runs /bin/<name> by asking init to load it (init alone may
|
||||
create processes), then gives the program the terminal as its output
|
||||
and a read-only storage grant to the current directory, writes its
|
||||
arguments onto a page it moves into the program, starts it and waits
|
||||
for it to die. Then it takes the grant back. Ctrl+C kills it.
|
||||
|
||||
A program that needs more says so in /bin/<name>.manifest, e.g.
|
||||
`needs = display` for snake, which then gets the screen and keyboard
|
||||
until it quits. A need the shell does not know, it refuses to run.
|
||||
|
||||
Built-ins: help, echo, clear, cd, pwd.
|
||||
10
disk/home/docs/shell.txt
Normal file
10
disk/home/docs/shell.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
The shell runs an app by asking the permission manager, passing along
|
||||
the terminal as its output and a read-only grant to the current folder:
|
||||
all the shell has to give. The permission manager adds what the app's
|
||||
manifest asks for and you allowed, and runs it. Ctrl+C stops it.
|
||||
|
||||
You decide what each app may use: `install <folder>` asks you, `apps`
|
||||
shows what each may use, `allow` and `revoke` change it (revoking from
|
||||
a running app stops it).
|
||||
|
||||
Built-ins: help, echo, clear, cd, pwd, apps, install, allow, revoke.
|
||||
9
disk/home/readme.txt
Normal file
9
disk/home/readme.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
Welcome to k-os.
|
||||
|
||||
Every program here is an app that runs with only the capabilities it
|
||||
was given: this file reached your screen because the shell handed `cat`
|
||||
the terminal and read-only access to this folder, and nothing else.
|
||||
|
||||
Try: ls cd docs cat shell.txt cat ../readme.txt
|
||||
apps snake (q quits) sleep 30, then Ctrl+C
|
||||
install inbox/counter, then counter, counter
|
||||
@@ -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
|
||||
30
docs/ipc.md
30
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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user