feat(settings): settings app: apps list + per-capability allow/revoke, about (who/role), sign out; system-only caps permissions/session (install refused); pm MANAGE_BADGE copy (list/apps/allow/revoke only), APPS op; init WHO + session pass-on; factory decisions seeded for new system apps; k-ui list_item/toggle, screens/back/scroll

This commit is contained in:
2026-10-28 13:14:35 +01:00
parent a2d4a7d085
commit 29becf297c
38 changed files with 947 additions and 85 deletions

View File

@@ -29,6 +29,8 @@ const DISPLAY: CapSlot = CapSlot::const_new(2);
const USER: CapSlot = CapSlot::const_new(3);
const SYSTEM: CapSlot = CapSlot::const_new(4);
const INIT: CapSlot = CapSlot::const_new(5);
/// Init's session, to pass on to apps granted `session`.
const SESSION: CapSlot = CapSlot::const_new(6);
const USER_BUF_VA: VirtualAddress = VirtualAddress::from_raw(0x0180_0000);
const SOURCE_BUF_VA: VirtualAddress = VirtualAddress::from_raw(0x0190_0000);
@@ -45,7 +47,8 @@ pub fn run() -> ! {
};
let store = store::DiskStore::new(user, system, SOURCE_BUF_VA);
let consent = consent::TerminalConsent { terminal: TERMINAL };
let mut srv = server::PermissionServer::new(store, consent, INIT, Some(DISPLAY), deaths);
let ports = server::Ports { init: INIT, display: Some(DISPLAY), session: Some(SESSION), endpoint: EP };
let mut srv = server::PermissionServer::new(store, consent, ports, deaths);
libkos::println!("permission: serving");
server_framework::serve(&mut srv, EP)
}

View File

@@ -46,6 +46,8 @@ pub enum Refusal {
Missing(Capabilities),
/// It never asked for that capability.
NotRequested,
/// It asks for these, which only system apps may have.
SystemOnly(Capabilities),
/// Reading or writing the store failed.
Store,
}
@@ -68,6 +70,10 @@ pub fn install<S: Store>(
if store.is_installed(&app.id) {
return Err(Refusal::AlreadyInstalled);
}
let system = app.requested().intersect(Capabilities::SYSTEM_ONLY);
if !system.is_empty() {
return Err(Refusal::SystemOnly(system));
}
let lacking = app.required.minus(available);
if !lacking.is_empty() {
return Err(Refusal::Unavailable(lacking));

View File

@@ -46,6 +46,9 @@ struct Running {
data: Option<CapSlot>,
/// Its window, closed when it dies.
window: Option<CapSlot>,
/// The management copy of this server's endpoint (`permissions`),
/// revoked when it dies.
manage: Option<CapSlot>,
}
pub struct PermissionServer {
@@ -54,6 +57,11 @@ pub struct PermissionServer {
/// Init's spawn endpoint.
init: CapSlot,
display: Option<CapSlot>,
/// Init's session (who is signed in, signing out), for apps granted
/// `session`.
session: Option<CapSlot>,
/// This server's own endpoint, which management copies are minted from.
endpoint: CapSlot,
/// Bound to the serving thread: deaths of running apps.
deaths: CapSlot,
staged: Staged,
@@ -70,6 +78,7 @@ fn refused(r: Refusal) -> [usize; 4] {
Refusal::Declined => (reply::DECLINED, 0),
Refusal::Missing(c) => (reply::MISSING, c.bits() as usize),
Refusal::NotRequested => (reply::NOT_REQUESTED, 0),
Refusal::SystemOnly(c) => (reply::SYSTEM_ONLY, c.bits() as usize),
Refusal::Store => (reply::FAILED, 0),
};
[code, detail, 0, 0]
@@ -116,14 +125,21 @@ fn parts(page: &[u8]) -> impl Iterator<Item = &str> {
}
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] }
pub fn new(store: DiskStore, consent: TerminalConsent, ports: Ports, deaths: CapSlot) -> Self {
let Ports { init, display, session, endpoint } = ports;
Self { store, consent, init, display, session, endpoint, deaths, staged: Staged::default(), running: [const { None }; MAX_RUNNING] }
}
/// What this device has a server for.
fn available(&self) -> Capabilities {
let base = Capabilities::NONE.with(Capability::Storage);
if self.display.is_some() { base.with(Capability::Display) } else { base }
let mut c = Capabilities::NONE.with(Capability::Storage).with(Capability::Permissions);
if self.display.is_some() {
c = c.with(Capability::Display);
}
if self.session.is_some() {
c = c.with(Capability::Session);
}
c
}
fn stage(&mut self, badge: usize, op: usize, cap: Option<CapSlot>) -> [usize; 4] {
@@ -185,6 +201,16 @@ impl PermissionServer {
true => self.display.and_then(|d| libkos::display::open(d, plan.app.name.as_str().as_bytes())).map(|o| o.window),
false => None,
};
// Managing others through a copy of this endpoint that can do no
// more (see `MANAGE_BADGE`).
let manage = match plan.granted.contains(Capability::Permissions) {
true => {
let perms = PermissionSet::empty().with(Permission::Execute).with(Permission::Delegate);
syscall::mint(self.endpoint, types::badge::Badge::new(wire::MANAGE_BADGE | index), perms).ok()
}
false => None,
};
let session = self.session.filter(|_| plan.granted.contains(Capability::Session));
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()),
@@ -195,6 +221,9 @@ impl PermissionServer {
display: window.and_then(|w| child.give_sending(w, call)),
args: child.set_args(args),
console: None,
// Delegate, to lend its request page with each call.
permissions: manage.and_then(|m| child.give_sending(m, call)),
session: session.and_then(|s| child.give(s, call)),
};
let watched = self.watch(child.process, index);
if !watched || !child.start_with(arg) {
@@ -203,10 +232,11 @@ impl PermissionServer {
let _ = libkos::fs::release(d);
}
close_window(window);
revoke(manage);
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, window });
self.running[index] = Some(Running { id: plan.app.id, process: child.process, granted: plan.granted, data, window, manage });
Ok((child.process, plan.granted))
}
@@ -248,6 +278,20 @@ impl PermissionServer {
if write_page(page, &text.0[..len]) { [reply::OK, len, 0, 0] } else { code(reply::FAILED) }
}
/// Every app, for a program to read (`types::apps::APPS`).
fn apps(&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, "{}\t{}\t{}\t{}\t{}", app.id, app.name.as_str(), app.required.bits(), app.optional.bits(), d.granted.bits());
}
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);
@@ -282,6 +326,7 @@ impl PermissionServer {
let _ = libkos::fs::release(d);
}
close_window(r.window);
revoke(r.manage);
syscall::drop_cap(r.process);
}
}
@@ -322,11 +367,18 @@ impl Server for PermissionServer {
}
fn handle_call(&mut self, badge: usize, words: [usize; 4], cap: Option<CapSlot>) -> ([usize; 4], Option<Give>) {
if !allowed(badge, words[0]) {
if let Some(c) = cap {
syscall::drop_cap(c);
}
return ([MALFORMED, 0, 0, 0], None);
}
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::APPS, Some(page)) => (self.apps(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) => {
@@ -349,3 +401,26 @@ fn close_window(window: Option<CapSlot>) {
syscall::drop_cap(w);
}
}
/// Whether `op` may come through the copy of this endpoint badged `badge`:
/// a management copy (`permissions`) decides, never installs or launches.
pub fn allowed(badge: usize, op: usize) -> bool {
badge & wire::MANAGE_BADGE == 0 || matches!(op, wire::LIST | wire::APPS | wire::REVOKE | wire::ALLOW)
}
fn revoke(cap: Option<CapSlot>) {
if let Some(c) = cap {
let _ = syscall::revoke(c);
syscall::drop_cap(c);
}
}
/// What the permission manager was given to reach others.
pub struct Ports {
/// Init's spawn endpoint.
pub init: CapSlot,
pub display: Option<CapSlot>,
pub session: Option<CapSlot>,
/// Its own endpoint.
pub endpoint: CapSlot,
}

View File

@@ -1 +1,15 @@
mod policy;
#[test]
fn a_management_copy_only_decides() {
use crate::server::allowed;
use types::apps::*;
let manager = MANAGE_BADGE | 3;
for op in [LIST, APPS, REVOKE, ALLOW] {
assert!(allowed(manager, op));
}
for op in [LAUNCH, INSTALL, PASS_OUTPUT, PASS_FOLDER, 99] {
assert!(!allowed(manager, op), "op {op}");
}
assert!(allowed(5, LAUNCH) && allowed(5, INSTALL), "a launcher's copy does everything");
}

View File

@@ -202,3 +202,16 @@ fn a_server_gone_since_install_stops_an_app_that_requires_it() {
let headless = Capabilities::NONE.with(STORAGE);
assert_eq!(plan_launch(&mut store, "snake", headless), Err(Refusal::Missing(set(&[DISPLAY]))));
}
#[test]
fn system_only_capabilities_are_refused_to_installed_apps() {
let mut store = MemoryStore::default();
let mut owner = Owner::says(true, &[]);
for (required, optional) in [("\"permissions\"", ""), ("", "\"session\"")] {
let src = app("kos.sneaky", "sneaky", required, optional);
let refused = install(&mut store, &mut owner, &src, device());
assert!(matches!(refused, Err(Refusal::SystemOnly(c)) if !c.is_empty()), "{refused:?}");
}
assert!(owner.asked.is_empty(), "the user is never asked to hand them over");
assert!(!store.is_installed(&id("kos.sneaky")));
}