feat(apps): permission manager equips apps (manifest+consent+decisions in /system), launches via init w/ file grant; shell unprivileged (/home); install/apps/allow/revoke; revoke stops running app; app-image tool; counter app

This commit is contained in:
2026-10-27 22:56:44 +01:00
parent 9bc31a9bbe
commit 90e11eb2e8
51 changed files with 1766 additions and 644 deletions

View File

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