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

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

View File

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

View File

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

View File

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