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:
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 {
|
||||
endpoints: 256,
|
||||
..l
|
||||
}
|
||||
)
|
||||
request(Limits {
|
||||
endpoints: 256,
|
||||
..Limits::default()
|
||||
})
|
||||
.is_none()
|
||||
);
|
||||
assert!(parse([SPAWN | 17 << 8, 0, 0, 0]).is_none());
|
||||
assert!(parse([1 | 3 << 8, 0, 0, 0]).is_none());
|
||||
assert!(parse([1, 0, 0, 0]).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user