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::apps::reply;
use types::bounded::CapSlot; 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 TERMINAL: CapSlot = CapSlot::const_new(0);
const PERMISSION: CapSlot = CapSlot::const_new(1); 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 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 /// Bits on the shell's notification: the program died (its death watch is
/// unbadged), or Ctrl+C was typed (the terminal's copy). /// 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() { match args.first().copied() {
None => {} None => {}
Some("help") => { 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!("apps: apps, install <folder>, allow|revoke <app id> <capability>");
println!("anything else runs an app: try ls, cat readme.txt, snake"); println!("anything else runs an app: try ls, cat readme.txt, snake");
println!("apps see the current directory, read-only, and only what you allowed"); println!("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("pwd") => println!("{}", sh.cwd),
Some("echo") => println!("{}", Joined(&args[1..])), Some("echo") => println!("{}", Joined(&args[1..])),
Some("clear") => libkos::terminal::clear(TERMINAL), 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), Some(_) => run_program(sh, args),
} }
} }

View File

@@ -14,16 +14,18 @@ use libkos::syscall;
use types::bounded::CapSlot; use types::bounded::CapSlot;
use types::budget::Limits; use types::budget::Limits;
/// Where init put them (see init's manifest: `uses`, the spawn endpoint, /// Where init put them (see init's manifest: `uses`, storage (the disk,
/// then control). /// the user's home), the spawn endpoint, then control).
const PERMISSION: CapSlot = CapSlot::const_new(0); const PERMISSION: CapSlot = CapSlot::const_new(0);
const STORAGE: CapSlot = CapSlot::const_new(1); const STORAGE: CapSlot = CapSlot::const_new(1);
const INIT: CapSlot = CapSlot::const_new(2); const HOME: CapSlot = CapSlot::const_new(2);
const CONTROL: CapSlot = CapSlot::const_new(3); 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. /// 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 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 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 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. /// Read back from the disk image by scripts/test-qemu.sh.
const NOTE: &[u8] = b"written by k-os on ext4\n"; 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(()) Ok(())
} }
/// The owner's area is sealed: seeded from the factory, and what is /// The user's home is sealed: seeded from the factory, and what is written
/// written there one boot reads back the next. /// there one boot reads back the next. Theirs is the only folder that opens.
fn encrypted_home(fs: &mut Storage) -> Result<(), &'static str> { fn encrypted_home(fs: &mut Storage, home_fs: &mut Storage) -> Result<(), &'static str> {
let mut home = [0u8; 512]; let mut home = [0u8; 512];
let mut factory = [0u8; 512]; let mut factory = [0u8; 512];
let read = |fs: &mut Storage, path: &[u8], out: &mut [u8]| -> Result<usize, &'static str> { 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); let _ = fs.close(h);
Ok(n) 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)?; let m = read(fs, b"factory/home/readme.txt", &mut factory)?;
if home[..n] != factory[..m] { if home[..n] != factory[..m] {
return Err("the encrypted home does not hold the factory's readme"); 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"); libkos::println!("spawntest: home seeded from the factory, reads back through its seal: ok");
const NOTE: &[u8] = b"sealed across a reboot"; const NOTE: &[u8] = b"sealed across a reboot";
let mut back = [0u8; 64]; 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(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"), Ok(_) => return Err("an encrypted file from the last boot reads back wrong"),
Err(_) => { Err(_) => {
let h = fs.create(b"home/persist.txt").map_err(|_| "cannot create in home")?; let h = home_fs.create(b"persist.txt").map_err(|_| "cannot create in home")?;
let wrote = fs.write(h, NOTE) == Ok(NOTE.len()); let wrote = home_fs.write(h, NOTE) == Ok(NOTE.len());
let _ = fs.close(h); let _ = home_fs.close(h);
if !wrote { if !wrote {
return Err("cannot write in home"); return Err("cannot write in home");
} }
libkos::println!("spawntest: wrote an encrypted file for the next boot"); 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(()) 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")?; let mut fs = Storage::attach(STORAGE, STORAGE_BUF_VA).map_err(|_| "cannot attach to storage")?;
write_a_file(&mut fs)?; write_a_file(&mut fs)?;
libkos::println!("spawntest: a file written to ext4 reads back: ok"); 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")?; let died = syscall::create_notification().map_err(|_| "no notification")?;
narrow_storage(&mut fs, died)?; narrow_storage(&mut fs, died)?;

View File

@@ -1,15 +1,14 @@
//! The encrypted areas' starting point. The image ships starter content //! A user's folder's starting point. The image ships starter content (a
//! (the owner's first files, the pre-approved decisions) in plaintext under //! user's first files, the pre-approved decisions) in plaintext under
//! `factory/`; with a new master key (a fresh device, or one just wiped) //! `factory/`; each area of the signed-in user's folder that is missing (a
//! init empties the encrypted areas and seeds them from it, storage //! new user, a guest, a disk image rebuilt) is made and seeded from it,
//! sealing what is written. A device just wiped so looks factory-fresh, //! storage sealing what is written.
//! not full of files no key opens. An area simply missing (a disk image
//! rebuilt) is seeded too.
use libkos::fs::Storage; use libkos::fs::Storage;
use types::storage::MAX_PATH; 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. /// Entries handled per listing; a folder with more is listed again.
const BATCH: usize = 32; const BATCH: usize = 32;
const NAME: usize = types::storage::DIR_NAME_SIZE; 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)) 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`. /// 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> { 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")?; 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(()) Ok(())
} }
/// Readies the encrypted areas once storage has the key: `fresh` when the /// Readies `folder`'s areas once storage has its key.
/// master key is new. pub(crate) fn prepare(fs: &mut Storage, folder: types::user::UserFolder, buf: &mut [u8]) -> Result<(), &'static str> {
pub(crate) fn prepare(fs: &mut Storage, fresh: bool, buf: &mut [u8]) -> Result<(), &'static str> { let name = core::str::from_utf8(folder.as_bytes()).map_err(|_| "a user folder named oddly")?;
for area in AREAS { let base = Path::new("users").join(name.as_bytes()).ok_or("path too long")?;
let path = Path::new(area); for dir in [Path::new("users"), base] {
let present = fs.list_dir(path.as_bytes(), |_| {}).is_ok(); if fs.list_dir(dir.as_bytes(), |_| {}).is_err() {
if present && !fresh { 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; continue;
} }
if present { fs.mkdir(path.as_bytes()).map_err(|_| "cannot make an area")?;
empty(fs, &path)?; copy_tree(fs, &Path::new(factory), &path, buf)?;
} else { libkos::println!("init: set up {}", area);
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);
} }
Ok(()) Ok(())
} }

View File

@@ -9,9 +9,11 @@ use types::badge::Badge;
use types::bounded::CapSlot; use types::bounded::CapSlot;
use types::permission::{Permission, PermissionSet}; use types::permission::{Permission, PermissionSet};
use types::user::UserFolder;
use crate::devices::Devices; use crate::devices::Devices;
use crate::load::{self, LoadedProcess}; use crate::load::{self, LoadedProcess};
use crate::manifest::{Device, Program}; use crate::manifest::{Device, Program, Scope};
use crate::spawn::Spawner; use crate::spawn::Spawner;
const USER_STACK_TOP: StackPointer = StackPointer::from_raw(0x10000); 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. /// Bound to init's thread: spawned children's deaths, a bit each.
pub(crate) deaths: CapSlot, pub(crate) deaths: CapSlot,
pub(crate) spawner: Spawner, 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> { impl<'a> Launcher<'a> {
@@ -47,25 +65,46 @@ impl<'a> Launcher<'a> {
// Storage, started by boot, is not among them: it is reached // Storage, started by boot, is not among them: it is reached
// through grants (a program's `storage` scope). // through grants (a program's `storage` scope).
let served = [None; MAX_SERVED]; 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]) { pub(crate) fn storage(&mut self) -> (&mut Storage, &mut [u8]) {
(self.storage, self.scratch) (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> { fn endpoint(&self, name: &str) -> Option<CapSlot> {
self.served.iter().flatten().find(|(n, _)| *n == name).map(|(_, ep)| *ep) 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<Launched, &'static str> {
pub(crate) fn launch(&mut self, p: &Program) -> Result<&'static str, &'static str> {
let loaded = 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 child = loaded.process_cap;
let ep = match p.serves { let ep = match p.serves {
true => { 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. // Read to receive on it; init keeps the original.
let server = perms(&[Permission::Read, Permission::Execute]); let server = perms(&[Permission::Read, Permission::Execute]);
syscall::delegate(ep, child, server).ok_or("cannot give its endpoint")?; 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")?; given.ok_or("cannot give an endpoint")?;
} }
// Storage issues its own badges, each naming a scope. // Storage issues its own badges, each naming a scope.
if let Some(scope) = p.storage { for scope in p.storage {
let grant = self.storage.grant(scope.path, scope.write).map_err(|_| "storage refused a grant")?; 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])); let given = syscall::delegate(grant, child, perms(&[Permission::Execute, Permission::Delegate]));
syscall::drop_cap(grant); syscall::drop_cap(grant);
given.ok_or("cannot give storage")?; 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")?; let control = crate::devices::system_control().ok_or("no SystemControl to give")?;
syscall::delegate(control, child, perms(&[Permission::Execute])).ok_or("cannot give SystemControl")?; 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() let console = libkos::start::console()
.filter(|_| p.console) .filter(|_| p.console)
.and_then(|c| syscall::delegate(c, child, perms(&[Permission::Write]))); .and_then(|c| syscall::delegate(c, child, perms(&[Permission::Write])));
@@ -107,23 +156,34 @@ impl<'a> Launcher<'a> {
let mut note = ""; let mut note = "";
if let Some(ep) = ep { 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)); *free = Some((p.name, ep));
} }
for device in p.devices { for device in p.devices {
note = self.give(*device, ep); 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() { if console.is_some() {
hand_over_console(ep); hand_over_console(ep);
note = ", console handed over"; 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 /// 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, /// From here on init prints through the log server, like everyone else,
/// and holds no console of its own. /// and holds no console of its own.
fn hand_over_console(log: CapSlot) { fn hand_over_console(log: CapSlot) {

View File

@@ -9,6 +9,7 @@ mod launch;
mod load; mod load;
mod manifest; mod manifest;
mod selftest; mod selftest;
mod session;
mod spawn; mod spawn;
use libkos::fs::Storage; use libkos::fs::Storage;
@@ -59,15 +60,20 @@ pub fn run() -> ! {
// The test image carries the SMP self-test; it and the tests 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. // instead of the desktop, and the last one halts the machine.
let (storage, scratch) = launcher.storage(); let (storage, scratch) = launcher.storage();
if storage launcher.desktop = if storage
.open(b"bin/smptest") .open(b"bin/smptest")
.is_ok_and(|h| storage.close(h).is_ok()) .is_ok_and(|h| storage.close(h).is_ok())
{ {
libkos::println!("init: running the SMP self-test"); libkos::println!("init: running the SMP self-test");
selftest::run(storage, scratch); selftest::run(storage, scratch);
launch_all(&mut launcher, manifest::TESTS); manifest::TESTS
} else { } 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"); 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 { for p in programs {
libkos::print!("init: loading {}... ", p.name); libkos::print!("init: loading {}... ", p.name);
match launcher.launch(p) { match launcher.launch(p) {
Ok(note) => libkos::println!("ok{}", note), Ok(started) => libkos::println!("ok{}", started.note),
// Without the owner, nothing that touches user data may start.
Err(why) if p.unlocks => {
libkos::println!("STOP ({})", why);
idle();
}
Err(why) => libkos::println!("SKIP ({})", why), Err(why) => libkos::println!("SKIP ({})", why),
} }
} }

View File

@@ -8,12 +8,15 @@ use types::budget::Limits;
const PAGE_SIZE: usize = 4096; const PAGE_SIZE: usize = 4096;
const USER_CODE_BASE: usize = 0x10000; 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); const CREATE_CAP: CapSlot = CapSlot::const_new(0);
/// Where a program file is read to before it is loaded (16 pages: the /// Where a program file is read to before it is loaded (32 pages: the
/// largest program init will load). /// largest program init will load; the gatekeeper is past 16).
const SCRATCH_VA: VirtualAddress = VirtualAddress::from_raw(0x30000); const SCRATCH_VA: VirtualAddress = VirtualAddress::from_raw(0x0120_0000);
const SCRATCH_PAGES: u16 = 16; const SCRATCH_PAGES: u16 = 32;
pub(crate) struct LoadedProcess { pub(crate) struct LoadedProcess {
pub(crate) process_cap: CapSlot, pub(crate) process_cap: CapSlot,
@@ -35,7 +38,9 @@ pub(crate) fn load_binary(
scratch: &mut [u8], scratch: &mut [u8],
filename: &[u8], filename: &[u8],
limits: Limits, limits: Limits,
stack_pages: usize,
) -> Option<LoadedProcess> { ) -> Option<LoadedProcess> {
let stack_pages = stack_pages.min(MAX_STACK_PAGES);
let file_size = read_file(storage, scratch, filename)?; let file_size = read_file(storage, scratch, filename)?;
let elf_data = &scratch[..file_size]; let elf_data = &scratch[..file_size];
let elf_info = match elf::parse(elf_data) { 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 page_count = PageCount::new(1)?;
let stack_va = syscall::request_pages(page_count)?; 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); 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 /// 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)] #[derive(Clone, Copy)]
pub struct Scope { pub struct Scope {
pub path: &'static [u8], pub path: &'static [u8],
pub write: bool, 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 struct Program {
pub name: &'static str, pub name: &'static str,
pub file: &'static [u8], pub file: &'static [u8],
pub limits: Limits, pub limits: Limits,
/// Its stack, in pages.
pub stack_pages: usize,
/// Serves an endpoint, which it finds in slot 0. /// Serves an endpoint, which it finds in slot 0.
pub serves: bool, pub serves: bool,
/// Handed over in this order, after it starts. /// Handed over in this order, after it starts.
pub devices: &'static [Device], 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 /// 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. /// program's copy carries its own badge, so a server can tell them apart.
pub uses: &'static [&'static str], pub uses: &'static [&'static str],
/// Storage it may reach, in the slot after `uses`. /// Storage it may reach, in the slots after `uses`.
pub storage: Option<Scope>, pub storage: &'static [Scope],
/// Takes the console over from init; everyone else prints through it. /// Takes the console over from init; everyone else prints through it.
pub console: bool, pub console: bool,
/// May have init spawn programs, whose limits together stay within this. /// May have init spawn programs, whose limits together stay within this.
pub spawns: Option<Limits>, pub spawns: Option<Limits>,
/// May halt the machine. /// May halt the machine.
pub system_control: bool, 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. /// 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, name,
file, file,
limits, limits,
stack_pages: crate::load::STACK_PAGES,
serves: true, serves: true,
devices: &[], devices: &[],
unlocks: false,
uses: &[], uses: &[],
storage: None, storage: &[],
console: false, console: false,
spawns: None, spawns: None,
system_control: false, 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). // Text on the display, lines from the keyboard (it takes input focus).
Program { uses: &["display"], ..server("terminal", b"bin/terminal", SMALL) }, 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), 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: /// Run on the test image after the SMP self-test, instead of the desktop:
/// spawns through init, then reports and halts. /// spawns through init, then reports and halts.
pub const TESTS: &[Program] = &[Program { pub const TESTS: &[Program] = &[Program {
serves: false, serves: false,
// The permission manager, to run an app the way the shell does. // The permission manager, to run an app the way the shell does.
uses: &["permission"], 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 }), spawns: Some(Limits { pages: 128, endpoints: 2, notifications: 2, regions: 2 }),
system_control: true, system_control: true,
..server("spawntest", b"bin/spawntest", SMALL) ..server("spawntest", b"bin/spawntest", SMALL)
}]; }];
/// The desktop; the test image runs the SMP self-test instead. The shell /// 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; /// holds the user's files and asks the permission manager to run apps;
/// it holds nothing to give them beyond that. /// it holds nothing to give them beyond that, and may sign the user out.
pub const DESKTOP: &[Program] = &[Program { pub const DESKTOP: &[Program] = &[Program {
serves: false, serves: false,
uses: &["terminal", "permission"], 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) ..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 { for w in words {
w.store(0, Ordering::SeqCst); 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() let rw = PermissionSet::empty()
.with(Permission::Read) .with(Permission::Read)
.with(Permission::Write) .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. /// Where init maps a spawn request's file grant while it reads it.
const PROGRAM_BUF_VA: VirtualAddress = VirtualAddress::from_raw(0x01A0_0000); 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. // 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 found = file.open(b"").and_then(|h| file.close(h)).is_ok();
let (_, scratch) = self.storage(); 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(); file.detach();
let Some(loaded) = loaded else { let Some(loaded) = loaded else {
return refused(if found { reply::ERROR } else { reply::NOT_FOUND }); 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>) { 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) { match (Request::decode(words), cap) {
(Some(r), Some(binary)) => { (Some(r), Some(binary)) => {
let (reply, process) = self.spawn(badge, &r, 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(|_| ()) 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<()> { pub fn close(&mut self, handle: FileHandle) -> Result<()> {
self.expect([op::CLOSE, handle.0 as usize, 0, 0], reply::OK).map(|_| ()) 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], 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. /// The keystore's newest record, and where the next write goes.
#[inline(never)]
fn load(ks: &mut impl Keystore) -> Result<(Option<Record>, usize), Failure> { 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) { if !ks.read(0, &mut a) || !ks.read(1, &mut b) {
return Err(Failure::Device); 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())) Ok((copies.newest().map(|(r, _)| r), copies.next_copy()))
} }
#[inline(never)]
fn store(ks: &mut impl Keystore, copy: usize, r: &Record) -> Result<(), Failure> { fn store(ks: &mut impl Keystore, copy: usize, r: &Record) -> Result<(), Failure> {
if ks.write(copy, &r.encode()) { Ok(()) } else { Err(Failure::Device) } 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, /// Destroys every slot and the directory: noise over both copies, twice,
/// then blank (the device then looks never set up). Marked as wiping /// 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. /// 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> { 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 first = record.wiping(rng).ok_or(Failure::Random)?;
let second = first.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. /// 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> { 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. // After a wipe nothing earlier may show: it must look like a new device.
prompt.clear(); 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. /// 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> { 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 { loop {
let (Some(record), _) = load(ks)? else { return Err(Failure::Device) }; let (Some(record), _) = load(ks)? else { return Err(Failure::Device) };

View File

@@ -22,18 +22,20 @@ use types::address::VirtualAddress;
use types::bounded::CapSlot; use types::bounded::CapSlot;
/// Where init's manifest put them: its own endpoint, then `uses`, then /// 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 EP: CapSlot = CapSlot::const_new(0);
const TERMINAL: CapSlot = CapSlot::const_new(1); const TERMINAL: CapSlot = CapSlot::const_new(1);
const DISPLAY: CapSlot = CapSlot::const_new(2); const DISPLAY: CapSlot = CapSlot::const_new(2);
const STORAGE: CapSlot = CapSlot::const_new(3); const USER: CapSlot = CapSlot::const_new(3);
const INIT: CapSlot = CapSlot::const_new(4); 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 SOURCE_BUF_VA: VirtualAddress = VirtualAddress::from_raw(0x0190_0000);
const SYSTEM_BUF_VA: VirtualAddress = VirtualAddress::from_raw(0x01B0_0000);
pub fn run() -> ! { 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"); libkos::println!("permission: no storage, exiting");
syscall::exit(); syscall::exit();
}; };
@@ -41,7 +43,7 @@ pub fn run() -> ! {
libkos::println!("permission: no notification, exiting"); libkos::println!("permission: no notification, exiting");
syscall::exit(); 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 consent = consent::TerminalConsent { terminal: TERMINAL };
let mut srv = server::PermissionServer::new(store, consent, INIT, Some(DISPLAY), deaths); let mut srv = server::PermissionServer::new(store, consent, INIT, Some(DISPLAY), deaths);
libkos::println!("permission: serving"); libkos::println!("permission: serving");

View File

@@ -19,7 +19,7 @@ use types::start_arg::StartArg;
use crate::consent::{CapabilityNames, TerminalConsent, version}; use crate::consent::{CapabilityNames, TerminalConsent, version};
use crate::policy::{self, Refusal, Store}; 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. /// 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 }; 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 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 index = self.running.iter().position(Option::is_none).ok_or(code(reply::BUSY))?;
let mut p = [0u8; PATH_BUF]; let file = self.store.binary_grant(&plan.app).ok_or(code(reply::FAILED))?;
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 spawned = libkos::spawn::spawn(self.init, file, PROGRAM); let spawned = libkos::spawn::spawn(self.init, file, PROGRAM);
let _ = libkos::fs::release(file); let _ = libkos::fs::release(file);
let child = spawned.map_err(|e| match e { let child = spawned.map_err(|e| match e {
@@ -176,7 +174,7 @@ impl PermissionServer {
let call = PermissionSet::empty().with(Permission::Execute); let call = PermissionSet::empty().with(Permission::Execute);
let data = match plan.granted.contains(Capability::Storage) { 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, false => None,
}; };
let staged = (badge == self.staged.badge).then_some(&self.staged); 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 //! The `Store` port on the storage server, through two grants: the
//! data in `/data/<id>/`, the owner's decisions in `/system/permissions/`. //! 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 libkos::fs::Storage;
use manifest::{AppId, Decision, MAX_DECISION_TEXT, Manifest}; use manifest::{AppId, Decision, MAX_DECISION_TEXT, Manifest};
@@ -10,18 +12,18 @@ use crate::policy::{MAX_MANIFEST, Store};
const APPS: &str = "apps"; const APPS: &str = "apps";
const DATA: &str = "data"; const DATA: &str = "data";
const DECISIONS: &str = "system/permissions"; const DECISIONS: &str = "permissions";
const MANIFEST: &str = "manifest.toml"; const MANIFEST: &str = "manifest.toml";
/// Apps `find` looks through. /// Apps `find` looks through.
pub const MAX_APPS: usize = 32; pub const MAX_APPS: usize = 32;
const LONGEST_PATH: usize = 160; const LONGEST_PATH: usize = 160;
const COPY_CHUNK: usize = 4096; 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]> { fn path<'a>(buf: &'a mut [u8; LONGEST_PATH], parts: &[&str]) -> Option<&'a [u8]> {
let mut len = 0; let mut len = 0;
for (i, p) in parts.iter().enumerate() { for p in parts.iter().filter(|p| !p.is_empty()) {
let sep = usize::from(i > 0); let sep = usize::from(len > 0);
let end = len + sep + p.len(); let end = len + sep + p.len();
let room = buf.get_mut(len..end)?; let room = buf.get_mut(len..end)?;
room[..sep].copy_from_slice(&b"/"[..sep]); 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 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 { 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. /// Where an offered app folder's grant is mapped while it is read.
source_va: VirtualAddress, source_va: VirtualAddress,
} }
impl DiskStore { impl DiskStore {
pub fn new(fs: Storage, source_va: VirtualAddress) -> Self { pub fn new(user: Storage, system: Storage, source_va: VirtualAddress) -> Self {
Self { fs, source_va } Self { user, system, source_va }
} }
/// Storage itself, for grants on the app's behalf. /// The storage holding `shelf`, and its apps' folder in it.
pub fn storage(&mut self) -> &mut Storage { fn on(&mut self, shelf: Shelf) -> (&mut Storage, &'static str) {
&mut self.fs 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)) { pub fn ids(&mut self, mut each: impl FnMut(AppId)) {
let _ = self.fs.list_dir(APPS.as_bytes(), |e| { for shelf in [Shelf::System, Shelf::User] {
if let Some(id) = core::str::from_utf8(e.name()).ok().and_then(AppId::new) { let (fs, apps) = self.on(shelf);
each(id); 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> { 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 { fn is_installed(&mut self, id: &AppId) -> bool {
let mut p = [0u8; LONGEST_PATH]; self.shelf(id).is_some()
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()
} }
fn install(&mut self, source: &CapSlot, app: &Manifest, data_folder: bool) -> bool { 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 // There already if an earlier attempt failed part way; an app
// counts as installed only once its manifest is in. // counts as installed only once its manifest is in.
let _ = self.fs.mkdir(folder); let _ = self.user.mkdir(folder);
let fs = &mut self.fs; let fs = &mut self.user;
let copied = Storage::attach(*source, self.source_va).ok().map(|mut src| { let copied = Storage::attach(*source, self.source_va).ok().map(|mut src| {
// The binary first: an app folder with a manifest is installed. // 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) 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]; let mut data = [0u8; LONGEST_PATH];
// Kept from an earlier install if there is one: the app's data. // 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> { 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 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 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. // A manifest in the wrong folder is no app.
(m.id == *id).then_some(m) (m.id == *id).then_some(m)
} }
@@ -167,7 +209,7 @@ impl Store for DiskStore {
let mut p = [0u8; LONGEST_PATH]; let mut p = [0u8; LONGEST_PATH];
let p = path(&mut p, &[DECISIONS, id.as_str()])?; let p = path(&mut p, &[DECISIONS, id.as_str()])?;
let mut text = [0u8; MAX_DECISION_TEXT]; 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 { 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 mut text = [0u8; MAX_DECISION_TEXT];
let n = decision.write(&mut text); let n = decision.write(&mut text);
// Replaced whole: there is no truncate. // Replaced whole: there is no truncate.
let _ = self.fs.delete(p); let _ = self.user.delete(p);
let Ok(h) = self.fs.create(p) else { return false }; let Ok(h) = self.user.create(p) else { return false };
let written = self.fs.write(h, &text[..n]) == Ok(n); let written = self.user.write(h, &text[..n]) == Ok(n);
let _ = self.fs.close(h); let _ = self.user.close(h);
written 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 { fn rmdir(&mut self, _path: &[u8]) -> bool {
false 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 //! The encrypted areas (see the vision's Encryption and Users notes), over
//! system: each app's `data/<id>/`, the owner's `home/`, and `system/` are //! any file system. Each user's folder `users/<id>/` holds their areas:
//! kept as `filecrypt` files under their own keys, derived from the //! `home/`, `permissions/`, and per app `apps/<app>/` and `data/<app>/`,
//! master key; the rest (programs, `factory/`) is plaintext. Before the key //! each kept as `filecrypt` files under its own key, derived from that
//! arrives the encrypted areas refuse everything, and a plaintext file in //! user's master key. Only the signed-in user's areas open; anyone else's
//! one is refused too: nothing there is trusted unless it was sealed. //! 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}; 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, 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 path = path.strip_prefix(b"/").unwrap_or(path);
let mut parts = path.splitn(3, |b| *b == b'/'); let mut parts = path.splitn(5, |b| *b == b'/');
match (parts.next(), parts.next(), parts.next()) { let (users, user, kind, app, rest) = (parts.next(), parts.next(), parts.next(), parts.next(), parts.next());
(Some(b"home" | b"system"), _, _) => Area::Sealed(&path[..path.iter().position(|b| *b == b'/').unwrap_or(path.len())]), if users != Some(b"users") {
(Some(b"data"), Some(id), Some(rest)) if !id.is_empty() && !rest.is_empty() => Area::Sealed(&path[..5 + id.len()]), return (Area::Plain, None);
(Some(b"data"), _, _) => Area::Neither,
_ => Area::Plain,
} }
// `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. /// An open encrypted file.
@@ -44,15 +60,20 @@ struct Open {
pub struct Encrypted<F: FileSystem> { pub struct Encrypted<F: FileSystem> {
inner: F, 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>, master: Option<Key>,
user: [u8; USER_NAME],
user_len: usize,
rng: Option<Drbg>, rng: Option<Drbg>,
open: [Option<Open>; MAX_HANDLES], 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> { impl<F: FileSystem> Encrypted<F> {
pub fn new(inner: F) -> Self { 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 { pub fn inner(&self) -> &F {
@@ -68,21 +89,27 @@ impl<F: FileSystem> Encrypted<F> {
self.inner 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. /// `Ok(None)` for a plaintext path, `Err` for one it must refuse.
fn key_for(&self, path: &[u8]) -> Result<Option<Key>, ()> { fn key_for(&self, path: &[u8]) -> Result<Option<Key>, ()> {
match area(path) { match area(path) {
Area::Plain => Ok(None), (Area::Plain, _) => Ok(None),
Area::Neither => Err(()), (Area::Sealed(name), user) if self.signed_in(user) => Ok(self.master.as_ref().map(|m| area_key(m, name))),
Area::Sealed(name) => self.master.as_ref().map(|m| Some(area_key(m, name))).ok_or(()), _ => 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 { fn dir_allowed(&self, path: &[u8]) -> bool {
match area(path) { match area(path) {
Area::Plain => true, (Area::Plain, _) => true,
_ => self.master.is_some(), (_, user) => self.signed_in(user),
} }
} }
@@ -115,13 +142,33 @@ impl<F: FileSystem> FileSystem for Encrypted<F> {
self.inner.writable() self.inner.writable()
} }
fn unlock(&mut self, master: &[u8; 32], seed: &[u8; 32]) { fn unlock(&mut self, master: &[u8; 32], seed: &[u8; 32], user: &[u8]) {
if self.master.is_none() { if self.master.is_none() && !user.is_empty() && user.len() <= USER_NAME {
self.master = Some(Key(*master)); self.master = Some(Key(*master));
self.rng = Some(Drbg::new(*seed)); 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> { fn open(&mut self, path: &[u8]) -> Option<FileHandle> {
let key = self.key_for(path).ok()?; let key = self.key_for(path).ok()?;
let h = self.inner.open(path)?; 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)?; let n = self.inner.list_dir(path, entries)?;
// In an encrypted area a file's size is what it holds, not its disk size. // 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.) // In a user's folder, a file's size is what it holds.
let sealed = area(path) != Area::Plain; let sealed = area(path).0 != Area::Plain;
for e in entries[..n].iter_mut().filter(|e| sealed && !e.is_dir) { for e in entries[..n].iter_mut().filter(|e| sealed && !e.is_dir) {
e.size = plain_len(e.size).unwrap_or(0); e.size = plain_len(e.size).unwrap_or(0);
} }
@@ -265,7 +312,7 @@ impl<F: FileSystem> FileSystem for Encrypted<F> {
#[cfg(test)] #[cfg(test)]
pub(crate) fn area_of(path: &[u8]) -> &'static str { pub(crate) fn area_of(path: &[u8]) -> &'static str {
match area(path) { match area(path).0 {
Area::Plain => "plain", Area::Plain => "plain",
Area::Sealed(_) => "sealed", Area::Sealed(_) => "sealed",
Area::Neither => "neither", Area::Neither => "neither",

View File

@@ -78,12 +78,55 @@ pub trait FileSystem {
fn seek(&mut self, handle: FileHandle, offset: usize) -> bool; fn seek(&mut self, handle: FileHandle, offset: usize) -> bool;
/// Removes the directory at `path`, which must be empty. /// Removes the directory at `path`, which must be empty.
fn rmdir(&mut self, path: &[u8]) -> bool; fn rmdir(&mut self, path: &[u8]) -> bool;
/// The master key and a fresh random seed, once the owner is in; only /// The signed-in user's master key, a fresh random seed, and the name
/// an encrypting file system has a use for them. /// of their folder; only an encrypting file system has a use for them.
fn unlock(&mut self, _master: &[u8; 32], _seed: &[u8; 32]) {} 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 /// Whether anything can be written at all; the server refuses
/// writing requests up front when not. /// writing requests up front when not.
fn writable(&self) -> bool { fn writable(&self) -> bool {
true 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) 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, /// Forgets `badge` and every grant narrowed from it, transitively,
/// calling `gone` with each one's badge and capability. /// calling `gone` with each one's badge and capability.
pub fn release(&mut self, badge: usize, mut gone: impl FnMut(usize, CapSlot)) { 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. /// Half of the seed for randomness.
SetSeed { half: usize, words: [usize; 2] }, SetSeed { half: usize, words: [usize; 2] },
Rmdir { path_len: usize }, 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 /// A capability to the path (`path_len` bytes of the buffer), inside
/// the caller's scope. /// the caller's scope.
Grant { Grant {
@@ -82,6 +86,9 @@ impl Encode for StorageRequest {
SetKey { half, words } => [op::SET_KEY, half, words[0], words[1]], SetKey { half, words } => [op::SET_KEY, half, words[0], words[1]],
SetSeed { half, words } => [op::SET_SEED, 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], 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], Grant { path_len, write } => [op::GRANT, path_len, usize::from(write), 0],
Release => [op::RELEASE, 0, 0, 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_KEY => SetKey { half: w[1], words: [w[2], w[3]] },
op::SET_SEED => SetSeed { 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::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 { op::GRANT => Grant {
path_len: w[1], path_len: w[1],
write: w[2] != 0, 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::protocol::{DIR_ENTRY_SIZE, DIR_NAME_SIZE, MAX_PATH, StorageRequest, StorageResponse};
use crate::scope::Scope; use crate::scope::Scope;
mod session;
const MAX_CLIENTS: usize = 8; const MAX_CLIENTS: usize = 8;
/// Handles at or past this are never valid (the file systems hand out /// Handles at or past this are never valid (the file systems hand out
/// fewer), so ownership fits a small table. /// 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. /// init; handed to the file system (and forgotten here) once all in.
key: [Option<[usize; 2]>; 2], key: [Option<[usize; 2]>; 2],
seed: [Option<[usize; 2]>; 2], seed: [Option<[usize; 2]>; 2],
/// Whose areas they open, said first.
user: Option<types::user::UserFolder>,
unlocked: bool, unlocked: bool,
} }
@@ -68,6 +72,7 @@ impl<F: FileSystem, H: Host> StorageServer<F, H> {
log: None, log: None,
key: [None; 2], key: [None; 2],
seed: [None; 2], seed: [None; 2],
user: None,
unlocked: false, 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. /// Whether the encrypted areas are open: the key and seed are in.
pub fn unlocked(&self) -> bool { pub fn unlocked(&self) -> bool {
self.unlocked self.unlocked
@@ -300,7 +284,7 @@ impl<F: FileSystem, H: Host> StorageServer<F, H> {
None => R::NotFound, None => R::NotFound,
}; };
let response = match request { 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 } => { Grant { path_len, write } => {
let narrower = scope.narrow(&buf[sent(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))) { 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::SetLog) => (self.set_log(badge, cap).encode(), None),
Some(StorageRequest::SetKey { half, words }) => (self.set_half(badge, false, half, words).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::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. // A badge storage never issued, or one it has taken back.
Some(request) => match self.grants.scope(badge) { Some(request) => match self.grants.scope(badge) {
None => (StorageResponse::Denied.encode(), None), 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())?; let (path, _) = self.open.get(&h.raw())?;
Some(self.files[path.as_str()].len()) 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> { 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; let mut n = 0;
for (name, data) in self.files.iter().filter(|(k, _)| k.starts_with(&prefix)) { let names = self.files.keys().map(|k| (k, true)).chain(self.dirs.iter().map(|d| (d, false)));
entries[n] = DirEntry { name: FileName::from_bytes(name[prefix.len()..].as_bytes()), size: data.len(), is_dir: 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; n += 1;
} }
Some(n) Some(n)
@@ -87,14 +101,22 @@ impl FileSystem for Memory {
self.files.remove(&key(path)).is_some() self.files.remove(&key(path)).is_some()
} }
fn mkdir(&mut self, path: &[u8]) -> bool { 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 true
} }
/// Only an empty folder.
fn rmdir(&mut self, path: &[u8]) -> bool { fn rmdir(&mut self, path: &[u8]) -> bool {
let k = key(path); let k = key(path);
let before = self.dirs.len(); let prefix = std::format!("{k}/");
self.dirs.retain(|d| *d != k); let empty = !self.files.keys().chain(self.dirs.iter()).any(|f| f.starts_with(&prefix));
self.dirs.len() < before 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> { fn unlocked() -> Encrypted<Memory> {
let mut fs = Encrypted::new(Memory::default()); let mut fs = Encrypted::new(Memory::default());
fs.unlock(&MASTER, &[3; 32]); fs.unlock(&MASTER, &[3; 32], b"u1");
fs fs
} }
@@ -129,17 +151,22 @@ fn on_disk(fs: &Encrypted<Memory>, path: &str) -> Vec<u8> {
#[test] #[test]
fn areas_are_told_by_path() { fn areas_are_told_by_path() {
for (p, a) in [ for (p, a) in [
(&b"home/readme.txt"[..], "sealed"), (&b"users/u1/home/readme.txt"[..], "sealed"),
(b"/home", "sealed"), (b"/users/u1/home", "sealed"),
(b"system/permissions/kos.snake", "sealed"), (b"users/u1/permissions/kos.snake", "sealed"),
(b"data/kos.notes/a", "sealed"), (b"users/u1/data/kos.notes/a", "sealed"),
(b"data/kos.notes", "neither"), (b"users/u1/apps/kos.notes/notes", "sealed"),
(b"data", "neither"), (b"users/u1/data/kos.notes", "neither"),
(b"data/", "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"bin/shell", "plain"),
(b"apps/kos.cat/cat", "plain"), (b"apps/kos.cat/cat", "plain"),
(b"factory/home/readme.txt", "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"), (b"selftest.txt", "plain"),
] { ] {
assert_eq!(area_of(p), a, "{}", String::from_utf8_lossy(p)); 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() { fn sealed_files_read_back_and_are_noise_on_disk() {
let mut fs = unlocked(); let mut fs = unlocked();
let text = b"Dear diary, nobody else may read this."; let text = b"Dear diary, nobody else may read this.";
put(&mut fs, b"home/diary.txt", text); put(&mut fs, b"users/u1/home/diary.txt", text);
assert_eq!(get(&mut fs, b"home/diary.txt").unwrap(), text); assert_eq!(get(&mut fs, b"users/u1/home/diary.txt").unwrap(), text);
let disk = on_disk(&fs, "home/diary.txt"); let disk = on_disk(&fs, "users/u1/home/diary.txt");
assert!(disk.starts_with(b"KOSFILE1")); assert!(disk.starts_with(b"KOSFILE1"));
assert!(!disk.windows(5).any(|w| w == b"diary"), "the words are not on the disk"); 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())); 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() { fn big_files_and_writes_across_chunks_round_trip() {
let mut fs = unlocked(); let mut fs = unlocked();
let big: Vec<u8> = (0..3 * filecrypt::PLAIN_CHUNK + 123).map(|i| (i % 251) as u8).collect(); 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); put(&mut fs, b"users/u1/data/kos.notes/big", &big);
assert_eq!(get(&mut fs, b"data/kos.notes/big").unwrap(), big); assert_eq!(get(&mut fs, b"users/u1/data/kos.notes/big").unwrap(), big);
// Overwrite a stretch spanning two chunks in the middle. // 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; let at = filecrypt::PLAIN_CHUNK - 10;
assert!(fs.seek(h, at)); assert!(fs.seek(h, at));
assert_eq!(fs.write(h, &[0xAA; 30]), Some(30)); assert_eq!(fs.write(h, &[0xAA; 30]), Some(30));
fs.close(h); fs.close(h);
let mut expect = big.clone(); let mut expect = big.clone();
expect[at..at + 30].fill(0xAA); 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. // Grow exactly from a chunk boundary: the old last chunk stops being last.
let mut fs = unlocked(); let mut fs = unlocked();
let full = vec![7u8; filecrypt::PLAIN_CHUNK]; let full = vec![7u8; filecrypt::PLAIN_CHUNK];
put(&mut fs, b"home/f", &full); put(&mut fs, b"users/u1/home/f", &full);
let h = fs.open(b"home/f").unwrap(); let h = fs.open(b"users/u1/home/f").unwrap();
assert!(fs.seek(h, filecrypt::PLAIN_CHUNK)); assert!(fs.seek(h, filecrypt::PLAIN_CHUNK));
assert_eq!(fs.write(h, b"more"), Some(4)); assert_eq!(fs.write(h, b"more"), Some(4));
fs.close(h); fs.close(h);
let mut expect = full.clone(); let mut expect = full.clone();
expect.extend_from_slice(b"more"); 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] #[test]
fn one_app_cannot_read_anothers_file_even_moved_into_its_folder() { fn one_app_cannot_read_anothers_file_even_moved_into_its_folder() {
let mut fs = unlocked(); let mut fs = unlocked();
put(&mut fs, b"data/kos.notes/secret", b"notes only"); put(&mut fs, b"users/u1/data/kos.notes/secret", b"notes only");
let stolen = on_disk(&fs, "data/kos.notes/secret"); let stolen = on_disk(&fs, "users/u1/data/kos.notes/secret");
fs.inner_mut_for_test().files.insert("data/kos.chat/secret".to_string(), stolen); fs.inner_mut_for_test().files.insert("users/u1/data/kos.chat/secret".to_string(), stolen);
let h = fs.open(b"data/kos.chat/secret").expect("it looks like a sealed file"); let h = fs.open(b"users/u1/data/kos.chat/secret").expect("it looks like a sealed file");
let mut out = [0u8; 64]; let mut out = [0u8; 64];
assert_eq!(fs.read(h, &mut out), None, "but it opens only under kos.notes' key"); 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] #[test]
fn locked_the_areas_refuse_everything() { fn locked_the_areas_refuse_everything() {
let mut fs = unlocked(); 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()); let mut locked = Encrypted::new(fs.into_inner());
assert!(locked.open(b"home/x").is_none()); assert!(locked.open(b"users/u1/home/x").is_none());
assert!(locked.create(b"home/z").is_none()); assert!(locked.create(b"users/u1/home/z").is_none());
assert!(!locked.delete(b"home/x")); assert!(!locked.delete(b"users/u1/home/x"));
assert!(!locked.mkdir(b"data/kos.notes")); assert!(!locked.mkdir(b"users/u1/data/kos.notes"));
assert!(locked.list_dir(b"home", &mut [DirEntry::empty(); 4]).is_none()); 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"); 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. // Another master key (a wiped and re-set-up device) reads noise.
let mut other = Encrypted::new(locked.into_inner()); let mut other = Encrypted::new(locked.into_inner());
other.unlock(&[1; 32], &[2; 32]); other.unlock(&[1; 32], &[2; 32], b"u1");
let h = other.open(b"home/x").unwrap(); let h = other.open(b"users/u1/home/x").unwrap();
assert_eq!(other.read(h, &mut [0u8; 8]), None); assert_eq!(other.read(h, &mut [0u8; 8]), None);
} }
#[test] #[test]
fn plaintext_planted_in_an_encrypted_area_is_refused() { fn plaintext_planted_in_an_encrypted_area_is_refused() {
let mut fs = unlocked(); let mut fs = unlocked();
fs.inner_mut_for_test().files.insert("system/permissions/kos.evil".to_string(), b"[decision]\ngranted = [\"network\"]\n".to_vec()); 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"system/permissions/kos.evil").is_none()); assert!(fs.open(b"users/u1/permissions/kos.evil").is_none());
} }
#[test] #[test]
fn tampering_and_truncation_are_caught() { fn tampering_and_truncation_are_caught() {
let mut fs = unlocked(); let mut fs = unlocked();
let data = vec![5u8; 2 * filecrypt::PLAIN_CHUNK + 50]; let data = vec![5u8; 2 * filecrypt::PLAIN_CHUNK + 50];
put(&mut fs, b"home/t", &data); put(&mut fs, b"users/u1/home/t", &data);
let good = on_disk(&fs, "home/t"); let good = on_disk(&fs, "users/u1/home/t");
// One flipped byte. // One flipped byte.
let mut bent = good.clone(); let mut bent = good.clone();
bent[100] ^= 1; bent[100] ^= 1;
fs.inner_mut_for_test().files.insert("home/t".to_string(), bent); fs.inner_mut_for_test().files.insert("users/u1/home/t".to_string(), bent);
assert_eq!(get(&mut fs, b"home/t"), None); 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. // Cut at a chunk boundary: the remaining last chunk was not sealed as last.
let cut = good[..filecrypt::chunk_offset(2)].to_vec(); let cut = good[..filecrypt::chunk_offset(2)].to_vec();
fs.inner_mut_for_test().files.insert("home/t".to_string(), cut); fs.inner_mut_for_test().files.insert("users/u1/home/t".to_string(), cut);
assert_eq!(get(&mut fs, b"home/t"), None); assert_eq!(get(&mut fs, b"users/u1/home/t"), None);
// Chunks swapped. // Chunks swapped.
let (a, b) = (filecrypt::chunk_offset(0), filecrypt::chunk_offset(1)); let (a, b) = (filecrypt::chunk_offset(0), filecrypt::chunk_offset(1));
let mut swapped = good.clone(); let mut swapped = good.clone();
swapped[a..b].copy_from_slice(&good[b..b + filecrypt::CHUNK]); swapped[a..b].copy_from_slice(&good[b..b + filecrypt::CHUNK]);
swapped[b..b + filecrypt::CHUNK].copy_from_slice(&good[a..b]); swapped[b..b + filecrypt::CHUNK].copy_from_slice(&good[a..b]);
fs.inner_mut_for_test().files.insert("home/t".to_string(), swapped); fs.inner_mut_for_test().files.insert("users/u1/home/t".to_string(), swapped);
assert_eq!(get(&mut fs, b"home/t"), None); assert_eq!(get(&mut fs, b"users/u1/home/t"), None);
} }
#[test] #[test]
fn listings_show_what_files_hold() { fn listings_show_what_files_hold() {
let mut fs = unlocked(); 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 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)); assert_eq!((n, entries[0].size), (1, 5));
} }
#[test] #[test]
fn files_cannot_live_loose_in_data() { fn files_cannot_live_loose_in_a_users_folder() {
let mut fs = unlocked(); let mut fs = unlocked();
assert!(fs.create(b"data/loose").is_none()); assert!(fs.create(b"users/u1/data/loose").is_none());
assert!(fs.mkdir(b"data/kos.notes"), "an app's folder is fine"); 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]; let mut entries = [DirEntry::empty(); 2];
put(&mut fs, b"data/kos.notes/a", b"xyz"); put(&mut fs, b"users/u1/data/kos.notes/a", b"xyz");
let n = fs.list_dir(b"data/kos.notes", &mut entries).unwrap(); 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"); 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; use crate::encrypted::Encrypted;
let Some((dir, img)) = setup("sealed") else { return }; let Some((dir, img)) = setup("sealed") else { return };
let mut fs = Encrypted::new(Ext4::mount(FileDisk(img)).expect("mount")); let mut fs = Encrypted::new(Ext4::mount(FileDisk(img)).expect("mount"));
fs.unlock(&[4; 32], &[5; 32]); fs.unlock(&[4; 32], &[5; 32], b"u1");
assert!(fs.mkdir(b"/home")); 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 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_eq!(fs.write(h, &text), Some(text.len()));
assert!(fs.seek(h, 5000)); assert!(fs.seek(h, 5000));
assert_eq!(fs.write(h, b"LAZY DOG"), Some(8)); assert_eq!(fs.write(h, b"LAZY DOG"), Some(8));
fs.close(h); fs.close(h);
let mut expect = text.clone(); let mut expect = text.clone();
expect[5000..5008].copy_from_slice(b"LAZY DOG"); 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()]; let mut back = std::vec![0u8; expect.len()];
assert_eq!(fs.read(h, &mut back), Some(expect.len())); assert_eq!(fs.read(h, &mut back), Some(expect.len()));
fs.close(h); 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); let (clean, report) = fsck_clean(&dir, &disk);
assert!(clean, "e2fsck: {report}"); 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 set = |s: &mut Srv, badge, req| call(s, badge, req).0;
let key = |half| StorageRequest::SetKey { half, words: [1, 2] }; let key = |half| StorageRequest::SetKey { half, words: [1, 2] };
let seed = |half| StorageRequest::SetSeed { half, words: [3, 4] }; 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, 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::Ok);
assert_eq!(set(&mut s, 0, key(0)), StorageResponse::Error, "nor can a half be replaced"); 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!(s.unlocked());
assert_eq!(set(&mut s, 0, key(0)), StorageResponse::Error, "once unlocked, never again"); 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) { fn interrupt(&mut self, notification: CapSlot) {
let _ = syscall::signal(notification); let _ = syscall::signal(notification);
} }
fn forget(&mut self, notification: CapSlot) {
syscall::drop_cap(notification);
}
} }
pub fn run() -> ! { pub fn run() -> ! {

View File

@@ -24,6 +24,8 @@ pub trait Console {
/// Takes input focus back from whoever took it, and clears the screen. /// Takes input focus back from whoever took it, and clears the screen.
fn take_focus(&mut self); fn take_focus(&mut self);
fn interrupt(&mut self, notification: CapSlot); 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; pub const BACKSPACE: u8 = 8;
@@ -232,7 +234,7 @@ impl<C: Console> Server for TerminalServer<C> {
Done 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] { let reply = match words[0] {
types::terminal::READ => self.read(false), types::terminal::READ => self.read(false),
types::terminal::READ_SECRET => self.read(true), types::terminal::READ_SECRET => self.read(true),
@@ -252,6 +254,13 @@ impl<C: Console> Server for TerminalServer<C> {
} }
_ => [MALFORMED, 0, 0, 0], _ => [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 => { types::terminal::CLEAR => {
self.handle(Request::Clear); self.handle(Request::Clear);
[0; 4] [0; 4]

View File

@@ -20,6 +20,7 @@ struct Fake {
/// Every row drawn, with how many cells. /// Every row drawn, with how many cells.
rows_drawn: Vec<(usize, usize)>, rows_drawn: Vec<(usize, usize)>,
interrupted: Vec<CapSlot>, interrupted: Vec<CapSlot>,
forgotten: Vec<CapSlot>,
} }
impl Console for Fake { impl Console for Fake {
@@ -48,6 +49,9 @@ impl Console for Fake {
fn interrupt(&mut self, notification: CapSlot) { fn interrupt(&mut self, notification: CapSlot) {
self.interrupted.push(notification); self.interrupted.push(notification);
} }
fn forget(&mut self, notification: CapSlot) {
self.forgotten.push(notification);
}
} }
fn term(keys: &[u8]) -> TerminalServer<Fake> { 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_eq!(&t.grid().row(0)[..8], b"*******\x20");
assert!(!t.grid().row(0).windows(7).any(|w| w == b"hunter2")); 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> //! app-image install <root> <manifest.toml> <binary>
//! an installed, pre-approved app: /apps/<id>/, and (under factory/, //! 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 //! everything it asks for, and data/<id>/ if that includes storage
//! app-image stage <folder> <manifest.toml> <binary> //! app-image stage <folder> <manifest.toml> <binary>
//! an app folder the owner can install later (`install <folder>`) //! 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 mut out = [0u8; MAX_DECISION_TEXT];
let n = decision.write(&mut out); 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::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())?; fs::write(decisions.join(m.id.as_str()), &out[..n]).map_err(|e| e.to_string())?;
if decision.granted.contains(Capability::Storage) { if decision.granted.contains(Capability::Storage) {

View File

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

View File

@@ -17,6 +17,12 @@ pub const STACK_TOP: usize = 0x10000;
/// A slot word meaning "none". /// A slot word meaning "none".
pub const NONE: usize = usize::MAX; 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 mod reply {
pub const OK: usize = 0; pub const OK: usize = 0;
/// The grant names no file init can read. /// The grant names no file init can read.

View File

@@ -41,6 +41,17 @@ pub mod op {
pub const SET_SEED: usize = 14; pub const SET_SEED: usize = 14;
/// Removes the (empty) directory at the path in the buffer. /// Removes the (empty) directory at the path in the buffer.
pub const RMDIR: usize = 15; 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). /// Reply tags (word 0).

View File

@@ -12,8 +12,11 @@ pub const READ_SECRET: usize = 14;
/// that drew on the screen itself. /// that drew on the screen itself.
pub const REDRAW: usize = 12; pub const REDRAW: usize = 12;
/// Carries a notification the terminal signals on Ctrl+C while no line is /// 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; 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. /// What Ctrl+C types.
pub const CTRL_C: u8 = 3; 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 # Apps, manifests and the permission manager
How a program gets its authority. The kernel only knows capabilities; this 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). (see the vision's Capability Management and App Manifest notes).
## Apps ## 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 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 process id. The id is what decisions, the app's data folder and (later) its
encryption keys hang off. 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: 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 (`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 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 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 `cat` declares nothing and gets only what the shell passes it. Snake
declares `display`. Nothing gets anything "just in case". declares `display`. Nothing gets anything "just in case".
@@ -31,22 +31,27 @@ declares `display`. Nothing gets anything "just in case".
## Disk layout ## Disk layout
``` ```
/home/ the owner's files; the shell's `/` (encrypted) /apps/<id>/ a system app: manifest.toml and its binary, shared by every user
/apps/<id>/ an installed app: manifest.toml and its binary /users/<user>/ one user's folder (encrypted, under their own key)
/data/<id>/ the app's private folder (its `storage`) (encrypted, its own key) home/ their files; the shell's `/`
/system/permissions/ the owner's decisions, one file per app id (encrypted) apps/<id>/ an app they installed
/bin/ system servers and the shell (init starts these) data/<id>/ an app's private folder (its `storage`, its own key)
/factory/ plaintext starter content the encrypted areas are permissions/<id> their decisions, one file per app id
seeded from with a new master key /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 Users each decide for themselves, even about the system's apps, and what
shell's storage grant is `/home`, so neither it nor anything it runs can one installs the others do not see. An app id a system app has cannot be
read another app's data or rewrite a decision. 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 ## Manifest
`/apps/<id>/manifest.toml`, a strict subset of TOML: `apps/<id>/manifest.toml`, a strict subset of TOML:
```toml ```toml
[app] [app]
@@ -60,7 +65,7 @@ icon = "icon.png" # optional: a file in the app's folder
[capabilities] [capabilities]
required = ["display"] # cannot work without; denied = not installed required = ["display"] # cannot work without; denied = not installed
optional = [] # works without; the owner decides each optional = [] # works without; the user decides each
[appearance] [appearance]
accent = "#e91e63" # optional: checked for format here, contrast by K-UI 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 | | Capability | What the app gets |
|---|---| |---|---|
| `display` | the display server: the screen, and the keyboard while in focus | | `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 ## Decisions
`/system/permissions/<id>`, the same TOML subset: The user's `permissions/<id>`, the same TOML subset:
```toml ```toml
[decision] [decision]
granted = ["display"] 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 system image ship with their decisions already recorded under
`factory/system/` (pre-approved by whoever built the image), and init `factory/permissions/` (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 seeds them into each new user's `permissions/`. Changing one later is that
the permission manager. user's call, through the permission manager.
## Permission manager ## Permission manager
A system server with no hardware. Its logic (checking manifests, deciding A system server with no hardware. Its logic (checking manifests, deciding
what an app gets, keeping decisions) is pure and generic over two ports: 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. - `Store`: reads manifests and decisions, writes decisions, installs files.
Today: the storage server. 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. 1. Read and check its `manifest.toml`; refuse if the id is installed.
2. Required capabilities this device lacks: refuse. 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. 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. `storage` was granted, record the decision.
### Launch ### Launch
@@ -136,7 +141,7 @@ permission manager:
### Revoke ### 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 gets less. A running app that held it is **stopped**: all its access dies
at once. (Endpoint capabilities like `display` are copies the permission at once. (Endpoint capabilities like `display` are copies the permission
manager cannot take back from one app alone yet; once servers issue 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 its log endpoint from init later (`SET_LOG`, accepted only from the
unbadged holder of its endpoint). 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 Init starts the log, input, display and terminal servers, then the
gatekeeper (`types::gatekeeper`), handing it only the terminal, the keystore 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 (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, virtio-rng device. Init calls `UNLOCK`, which returns once someone is in:
then `KEY`, whose reply is the master key; init passes it to storage `[OK, flags, user id, role]`, a guest (`GUEST`) or a new keystore
(`SET_KEY`, from the unbadged holder only, each half once), and a fresh (`FRESH`) among the flags. Then `KEY` (their master key), `SEED` (32 fresh
32-byte seed (`SEED` → `SET_SEED`) for storage's randomness this boot. random bytes for storage this sign-in), `NAME`, and `REMOVED` until `DONE`
Storage's encrypted areas (`home/`, `system/`, each app's `data/<id>/`; (users the owner removed just now). The gatekeeper exits.
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) Each user's files are in `users/<id>/` (`types::user`: the id in hex, or
init empties those areas and seeds them from the plaintext `factory/`; `guest`): `home/`, `permissions/`, and per app `apps/<id>/` and
an area that is missing is seeded too. Only then does init start the `data/<id>/`, each sealed under its own key derived from that user's
permission manager and the desktop. If the gatekeeper cannot master key (crate `filecrypt`, layer `storage_server::encrypted`); the rest
unlock, init stops there. The gatekeeper exits, and its capabilities go of the disk is plaintext. While storage is still locked, init removes what
with it. 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 A program's `uses` endpoints arrive as badged copies with `Execute` and
`Delegate`: the badge (unique per copy) tells the server which client is `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`) 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 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 `\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 kept for the next one, except Ctrl+C, which signals the notification the
first `ON_INTERRUPT` caller gave. `REDRAW` repaints the screen and takes session's first `ON_INTERRUPT` caller (its shell) gave. `REDRAW` repaints the screen and takes
input focus back. input focus back.
The desktop is the shell, given the terminal, the permission manager and The desktop is the shell, given the terminal, the permission manager,
`/home` (its `/`). Everything the owner runs is an app (docs/apps.md): a 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`, 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 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 read-only grant to the current folder (`cd`, `pwd`). The permission manager

View File

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

View File

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