feat(users): per-user sealed folders users/<id>/{home,apps,data,permissions}; storage SET_USER/LOCK/PURGE; init sign-in/sign-out loop (purge fresh/removed/guest, seed from factory, session start/stop); shell logout; pm user+system app shelves; terminal SIGNED_OUT; per-program stack (gatekeeper 12p); 128K load scratch

This commit is contained in:
2026-10-28 01:08:50 +01:00
parent 29a25bd8c2
commit 91f7d28834
38 changed files with 1156 additions and 378 deletions

View File

@@ -19,11 +19,12 @@ use types::address::VirtualAddress;
use types::apps::reply;
use types::bounded::CapSlot;
/// Where init's manifest put them: `uses`, then storage.
/// Where init's manifest put them: `uses`, then storage, then signing out.
const TERMINAL: CapSlot = CapSlot::const_new(0);
const PERMISSION: CapSlot = CapSlot::const_new(1);
/// The owner's files, `/home`: the shell's `/`.
/// The user's files, their `home`: the shell's `/`.
const HOME: CapSlot = CapSlot::const_new(2);
const SESSION: CapSlot = CapSlot::const_new(3);
/// Bits on the shell's notification: the program died (its death watch is
/// unbadged), or Ctrl+C was typed (the terminal's copy).
@@ -227,7 +228,7 @@ fn run(sh: &mut Shell, line: &str) {
match args.first().copied() {
None => {}
Some("help") => {
println!("built-ins: help, echo <text>, clear, cd <dir>, pwd");
println!("built-ins: help, echo <text>, clear, cd <dir>, pwd, logout");
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");
@@ -244,6 +245,19 @@ fn run(sh: &mut Shell, line: &str) {
Some("pwd") => println!("{}", sh.cwd),
Some("echo") => println!("{}", Joined(&args[1..])),
Some("clear") => libkos::terminal::clear(TERMINAL),
// Init stops this shell and everything it ran, and the lock
// screen comes back.
Some("logout") => {
libkos::terminal::clear(TERMINAL);
if syscall::send_msg(SESSION, [types::spawn::SIGN_OUT, 0, 0, 0], None).is_err() {
println!("logout: cannot sign out");
return;
}
// No prompt over the lock screen: wait to be stopped.
loop {
let _ = syscall::wait(sh.done);
}
}
Some(_) => run_program(sh, args),
}
}

View File

@@ -14,16 +14,18 @@ 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).
/// Where init put them (see init's manifest: `uses`, storage (the disk,
/// the user's home), the spawn endpoint, then control).
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);
const HOME: CapSlot = CapSlot::const_new(2);
const INIT: CapSlot = CapSlot::const_new(3);
const CONTROL: CapSlot = CapSlot::const_new(4);
/// 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);
const HOME_BUF_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x01B0_0000);
/// Read back from the disk image by scripts/test-qemu.sh.
const NOTE: &[u8] = b"written by k-os on ext4\n";
@@ -125,9 +127,9 @@ fn revoke_stops_a_running_app(died: CapSlot) -> Result<(), &'static str> {
Ok(())
}
/// The owner's area is sealed: seeded from the factory, and what is
/// written there one boot reads back the next.
fn encrypted_home(fs: &mut Storage) -> Result<(), &'static str> {
/// The user's home is sealed: seeded from the factory, and what is written
/// there one boot reads back the next. Theirs is the only folder that opens.
fn encrypted_home(fs: &mut Storage, home_fs: &mut Storage) -> Result<(), &'static str> {
let mut home = [0u8; 512];
let mut factory = [0u8; 512];
let read = |fs: &mut Storage, path: &[u8], out: &mut [u8]| -> Result<usize, &'static str> {
@@ -136,7 +138,7 @@ fn encrypted_home(fs: &mut Storage) -> Result<(), &'static str> {
let _ = fs.close(h);
Ok(n)
};
let n = read(fs, b"home/readme.txt", &mut home)?;
let n = read(home_fs, b"readme.txt", &mut home)?;
let m = read(fs, b"factory/home/readme.txt", &mut factory)?;
if home[..n] != factory[..m] {
return Err("the encrypted home does not hold the factory's readme");
@@ -144,19 +146,29 @@ fn encrypted_home(fs: &mut Storage) -> Result<(), &'static str> {
libkos::println!("spawntest: home seeded from the factory, reads back through its seal: ok");
const NOTE: &[u8] = b"sealed across a reboot";
let mut back = [0u8; 64];
match read(fs, b"home/persist.txt", &mut back) {
match read(home_fs, b"persist.txt", &mut back) {
Ok(n) if &back[..n] == NOTE => libkos::println!("spawntest: an encrypted file from the last boot reads back: ok"),
Ok(_) => return Err("an encrypted file from the last boot reads back wrong"),
Err(_) => {
let h = fs.create(b"home/persist.txt").map_err(|_| "cannot create in home")?;
let wrote = fs.write(h, NOTE) == Ok(NOTE.len());
let _ = fs.close(h);
let h = home_fs.create(b"persist.txt").map_err(|_| "cannot create in home")?;
let wrote = home_fs.write(h, NOTE) == Ok(NOTE.len());
let _ = home_fs.close(h);
if !wrote {
return Err("cannot write in home");
}
libkos::println!("spawntest: wrote an encrypted file for the next boot");
}
}
// A made-up user's: nobody may reach it, the whole disk's holder neither.
if fs.create(b"users/0123456789abcdef/home/planted").is_ok() || fs.mkdir(b"users/0123456789abcdef").is_ok() {
return Err("someone else's folder could be written");
}
let mut folders = 0;
fs.list_dir(b"users", |e| folders += usize::from(e.name() != b"." && e.name() != b"..")).map_err(|_| "cannot list the users")?;
if folders != 1 {
return Err("not exactly one user's folder");
}
libkos::println!("spawntest: only the signed-in user's folder opens: ok");
Ok(())
}
@@ -164,7 +176,8 @@ 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)?;
libkos::println!("spawntest: a file written to ext4 reads back: ok");
encrypted_home(&mut fs)?;
let mut home = Storage::attach(HOME, HOME_BUF_VA).map_err(|_| "cannot attach to home")?;
encrypted_home(&mut fs, &mut home)?;
let died = syscall::create_notification().map_err(|_| "no notification")?;
narrow_storage(&mut fs, died)?;

View File

@@ -1,15 +1,14 @@
//! The encrypted areas' starting point. The image ships starter content
//! (the owner's first files, the pre-approved decisions) in plaintext under
//! `factory/`; with a new master key (a fresh device, or one just wiped)
//! init empties the encrypted areas and seeds them from it, storage
//! sealing what is written. A device just wiped so looks factory-fresh,
//! not full of files no key opens. An area simply missing (a disk image
//! rebuilt) is seeded too.
//! A user's folder's starting point. The image ships starter content (a
//! user's first files, the pre-approved decisions) in plaintext under
//! `factory/`; each area of the signed-in user's folder that is missing (a
//! new user, a guest, a disk image rebuilt) is made and seeded from it,
//! storage sealing what is written.
use libkos::fs::Storage;
use types::storage::MAX_PATH;
const AREAS: [&str; 3] = ["home", "system", "data"];
/// Each area of a user's folder, and what it starts with.
const AREAS: [(&str, &str); 4] = [("home", "factory/home"), ("permissions", "factory/permissions"), ("data", "factory/data"), ("apps", "factory/apps")];
/// Entries handled per listing; a folder with more is listed again.
const BATCH: usize = 32;
const NAME: usize = types::storage::DIR_NAME_SIZE;
@@ -61,25 +60,6 @@ fn entries(fs: &mut Storage, dir: &Path) -> Option<([([u8; NAME], usize, bool);
Some((out, n))
}
/// Removes everything inside `dir`, keeping `dir`.
fn empty(fs: &mut Storage, dir: &Path) -> Result<(), &'static str> {
loop {
let (list, n) = entries(fs, dir).ok_or("cannot list an area to empty it")?;
if n == 0 {
return Ok(());
}
for (name, len, is_dir) in &list[..n] {
let child = dir.join(&name[..*len]).ok_or("path too long")?;
if *is_dir {
empty(fs, &child)?;
fs.rmdir(child.as_bytes()).map_err(|_| "cannot remove a folder")?;
} else {
fs.delete(child.as_bytes()).map_err(|_| "cannot remove a file")?;
}
}
}
}
/// Copies the file `from` to a new file `to`.
fn copy_file(fs: &mut Storage, from: &Path, to: &Path, buf: &mut [u8]) -> Result<(), &'static str> {
let src = fs.open(from.as_bytes()).map_err(|_| "cannot open a factory file")?;
@@ -115,23 +95,23 @@ fn copy_tree(fs: &mut Storage, from: &Path, to: &Path, buf: &mut [u8]) -> Result
Ok(())
}
/// Readies the encrypted areas once storage has the key: `fresh` when the
/// master key is new.
pub(crate) fn prepare(fs: &mut Storage, fresh: bool, buf: &mut [u8]) -> Result<(), &'static str> {
for area in AREAS {
let path = Path::new(area);
let present = fs.list_dir(path.as_bytes(), |_| {}).is_ok();
if present && !fresh {
/// Readies `folder`'s areas once storage has its key.
pub(crate) fn prepare(fs: &mut Storage, folder: types::user::UserFolder, buf: &mut [u8]) -> Result<(), &'static str> {
let name = core::str::from_utf8(folder.as_bytes()).map_err(|_| "a user folder named oddly")?;
let base = Path::new("users").join(name.as_bytes()).ok_or("path too long")?;
for dir in [Path::new("users"), base] {
if fs.list_dir(dir.as_bytes(), |_| {}).is_err() {
fs.mkdir(dir.as_bytes()).map_err(|_| "cannot make a user's folder")?;
}
}
for (area, factory) in AREAS {
let path = base.join(area.as_bytes()).ok_or("path too long")?;
if fs.list_dir(path.as_bytes(), |_| {}).is_ok() {
continue;
}
if present {
empty(fs, &path)?;
} else {
fs.mkdir(path.as_bytes()).map_err(|_| "cannot make an area")?;
}
let factory = Path::new("factory").join(area.as_bytes()).ok_or("path too long")?;
copy_tree(fs, &factory, &path, buf)?;
libkos::println!("init: {} {}", if fresh { "set up" } else { "seeded" }, area);
fs.mkdir(path.as_bytes()).map_err(|_| "cannot make an area")?;
copy_tree(fs, &Path::new(factory), &path, buf)?;
libkos::println!("init: set up {}", area);
}
Ok(())
}

View File

@@ -9,9 +9,11 @@ use types::badge::Badge;
use types::bounded::CapSlot;
use types::permission::{Permission, PermissionSet};
use types::user::UserFolder;
use crate::devices::Devices;
use crate::load::{self, LoadedProcess};
use crate::manifest::{Device, Program};
use crate::manifest::{Device, Program, Scope};
use crate::spawn::Spawner;
const USER_STACK_TOP: StackPointer = StackPointer::from_raw(0x10000);
@@ -34,6 +36,22 @@ pub(crate) struct Launcher<'a> {
/// Bound to init's thread: spawned children's deaths, a bit each.
pub(crate) deaths: CapSlot,
pub(crate) spawner: Spawner,
/// Who is signed in, and what runs for them (stopped at sign-out).
pub(crate) user: Option<crate::session::User>,
pub(crate) running: [Option<CapSlot>; MAX_SESSION],
/// What a session runs after `SESSION`: the desktop, or the tests.
pub(crate) desktop: &'static [Program],
}
/// Programs one session runs at once, at most.
pub(crate) const MAX_SESSION: usize = 4;
/// What became of starting a program.
pub(crate) struct Launched {
pub(crate) note: &'static str,
pub(crate) process: CapSlot,
/// Its endpoint, if it serves one.
pub(crate) ep: Option<CapSlot>,
}
impl<'a> Launcher<'a> {
@@ -47,25 +65,46 @@ impl<'a> Launcher<'a> {
// 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() })
Ok(Self {
storage,
scratch,
devices,
served,
next_badge: 1,
spawn_ep,
deaths,
spawner: Spawner::new(),
user: None,
running: [None; MAX_SESSION],
desktop: &[],
})
}
pub(crate) fn storage(&mut self) -> (&mut Storage, &mut [u8]) {
(self.storage, self.scratch)
}
/// The endpoint of the running server `name`.
pub(crate) fn served(&self, name: &str) -> Option<CapSlot> {
self.endpoint(name)
}
fn endpoint(&self, name: &str) -> Option<CapSlot> {
self.served.iter().flatten().find(|(n, _)| *n == name).map(|(_, ep)| *ep)
}
/// What became of the program's device, if it has one.
pub(crate) fn launch(&mut self, p: &Program) -> Result<&'static str, &'static str> {
pub(crate) fn launch(&mut self, p: &Program) -> Result<Launched, &'static str> {
let loaded =
load::load_binary(self.storage, self.scratch, p.file, p.limits).ok_or("cannot load")?;
load::load_binary(self.storage, self.scratch, p.file, p.limits, p.stack_pages).ok_or("cannot load")?;
let child = loaded.process_cap;
let ep = match p.serves {
true => {
let ep = syscall::create_endpoint().ok_or("no endpoint")?;
// Started again (a new sign-in): its clients went with the
// last one, so the endpoint is reused.
let ep = match self.endpoint(p.name) {
Some(ep) => ep,
None => syscall::create_endpoint().ok_or("no endpoint")?,
};
// Read to receive on it; init keeps the original.
let server = perms(&[Permission::Read, Permission::Execute]);
syscall::delegate(ep, child, server).ok_or("cannot give its endpoint")?;
@@ -87,8 +126,10 @@ impl<'a> Launcher<'a> {
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")?;
for scope in p.storage {
let mut path = [0u8; types::storage::MAX_PATH];
let path = self.scope_path(scope, &mut path).ok_or("a storage scope for nobody")?;
let grant = self.storage.grant(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")?;
@@ -100,6 +141,14 @@ impl<'a> Launcher<'a> {
let control = crate::devices::system_control().ok_or("no SystemControl to give")?;
syscall::delegate(control, child, perms(&[Permission::Execute])).ok_or("cannot give SystemControl")?;
}
if p.signs_out {
let badge = Badge::new(types::spawn::SESSION_BADGE);
// Delegate only to hand it over: the shell's copy cannot pass it on.
let copy = syscall::mint(self.spawn_ep, badge, perms(&[Permission::Execute, Permission::Delegate])).map_err(|_| "cannot mint")?;
let given = syscall::delegate(copy, child, perms(&[Permission::Execute]));
syscall::drop_cap(copy);
given.ok_or("cannot give signing out")?;
}
let console = libkos::start::console()
.filter(|_| p.console)
.and_then(|c| syscall::delegate(c, child, perms(&[Permission::Write])));
@@ -107,23 +156,34 @@ impl<'a> Launcher<'a> {
let mut note = "";
if let Some(ep) = ep {
if let Some(free) = self.served.iter_mut().find(|s| s.is_none()) {
let known = self.served.iter().flatten().any(|(n, _)| *n == p.name);
if let Some(free) = self.served.iter_mut().find(|s| s.is_none()).filter(|_| !known) {
*free = Some((p.name, ep));
}
for device in p.devices {
note = self.give(*device, ep);
}
if p.unlocks {
let fresh = unlock(ep)?;
crate::factory::prepare(self.storage, fresh, self.scratch)?;
note = if fresh { ", the owner is in (a new key)" } else { ", the owner is in" };
}
if console.is_some() {
hand_over_console(ep);
note = ", console handed over";
}
}
Ok(note)
Ok(Launched { note, process: child, ep })
}
/// The path on disk `scope` names: inside the user's folder if it is a
/// user's scope.
fn scope_path<'p>(&self, scope: &Scope, buf: &'p mut [u8; types::storage::MAX_PATH]) -> Option<&'p [u8]> {
if !scope.user {
return Some(scope.path);
}
let folder: UserFolder = self.user?.folder;
let mut len = 0;
for part in [&b"users/"[..], folder.as_bytes(), if scope.path.is_empty() { b"" } else { b"/" }, scope.path] {
buf.get_mut(len..len + part.len())?.copy_from_slice(part);
len += part.len();
}
Some(&buf[..len])
}
/// A badged copy of init's endpoint: the badge names the client and so
@@ -154,29 +214,6 @@ impl<'a> Launcher<'a> {
}
}
/// Waits for the gatekeeper behind `ep` to authenticate the owner, then
/// passes the master key and a fresh seed on to storage, which alone keeps
/// them. Whether the key is new.
fn unlock(ep: CapSlot) -> Result<bool, &'static str> {
use types::gatekeeper::{KEY, SEED, UNLOCK, reply};
let fresh = match syscall::request(ep, [UNLOCK, 0, 0, 0]) {
Ok([reply::OK, fresh, ..]) => fresh != 0,
_ => return Err("the gatekeeper could not unlock"),
};
for (ask, give) in [(KEY, types::storage::op::SET_KEY), (SEED, types::storage::op::SET_SEED)] {
let mut secret = syscall::request(ep, [ask, 0, 0, 0]).map_err(|_| "the gatekeeper gave no key")?;
let given = [0, 1].iter().all(|&half| {
let words = [give, half, secret[half * 2], secret[half * 2 + 1]];
syscall::request(crate::STORAGE_EP, words).is_ok_and(|r| r[0] == types::storage::reply::OK)
});
secret.fill(0);
if !given {
return Err("storage would not take the key");
}
}
Ok(fresh)
}
/// From here on init prints through the log server, like everyone else,
/// and holds no console of its own.
fn hand_over_console(log: CapSlot) {

View File

@@ -9,6 +9,7 @@ mod launch;
mod load;
mod manifest;
mod selftest;
mod session;
mod spawn;
use libkos::fs::Storage;
@@ -59,15 +60,20 @@ pub fn run() -> ! {
// The test image carries the SMP self-test; it and the tests run
// instead of the desktop, and the last one halts the machine.
let (storage, scratch) = launcher.storage();
if storage
launcher.desktop = if storage
.open(b"bin/smptest")
.is_ok_and(|h| storage.close(h).is_ok())
{
libkos::println!("init: running the SMP self-test");
selftest::run(storage, scratch);
launch_all(&mut launcher, manifest::TESTS);
manifest::TESTS
} else {
launch_all(&mut launcher, manifest::DESKTOP);
manifest::DESKTOP
};
// Nothing that touches a user's files starts before someone is in.
if let Err(why) = session::sign_in(&mut launcher) {
libkos::println!("init: STOP ({})", why);
idle();
}
libkos::println!("init: all loaded, serving spawns");
@@ -80,12 +86,7 @@ fn launch_all(launcher: &mut launch::Launcher, programs: &[manifest::Program]) {
for p in programs {
libkos::print!("init: loading {}... ", p.name);
match launcher.launch(p) {
Ok(note) => libkos::println!("ok{}", note),
// Without the owner, nothing that touches user data may start.
Err(why) if p.unlocks => {
libkos::println!("STOP ({})", why);
idle();
}
Ok(started) => libkos::println!("ok{}", started.note),
Err(why) => libkos::println!("SKIP ({})", why),
}
}

View File

@@ -8,12 +8,15 @@ use types::budget::Limits;
const PAGE_SIZE: usize = 4096;
const USER_CODE_BASE: usize = 0x10000;
const USER_STACK_PAGES: usize = 4;
/// A program's stack, below its image, unless its manifest says more.
pub(crate) const STACK_PAGES: usize = 4;
/// The most there is room for under the image (page 0 stays unmapped).
const MAX_STACK_PAGES: usize = USER_CODE_BASE / PAGE_SIZE - 1;
const CREATE_CAP: CapSlot = CapSlot::const_new(0);
/// Where a program file is read to before it is loaded (16 pages: the
/// largest program init will load).
const SCRATCH_VA: VirtualAddress = VirtualAddress::from_raw(0x30000);
const SCRATCH_PAGES: u16 = 16;
/// Where a program file is read to before it is loaded (32 pages: the
/// largest program init will load; the gatekeeper is past 16).
const SCRATCH_VA: VirtualAddress = VirtualAddress::from_raw(0x0120_0000);
const SCRATCH_PAGES: u16 = 32;
pub(crate) struct LoadedProcess {
pub(crate) process_cap: CapSlot,
@@ -35,7 +38,9 @@ pub(crate) fn load_binary(
scratch: &mut [u8],
filename: &[u8],
limits: Limits,
stack_pages: usize,
) -> Option<LoadedProcess> {
let stack_pages = stack_pages.min(MAX_STACK_PAGES);
let file_size = read_file(storage, scratch, filename)?;
let elf_data = &scratch[..file_size];
let elf_info = match elf::parse(elf_data) {
@@ -71,10 +76,10 @@ pub(crate) fn load_binary(
}
}
for i in 0..USER_STACK_PAGES {
for i in 0..stack_pages {
let page_count = PageCount::new(1)?;
let stack_va = syscall::request_pages(page_count)?;
let dst = VirtualAddress::from_raw(USER_CODE_BASE - (USER_STACK_PAGES - i) * PAGE_SIZE);
let dst = VirtualAddress::from_raw(USER_CODE_BASE - (stack_pages - i) * PAGE_SIZE);
syscall::map_page(process_cap, stack_va, dst, false);
}

View File

@@ -12,35 +12,45 @@ pub enum Device {
}
/// A storage grant: a folder (or the whole disk, `b""`) and whether it may
/// be changed.
/// be changed. With `user`, the path is inside the signed-in user's folder.
#[derive(Clone, Copy)]
pub struct Scope {
pub path: &'static [u8],
pub write: bool,
pub user: bool,
}
const fn disk(path: &'static [u8], write: bool) -> Scope {
Scope { path, write, user: false }
}
const fn users(path: &'static [u8], write: bool) -> Scope {
Scope { path, write, user: true }
}
pub struct Program {
pub name: &'static str,
pub file: &'static [u8],
pub limits: Limits,
/// Its stack, in pages.
pub stack_pages: usize,
/// Serves an endpoint, which it finds in slot 0.
pub serves: bool,
/// Handed over in this order, after it starts.
pub devices: &'static [Device],
/// Authenticates the owner: init waits for the master key before
/// starting anything that touches user data.
pub unlocks: bool,
/// 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>,
/// Storage it may reach, in the slots after `uses`.
pub storage: &'static [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.
pub spawns: Option<Limits>,
/// May halt the machine.
pub system_control: bool,
/// May sign the user out (`types::spawn::SIGN_OUT`), in the last slot.
pub signs_out: bool,
}
/// Enough for a small program: its image, stack and a little heap.
@@ -56,14 +66,15 @@ const fn server(name: &'static str, file: &'static [u8], limits: Limits) -> Prog
name,
file,
limits,
stack_pages: crate::load::STACK_PAGES,
serves: true,
devices: &[],
unlocks: false,
uses: &[],
storage: None,
storage: &[],
console: false,
spawns: None,
system_control: false,
signs_out: false,
}
}
@@ -80,46 +91,52 @@ 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) },
// Nothing that touches user data starts before the owner is in. It
// holds the screen (through the terminal), its keystore and randomness,
// and exits once it has handed over the key.
Program {
uses: &["terminal"],
devices: &[Device::Keystore, Device::Random],
unlocks: true,
// argon2's 16 MiB, its image and a little more.
..server("gatekeeper", b"bin/gatekeeper", Limits { pages: 4096 + 160, regions: 6, ..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),
];
/// Started for each sign-in, before anything that touches a user's files.
/// It holds the screen (through the terminal), its keystore and randomness,
/// and exits once it has handed over who is in and their key.
pub const GATEKEEPER: Program = Program {
uses: &["terminal"],
devices: &[Device::Keystore, Device::Random],
// A user's record is KiBs, and it handles a few at once.
stack_pages: 12,
// argon2's 16 MiB, its image and a little more.
..server("gatekeeper", b"bin/gatekeeper", Limits { pages: 4096 + 160, regions: 6, ..SMALL })
};
/// Started for each signed-in user, and stopped when they sign out. The
/// permission manager is the only one that equips apps (docs/apps.md): it
/// reaches the user's apps, their data and decisions, and the system's apps
/// (read-only), and runs apps through init.
pub const SESSION: &[Program] = &[Program {
uses: &["terminal", "display"],
storage: &[users(b"", true), disk(b"apps", false)],
// 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 })
}];
/// Run on the test image after the SMP self-test, instead of the desktop:
/// spawns through init, then reports and halts.
pub const TESTS: &[Program] = &[Program {
serves: false,
// The permission manager, to run an app the way the shell does.
uses: &["permission"],
storage: Some(Scope { path: b"", write: true }),
storage: &[disk(b"", true), users(b"home", 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. The shell
/// holds the owner's files and asks the permission manager to run apps;
/// it holds nothing to give them beyond that.
/// holds the user's files and asks the permission manager to run apps;
/// it holds nothing to give them beyond that, and may sign the user out.
pub const DESKTOP: &[Program] = &[Program {
serves: false,
uses: &["terminal", "permission"],
storage: Some(Scope { path: b"home", write: true }),
storage: &[users(b"home", true)],
signs_out: true,
..server("shell", b"bin/shell", SMALL)
}];

View File

@@ -39,7 +39,7 @@ fn start_spinner(storage: &mut Storage, scratch: &mut [u8], page: CapSlot, words
for w in words {
w.store(0, Ordering::SeqCst);
}
let spinner = crate::load::load_binary(storage, scratch, b"bin/smptest", crate::manifest::SMALL).ok_or("cannot load bin/smptest")?;
let spinner = crate::load::load_binary(storage, scratch, b"bin/smptest", crate::manifest::SMALL, crate::load::STACK_PAGES).ok_or("cannot load bin/smptest")?;
let rw = PermissionSet::empty()
.with(Permission::Read)
.with(Permission::Write)

163
crates/init/src/session.rs Normal file
View File

@@ -0,0 +1,163 @@
//! Signing in and out (see the vision's Users note). The gatekeeper says
//! who is in; init clears away what must go (everyone's files on a new
//! keystore, a removed user's, the last guest's), hands storage the user
//! and their key, readies their folder and starts their session. Signing
//! out stops everything the session ran, locks storage, and the gatekeeper
//! runs again.
use libkos::fs::Storage;
use libkos::syscall;
use types::bounded::CapSlot;
use types::gatekeeper::{KEY, NAME, REMOVED, SEED, UNLOCK, reply, unlocked};
use types::user::UserFolder;
use crate::launch::Launcher;
use crate::manifest::{GATEKEEPER, SESSION};
/// Users a keystore holds, so removed at one sign-in at most.
const MAX_REMOVED: usize = 8;
const LONGEST_NAME: usize = 16;
#[derive(Clone, Copy)]
pub(crate) struct User {
pub(crate) folder: UserFolder,
pub(crate) guest: bool,
}
/// All the gatekeeper hands over.
struct Handover {
flags: usize,
id: u64,
key: [usize; 4],
seed: [usize; 4],
name: [u8; LONGEST_NAME],
name_len: usize,
removed: [Option<u64>; MAX_REMOVED],
}
impl Drop for Handover {
fn drop(&mut self) {
self.key.fill(0);
self.seed.fill(0);
}
}
/// Waits for the gatekeeper behind `ep` to let someone in.
fn ask(ep: CapSlot) -> Result<Handover, &'static str> {
let [reply::OK, flags, id, _role] = syscall::request(ep, [UNLOCK, 0, 0, 0]).map_err(|_| "the gatekeeper is gone")? else {
return Err("the gatekeeper could not unlock");
};
let mut h = Handover { flags, id: id as u64, key: [0; 4], seed: [0; 4], name: [0; LONGEST_NAME], name_len: 0, removed: [None; MAX_REMOVED] };
h.key = syscall::request(ep, [KEY, 0, 0, 0]).map_err(|_| "the gatekeeper gave no key")?;
h.seed = syscall::request(ep, [SEED, 0, 0, 0]).map_err(|_| "the gatekeeper gave no seed")?;
let name = syscall::request(ep, [NAME, 0, 0, 0]).map_err(|_| "the gatekeeper gave no name")?;
if let Some((text, len)) = types::log::text_of(name) {
h.name_len = len.min(LONGEST_NAME);
h.name[..h.name_len].copy_from_slice(&text[..h.name_len]);
}
for slot in h.removed.iter_mut() {
match syscall::request(ep, [REMOVED, 0, 0, 0]) {
Ok([reply::OK, id, ..]) => *slot = Some(id as u64),
_ => break,
}
}
Ok(h)
}
fn user_path(folder: UserFolder, buf: &mut [u8; 32]) -> &[u8] {
buf[..6].copy_from_slice(b"users/");
let f = folder.as_bytes();
buf[6..6 + f.len()].copy_from_slice(f);
&buf[..6 + f.len()]
}
/// Removes `folder`'s files; none there is fine.
fn purge(fs: &mut Storage, folder: UserFolder) -> Result<(), &'static str> {
fs.purge(user_path(folder, &mut [0; 32])).map_err(|_| "cannot remove a user's files")
}
/// Runs the gatekeeper until someone is in and starts their session.
pub(crate) fn sign_in(l: &mut Launcher) -> Result<(), &'static str> {
libkos::print!("init: loading gatekeeper... ");
let gatekeeper = l.launch(&GATEKEEPER)?;
libkos::println!("ok{}", gatekeeper.note);
let handover = gatekeeper.ep.ok_or("the gatekeeper serves nothing").and_then(ask);
// It exits on its own once it has handed over.
syscall::drop_cap(gatekeeper.process);
let h = handover?;
let guest = h.flags & unlocked::GUEST != 0;
let folder = if guest { UserFolder::GUEST } else { UserFolder::of(h.id) };
let (fs, scratch) = l.storage();
// While storage is locked: it removes trees only then.
if h.flags & unlocked::FRESH != 0 {
fs.purge(b"users").map_err(|_| "cannot clear the users of an old keystore")?;
libkos::println!("init: a new keystore, every earlier user's files removed");
}
for id in h.removed.iter().flatten() {
purge(fs, UserFolder::of(*id))?;
libkos::println!("init: a removed user's files removed");
}
if guest {
purge(fs, UserFolder::GUEST)?;
}
let handed = fs.set_user(h.id, guest).is_ok()
&& [(false, h.key), (true, h.seed)].iter().all(|(seed, w)| (0..2).all(|half| fs.set_half(*seed, half, [w[half * 2], w[half * 2 + 1]]).is_ok()));
if !handed {
return Err("storage would not take the key");
}
crate::factory::prepare(fs, folder, scratch)?;
let name = core::str::from_utf8(&h.name[..h.name_len]).unwrap_or("someone");
libkos::println!("init: {} is in", name);
l.user = Some(User { folder, guest });
start_session(l);
Ok(())
}
/// Starts what runs for a signed-in user, keeping each process to stop
/// at sign-out.
fn start_session(l: &mut Launcher) {
let desktop = l.desktop;
for p in SESSION.iter().chain(desktop) {
libkos::print!("init: loading {}... ", p.name);
match l.launch(p) {
Ok(started) => {
libkos::println!("ok{}", started.note);
match l.running.iter_mut().find(|r| r.is_none()) {
Some(free) => *free = Some(started.process),
None => {
// Untracked, it could not be stopped at sign-out.
let _ = syscall::kill(started.process);
syscall::drop_cap(started.process);
}
}
}
Err(why) => libkos::println!("SKIP ({})", why),
}
}
}
/// Stops the session, locks storage, and lets the next one in.
pub(crate) fn sign_out(l: &mut Launcher) {
l.spawner.clear();
for process in l.running.iter_mut().filter_map(Option::take) {
let _ = syscall::kill(process);
syscall::drop_cap(process);
}
if let Some(terminal) = l.served("terminal") {
let _ = syscall::request(terminal, [types::terminal::SIGNED_OUT, 0, 0, 0]);
}
let user = l.user.take();
let (fs, _) = l.storage();
let locked = fs.lock().is_ok();
// A guest leaves nothing behind.
let cleared = !user.is_some_and(|u| u.guest) || purge(fs, UserFolder::GUEST).is_ok();
libkos::println!("init: signed out");
let next = match locked && cleared {
true => sign_in(l),
false => Err("storage would not lock"),
};
if let Err(why) = next {
libkos::println!("init: STOP ({})", why);
crate::idle();
}
}

View File

@@ -68,6 +68,17 @@ impl Spawner {
}
}
impl Spawner {
/// Sign-out: every client goes, and every child is stopped.
pub(crate) fn clear(&mut self) {
for child in self.children.iter_mut().filter_map(Option::take) {
let _ = syscall::kill(child.process);
syscall::drop_cap(child.process);
}
self.allowances = [None; MAX_CLIENTS];
}
}
/// Where init maps a spawn request's file grant while it reads it.
const PROGRAM_BUF_VA: VirtualAddress = VirtualAddress::from_raw(0x01A0_0000);
@@ -115,7 +126,7 @@ impl Launcher<'_> {
// 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();
let loaded = found.then(|| load::load_binary(&mut file, scratch, b"", r.limits, load::STACK_PAGES)).flatten();
file.detach();
let Some(loaded) = loaded else {
return refused(if found { reply::ERROR } else { reply::NOT_FOUND });
@@ -170,6 +181,12 @@ impl Server for Launcher<'_> {
}
fn handle_call(&mut self, badge: usize, words: [usize; 4], cap: Option<CapSlot>) -> ([usize; 4], Option<libkos::syscall::Give>) {
if badge == types::spawn::SESSION_BADGE {
if words[0] == types::spawn::SIGN_OUT {
crate::session::sign_out(self);
}
return ([MALFORMED, 0, 0, 0], None);
}
match (Request::decode(words), cap) {
(Some(r), Some(binary)) => {
let (reply, process) = self.spawn(badge, &r, binary);

View File

@@ -141,6 +141,30 @@ impl Storage {
self.with_path(op::RMDIR, path, reply::OK).map(|_| ())
}
/// Init only, while nobody is signed in: removes the tree at `path`
/// (in `users/`) without its key.
pub fn purge(&mut self, path: &[u8]) -> Result<()> {
self.with_path(op::PURGE, path, reply::OK).map(|_| ())
}
/// Init only: whose areas the key about to be sent opens (see
/// `types::user`).
pub fn set_user(&mut self, id: u64, guest: bool) -> Result<()> {
self.expect([op::SET_USER, id as usize, usize::from(guest), 0], reply::OK).map(|_| ())
}
/// Init only: half (`half` 0 or 1) of the master key, or with `seed` of
/// the seed for storage's randomness.
pub fn set_half(&mut self, seed: bool, half: usize, words: [usize; 2]) -> Result<()> {
let opcode = if seed { op::SET_SEED } else { op::SET_KEY };
self.expect([opcode, half, words[0], words[1]], reply::OK).map(|_| ())
}
/// Init only: the user signed out; every grant and file is gone.
pub fn lock(&mut self) -> Result<()> {
self.expect([op::LOCK, 0, 0, 0], reply::OK).map(|_| ())
}
pub fn close(&mut self, handle: FileHandle) -> Result<()> {
self.expect([op::CLOSE, handle.0 as usize, 0, 0], reply::OK).map(|_| ())
}

View File

@@ -94,16 +94,21 @@ pub struct Unlocked {
pub removed: [Option<UserId>; SLOTS],
}
// Each step keeps its records and buffers (KiBs) to itself: inlined into
// `run` together they would overflow the 16 KiB stack init gives a program.
/// The keystore's newest record, and where the next write goes.
#[inline(never)]
fn load(ks: &mut impl Keystore) -> Result<(Option<Record>, usize), Failure> {
let (mut a, mut b) = ([0u8; RECORD_SIZE], [0u8; RECORD_SIZE]);
let (mut a, mut b) = (alloc::boxed::Box::new([0u8; RECORD_SIZE]), alloc::boxed::Box::new([0u8; RECORD_SIZE]));
if !ks.read(0, &mut a) || !ks.read(1, &mut b) {
return Err(Failure::Device);
}
let copies = Copies { copies: [&a, &b] };
let copies = Copies { copies: [&a[..], &b[..]] };
Ok((copies.newest().map(|(r, _)| r), copies.next_copy()))
}
#[inline(never)]
fn store(ks: &mut impl Keystore, copy: usize, r: &Record) -> Result<(), Failure> {
if ks.write(copy, &r.encode()) { Ok(()) } else { Err(Failure::Device) }
}
@@ -117,6 +122,7 @@ fn save(ks: &mut impl Keystore, r: &Record) -> Result<(), Failure> {
/// Destroys every slot and the directory: noise over both copies, twice,
/// then blank (the device then looks never set up). Marked as wiping
/// first, so a power cut part way is finished on the next boot.
#[inline(never)]
fn wipe(ks: &mut impl Keystore, record: &Record, next: usize, rng: &mut impl Random) -> Result<(), Failure> {
let first = record.wiping(rng).ok_or(Failure::Random)?;
let second = first.wiping(rng).ok_or(Failure::Random)?;
@@ -175,6 +181,7 @@ fn ask_passcodes(prompt: &mut impl Prompt, real: &mut Secret, duress: &mut Secre
}
/// A device never set up (or wiped): its first user, an owner.
#[inline(never)]
fn setup(ks: &mut impl Keystore, rng: &mut impl Random, prompt: &mut impl Prompt, params: KdfParams, memory: &mut [Block]) -> Result<Unlocked, Failure> {
// After a wipe nothing earlier may show: it must look like a new device.
prompt.clear();
@@ -190,6 +197,7 @@ fn setup(ks: &mut impl Keystore, rng: &mut impl Random, prompt: &mut impl Prompt
}
/// What the owner may change at the lock screen, after signing in.
#[inline(never)]
fn manage(ks: &mut impl Keystore, rng: &mut impl Random, prompt: &mut impl Prompt, memory: &mut [Block], session: &Session, removed: &mut [Option<UserId>; SLOTS]) -> Result<(), Failure> {
loop {
let (Some(record), _) = load(ks)? else { return Err(Failure::Device) };

View File

@@ -22,18 +22,20 @@ 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.
/// storage (the user's folder, the system's apps), 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 USER: CapSlot = CapSlot::const_new(3);
const SYSTEM: CapSlot = CapSlot::const_new(4);
const INIT: CapSlot = CapSlot::const_new(5);
const STORAGE_BUF_VA: VirtualAddress = VirtualAddress::from_raw(0x0180_0000);
const USER_BUF_VA: VirtualAddress = VirtualAddress::from_raw(0x0180_0000);
const SOURCE_BUF_VA: VirtualAddress = VirtualAddress::from_raw(0x0190_0000);
const SYSTEM_BUF_VA: VirtualAddress = VirtualAddress::from_raw(0x01B0_0000);
pub fn run() -> ! {
let Ok(fs) = Storage::attach(STORAGE, STORAGE_BUF_VA) else {
let (Ok(user), Ok(system)) = (Storage::attach(USER, USER_BUF_VA), Storage::attach(SYSTEM, SYSTEM_BUF_VA)) else {
libkos::println!("permission: no storage, exiting");
syscall::exit();
};
@@ -41,7 +43,7 @@ pub fn run() -> ! {
libkos::println!("permission: no notification, exiting");
syscall::exit();
};
let store = store::DiskStore::new(fs, SOURCE_BUF_VA);
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);
libkos::println!("permission: serving");

View File

@@ -19,7 +19,7 @@ use types::start_arg::StartArg;
use crate::consent::{CapabilityNames, TerminalConsent, version};
use crate::policy::{self, Refusal, Store};
use crate::store::{self, DiskStore, PATH_BUF};
use crate::store::DiskStore;
/// 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 };
@@ -164,9 +164,7 @@ impl PermissionServer {
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 file = self.store.binary_grant(&plan.app).ok_or(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 {
@@ -176,7 +174,7 @@ impl PermissionServer {
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()),
true => self.store.data_grant(&plan.app.id),
false => None,
};
let staged = (badge == self.staged.badge).then_some(&self.staged);

View File

@@ -1,5 +1,7 @@
//! The `Store` port on the storage server: apps in `/apps/<id>/`, their
//! data in `/data/<id>/`, the owner's decisions in `/system/permissions/`.
//! The `Store` port on the storage server, through two grants: the
//! signed-in user's folder (their apps in `apps/<id>/`, each app's data in
//! `data/<id>/`, their decisions in `permissions/`) and, read-only, the
//! system image's apps (`/apps/<id>/`), which every user shares.
use libkos::fs::Storage;
use manifest::{AppId, Decision, MAX_DECISION_TEXT, Manifest};
@@ -10,18 +12,18 @@ use crate::policy::{MAX_MANIFEST, Store};
const APPS: &str = "apps";
const DATA: &str = "data";
const DECISIONS: &str = "system/permissions";
const DECISIONS: &str = "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`.
/// `parts` (the empty ones skipped) 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);
for p in parts.iter().filter(|p| !p.is_empty()) {
let sep = usize::from(len > 0);
let end = len + sep + p.len();
let room = buf.get_mut(len..end)?;
room[..sep].copy_from_slice(&b"/"[..sep]);
@@ -63,29 +65,69 @@ fn copy(src: &mut Storage, from: &[u8], dst: &mut Storage, to: &[u8]) -> bool {
ok
}
/// Where an app is installed: on the system image, or by the user.
#[derive(Clone, Copy, PartialEq, Eq)]
enum Shelf {
System,
User,
}
pub struct DiskStore {
fs: Storage,
/// The signed-in user's folder.
user: Storage,
/// The system image's apps, read-only.
system: 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 }
pub fn new(user: Storage, system: Storage, source_va: VirtualAddress) -> Self {
Self { user, system, source_va }
}
/// Storage itself, for grants on the app's behalf.
pub fn storage(&mut self) -> &mut Storage {
&mut self.fs
/// The storage holding `shelf`, and its apps' folder in it.
fn on(&mut self, shelf: Shelf) -> (&mut Storage, &'static str) {
match shelf {
Shelf::System => (&mut self.system, ""),
Shelf::User => (&mut self.user, APPS),
}
}
/// The ids of the installed apps (folder names in `/apps`).
/// The ids of the installed apps (folder names), the system's first.
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);
}
});
for shelf in [Shelf::System, Shelf::User] {
let (fs, apps) = self.on(shelf);
let _ = fs.list_dir(apps.as_bytes(), |e| {
if let Some(id) = core::str::from_utf8(e.name()).ok().and_then(AppId::new) {
each(id);
}
});
}
}
/// Which shelf holds `id`'s manifest; the system's wins, so a user
/// cannot stand in for a system app.
fn shelf(&mut self, id: &AppId) -> Option<Shelf> {
[Shelf::System, Shelf::User].into_iter().find(|shelf| {
let (fs, apps) = self.on(*shelf);
let mut p = [0u8; LONGEST_PATH];
path(&mut p, &[apps, id.as_str(), MANIFEST]).is_some_and(|p| fs.open(p).and_then(|h| fs.close(h)).is_ok())
})
}
/// A read-only grant to `app`'s binary, for init to load it from.
pub fn binary_grant(&mut self, app: &Manifest) -> Option<CapSlot> {
let shelf = self.shelf(&app.id)?;
let (fs, apps) = self.on(shelf);
let mut p = [0u8; LONGEST_PATH];
fs.grant(path(&mut p, &[apps, app.id.as_str(), app.binary.as_str()])?, false).ok()
}
/// A read-write grant to `id`'s own data folder, for the app.
pub fn data_grant(&mut self, id: &AppId) -> Option<CapSlot> {
let mut p = [0u8; LONGEST_PATH];
self.user.grant(path(&mut p, &[DATA, id.as_str()])?, true).ok()
}
fn with_source<T>(&self, source: CapSlot, f: impl FnOnce(&mut Storage) -> T) -> Option<T> {
@@ -108,9 +150,7 @@ impl Store for DiskStore {
}
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()
self.shelf(id).is_some()
}
fn install(&mut self, source: &CapSlot, app: &Manifest, data_folder: bool) -> bool {
@@ -125,8 +165,8 @@ impl Store for DiskStore {
};
// 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 _ = self.user.mkdir(folder);
let fs = &mut self.user;
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)
@@ -139,14 +179,16 @@ impl Store for DiskStore {
}
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())
!data_folder || path(&mut data, &[DATA, id]).is_some_and(|d| self.user.mkdir(d).is_ok() || self.user.list_dir(d, |_| {}).is_ok())
}
fn manifest(&mut self, id: &AppId) -> Option<Manifest> {
let shelf = self.shelf(id)?;
let (fs, apps) = self.on(shelf);
let mut p = [0u8; LONGEST_PATH];
let p = path(&mut p, &[APPS, id.as_str(), MANIFEST])?;
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()?;
let m = Manifest::parse(read_text(fs, p, &mut text)?).ok()?;
// A manifest in the wrong folder is no app.
(m.id == *id).then_some(m)
}
@@ -167,7 +209,7 @@ impl Store for DiskStore {
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()
Decision::parse(read_text(&mut self.user, p, &mut text)?).ok()
}
fn record(&mut self, id: &AppId, decision: &Decision) -> bool {
@@ -176,21 +218,10 @@ impl Store for DiskStore {
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);
let _ = self.user.delete(p);
let Ok(h) = self.user.create(p) else { return false };
let written = self.user.write(h, &text[..n]) == Ok(n);
let _ = self.user.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;

View File

@@ -139,4 +139,14 @@ impl FileSystem for MockFileSystem {
fn rmdir(&mut self, _path: &[u8]) -> bool {
false
}
/// Flat: a tree is the files named under it.
fn purge(&mut self, path: &[u8]) -> bool {
for slot in self.files.iter_mut() {
if slot.as_ref().is_some_and(|f| f.name.as_bytes().starts_with(path)) {
*slot = None;
}
}
true
}
}

View File

@@ -1,9 +1,12 @@
//! The encrypted areas (see the vision's Encryption note), over any file
//! system: each app's `data/<id>/`, the owner's `home/`, and `system/` are
//! kept as `filecrypt` files under their own keys, derived from the
//! master key; the rest (programs, `factory/`) is plaintext. Before the key
//! arrives the encrypted areas refuse everything, and a plaintext file in
//! one is refused too: nothing there is trusted unless it was sealed.
//! The encrypted areas (see the vision's Encryption and Users notes), over
//! any file system. Each user's folder `users/<id>/` holds their areas:
//! `home/`, `permissions/`, and per app `apps/<app>/` and `data/<app>/`,
//! each kept as `filecrypt` files under its own key, derived from that
//! user's master key. Only the signed-in user's areas open; anyone else's
//! are refused (and noise to their key anyway). The rest (programs,
//! `factory/`) is plaintext. Signed out, every area refuses everything, and
//! a plaintext file in one is refused too: nothing there is trusted unless
//! it was sealed.
use filecrypt::{CHUNK, Drbg, HEADER, Header, Key, OVERHEAD, PLAIN_CHUNK, area_key, chunk_offset, chunks, plain_len};
@@ -22,15 +25,28 @@ enum Area<'a> {
Neither,
}
fn area(path: &[u8]) -> Area<'_> {
/// Where `path` falls, and whose it is: the user folder's name.
fn area(path: &[u8]) -> (Area<'_>, Option<&[u8]>) {
let path = path.strip_prefix(b"/").unwrap_or(path);
let mut parts = path.splitn(3, |b| *b == b'/');
match (parts.next(), parts.next(), parts.next()) {
(Some(b"home" | b"system"), _, _) => Area::Sealed(&path[..path.iter().position(|b| *b == b'/').unwrap_or(path.len())]),
(Some(b"data"), Some(id), Some(rest)) if !id.is_empty() && !rest.is_empty() => Area::Sealed(&path[..5 + id.len()]),
(Some(b"data"), _, _) => Area::Neither,
_ => Area::Plain,
let mut parts = path.splitn(5, |b| *b == b'/');
let (users, user, kind, app, rest) = (parts.next(), parts.next(), parts.next(), parts.next(), parts.next());
if users != Some(b"users") {
return (Area::Plain, None);
}
// `users/` itself holds only the folders, which anyone may list.
let Some(user) = user else { return (Area::Plain, None) };
if user.is_empty() {
return (Area::Neither, None);
}
// `users/<id>/` up to and including `kind`, and the app's folder.
let upto = |n: usize| &path[..n];
let base = 6 + user.len() + 1;
let area = match (kind, app, rest) {
(Some(k @ (b"home" | b"permissions")), _, _) => Area::Sealed(upto(base + k.len())),
(Some(k @ (b"apps" | b"data")), Some(a), Some(r)) if !a.is_empty() && !r.is_empty() => Area::Sealed(upto(base + k.len() + 1 + a.len())),
_ => Area::Neither,
};
(area, Some(user))
}
/// An open encrypted file.
@@ -44,15 +60,20 @@ struct Open {
pub struct Encrypted<F: FileSystem> {
inner: F,
/// The master key and randomness, once the gatekeeper has unlocked.
/// The signed-in user's master key and folder name, and randomness.
master: Option<Key>,
user: [u8; USER_NAME],
user_len: usize,
rng: Option<Drbg>,
open: [Option<Open>; MAX_HANDLES],
}
/// A user folder's name: 16 hex digits, or `guest`.
pub const USER_NAME: usize = 16;
impl<F: FileSystem> Encrypted<F> {
pub fn new(inner: F) -> Self {
Self { inner, master: None, rng: None, open: [const { None }; MAX_HANDLES] }
Self { inner, master: None, user: [0; USER_NAME], user_len: 0, rng: None, open: [const { None }; MAX_HANDLES] }
}
pub fn inner(&self) -> &F {
@@ -68,21 +89,27 @@ impl<F: FileSystem> Encrypted<F> {
self.inner
}
/// The key of `path`'s area, if it is encrypted and the store unlocked:
/// Whether `user`'s folder is the signed-in user's.
fn signed_in(&self, user: Option<&[u8]>) -> bool {
self.master.is_some() && user == Some(&self.user[..self.user_len])
}
/// The key of `path`'s area, if it is encrypted and its user signed in:
/// `Ok(None)` for a plaintext path, `Err` for one it must refuse.
fn key_for(&self, path: &[u8]) -> Result<Option<Key>, ()> {
match area(path) {
Area::Plain => Ok(None),
Area::Neither => Err(()),
Area::Sealed(name) => self.master.as_ref().map(|m| Some(area_key(m, name))).ok_or(()),
(Area::Plain, _) => Ok(None),
(Area::Sealed(name), user) if self.signed_in(user) => Ok(self.master.as_ref().map(|m| area_key(m, name))),
_ => Err(()),
}
}
/// Whether `path` may be used as a directory (made, listed, removed).
/// Whether `path` may be used as a directory (made, listed, removed):
/// anywhere plaintext, and in the signed-in user's own folder.
fn dir_allowed(&self, path: &[u8]) -> bool {
match area(path) {
Area::Plain => true,
_ => self.master.is_some(),
(Area::Plain, _) => true,
(_, user) => self.signed_in(user),
}
}
@@ -115,13 +142,33 @@ impl<F: FileSystem> FileSystem for Encrypted<F> {
self.inner.writable()
}
fn unlock(&mut self, master: &[u8; 32], seed: &[u8; 32]) {
if self.master.is_none() {
fn unlock(&mut self, master: &[u8; 32], seed: &[u8; 32], user: &[u8]) {
if self.master.is_none() && !user.is_empty() && user.len() <= USER_NAME {
self.master = Some(Key(*master));
self.rng = Some(Drbg::new(*seed));
self.user[..user.len()].copy_from_slice(user);
self.user_len = user.len();
}
}
fn lock(&mut self) {
for (i, slot) in self.open.iter_mut().enumerate() {
if slot.take().is_some() {
self.inner.close(FileHandle::new(i as u16));
}
}
self.master = None;
self.rng = None;
self.user = [0; USER_NAME];
self.user_len = 0;
}
/// Removes a whole tree without opening anything in it: a removed
/// user's or a guest's files go though no key for them is held.
fn purge(&mut self, path: &[u8]) -> bool {
crate::filesystem::remove_tree(&mut self.inner, path)
}
fn open(&mut self, path: &[u8]) -> Option<FileHandle> {
let key = self.key_for(path).ok()?;
let h = self.inner.open(path)?;
@@ -242,8 +289,8 @@ impl<F: FileSystem> FileSystem for Encrypted<F> {
}
let n = self.inner.list_dir(path, entries)?;
// In an encrypted area a file's size is what it holds, not its disk size.
// (Listing an app's own folder, `data/<id>`, shows its sealed files.)
let sealed = area(path) != Area::Plain;
// In a user's folder, a file's size is what it holds.
let sealed = area(path).0 != Area::Plain;
for e in entries[..n].iter_mut().filter(|e| sealed && !e.is_dir) {
e.size = plain_len(e.size).unwrap_or(0);
}
@@ -265,7 +312,7 @@ impl<F: FileSystem> FileSystem for Encrypted<F> {
#[cfg(test)]
pub(crate) fn area_of(path: &[u8]) -> &'static str {
match area(path) {
match area(path).0 {
Area::Plain => "plain",
Area::Sealed(_) => "sealed",
Area::Neither => "neither",

View File

@@ -78,12 +78,55 @@ pub trait FileSystem {
fn seek(&mut self, handle: FileHandle, offset: usize) -> bool;
/// Removes the directory at `path`, which must be empty.
fn rmdir(&mut self, path: &[u8]) -> bool;
/// The master key and a fresh random seed, once the owner is in; only
/// an encrypting file system has a use for them.
fn unlock(&mut self, _master: &[u8; 32], _seed: &[u8; 32]) {}
/// The signed-in user's master key, a fresh random seed, and the name
/// of their folder; only an encrypting file system has a use for them.
fn unlock(&mut self, _master: &[u8; 32], _seed: &[u8; 32], _user: &[u8]) {}
/// The user signed out: their key is forgotten.
fn lock(&mut self) {}
/// Removes the tree at `path`, whatever is in it.
fn purge(&mut self, path: &[u8]) -> bool {
remove_tree(self, path)
}
/// Whether anything can be written at all; the server refuses
/// writing requests up front when not.
fn writable(&self) -> bool {
true
}
}
/// Removes everything under `path` and `path` itself, a directory or a
/// file; true if nothing is left there.
pub fn remove_tree<F: FileSystem + ?Sized>(fs: &mut F, path: &[u8]) -> bool {
if fs.delete(path) {
return true;
}
const BATCH: usize = 16;
const LONGEST: usize = 255;
loop {
let mut entries = [DirEntry::empty(); BATCH];
let Some(n) = fs.list_dir(path, &mut entries) else {
// Neither a file nor a directory: nothing there.
return true;
};
let children = entries[..n].iter().filter(|e| e.name.as_bytes() != b"." && e.name.as_bytes() != b"..");
let mut any = false;
for e in children {
any = true;
let mut child = [0u8; LONGEST];
let name = e.name.as_bytes();
let len = path.len() + 1 + name.len();
if len > LONGEST {
return false;
}
child[..path.len()].copy_from_slice(path);
child[path.len()] = b'/';
child[path.len() + 1..len].copy_from_slice(name);
if !remove_tree(fs, &child[..len]) {
return false;
}
}
if !any {
return fs.rmdir(path);
}
}
}

View File

@@ -66,6 +66,16 @@ impl Grants {
Some(cap)
}
/// The grants init made directly, into `out`; how many.
pub fn roots(&self, out: &mut [usize; MAX_GRANTS]) -> usize {
let mut n = 0;
for g in self.grants.iter().flatten().filter(|g| g.parent == 0) {
out[n] = g.badge;
n += 1;
}
n
}
/// Forgets `badge` and every grant narrowed from it, transitively,
/// calling `gone` with each one's badge and capability.
pub fn release(&mut self, badge: usize, mut gone: impl FnMut(usize, CapSlot)) {

View File

@@ -30,6 +30,10 @@ pub enum StorageRequest {
/// Half of the seed for randomness.
SetSeed { half: usize, words: [usize; 2] },
Rmdir { path_len: usize },
/// Whose areas the key will open.
SetUser { id: u64, guest: bool },
Lock,
Purge { path_len: usize },
/// A capability to the path (`path_len` bytes of the buffer), inside
/// the caller's scope.
Grant {
@@ -82,6 +86,9 @@ impl Encode for StorageRequest {
SetKey { half, words } => [op::SET_KEY, half, words[0], words[1]],
SetSeed { half, words } => [op::SET_SEED, half, words[0], words[1]],
Rmdir { path_len } => [op::RMDIR, path_len, 0, 0],
SetUser { id, guest } => [op::SET_USER, id as usize, usize::from(guest), 0],
Lock => [op::LOCK, 0, 0, 0],
Purge { path_len } => [op::PURGE, path_len, 0, 0],
Grant { path_len, write } => [op::GRANT, path_len, usize::from(write), 0],
Release => [op::RELEASE, 0, 0, 0],
}
@@ -106,6 +113,9 @@ impl Decode for StorageRequest {
op::SET_KEY => SetKey { half: w[1], words: [w[2], w[3]] },
op::SET_SEED => SetSeed { half: w[1], words: [w[2], w[3]] },
op::RMDIR => Rmdir { path_len: w[1] },
op::SET_USER => SetUser { id: w[1] as u64, guest: w[2] != 0 },
op::LOCK => Lock,
op::PURGE => Purge { path_len: w[1] },
op::GRANT => Grant {
path_len: w[1],
write: w[2] != 0,

View File

@@ -7,6 +7,8 @@ use crate::grants::Grants;
use crate::protocol::{DIR_ENTRY_SIZE, DIR_NAME_SIZE, MAX_PATH, StorageRequest, StorageResponse};
use crate::scope::Scope;
mod session;
const MAX_CLIENTS: usize = 8;
/// Handles at or past this are never valid (the file systems hand out
/// fewer), so ownership fits a small table.
@@ -54,6 +56,8 @@ pub struct StorageServer<F: FileSystem, H: Host> {
/// init; handed to the file system (and forgotten here) once all in.
key: [Option<[usize; 2]>; 2],
seed: [Option<[usize; 2]>; 2],
/// Whose areas they open, said first.
user: Option<types::user::UserFolder>,
unlocked: bool,
}
@@ -68,6 +72,7 @@ impl<F: FileSystem, H: Host> StorageServer<F, H> {
log: None,
key: [None; 2],
seed: [None; 2],
user: None,
unlocked: false,
}
}
@@ -91,27 +96,6 @@ impl<F: FileSystem, H: Host> StorageServer<F, H> {
}
}
/// Only the unbadged holder (init), each half only once: nobody can
/// replace the key or the seed later.
fn set_half(&mut self, badge: usize, seed: bool, half: usize, words: [usize; 2]) -> StorageResponse {
let halves = if seed { &mut self.seed } else { &mut self.key };
match (badge, self.unlocked, halves.get_mut(half)) {
(0, false, Some(slot @ None)) => *slot = Some(words),
_ => return StorageResponse::Error,
}
if let ([Some(k0), Some(k1)], [Some(s0), Some(s1)]) = (self.key, self.seed) {
let bytes = |w: [usize; 4]| -> [u8; 32] { core::array::from_fn(|i| w[i / 8].to_le_bytes()[i % 8]) };
let (mut master, mut seed) = (bytes([k0[0], k0[1], k1[0], k1[1]]), bytes([s0[0], s0[1], s1[0], s1[1]]));
self.fs.unlock(&master, &seed);
master.fill(0);
seed.fill(0);
self.key = [None; 2];
self.seed = [None; 2];
self.unlocked = true;
}
StorageResponse::Ok
}
/// Whether the encrypted areas are open: the key and seed are in.
pub fn unlocked(&self) -> bool {
self.unlocked
@@ -300,7 +284,7 @@ impl<F: FileSystem, H: Host> StorageServer<F, H> {
None => R::NotFound,
};
let response = match request {
Attach | SetLog | SetKey { .. } | SetSeed { .. } | Release => R::Error,
Attach | SetLog | SetKey { .. } | SetSeed { .. } | SetUser { .. } | Lock | Purge { .. } | Release => R::Error,
Grant { path_len, write } => {
let narrower = scope.narrow(&buf[sent(path_len)], write);
return match narrower.and_then(|s| grants.add(badge, s, |b| host.mint(b))) {
@@ -412,6 +396,9 @@ impl<F: FileSystem, H: Host> Server for StorageServer<F, H> {
Some(StorageRequest::SetLog) => (self.set_log(badge, cap).encode(), None),
Some(StorageRequest::SetKey { half, words }) => (self.set_half(badge, false, half, words).encode(), None),
Some(StorageRequest::SetSeed { half, words }) => (self.set_half(badge, true, half, words).encode(), None),
Some(StorageRequest::SetUser { id, guest }) => (self.set_user(badge, id, guest).encode(), None),
Some(StorageRequest::Lock) => (self.lock(badge).encode(), None),
Some(StorageRequest::Purge { path_len }) => (self.purge(badge, path_len).encode(), None),
// A badge storage never issued, or one it has taken back.
Some(request) => match self.grants.scope(badge) {
None => (StorageResponse::Denied.encode(), None),

View File

@@ -0,0 +1,88 @@
//! Who is signed in, as far as storage cares: init says whose areas open,
//! hands over the key and seed, and at sign-out locks everything again.
//! Every one of these is init's alone (the unbadged holder).
use types::user::UserFolder;
use super::{Host, StorageServer};
use crate::filesystem::{FileHandle, FileSystem};
use crate::grants::MAX_GRANTS;
use crate::protocol::{MAX_PATH, StorageResponse};
impl<F: FileSystem, H: Host> StorageServer<F, H> {
/// Once per sign-in, before the key.
pub(super) fn set_user(&mut self, badge: usize, id: u64, guest: bool) -> StorageResponse {
if badge != 0 || self.unlocked || self.user.is_some() {
return StorageResponse::Error;
}
self.user = Some(if guest { UserFolder::GUEST } else { UserFolder::of(id) });
StorageResponse::Ok
}
/// Each half only once per sign-in: nobody can replace the key or the
/// seed while someone is in. Without a user said first, refused.
pub(super) fn set_half(&mut self, badge: usize, seed: bool, half: usize, words: [usize; 2]) -> StorageResponse {
let halves = if seed { &mut self.seed } else { &mut self.key };
match (badge, self.unlocked, self.user, halves.get_mut(half)) {
(0, false, Some(_), Some(slot @ None)) => *slot = Some(words),
_ => return StorageResponse::Error,
}
if let ([Some(k0), Some(k1)], [Some(s0), Some(s1)], Some(user)) = (self.key, self.seed, self.user) {
let bytes = |w: [usize; 4]| -> [u8; 32] { core::array::from_fn(|i| w[i / 8].to_le_bytes()[i % 8]) };
let (mut master, mut seed) = (bytes([k0[0], k0[1], k1[0], k1[1]]), bytes([s0[0], s0[1], s1[0], s1[1]]));
self.fs.unlock(&master, &seed, user.as_bytes());
master.fill(0);
seed.fill(0);
self.key = [None; 2];
self.seed = [None; 2];
self.unlocked = true;
}
StorageResponse::Ok
}
/// Sign-out: every grant goes (and with them every app's files and
/// buffers), init's own files close, the key is forgotten.
pub(super) fn lock(&mut self, badge: usize) -> StorageResponse {
if badge != 0 {
return StorageResponse::Error;
}
let mut roots = [0usize; MAX_GRANTS];
let n = self.grants.roots(&mut roots);
for &root in &roots[..n] {
self.release(root);
}
for (raw, owner) in self.owners.iter_mut().enumerate() {
if owner.take().is_some() {
self.fs.close(FileHandle::new(raw as u16));
}
}
self.fs.lock();
self.key = [None; 2];
self.seed = [None; 2];
self.user = None;
self.unlocked = false;
StorageResponse::Ok
}
/// While nobody is in, so no one's open file can be pulled away.
pub(super) fn purge(&mut self, badge: usize, path_len: usize) -> StorageResponse {
if badge != 0 || self.unlocked || !self.fs.writable() {
return StorageResponse::Error;
}
let Some(client) = self.clients.iter().flatten().find(|c| c.badge == Some(0)) else {
return StorageResponse::NotAttached;
};
let mut path = [0u8; MAX_PATH];
let len = path_len.min(MAX_PATH).min(client.buf.len());
path[..len].copy_from_slice(&client.buf[..len]);
let path = &path[..len];
let path = path.strip_prefix(b"/").unwrap_or(path);
// `users` (everyone, on a new keystore) or what is in it.
let inside = path == b"users"
|| path.strip_prefix(b"users/").is_some_and(|rest| !rest.split(|b| *b == b'/').any(|p| p.is_empty() || p == b"." || p == b".."));
if !inside {
return StorageResponse::Denied;
}
if self.fs.purge(path) { StorageResponse::Ok } else { StorageResponse::Error }
}
}

View File

@@ -74,11 +74,25 @@ impl FileSystem for Memory {
let (path, _) = self.open.get(&h.raw())?;
Some(self.files[path.as_str()].len())
}
/// The direct children: files, and folders made or implied by a file.
fn list_dir(&mut self, path: &[u8], entries: &mut [DirEntry]) -> Option<usize> {
let prefix = std::format!("{}/", key(path));
let dir = key(path);
let prefix = std::format!("{dir}/");
if !self.dirs.contains(&dir) && !self.files.keys().any(|k| k.starts_with(&prefix)) {
return None;
}
let mut seen: Vec<String> = Vec::new();
let mut n = 0;
for (name, data) in self.files.iter().filter(|(k, _)| k.starts_with(&prefix)) {
entries[n] = DirEntry { name: FileName::from_bytes(name[prefix.len()..].as_bytes()), size: data.len(), is_dir: false };
let names = self.files.keys().map(|k| (k, true)).chain(self.dirs.iter().map(|d| (d, false)));
for (name, file) in names.filter(|(k, _)| k.starts_with(&prefix)) {
let rest = &name[prefix.len()..];
let (child, deeper) = rest.split_once('/').map_or((rest, false), |(c, _)| (c, true));
if seen.iter().any(|s| s == child) || n == entries.len() {
continue;
}
seen.push(child.to_string());
let size = if file && !deeper { self.files[name.as_str()].len() } else { 0 };
entries[n] = DirEntry { name: FileName::from_bytes(child.as_bytes()), size, is_dir: deeper || !file };
n += 1;
}
Some(n)
@@ -87,14 +101,22 @@ impl FileSystem for Memory {
self.files.remove(&key(path)).is_some()
}
fn mkdir(&mut self, path: &[u8]) -> bool {
self.dirs.push(key(path));
let k = key(path);
if !self.dirs.contains(&k) {
self.dirs.push(k);
}
true
}
/// Only an empty folder.
fn rmdir(&mut self, path: &[u8]) -> bool {
let k = key(path);
let before = self.dirs.len();
self.dirs.retain(|d| *d != k);
self.dirs.len() < before
let prefix = std::format!("{k}/");
let empty = !self.files.keys().chain(self.dirs.iter()).any(|f| f.starts_with(&prefix));
let was = self.dirs.contains(&k) || !empty;
if empty {
self.dirs.retain(|d| *d != k);
}
empty && was
}
}
@@ -102,7 +124,7 @@ const MASTER: [u8; 32] = [9; 32];
fn unlocked() -> Encrypted<Memory> {
let mut fs = Encrypted::new(Memory::default());
fs.unlock(&MASTER, &[3; 32]);
fs.unlock(&MASTER, &[3; 32], b"u1");
fs
}
@@ -129,17 +151,22 @@ fn on_disk(fs: &Encrypted<Memory>, path: &str) -> Vec<u8> {
#[test]
fn areas_are_told_by_path() {
for (p, a) in [
(&b"home/readme.txt"[..], "sealed"),
(b"/home", "sealed"),
(b"system/permissions/kos.snake", "sealed"),
(b"data/kos.notes/a", "sealed"),
(b"data/kos.notes", "neither"),
(b"data", "neither"),
(b"data/", "neither"),
(&b"users/u1/home/readme.txt"[..], "sealed"),
(b"/users/u1/home", "sealed"),
(b"users/u1/permissions/kos.snake", "sealed"),
(b"users/u1/data/kos.notes/a", "sealed"),
(b"users/u1/apps/kos.notes/notes", "sealed"),
(b"users/u1/data/kos.notes", "neither"),
(b"users/u1/apps", "neither"),
(b"users/u1/loose", "neither"),
(b"users/u1", "neither"),
(b"users//home", "neither"),
(b"users", "plain"),
(b"bin/shell", "plain"),
(b"apps/kos.cat/cat", "plain"),
(b"factory/home/readme.txt", "plain"),
(b"homework/x", "plain"),
(b"home/x", "plain"),
(b"usersx/u1/home/x", "plain"),
(b"selftest.txt", "plain"),
] {
assert_eq!(area_of(p), a, "{}", String::from_utf8_lossy(p));
@@ -150,9 +177,9 @@ fn areas_are_told_by_path() {
fn sealed_files_read_back_and_are_noise_on_disk() {
let mut fs = unlocked();
let text = b"Dear diary, nobody else may read this.";
put(&mut fs, b"home/diary.txt", text);
assert_eq!(get(&mut fs, b"home/diary.txt").unwrap(), text);
let disk = on_disk(&fs, "home/diary.txt");
put(&mut fs, b"users/u1/home/diary.txt", text);
assert_eq!(get(&mut fs, b"users/u1/home/diary.txt").unwrap(), text);
let disk = on_disk(&fs, "users/u1/home/diary.txt");
assert!(disk.starts_with(b"KOSFILE1"));
assert!(!disk.windows(5).any(|w| w == b"diary"), "the words are not on the disk");
assert_eq!(disk.len(), filecrypt::disk_len(text.len()));
@@ -170,37 +197,37 @@ fn plaintext_areas_stay_plaintext() {
fn big_files_and_writes_across_chunks_round_trip() {
let mut fs = unlocked();
let big: Vec<u8> = (0..3 * filecrypt::PLAIN_CHUNK + 123).map(|i| (i % 251) as u8).collect();
put(&mut fs, b"data/kos.notes/big", &big);
assert_eq!(get(&mut fs, b"data/kos.notes/big").unwrap(), big);
put(&mut fs, b"users/u1/data/kos.notes/big", &big);
assert_eq!(get(&mut fs, b"users/u1/data/kos.notes/big").unwrap(), big);
// Overwrite a stretch spanning two chunks in the middle.
let h = fs.open(b"data/kos.notes/big").unwrap();
let h = fs.open(b"users/u1/data/kos.notes/big").unwrap();
let at = filecrypt::PLAIN_CHUNK - 10;
assert!(fs.seek(h, at));
assert_eq!(fs.write(h, &[0xAA; 30]), Some(30));
fs.close(h);
let mut expect = big.clone();
expect[at..at + 30].fill(0xAA);
assert_eq!(get(&mut fs, b"data/kos.notes/big").unwrap(), expect);
assert_eq!(get(&mut fs, b"users/u1/data/kos.notes/big").unwrap(), expect);
// Grow exactly from a chunk boundary: the old last chunk stops being last.
let mut fs = unlocked();
let full = vec![7u8; filecrypt::PLAIN_CHUNK];
put(&mut fs, b"home/f", &full);
let h = fs.open(b"home/f").unwrap();
put(&mut fs, b"users/u1/home/f", &full);
let h = fs.open(b"users/u1/home/f").unwrap();
assert!(fs.seek(h, filecrypt::PLAIN_CHUNK));
assert_eq!(fs.write(h, b"more"), Some(4));
fs.close(h);
let mut expect = full.clone();
expect.extend_from_slice(b"more");
assert_eq!(get(&mut fs, b"home/f").unwrap(), expect);
assert_eq!(get(&mut fs, b"users/u1/home/f").unwrap(), expect);
}
#[test]
fn one_app_cannot_read_anothers_file_even_moved_into_its_folder() {
let mut fs = unlocked();
put(&mut fs, b"data/kos.notes/secret", b"notes only");
let stolen = on_disk(&fs, "data/kos.notes/secret");
fs.inner_mut_for_test().files.insert("data/kos.chat/secret".to_string(), stolen);
let h = fs.open(b"data/kos.chat/secret").expect("it looks like a sealed file");
put(&mut fs, b"users/u1/data/kos.notes/secret", b"notes only");
let stolen = on_disk(&fs, "users/u1/data/kos.notes/secret");
fs.inner_mut_for_test().files.insert("users/u1/data/kos.chat/secret".to_string(), stolen);
let h = fs.open(b"users/u1/data/kos.chat/secret").expect("it looks like a sealed file");
let mut out = [0u8; 64];
assert_eq!(fs.read(h, &mut out), None, "but it opens only under kos.notes' key");
}
@@ -208,68 +235,117 @@ fn one_app_cannot_read_anothers_file_even_moved_into_its_folder() {
#[test]
fn locked_the_areas_refuse_everything() {
let mut fs = unlocked();
put(&mut fs, b"home/x", b"y");
put(&mut fs, b"users/u1/home/x", b"y");
let mut locked = Encrypted::new(fs.into_inner());
assert!(locked.open(b"home/x").is_none());
assert!(locked.create(b"home/z").is_none());
assert!(!locked.delete(b"home/x"));
assert!(!locked.mkdir(b"data/kos.notes"));
assert!(locked.list_dir(b"home", &mut [DirEntry::empty(); 4]).is_none());
assert!(locked.open(b"users/u1/home/x").is_none());
assert!(locked.create(b"users/u1/home/z").is_none());
assert!(!locked.delete(b"users/u1/home/x"));
assert!(!locked.mkdir(b"users/u1/data/kos.notes"));
assert!(locked.list_dir(b"users/u1/home", &mut [DirEntry::empty(); 4]).is_none());
assert!(locked.open(b"bin/anything").is_none(), "(absent) but plaintext areas are not refused");
// Another master key (a wiped and re-set-up device) reads noise.
let mut other = Encrypted::new(locked.into_inner());
other.unlock(&[1; 32], &[2; 32]);
let h = other.open(b"home/x").unwrap();
other.unlock(&[1; 32], &[2; 32], b"u1");
let h = other.open(b"users/u1/home/x").unwrap();
assert_eq!(other.read(h, &mut [0u8; 8]), None);
}
#[test]
fn plaintext_planted_in_an_encrypted_area_is_refused() {
let mut fs = unlocked();
fs.inner_mut_for_test().files.insert("system/permissions/kos.evil".to_string(), b"[decision]\ngranted = [\"network\"]\n".to_vec());
assert!(fs.open(b"system/permissions/kos.evil").is_none());
fs.inner_mut_for_test().files.insert("users/u1/permissions/kos.evil".to_string(), b"[decision]\ngranted = [\"network\"]\n".to_vec());
assert!(fs.open(b"users/u1/permissions/kos.evil").is_none());
}
#[test]
fn tampering_and_truncation_are_caught() {
let mut fs = unlocked();
let data = vec![5u8; 2 * filecrypt::PLAIN_CHUNK + 50];
put(&mut fs, b"home/t", &data);
let good = on_disk(&fs, "home/t");
put(&mut fs, b"users/u1/home/t", &data);
let good = on_disk(&fs, "users/u1/home/t");
// One flipped byte.
let mut bent = good.clone();
bent[100] ^= 1;
fs.inner_mut_for_test().files.insert("home/t".to_string(), bent);
assert_eq!(get(&mut fs, b"home/t"), None);
fs.inner_mut_for_test().files.insert("users/u1/home/t".to_string(), bent);
assert_eq!(get(&mut fs, b"users/u1/home/t"), None);
// Cut at a chunk boundary: the remaining last chunk was not sealed as last.
let cut = good[..filecrypt::chunk_offset(2)].to_vec();
fs.inner_mut_for_test().files.insert("home/t".to_string(), cut);
assert_eq!(get(&mut fs, b"home/t"), None);
fs.inner_mut_for_test().files.insert("users/u1/home/t".to_string(), cut);
assert_eq!(get(&mut fs, b"users/u1/home/t"), None);
// Chunks swapped.
let (a, b) = (filecrypt::chunk_offset(0), filecrypt::chunk_offset(1));
let mut swapped = good.clone();
swapped[a..b].copy_from_slice(&good[b..b + filecrypt::CHUNK]);
swapped[b..b + filecrypt::CHUNK].copy_from_slice(&good[a..b]);
fs.inner_mut_for_test().files.insert("home/t".to_string(), swapped);
assert_eq!(get(&mut fs, b"home/t"), None);
fs.inner_mut_for_test().files.insert("users/u1/home/t".to_string(), swapped);
assert_eq!(get(&mut fs, b"users/u1/home/t"), None);
}
#[test]
fn listings_show_what_files_hold() {
let mut fs = unlocked();
put(&mut fs, b"home/a", b"12345");
put(&mut fs, b"users/u1/home/a", b"12345");
let mut entries = [DirEntry::empty(); 4];
let n = fs.list_dir(b"home", &mut entries).unwrap();
let n = fs.list_dir(b"users/u1/home", &mut entries).unwrap();
assert_eq!((n, entries[0].size), (1, 5));
}
#[test]
fn files_cannot_live_loose_in_data() {
fn files_cannot_live_loose_in_a_users_folder() {
let mut fs = unlocked();
assert!(fs.create(b"data/loose").is_none());
assert!(fs.mkdir(b"data/kos.notes"), "an app's folder is fine");
assert!(fs.create(b"users/u1/data/loose").is_none());
assert!(fs.create(b"users/u1/loose").is_none());
assert!(fs.create(b"users/loose").is_none());
assert!(fs.mkdir(b"users/u1/data/kos.notes"), "an app's folder is fine");
let mut entries = [DirEntry::empty(); 2];
put(&mut fs, b"data/kos.notes/a", b"xyz");
let n = fs.list_dir(b"data/kos.notes", &mut entries).unwrap();
put(&mut fs, b"users/u1/data/kos.notes/a", b"xyz");
let n = fs.list_dir(b"users/u1/data/kos.notes", &mut entries).unwrap();
assert_eq!((n, entries[0].size), (1, 3), "listing the folder shows what its files hold");
}
#[test]
fn only_the_signed_in_users_folder_opens() {
let mut fs = unlocked();
put(&mut fs, b"users/u1/home/mine", b"u1's");
let mut inner = fs.into_inner();
let theirs = inner.files["users/u1/home/mine"].clone();
let mut u2 = Encrypted::new(std::mem::take(&mut inner));
u2.unlock(&MASTER, &[3; 32], b"u2");
assert!(u2.open(b"users/u1/home/mine").is_none(), "not someone else's, even under the same key");
assert!(u2.create(b"users/u1/home/planted").is_none());
assert!(!u2.mkdir(b"users/u1/home/sub"));
assert!(u2.list_dir(b"users/u1/home", &mut [DirEntry::empty(); 4]).is_none());
assert!(!u2.delete(b"users/u1/home/mine"));
// Moved into u2's own home, it is u1's area key that sealed it.
u2.inner_mut_for_test().files.insert("users/u2/home/mine".to_string(), theirs);
let h = u2.open(b"users/u2/home/mine").unwrap();
assert_eq!(u2.read(h, &mut [0u8; 8]), None);
assert!(u2.list_dir(b"users", &mut [DirEntry::empty(); 4]).is_some(), "the folders themselves are no secret");
}
#[test]
fn locking_forgets_the_key_and_closes_sealed_files() {
let mut fs = unlocked();
put(&mut fs, b"users/u1/home/a", b"abc");
let h = fs.open(b"users/u1/home/a").unwrap();
fs.lock();
assert_eq!(fs.read(h, &mut [0u8; 4]), None, "the open file went with the key");
assert!(fs.open(b"users/u1/home/a").is_none());
fs.unlock(&MASTER, &[3; 32], b"u1");
assert_eq!(get(&mut fs, b"users/u1/home/a").unwrap(), b"abc", "and comes back with it");
}
#[test]
fn purging_removes_a_tree_without_its_key() {
let mut fs = unlocked();
put(&mut fs, b"users/u1/home/a", b"1");
put(&mut fs, b"users/u1/data/kos.notes/b", b"2");
assert!(fs.mkdir(b"users/u1/home/empty"));
fs.lock();
fs.inner_mut_for_test().files.insert("users/u2x/home/c".to_string(), b"3".to_vec());
assert!(fs.purge(b"users/u1"));
let left: Vec<&String> = fs.inner().files.keys().collect();
assert_eq!(left, ["users/u2x/home/c"]);
assert!(!fs.inner().dirs.iter().any(|d| d.starts_with("users/u1")), "folders too");
assert!(fs.purge(b"users/nobody"), "nothing there is nothing to remove");
}

View File

@@ -224,17 +224,19 @@ fn sealed_files_on_real_ext4_read_back_and_pass_e2fsck() {
use crate::encrypted::Encrypted;
let Some((dir, img)) = setup("sealed") else { return };
let mut fs = Encrypted::new(Ext4::mount(FileDisk(img)).expect("mount"));
fs.unlock(&[4; 32], &[5; 32]);
assert!(fs.mkdir(b"/home"));
fs.unlock(&[4; 32], &[5; 32], b"u1");
for dir in [&b"/users"[..], b"/users/u1", b"/users/u1/home"] {
assert!(fs.mkdir(dir));
}
let text: Vec<u8> = b"the quick brown fox ".iter().copied().cycle().take(3 * filecrypt::PLAIN_CHUNK + 99).collect();
let h = fs.create(b"/home/fox.txt").expect("create");
let h = fs.create(b"/users/u1/home/fox.txt").expect("create");
assert_eq!(fs.write(h, &text), Some(text.len()));
assert!(fs.seek(h, 5000));
assert_eq!(fs.write(h, b"LAZY DOG"), Some(8));
fs.close(h);
let mut expect = text.clone();
expect[5000..5008].copy_from_slice(b"LAZY DOG");
let h = fs.open(b"/home/fox.txt").expect("open");
let h = fs.open(b"/users/u1/home/fox.txt").expect("open");
let mut back = std::vec![0u8; expect.len()];
assert_eq!(fs.read(h, &mut back), Some(expect.len()));
fs.close(h);
@@ -244,3 +246,29 @@ fn sealed_files_on_real_ext4_read_back_and_pass_e2fsck() {
let (clean, report) = fsck_clean(&dir, &disk);
assert!(clean, "e2fsck: {report}");
}
#[test]
fn purging_a_users_tree_on_real_ext4_passes_e2fsck() {
use crate::encrypted::Encrypted;
let Some((dir, img)) = setup("purge") else { return };
let mut fs = Encrypted::new(Ext4::mount(FileDisk(img)).expect("mount"));
fs.unlock(&[4; 32], &[5; 32], b"u1");
for d in [&b"/users"[..], b"/users/u1", b"/users/u1/home", b"/users/u1/home/deep", b"/users/u1/data", b"/users/u1/data/kos.notes"] {
assert!(fs.mkdir(d));
}
// More files than one listing batch holds.
for i in 0..20u8 {
let name = std::format!("/users/u1/home/deep/f{i}");
let h = fs.create(name.as_bytes()).expect("create");
assert_eq!(fs.write(h, &[i; 100]), Some(100));
fs.close(h);
}
let h = fs.create(b"/users/u1/data/kos.notes/n").expect("create");
fs.close(h);
assert!(!fs.delete(b"/users/u1/home"), "a folder is not deleted as a file");
fs.lock();
assert!(fs.purge(b"/users/u1"));
assert!(fs.list_dir(b"/users", &mut [crate::filesystem::DirEntry::empty(); 4]).is_some_and(|n| n <= 2), "only . and ..");
let (clean, report) = fsck_clean(&dir, &fs.into_inner().into_device().0);
assert!(clean, "e2fsck: {report}");
}

View File

@@ -479,6 +479,11 @@ fn only_init_unlocks_and_only_once() {
let set = |s: &mut Srv, badge, req| call(s, badge, req).0;
let key = |half| StorageRequest::SetKey { half, words: [1, 2] };
let seed = |half| StorageRequest::SetSeed { half, words: [3, 4] };
let user = StorageRequest::SetUser { id: 7, guest: false };
assert_eq!(set(&mut s, 0, key(0)), StorageResponse::Error, "not before saying whose");
assert_eq!(set(&mut s, 5, user), StorageResponse::Error, "a client cannot say");
assert_eq!(set(&mut s, 0, user), StorageResponse::Ok);
assert_eq!(set(&mut s, 0, user), StorageResponse::Error, "once");
assert_eq!(set(&mut s, 5, key(0)), StorageResponse::Error, "a client cannot");
assert_eq!(set(&mut s, 0, key(0)), StorageResponse::Ok);
assert_eq!(set(&mut s, 0, key(0)), StorageResponse::Error, "nor can a half be replaced");
@@ -490,3 +495,40 @@ fn only_init_unlocks_and_only_once() {
assert!(s.unlocked());
assert_eq!(set(&mut s, 0, key(0)), StorageResponse::Error, "once unlocked, never again");
}
#[test]
fn locking_takes_every_grant_back_and_lets_the_next_user_in() {
let mut s = server();
let set = |s: &mut Srv, badge, req| call(s, badge, req).0;
let init = Client::attach(&mut s, 0);
let shell = init.narrowed(&mut s, b"", true);
let app = shell.narrowed(&mut s, b"", false);
for req in [StorageRequest::SetUser { id: 1, guest: false }, StorageRequest::SetKey { half: 0, words: [1, 2] }, StorageRequest::SetKey { half: 1, words: [1, 2] }, StorageRequest::SetSeed { half: 0, words: [1, 2] }, StorageRequest::SetSeed { half: 1, words: [1, 2] }] {
assert_eq!(set(&mut s, 0, req), StorageResponse::Ok);
}
assert!(s.unlocked());
assert_eq!(set(&mut s, shell.badge, StorageRequest::Lock), StorageResponse::Error, "only init signs out");
assert_eq!(set(&mut s, 0, StorageRequest::Lock), StorageResponse::Ok);
assert!(!s.unlocked());
let revoked: Vec<usize> = s.host().revoked.iter().map(|c| badge_of(*c)).collect();
assert_eq!(revoked, [shell.badge, app.badge]);
assert_eq!(set(&mut s, shell.badge, StorageRequest::FileSize { handle: crate::filesystem::FileHandle::new(0) }), StorageResponse::Denied);
assert_eq!(set(&mut s, 0, StorageRequest::SetUser { id: 2, guest: true }), StorageResponse::Ok, "someone else may sign in");
}
#[test]
fn only_init_purges_only_users_and_only_while_locked() {
let mut s = server();
let init = Client::attach(&mut s, 0);
let shell = init.narrowed(&mut s, b"", true);
let purge = |s: &mut Srv, badge: usize, path: &[u8]| {
init.put(s, path);
call(s, badge, StorageRequest::Purge { path_len: path.len() }).0
};
assert_eq!(purge(&mut s, shell.badge, b"users/x"), StorageResponse::Error);
assert_eq!(purge(&mut s, 0, b"bin"), StorageResponse::Denied);
assert_eq!(purge(&mut s, 0, b"users/../bin"), StorageResponse::Denied);
assert_eq!(purge(&mut s, 0, b"users/"), StorageResponse::Denied);
assert_eq!(purge(&mut s, 0, b"users/x"), StorageResponse::Ok);
assert_eq!(purge(&mut s, 0, b"users"), StorageResponse::Ok);
}

View File

@@ -103,6 +103,10 @@ impl server::Console for Screen {
fn interrupt(&mut self, notification: CapSlot) {
let _ = syscall::signal(notification);
}
fn forget(&mut self, notification: CapSlot) {
syscall::drop_cap(notification);
}
}
pub fn run() -> ! {

View File

@@ -24,6 +24,8 @@ pub trait Console {
/// Takes input focus back from whoever took it, and clears the screen.
fn take_focus(&mut self);
fn interrupt(&mut self, notification: CapSlot);
/// Lets go of a notification it was given.
fn forget(&mut self, notification: CapSlot);
}
pub const BACKSPACE: u8 = 8;
@@ -232,7 +234,7 @@ impl<C: Console> Server for TerminalServer<C> {
Done
}
fn handle_call(&mut self, _badge: usize, words: [usize; 4], cap: Option<CapSlot>) -> ([usize; 4], Option<libkos::syscall::Give>) {
fn handle_call(&mut self, badge: usize, words: [usize; 4], cap: Option<CapSlot>) -> ([usize; 4], Option<libkos::syscall::Give>) {
let reply = match words[0] {
types::terminal::READ => self.read(false),
types::terminal::READ_SECRET => self.read(true),
@@ -252,6 +254,13 @@ impl<C: Console> Server for TerminalServer<C> {
}
_ => [MALFORMED, 0, 0, 0],
},
types::terminal::SIGNED_OUT if badge == 0 => {
if let Some(n) = self.on_interrupt.take() {
self.console.forget(n);
}
[0; 4]
}
types::terminal::SIGNED_OUT => [MALFORMED, 0, 0, 0],
types::terminal::CLEAR => {
self.handle(Request::Clear);
[0; 4]

View File

@@ -20,6 +20,7 @@ struct Fake {
/// Every row drawn, with how many cells.
rows_drawn: Vec<(usize, usize)>,
interrupted: Vec<CapSlot>,
forgotten: Vec<CapSlot>,
}
impl Console for Fake {
@@ -48,6 +49,9 @@ impl Console for Fake {
fn interrupt(&mut self, notification: CapSlot) {
self.interrupted.push(notification);
}
fn forget(&mut self, notification: CapSlot) {
self.forgotten.push(notification);
}
}
fn term(keys: &[u8]) -> TerminalServer<Fake> {
@@ -186,3 +190,17 @@ fn a_secret_line_shows_stars_and_is_forgotten() {
assert_eq!(&t.grid().row(0)[..8], b"*******\x20");
assert!(!t.grid().row(0).windows(7).any(|w| w == b"hunter2"));
}
#[test]
fn only_init_lets_the_next_session_register_for_ctrl_c() {
use server_framework::MALFORMED;
let mut t = term(&[]);
assert_eq!(on_interrupt(&mut t, 7), 0);
assert_eq!(on_interrupt(&mut t, 8), MALFORMED, "an app cannot take Ctrl+C over");
let signed_out = |t: &mut TerminalServer<Fake>, badge| t.handle_call(badge, [types::terminal::SIGNED_OUT, 0, 0, 0], None).0[0];
assert_eq!(signed_out(&mut t, 1), MALFORMED, "nor say the user signed out");
assert_eq!(on_interrupt(&mut t, 8), MALFORMED);
assert_eq!(signed_out(&mut t, 0), 0);
assert_eq!(t.console().forgotten, [CapSlot::new(7).unwrap()]);
assert_eq!(on_interrupt(&mut t, 9), 0, "the next shell may");
}

View File

@@ -3,7 +3,7 @@
//!
//! app-image install <root> <manifest.toml> <binary>
//! an installed, pre-approved app: /apps/<id>/, and (under factory/,
//! which init seeds the encrypted areas from) its decision granting
//! which init seeds each user's folder from) 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>`)
@@ -37,7 +37,7 @@ fn install(root: &Path, manifest: &Path, binary: &Path) -> Result<(), String> {
};
let mut out = [0u8; MAX_DECISION_TEXT];
let n = decision.write(&mut out);
let decisions = root.join("factory/system/permissions");
let decisions = root.join("factory/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) {

View File

@@ -24,3 +24,4 @@ pub mod storage;
pub mod syscall_abi;
pub mod terminal;
pub mod time;
pub mod user;

View File

@@ -17,6 +17,12 @@ pub const STACK_TOP: usize = 0x10000;
/// A slot word meaning "none".
pub const NONE: usize = usize::MAX;
/// Sent one way (no reply: the sender is about to be stopped) through the
/// endpoint copy init gives a session's shell: the user signs out.
pub const SIGN_OUT: usize = 1;
/// The badge on that copy; spawn clients' badges are small indices.
pub const SESSION_BADGE: usize = 0x5E55;
pub mod reply {
pub const OK: usize = 0;
/// The grant names no file init can read.

View File

@@ -41,6 +41,17 @@ pub mod op {
pub const SET_SEED: usize = 14;
/// Removes the (empty) directory at the path in the buffer.
pub const RMDIR: usize = 15;
/// Whose areas the key opens (init only, before the key): word 1 the
/// user id as the gatekeeper sent it, word 2 non-zero for a guest. See
/// `types::user`.
pub const SET_USER: usize = 16;
/// The user signed out (init only): every grant is taken back, every
/// file closed, the key forgotten; a new user, key and seed may follow.
pub const LOCK: usize = 17;
/// Removes the tree at the path in the buffer, which must be in
/// `users/`, without its key (init only, while locked): a removed
/// user's or a guest's files.
pub const PURGE: usize = 18;
}
/// Reply tags (word 0).

View File

@@ -12,8 +12,11 @@ pub const READ_SECRET: usize = 14;
/// that drew on the screen itself.
pub const REDRAW: usize = 12;
/// Carries a notification the terminal signals on Ctrl+C while no line is
/// being read. Accepted once.
/// being read. Accepted once a session (see `SIGNED_OUT`).
pub const ON_INTERRUPT: usize = 13;
/// From the unbadged holder (init) only, at sign-out: forgets who
/// `ON_INTERRUPT` signals, so the next session's shell may register.
pub const SIGNED_OUT: usize = 15;
/// What Ctrl+C types.
pub const CTRL_C: u8 = 3;

45
crates/types/src/user.rs Normal file
View File

@@ -0,0 +1,45 @@
//! Where a user's files live: `users/<folder>/`, the folder named by the
//! user's id in hex (as the gatekeeper hands it over), or `guest`. Shared by
//! init and storage so the two name the same place.
/// A user folder's name.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct UserFolder {
bytes: [u8; 16],
len: usize,
}
impl UserFolder {
pub const GUEST: Self = Self {
bytes: *b"guest\0\0\0\0\0\0\0\0\0\0\0",
len: 5,
};
/// The folder of the user whose id the gatekeeper sent as `id`.
pub fn of(id: u64) -> Self {
let raw = id.to_le_bytes();
let digits = b"0123456789abcdef";
let bytes =
core::array::from_fn(|i| digits[(raw[i / 2] >> (4 * (1 - i % 2)) & 0xF) as usize]);
Self { bytes, len: 16 }
}
pub fn as_bytes(&self) -> &[u8] {
&self.bytes[..self.len]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn folders_are_hex_ids_or_guest() {
assert_eq!(
UserFolder::of(0x0123_4567_89ab_cdef).as_bytes(),
b"efcdab8967452301"
);
assert_eq!(UserFolder::GUEST.as_bytes(), b"guest");
assert_ne!(UserFolder::of(1), UserFolder::of(256));
}
}

View File

@@ -1,12 +1,12 @@
# Apps, manifests and the permission manager
How a program gets its authority. The kernel only knows capabilities; this
is the userspace layer that decides who holds which, on the owner's behalf
is the userspace layer that decides who holds which, on the user's behalf
(see the vision's Capability Management and App Manifest notes).
## Apps
Every program the owner runs is an **app**: a manifest plus a binary.
Every program a user runs is an **app**: a manifest plus a binary.
An app is identified by its **id** (`kos.snake`), never by a file name or a
process id. The id is what decisions, the app's data folder and (later) its
encryption keys hang off.
@@ -18,12 +18,12 @@ starts them from its own table, with exactly the capabilities listed there.
An app's capabilities have exactly two sources:
1. **Its manifest and the owner's consent.** Services the app declared
1. **Its manifest and the user's consent.** Services the app declared
(`display`, `storage`, …), granted only by the permission manager, only
as far as the owner approved.
as far as the user approved.
2. **Its launcher.** Whoever starts an app may pass along what it already
holds: the terminal as its output, a read-only grant to the current
folder, a file the owner picked. A launcher can only give what it has.
folder, a file the user picked. A launcher can only give what it has.
`cat` declares nothing and gets only what the shell passes it. Snake
declares `display`. Nothing gets anything "just in case".
@@ -31,22 +31,27 @@ declares `display`. Nothing gets anything "just in case".
## Disk layout
```
/home/ the owner's files; the shell's `/` (encrypted)
/apps/<id>/ an installed app: manifest.toml and its binary
/data/<id>/ the app's private folder (its `storage`) (encrypted, its own key)
/system/permissions/ the owner's decisions, one file per app id (encrypted)
/bin/ system servers and the shell (init starts these)
/factory/ plaintext starter content the encrypted areas are
seeded from with a new master key
/apps/<id>/ a system app: manifest.toml and its binary, shared by every user
/users/<user>/ one user's folder (encrypted, under their own key)
home/ their files; the shell's `/`
apps/<id>/ an app they installed
data/<id>/ an app's private folder (its `storage`, its own key)
permissions/<id> their decisions, one file per app id
/bin/ system servers and the shell (init starts these)
/factory/ plaintext starter content a user's folder is
seeded from (home/, permissions/, data/)
```
Only the permission manager reaches `/apps`, `/data` and `/system`. The
shell's storage grant is `/home`, so neither it nor anything it runs can
read another app's data or rewrite a decision.
Users each decide for themselves, even about the system's apps, and what
one installs the others do not see. An app id a system app has cannot be
installed. Only the signed-in user's folder opens. Only the permission
manager reaches their `apps/`, `data/` and `permissions/` (and `/apps`,
read-only). The shell's storage grant is their `home/`, so neither it nor
anything it runs can read an app's data or rewrite a decision.
## Manifest
`/apps/<id>/manifest.toml`, a strict subset of TOML:
`apps/<id>/manifest.toml`, a strict subset of TOML:
```toml
[app]
@@ -60,7 +65,7 @@ icon = "icon.png" # optional: a file in the app's folder
[capabilities]
required = ["display"] # cannot work without; denied = not installed
optional = [] # works without; the owner decides each
optional = [] # works without; the user decides each
[appearance]
accent = "#e91e63" # optional: checked for format here, contrast by K-UI
@@ -79,29 +84,29 @@ optional one is simply not granted.
| Capability | What the app gets |
|---|---|
| `display` | the display server: the screen, and the keyboard while in focus |
| `storage` | `/data/<id>/`, read-write (its own folder only) |
| `storage` | the user's `data/<id>/`, read-write (its own folder only) |
## Decisions
`/system/permissions/<id>`, the same TOML subset:
The user's `permissions/<id>`, the same TOML subset:
```toml
[decision]
granted = ["display"]
```
Written by the permission manager when the owner decides; apps on the
Written by the permission manager when the user decides; apps on the
system image ship with their decisions already recorded under
`factory/system/` (pre-approved by whoever built the image), and init
seeds them into the encrypted `system/` on a fresh setup. Changing one later is the owner's call, through
the permission manager.
`factory/permissions/` (pre-approved by whoever built the image), and init
seeds them into each new user's `permissions/`. Changing one later is that
user's call, through the permission manager.
## Permission manager
A system server with no hardware. Its logic (checking manifests, deciding
what an app gets, keeping decisions) is pure and generic over two ports:
- `Consent`: asks the owner. Today: a yes/no prompt on the terminal.
- `Consent`: asks the user. Today: a yes/no prompt on the terminal.
- `Store`: reads manifests and decisions, writes decisions, installs files.
Today: the storage server.
@@ -113,9 +118,9 @@ From a folder the launcher passes as a read-only grant:
1. Read and check its `manifest.toml`; refuse if the id is installed.
2. Required capabilities this device lacks: refuse.
3. Ask the owner about the required ones together (no = not installed),
3. Ask the user about the required ones together (no = not installed),
then each optional one.
4. Copy the binary and manifest to `/apps/<id>/`, create `/data/<id>/` if
4. Copy the binary and manifest to the user's `apps/<id>/`, create `data/<id>/` if
`storage` was granted, record the decision.
### Launch
@@ -136,7 +141,7 @@ permission manager:
### Revoke
The owner withdraws a capability: the decision changes and the next launch
The user withdraws a capability: the decision changes and the next launch
gets less. A running app that held it is **stopped**: all its access dies
at once. (Endpoint capabilities like `display` are copies the permission
manager cannot take back from one app alone yet; once servers issue

View File

@@ -91,22 +91,37 @@ and budget listed there. Storage, started before the log server, receives
its log endpoint from init later (`SET_LOG`, accepted only from the
unbadged holder of its endpoint).
Nothing that touches user data starts before the owner is authenticated.
Nothing that touches a user's files starts before someone has signed in.
Init starts the log, input, display and terminal servers, then the
gatekeeper (`types::gatekeeper`), handing it only the terminal, the keystore
(a small disk of its own, told from the data disk by its size) and the
virtio-rng device. Init calls `UNLOCK`, which returns once the owner is in,
then `KEY`, whose reply is the master key; init passes it to storage
(`SET_KEY`, from the unbadged holder only, each half once), and a fresh
32-byte seed (`SEED` → `SET_SEED`) for storage's randomness this boot.
Storage's encrypted areas (`home/`, `system/`, each app's `data/<id>/`;
crate `filecrypt`, layer `storage_server::encrypted`) open once both are
in. With a new key (a fresh device, or one just wiped: `UNLOCK`'s word 1)
init empties those areas and seeds them from the plaintext `factory/`;
an area that is missing is seeded too. Only then does init start the
permission manager and the desktop. If the gatekeeper cannot
unlock, init stops there. The gatekeeper exits, and its capabilities go
with it.
virtio-rng device. Init calls `UNLOCK`, which returns once someone is in:
`[OK, flags, user id, role]`, a guest (`GUEST`) or a new keystore
(`FRESH`) among the flags. Then `KEY` (their master key), `SEED` (32 fresh
random bytes for storage this sign-in), `NAME`, and `REMOVED` until `DONE`
(users the owner removed just now). The gatekeeper exits.
Each user's files are in `users/<id>/` (`types::user`: the id in hex, or
`guest`): `home/`, `permissions/`, and per app `apps/<id>/` and
`data/<id>/`, each sealed under its own key derived from that user's
master key (crate `filecrypt`, layer `storage_server::encrypted`); the rest
of the disk is plaintext. While storage is still locked, init removes what
must go (`PURGE`, init only, only in `users/`, only while locked): every
user's folder on a new keystore, a removed user's, the last guest's. Then
it tells storage who is in (`SET_USER`) and hands over the key and seed
(`SET_KEY`, `SET_SEED`, a half per call, each once a sign-in; init only).
Only the signed-in user's folder opens; anyone else's is refused. Init
seeds whatever area of theirs is missing from the plaintext `factory/`,
then starts the session: the permission manager (the user's folder, and
`apps/` read-only) and the desktop. If the gatekeeper cannot unlock, init
stops there.
Signing out (`types::spawn::SIGN_OUT`, sent one way through the copy of
init's endpoint the shell holds) stops everything the session ran (the
shell, the permission manager, every app), tells the terminal
(`SIGNED_OUT`, init only), locks storage (`LOCK`, init only: every grant
taken back, every file closed, the key forgotten), removes a guest's
files, and runs the gatekeeper again.
A program's `uses` endpoints arrive as badged copies with `Execute` and
`Delegate`: the badge (unique per copy) tells the server which client is
@@ -135,12 +150,12 @@ log endpoint is the terminal prints there with `println!`: standard output
is whatever text endpoint a program was given. `READ` (`types::terminal`)
blocks until a line is typed and returns it in chunks, the last ending in
`\n`. Ctrl+C there drops the line. Keys typed while no line is read are
kept for the next one, except Ctrl+C, which signals the notification its
first `ON_INTERRUPT` caller gave. `REDRAW` repaints the screen and takes
kept for the next one, except Ctrl+C, which signals the notification the
session's first `ON_INTERRUPT` caller (its shell) gave. `REDRAW` repaints the screen and takes
input focus back.
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
The desktop is the shell, given the terminal, the permission manager,
the user's `home/` (its `/`) and signing out. 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

View File

@@ -37,9 +37,9 @@ staged() {
fi
}
# home/, system/ and data/ are encrypted, so not made here: init seeds them
# from factory/ (plaintext) once the gatekeeper has the key.
mkdir -p "${ROOT}/bin" "${ROOT}/apps" "${ROOT}/factory/home" "${ROOT}/factory/system/permissions" "${ROOT}/factory/data"
# Each user's folder in users/ is encrypted, so not filled here: init seeds
# a new user's areas from factory/ (plaintext) once they are signed in.
mkdir -p "${ROOT}/bin" "${ROOT}/apps" "${ROOT}/users" "${ROOT}/factory/home" "${ROOT}/factory/permissions" "${ROOT}/factory/data" "${ROOT}/factory/apps"
# System servers and the shell: init starts these.
copy display-qemu display
copy input-qemu input
@@ -55,7 +55,7 @@ app ls
app sleep
app snake
staged counter inbox/counter
# The owner's first files.
# A user's first files.
cp -r disk/home/. "${ROOT}/factory/home/"
# Only the test image: its presence makes init run the self-test.
if [ -n "${SMPTEST:-}" ]; then

View File

@@ -1,9 +1,9 @@
#!/bin/bash
# Boots IMAGE headless and waits for the self-test (init's SMP checks, then
# spawntest) to report; the machine halts itself when done. Twice, on one
# fresh keystore: the first boot sets a passcode up at the gatekeeper, the
# second gets it wrong once and then right. Exit 0 only if both unlock and
# both say "SMPTEST PASS".
# fresh keystore: the first boot sets up its first user at the gatekeeper,
# the second gets the passcode wrong once and then right. Exit 0 only if
# both sign in and both say "SMPTEST PASS".
set -uo pipefail
IMAGE="${1:?usage: test-qemu.sh IMAGE}"
@@ -40,18 +40,20 @@ boot() {
return $s
}
# First boot: a new passcode (twice), no duress passcode (twice empty).
boot 1234 1234 "" ""
# First boot: the first user's name, a passcode (twice), no duress
# passcode (twice empty).
boot tester 1234 1234 "" ""
status=$?
first_ok=0
grep -aq "the owner is in" "${LOG}" && grep -aq "SMPTEST PASS" "${LOG}" && first_ok=1
echo "test-qemu: first boot: passcode set up: $([ ${first_ok} -eq 1 ] && echo ok || echo FAIL)"
# Second boot: the keystore remembers; a wrong guess, then the passcode.
boot 9999 1234
grep -aq "init: tester is in" "${LOG}" && grep -aq "SMPTEST PASS" "${LOG}" && first_ok=1
echo "test-qemu: first boot: first user set up: $([ ${first_ok} -eq 1 ] && echo ok || echo FAIL)"
# Second boot: the keystore remembers; a wrong guess, then the passcode,
# then on past the menu.
boot tester 9999 tester 1234 ""
[ ${status} -eq 0 ] && status=$?
second_ok=0
grep -aq "the owner is in" "${LOG}" && grep -aq "from the last boot reads back: ok" "${LOG}" && second_ok=1
echo "test-qemu: second boot: wrong passcode refused, right one unlocks, sealed file kept: $([ ${second_ok} -eq 1 ] && echo ok || echo FAIL)"
grep -aq "init: tester is in" "${LOG}" && grep -aq "from the last boot reads back: ok" "${LOG}" && second_ok=1
echo "test-qemu: second boot: wrong passcode refused, right one signs in, sealed file kept: $([ ${second_ok} -eq 1 ] && echo ok || echo FAIL)"
rm -f "${KEYS}" "${MON}"
grep -a "smptest:\|spawntest:\|Hello\|SMPTEST" "${LOG}"
@@ -62,10 +64,18 @@ if command -v e2fsck >/dev/null && command -v debugfs >/dev/null; then
echo "test-qemu: e2fsck rejects the disk:"; cat "${LOG}.fsck"; disk_ok=0
elif [ "$(debugfs -R 'cat /selftest.txt' "${IMAGE}" 2>/dev/null)" != "written by k-os on ext4" ]; then
echo "test-qemu: /selftest.txt is not on the disk"; disk_ok=0
elif debugfs -R 'cat /home/readme.txt' "${IMAGE}" 2>/dev/null | grep -aq "Welcome"; then
echo "test-qemu: /home/readme.txt is plaintext on the disk"; disk_ok=0
elif ! debugfs -R 'cat /home/readme.txt' "${IMAGE}" 2>/dev/null | head -c 8 | grep -aq "KOSFILE1"; then
echo "test-qemu: /home/readme.txt is not a sealed file"; disk_ok=0
fi
# The one user's folder: users/<16 hex digits>.
user="$(debugfs -R 'ls /users' "${IMAGE}" 2>/dev/null | grep -aoE '[0-9a-f]{16}' | head -1)"
readme="/users/${user}/home/readme.txt"
if [ "${disk_ok}" -eq 0 ]; then
:
elif [ -z "${user}" ]; then
echo "test-qemu: no user's folder on the disk"; disk_ok=0
elif debugfs -R "cat ${readme}" "${IMAGE}" 2>/dev/null | grep -aq "Welcome"; then
echo "test-qemu: ${readme} is plaintext on the disk"; disk_ok=0
elif ! debugfs -R "cat ${readme}" "${IMAGE}" 2>/dev/null | head -c 8 | grep -aq "KOSFILE1"; then
echo "test-qemu: ${readme} is not a sealed file"; disk_ok=0
else
echo "test-qemu: e2fsck clean, file on disk, home sealed"
fi