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:
@@ -19,11 +19,12 @@ use types::address::VirtualAddress;
|
||||
use types::apps::reply;
|
||||
use types::bounded::CapSlot;
|
||||
|
||||
/// Where init's manifest put them: `uses`, then storage.
|
||||
/// Where init's manifest put them: `uses`, then storage, then signing out.
|
||||
const TERMINAL: CapSlot = CapSlot::const_new(0);
|
||||
const PERMISSION: CapSlot = CapSlot::const_new(1);
|
||||
/// The owner's files, `/home`: the shell's `/`.
|
||||
/// The user's files, their `home`: the shell's `/`.
|
||||
const HOME: CapSlot = CapSlot::const_new(2);
|
||||
const SESSION: CapSlot = CapSlot::const_new(3);
|
||||
|
||||
/// Bits on the shell's notification: the program died (its death watch is
|
||||
/// unbadged), or Ctrl+C was typed (the terminal's copy).
|
||||
@@ -227,7 +228,7 @@ fn run(sh: &mut Shell, line: &str) {
|
||||
match args.first().copied() {
|
||||
None => {}
|
||||
Some("help") => {
|
||||
println!("built-ins: help, echo <text>, clear, cd <dir>, pwd");
|
||||
println!("built-ins: help, echo <text>, clear, cd <dir>, pwd, logout");
|
||||
println!("apps: apps, install <folder>, allow|revoke <app id> <capability>");
|
||||
println!("anything else runs an app: try ls, cat readme.txt, snake");
|
||||
println!("apps see the current directory, read-only, and only what you allowed");
|
||||
@@ -244,6 +245,19 @@ fn run(sh: &mut Shell, line: &str) {
|
||||
Some("pwd") => println!("{}", sh.cwd),
|
||||
Some("echo") => println!("{}", Joined(&args[1..])),
|
||||
Some("clear") => libkos::terminal::clear(TERMINAL),
|
||||
// Init stops this shell and everything it ran, and the lock
|
||||
// screen comes back.
|
||||
Some("logout") => {
|
||||
libkos::terminal::clear(TERMINAL);
|
||||
if syscall::send_msg(SESSION, [types::spawn::SIGN_OUT, 0, 0, 0], None).is_err() {
|
||||
println!("logout: cannot sign out");
|
||||
return;
|
||||
}
|
||||
// No prompt over the lock screen: wait to be stopped.
|
||||
loop {
|
||||
let _ = syscall::wait(sh.done);
|
||||
}
|
||||
}
|
||||
Some(_) => run_program(sh, args),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,16 +14,18 @@ use libkos::syscall;
|
||||
use types::bounded::CapSlot;
|
||||
use types::budget::Limits;
|
||||
|
||||
/// Where init put them (see init's manifest: `uses`, the spawn endpoint,
|
||||
/// then control).
|
||||
/// Where init put them (see init's manifest: `uses`, storage (the disk,
|
||||
/// the user's home), the spawn endpoint, then control).
|
||||
const PERMISSION: CapSlot = CapSlot::const_new(0);
|
||||
const STORAGE: CapSlot = CapSlot::const_new(1);
|
||||
const INIT: CapSlot = CapSlot::const_new(2);
|
||||
const CONTROL: CapSlot = CapSlot::const_new(3);
|
||||
const HOME: CapSlot = CapSlot::const_new(2);
|
||||
const INIT: CapSlot = CapSlot::const_new(3);
|
||||
const CONTROL: CapSlot = CapSlot::const_new(4);
|
||||
/// Where storage's buffers are mapped: the whole disk's, a narrowed one's.
|
||||
const STORAGE_BUF_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x0180_0000);
|
||||
const NARROW_BUF_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x0190_0000);
|
||||
const REQUEST_PAGE_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x01C0_0000);
|
||||
const HOME_BUF_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x01B0_0000);
|
||||
/// Read back from the disk image by scripts/test-qemu.sh.
|
||||
const NOTE: &[u8] = b"written by k-os on ext4\n";
|
||||
|
||||
@@ -125,9 +127,9 @@ fn revoke_stops_a_running_app(died: CapSlot) -> Result<(), &'static str> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The owner's area is sealed: seeded from the factory, and what is
|
||||
/// written there one boot reads back the next.
|
||||
fn encrypted_home(fs: &mut Storage) -> Result<(), &'static str> {
|
||||
/// The user's home is sealed: seeded from the factory, and what is written
|
||||
/// there one boot reads back the next. Theirs is the only folder that opens.
|
||||
fn encrypted_home(fs: &mut Storage, home_fs: &mut Storage) -> Result<(), &'static str> {
|
||||
let mut home = [0u8; 512];
|
||||
let mut factory = [0u8; 512];
|
||||
let read = |fs: &mut Storage, path: &[u8], out: &mut [u8]| -> Result<usize, &'static str> {
|
||||
@@ -136,7 +138,7 @@ fn encrypted_home(fs: &mut Storage) -> Result<(), &'static str> {
|
||||
let _ = fs.close(h);
|
||||
Ok(n)
|
||||
};
|
||||
let n = read(fs, b"home/readme.txt", &mut home)?;
|
||||
let n = read(home_fs, b"readme.txt", &mut home)?;
|
||||
let m = read(fs, b"factory/home/readme.txt", &mut factory)?;
|
||||
if home[..n] != factory[..m] {
|
||||
return Err("the encrypted home does not hold the factory's readme");
|
||||
@@ -144,19 +146,29 @@ fn encrypted_home(fs: &mut Storage) -> Result<(), &'static str> {
|
||||
libkos::println!("spawntest: home seeded from the factory, reads back through its seal: ok");
|
||||
const NOTE: &[u8] = b"sealed across a reboot";
|
||||
let mut back = [0u8; 64];
|
||||
match read(fs, b"home/persist.txt", &mut back) {
|
||||
match read(home_fs, b"persist.txt", &mut back) {
|
||||
Ok(n) if &back[..n] == NOTE => libkos::println!("spawntest: an encrypted file from the last boot reads back: ok"),
|
||||
Ok(_) => return Err("an encrypted file from the last boot reads back wrong"),
|
||||
Err(_) => {
|
||||
let h = fs.create(b"home/persist.txt").map_err(|_| "cannot create in home")?;
|
||||
let wrote = fs.write(h, NOTE) == Ok(NOTE.len());
|
||||
let _ = fs.close(h);
|
||||
let h = home_fs.create(b"persist.txt").map_err(|_| "cannot create in home")?;
|
||||
let wrote = home_fs.write(h, NOTE) == Ok(NOTE.len());
|
||||
let _ = home_fs.close(h);
|
||||
if !wrote {
|
||||
return Err("cannot write in home");
|
||||
}
|
||||
libkos::println!("spawntest: wrote an encrypted file for the next boot");
|
||||
}
|
||||
}
|
||||
// A made-up user's: nobody may reach it, the whole disk's holder neither.
|
||||
if fs.create(b"users/0123456789abcdef/home/planted").is_ok() || fs.mkdir(b"users/0123456789abcdef").is_ok() {
|
||||
return Err("someone else's folder could be written");
|
||||
}
|
||||
let mut folders = 0;
|
||||
fs.list_dir(b"users", |e| folders += usize::from(e.name() != b"." && e.name() != b"..")).map_err(|_| "cannot list the users")?;
|
||||
if folders != 1 {
|
||||
return Err("not exactly one user's folder");
|
||||
}
|
||||
libkos::println!("spawntest: only the signed-in user's folder opens: ok");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -164,7 +176,8 @@ fn check() -> Result<(), &'static str> {
|
||||
let mut fs = Storage::attach(STORAGE, STORAGE_BUF_VA).map_err(|_| "cannot attach to storage")?;
|
||||
write_a_file(&mut fs)?;
|
||||
libkos::println!("spawntest: a file written to ext4 reads back: ok");
|
||||
encrypted_home(&mut fs)?;
|
||||
let mut home = Storage::attach(HOME, HOME_BUF_VA).map_err(|_| "cannot attach to home")?;
|
||||
encrypted_home(&mut fs, &mut home)?;
|
||||
|
||||
let died = syscall::create_notification().map_err(|_| "no notification")?;
|
||||
narrow_storage(&mut fs, died)?;
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
//! The encrypted areas' starting point. The image ships starter content
|
||||
//! (the owner's first files, the pre-approved decisions) in plaintext under
|
||||
//! `factory/`; with a new master key (a fresh device, or one just wiped)
|
||||
//! init empties the encrypted areas and seeds them from it, storage
|
||||
//! sealing what is written. A device just wiped so looks factory-fresh,
|
||||
//! not full of files no key opens. An area simply missing (a disk image
|
||||
//! rebuilt) is seeded too.
|
||||
//! A user's folder's starting point. The image ships starter content (a
|
||||
//! user's first files, the pre-approved decisions) in plaintext under
|
||||
//! `factory/`; each area of the signed-in user's folder that is missing (a
|
||||
//! new user, a guest, a disk image rebuilt) is made and seeded from it,
|
||||
//! storage sealing what is written.
|
||||
|
||||
use libkos::fs::Storage;
|
||||
use types::storage::MAX_PATH;
|
||||
|
||||
const AREAS: [&str; 3] = ["home", "system", "data"];
|
||||
/// Each area of a user's folder, and what it starts with.
|
||||
const AREAS: [(&str, &str); 4] = [("home", "factory/home"), ("permissions", "factory/permissions"), ("data", "factory/data"), ("apps", "factory/apps")];
|
||||
/// Entries handled per listing; a folder with more is listed again.
|
||||
const BATCH: usize = 32;
|
||||
const NAME: usize = types::storage::DIR_NAME_SIZE;
|
||||
@@ -61,25 +60,6 @@ fn entries(fs: &mut Storage, dir: &Path) -> Option<([([u8; NAME], usize, bool);
|
||||
Some((out, n))
|
||||
}
|
||||
|
||||
/// Removes everything inside `dir`, keeping `dir`.
|
||||
fn empty(fs: &mut Storage, dir: &Path) -> Result<(), &'static str> {
|
||||
loop {
|
||||
let (list, n) = entries(fs, dir).ok_or("cannot list an area to empty it")?;
|
||||
if n == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
for (name, len, is_dir) in &list[..n] {
|
||||
let child = dir.join(&name[..*len]).ok_or("path too long")?;
|
||||
if *is_dir {
|
||||
empty(fs, &child)?;
|
||||
fs.rmdir(child.as_bytes()).map_err(|_| "cannot remove a folder")?;
|
||||
} else {
|
||||
fs.delete(child.as_bytes()).map_err(|_| "cannot remove a file")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Copies the file `from` to a new file `to`.
|
||||
fn copy_file(fs: &mut Storage, from: &Path, to: &Path, buf: &mut [u8]) -> Result<(), &'static str> {
|
||||
let src = fs.open(from.as_bytes()).map_err(|_| "cannot open a factory file")?;
|
||||
@@ -115,23 +95,23 @@ fn copy_tree(fs: &mut Storage, from: &Path, to: &Path, buf: &mut [u8]) -> Result
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Readies the encrypted areas once storage has the key: `fresh` when the
|
||||
/// master key is new.
|
||||
pub(crate) fn prepare(fs: &mut Storage, fresh: bool, buf: &mut [u8]) -> Result<(), &'static str> {
|
||||
for area in AREAS {
|
||||
let path = Path::new(area);
|
||||
let present = fs.list_dir(path.as_bytes(), |_| {}).is_ok();
|
||||
if present && !fresh {
|
||||
/// Readies `folder`'s areas once storage has its key.
|
||||
pub(crate) fn prepare(fs: &mut Storage, folder: types::user::UserFolder, buf: &mut [u8]) -> Result<(), &'static str> {
|
||||
let name = core::str::from_utf8(folder.as_bytes()).map_err(|_| "a user folder named oddly")?;
|
||||
let base = Path::new("users").join(name.as_bytes()).ok_or("path too long")?;
|
||||
for dir in [Path::new("users"), base] {
|
||||
if fs.list_dir(dir.as_bytes(), |_| {}).is_err() {
|
||||
fs.mkdir(dir.as_bytes()).map_err(|_| "cannot make a user's folder")?;
|
||||
}
|
||||
}
|
||||
for (area, factory) in AREAS {
|
||||
let path = base.join(area.as_bytes()).ok_or("path too long")?;
|
||||
if fs.list_dir(path.as_bytes(), |_| {}).is_ok() {
|
||||
continue;
|
||||
}
|
||||
if present {
|
||||
empty(fs, &path)?;
|
||||
} else {
|
||||
fs.mkdir(path.as_bytes()).map_err(|_| "cannot make an area")?;
|
||||
}
|
||||
let factory = Path::new("factory").join(area.as_bytes()).ok_or("path too long")?;
|
||||
copy_tree(fs, &factory, &path, buf)?;
|
||||
libkos::println!("init: {} {}", if fresh { "set up" } else { "seeded" }, area);
|
||||
fs.mkdir(path.as_bytes()).map_err(|_| "cannot make an area")?;
|
||||
copy_tree(fs, &Path::new(factory), &path, buf)?;
|
||||
libkos::println!("init: set up {}", area);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -9,9 +9,11 @@ use types::badge::Badge;
|
||||
use types::bounded::CapSlot;
|
||||
use types::permission::{Permission, PermissionSet};
|
||||
|
||||
use types::user::UserFolder;
|
||||
|
||||
use crate::devices::Devices;
|
||||
use crate::load::{self, LoadedProcess};
|
||||
use crate::manifest::{Device, Program};
|
||||
use crate::manifest::{Device, Program, Scope};
|
||||
use crate::spawn::Spawner;
|
||||
|
||||
const USER_STACK_TOP: StackPointer = StackPointer::from_raw(0x10000);
|
||||
@@ -34,6 +36,22 @@ pub(crate) struct Launcher<'a> {
|
||||
/// Bound to init's thread: spawned children's deaths, a bit each.
|
||||
pub(crate) deaths: CapSlot,
|
||||
pub(crate) spawner: Spawner,
|
||||
/// Who is signed in, and what runs for them (stopped at sign-out).
|
||||
pub(crate) user: Option<crate::session::User>,
|
||||
pub(crate) running: [Option<CapSlot>; MAX_SESSION],
|
||||
/// What a session runs after `SESSION`: the desktop, or the tests.
|
||||
pub(crate) desktop: &'static [Program],
|
||||
}
|
||||
|
||||
/// Programs one session runs at once, at most.
|
||||
pub(crate) const MAX_SESSION: usize = 4;
|
||||
|
||||
/// What became of starting a program.
|
||||
pub(crate) struct Launched {
|
||||
pub(crate) note: &'static str,
|
||||
pub(crate) process: CapSlot,
|
||||
/// Its endpoint, if it serves one.
|
||||
pub(crate) ep: Option<CapSlot>,
|
||||
}
|
||||
|
||||
impl<'a> Launcher<'a> {
|
||||
@@ -47,25 +65,46 @@ impl<'a> Launcher<'a> {
|
||||
// Storage, started by boot, is not among them: it is reached
|
||||
// through grants (a program's `storage` scope).
|
||||
let served = [None; MAX_SERVED];
|
||||
Ok(Self { storage, scratch, devices, served, next_badge: 1, spawn_ep, deaths, spawner: Spawner::new() })
|
||||
Ok(Self {
|
||||
storage,
|
||||
scratch,
|
||||
devices,
|
||||
served,
|
||||
next_badge: 1,
|
||||
spawn_ep,
|
||||
deaths,
|
||||
spawner: Spawner::new(),
|
||||
user: None,
|
||||
running: [None; MAX_SESSION],
|
||||
desktop: &[],
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn storage(&mut self) -> (&mut Storage, &mut [u8]) {
|
||||
(self.storage, self.scratch)
|
||||
}
|
||||
|
||||
/// The endpoint of the running server `name`.
|
||||
pub(crate) fn served(&self, name: &str) -> Option<CapSlot> {
|
||||
self.endpoint(name)
|
||||
}
|
||||
|
||||
fn endpoint(&self, name: &str) -> Option<CapSlot> {
|
||||
self.served.iter().flatten().find(|(n, _)| *n == name).map(|(_, ep)| *ep)
|
||||
}
|
||||
|
||||
/// What became of the program's device, if it has one.
|
||||
pub(crate) fn launch(&mut self, p: &Program) -> Result<&'static str, &'static str> {
|
||||
pub(crate) fn launch(&mut self, p: &Program) -> Result<Launched, &'static str> {
|
||||
let loaded =
|
||||
load::load_binary(self.storage, self.scratch, p.file, p.limits).ok_or("cannot load")?;
|
||||
load::load_binary(self.storage, self.scratch, p.file, p.limits, p.stack_pages).ok_or("cannot load")?;
|
||||
let child = loaded.process_cap;
|
||||
let ep = match p.serves {
|
||||
true => {
|
||||
let ep = syscall::create_endpoint().ok_or("no endpoint")?;
|
||||
// Started again (a new sign-in): its clients went with the
|
||||
// last one, so the endpoint is reused.
|
||||
let ep = match self.endpoint(p.name) {
|
||||
Some(ep) => ep,
|
||||
None => syscall::create_endpoint().ok_or("no endpoint")?,
|
||||
};
|
||||
// Read to receive on it; init keeps the original.
|
||||
let server = perms(&[Permission::Read, Permission::Execute]);
|
||||
syscall::delegate(ep, child, server).ok_or("cannot give its endpoint")?;
|
||||
@@ -87,8 +126,10 @@ impl<'a> Launcher<'a> {
|
||||
given.ok_or("cannot give an endpoint")?;
|
||||
}
|
||||
// Storage issues its own badges, each naming a scope.
|
||||
if let Some(scope) = p.storage {
|
||||
let grant = self.storage.grant(scope.path, scope.write).map_err(|_| "storage refused a grant")?;
|
||||
for scope in p.storage {
|
||||
let mut path = [0u8; types::storage::MAX_PATH];
|
||||
let path = self.scope_path(scope, &mut path).ok_or("a storage scope for nobody")?;
|
||||
let grant = self.storage.grant(path, scope.write).map_err(|_| "storage refused a grant")?;
|
||||
let given = syscall::delegate(grant, child, perms(&[Permission::Execute, Permission::Delegate]));
|
||||
syscall::drop_cap(grant);
|
||||
given.ok_or("cannot give storage")?;
|
||||
@@ -100,6 +141,14 @@ impl<'a> Launcher<'a> {
|
||||
let control = crate::devices::system_control().ok_or("no SystemControl to give")?;
|
||||
syscall::delegate(control, child, perms(&[Permission::Execute])).ok_or("cannot give SystemControl")?;
|
||||
}
|
||||
if p.signs_out {
|
||||
let badge = Badge::new(types::spawn::SESSION_BADGE);
|
||||
// Delegate only to hand it over: the shell's copy cannot pass it on.
|
||||
let copy = syscall::mint(self.spawn_ep, badge, perms(&[Permission::Execute, Permission::Delegate])).map_err(|_| "cannot mint")?;
|
||||
let given = syscall::delegate(copy, child, perms(&[Permission::Execute]));
|
||||
syscall::drop_cap(copy);
|
||||
given.ok_or("cannot give signing out")?;
|
||||
}
|
||||
let console = libkos::start::console()
|
||||
.filter(|_| p.console)
|
||||
.and_then(|c| syscall::delegate(c, child, perms(&[Permission::Write])));
|
||||
@@ -107,23 +156,34 @@ impl<'a> Launcher<'a> {
|
||||
|
||||
let mut note = "";
|
||||
if let Some(ep) = ep {
|
||||
if let Some(free) = self.served.iter_mut().find(|s| s.is_none()) {
|
||||
let known = self.served.iter().flatten().any(|(n, _)| *n == p.name);
|
||||
if let Some(free) = self.served.iter_mut().find(|s| s.is_none()).filter(|_| !known) {
|
||||
*free = Some((p.name, ep));
|
||||
}
|
||||
for device in p.devices {
|
||||
note = self.give(*device, ep);
|
||||
}
|
||||
if p.unlocks {
|
||||
let fresh = unlock(ep)?;
|
||||
crate::factory::prepare(self.storage, fresh, self.scratch)?;
|
||||
note = if fresh { ", the owner is in (a new key)" } else { ", the owner is in" };
|
||||
}
|
||||
if console.is_some() {
|
||||
hand_over_console(ep);
|
||||
note = ", console handed over";
|
||||
}
|
||||
}
|
||||
Ok(note)
|
||||
Ok(Launched { note, process: child, ep })
|
||||
}
|
||||
|
||||
/// The path on disk `scope` names: inside the user's folder if it is a
|
||||
/// user's scope.
|
||||
fn scope_path<'p>(&self, scope: &Scope, buf: &'p mut [u8; types::storage::MAX_PATH]) -> Option<&'p [u8]> {
|
||||
if !scope.user {
|
||||
return Some(scope.path);
|
||||
}
|
||||
let folder: UserFolder = self.user?.folder;
|
||||
let mut len = 0;
|
||||
for part in [&b"users/"[..], folder.as_bytes(), if scope.path.is_empty() { b"" } else { b"/" }, scope.path] {
|
||||
buf.get_mut(len..len + part.len())?.copy_from_slice(part);
|
||||
len += part.len();
|
||||
}
|
||||
Some(&buf[..len])
|
||||
}
|
||||
|
||||
/// A badged copy of init's endpoint: the badge names the client and so
|
||||
@@ -154,29 +214,6 @@ impl<'a> Launcher<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Waits for the gatekeeper behind `ep` to authenticate the owner, then
|
||||
/// passes the master key and a fresh seed on to storage, which alone keeps
|
||||
/// them. Whether the key is new.
|
||||
fn unlock(ep: CapSlot) -> Result<bool, &'static str> {
|
||||
use types::gatekeeper::{KEY, SEED, UNLOCK, reply};
|
||||
let fresh = match syscall::request(ep, [UNLOCK, 0, 0, 0]) {
|
||||
Ok([reply::OK, fresh, ..]) => fresh != 0,
|
||||
_ => return Err("the gatekeeper could not unlock"),
|
||||
};
|
||||
for (ask, give) in [(KEY, types::storage::op::SET_KEY), (SEED, types::storage::op::SET_SEED)] {
|
||||
let mut secret = syscall::request(ep, [ask, 0, 0, 0]).map_err(|_| "the gatekeeper gave no key")?;
|
||||
let given = [0, 1].iter().all(|&half| {
|
||||
let words = [give, half, secret[half * 2], secret[half * 2 + 1]];
|
||||
syscall::request(crate::STORAGE_EP, words).is_ok_and(|r| r[0] == types::storage::reply::OK)
|
||||
});
|
||||
secret.fill(0);
|
||||
if !given {
|
||||
return Err("storage would not take the key");
|
||||
}
|
||||
}
|
||||
Ok(fresh)
|
||||
}
|
||||
|
||||
/// From here on init prints through the log server, like everyone else,
|
||||
/// and holds no console of its own.
|
||||
fn hand_over_console(log: CapSlot) {
|
||||
|
||||
@@ -9,6 +9,7 @@ mod launch;
|
||||
mod load;
|
||||
mod manifest;
|
||||
mod selftest;
|
||||
mod session;
|
||||
mod spawn;
|
||||
|
||||
use libkos::fs::Storage;
|
||||
@@ -59,15 +60,20 @@ pub fn run() -> ! {
|
||||
// The test image carries the SMP self-test; it and the tests run
|
||||
// instead of the desktop, and the last one halts the machine.
|
||||
let (storage, scratch) = launcher.storage();
|
||||
if storage
|
||||
launcher.desktop = if storage
|
||||
.open(b"bin/smptest")
|
||||
.is_ok_and(|h| storage.close(h).is_ok())
|
||||
{
|
||||
libkos::println!("init: running the SMP self-test");
|
||||
selftest::run(storage, scratch);
|
||||
launch_all(&mut launcher, manifest::TESTS);
|
||||
manifest::TESTS
|
||||
} else {
|
||||
launch_all(&mut launcher, manifest::DESKTOP);
|
||||
manifest::DESKTOP
|
||||
};
|
||||
// Nothing that touches a user's files starts before someone is in.
|
||||
if let Err(why) = session::sign_in(&mut launcher) {
|
||||
libkos::println!("init: STOP ({})", why);
|
||||
idle();
|
||||
}
|
||||
|
||||
libkos::println!("init: all loaded, serving spawns");
|
||||
@@ -80,12 +86,7 @@ fn launch_all(launcher: &mut launch::Launcher, programs: &[manifest::Program]) {
|
||||
for p in programs {
|
||||
libkos::print!("init: loading {}... ", p.name);
|
||||
match launcher.launch(p) {
|
||||
Ok(note) => libkos::println!("ok{}", note),
|
||||
// Without the owner, nothing that touches user data may start.
|
||||
Err(why) if p.unlocks => {
|
||||
libkos::println!("STOP ({})", why);
|
||||
idle();
|
||||
}
|
||||
Ok(started) => libkos::println!("ok{}", started.note),
|
||||
Err(why) => libkos::println!("SKIP ({})", why),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,15 @@ use types::budget::Limits;
|
||||
|
||||
const PAGE_SIZE: usize = 4096;
|
||||
const USER_CODE_BASE: usize = 0x10000;
|
||||
const USER_STACK_PAGES: usize = 4;
|
||||
/// A program's stack, below its image, unless its manifest says more.
|
||||
pub(crate) const STACK_PAGES: usize = 4;
|
||||
/// The most there is room for under the image (page 0 stays unmapped).
|
||||
const MAX_STACK_PAGES: usize = USER_CODE_BASE / PAGE_SIZE - 1;
|
||||
const CREATE_CAP: CapSlot = CapSlot::const_new(0);
|
||||
/// Where a program file is read to before it is loaded (16 pages: the
|
||||
/// largest program init will load).
|
||||
const SCRATCH_VA: VirtualAddress = VirtualAddress::from_raw(0x30000);
|
||||
const SCRATCH_PAGES: u16 = 16;
|
||||
/// Where a program file is read to before it is loaded (32 pages: the
|
||||
/// largest program init will load; the gatekeeper is past 16).
|
||||
const SCRATCH_VA: VirtualAddress = VirtualAddress::from_raw(0x0120_0000);
|
||||
const SCRATCH_PAGES: u16 = 32;
|
||||
|
||||
pub(crate) struct LoadedProcess {
|
||||
pub(crate) process_cap: CapSlot,
|
||||
@@ -35,7 +38,9 @@ pub(crate) fn load_binary(
|
||||
scratch: &mut [u8],
|
||||
filename: &[u8],
|
||||
limits: Limits,
|
||||
stack_pages: usize,
|
||||
) -> Option<LoadedProcess> {
|
||||
let stack_pages = stack_pages.min(MAX_STACK_PAGES);
|
||||
let file_size = read_file(storage, scratch, filename)?;
|
||||
let elf_data = &scratch[..file_size];
|
||||
let elf_info = match elf::parse(elf_data) {
|
||||
@@ -71,10 +76,10 @@ pub(crate) fn load_binary(
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0..USER_STACK_PAGES {
|
||||
for i in 0..stack_pages {
|
||||
let page_count = PageCount::new(1)?;
|
||||
let stack_va = syscall::request_pages(page_count)?;
|
||||
let dst = VirtualAddress::from_raw(USER_CODE_BASE - (USER_STACK_PAGES - i) * PAGE_SIZE);
|
||||
let dst = VirtualAddress::from_raw(USER_CODE_BASE - (stack_pages - i) * PAGE_SIZE);
|
||||
syscall::map_page(process_cap, stack_va, dst, false);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,35 +12,45 @@ pub enum Device {
|
||||
}
|
||||
|
||||
/// A storage grant: a folder (or the whole disk, `b""`) and whether it may
|
||||
/// be changed.
|
||||
/// be changed. With `user`, the path is inside the signed-in user's folder.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Scope {
|
||||
pub path: &'static [u8],
|
||||
pub write: bool,
|
||||
pub user: bool,
|
||||
}
|
||||
|
||||
const fn disk(path: &'static [u8], write: bool) -> Scope {
|
||||
Scope { path, write, user: false }
|
||||
}
|
||||
|
||||
const fn users(path: &'static [u8], write: bool) -> Scope {
|
||||
Scope { path, write, user: true }
|
||||
}
|
||||
|
||||
pub struct Program {
|
||||
pub name: &'static str,
|
||||
pub file: &'static [u8],
|
||||
pub limits: Limits,
|
||||
/// Its stack, in pages.
|
||||
pub stack_pages: usize,
|
||||
/// Serves an endpoint, which it finds in slot 0.
|
||||
pub serves: bool,
|
||||
/// Handed over in this order, after it starts.
|
||||
pub devices: &'static [Device],
|
||||
/// Authenticates the owner: init waits for the master key before
|
||||
/// starting anything that touches user data.
|
||||
pub unlocks: bool,
|
||||
/// Earlier programs' endpoints it may call, in its next slots; each
|
||||
/// program's copy carries its own badge, so a server can tell them apart.
|
||||
pub uses: &'static [&'static str],
|
||||
/// Storage it may reach, in the slot after `uses`.
|
||||
pub storage: Option<Scope>,
|
||||
/// Storage it may reach, in the slots after `uses`.
|
||||
pub storage: &'static [Scope],
|
||||
/// Takes the console over from init; everyone else prints through it.
|
||||
pub console: bool,
|
||||
/// May have init spawn programs, whose limits together stay within this.
|
||||
pub spawns: Option<Limits>,
|
||||
/// May halt the machine.
|
||||
pub system_control: bool,
|
||||
/// May sign the user out (`types::spawn::SIGN_OUT`), in the last slot.
|
||||
pub signs_out: bool,
|
||||
}
|
||||
|
||||
/// Enough for a small program: its image, stack and a little heap.
|
||||
@@ -56,14 +66,15 @@ const fn server(name: &'static str, file: &'static [u8], limits: Limits) -> Prog
|
||||
name,
|
||||
file,
|
||||
limits,
|
||||
stack_pages: crate::load::STACK_PAGES,
|
||||
serves: true,
|
||||
devices: &[],
|
||||
unlocks: false,
|
||||
uses: &[],
|
||||
storage: None,
|
||||
storage: &[],
|
||||
console: false,
|
||||
spawns: None,
|
||||
system_control: false,
|
||||
signs_out: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,46 +91,52 @@ pub const SERVERS: &[Program] = &[
|
||||
},
|
||||
// Text on the display, lines from the keyboard (it takes input focus).
|
||||
Program { uses: &["display"], ..server("terminal", b"bin/terminal", SMALL) },
|
||||
// Nothing that touches user data starts before the owner is in. It
|
||||
// holds the screen (through the terminal), its keystore and randomness,
|
||||
// and exits once it has handed over the key.
|
||||
Program {
|
||||
uses: &["terminal"],
|
||||
devices: &[Device::Keystore, Device::Random],
|
||||
unlocks: true,
|
||||
// argon2's 16 MiB, its image and a little more.
|
||||
..server("gatekeeper", b"bin/gatekeeper", Limits { pages: 4096 + 160, regions: 6, ..SMALL })
|
||||
},
|
||||
// The only one that equips apps (docs/apps.md): it reaches every app's
|
||||
// files and the owner's decisions, and runs apps through init.
|
||||
Program {
|
||||
uses: &["terminal", "display"],
|
||||
storage: Some(Scope { path: b"", write: true }),
|
||||
// Room for a few apps at a time.
|
||||
spawns: Some(Limits { pages: 512, endpoints: 8, notifications: 8, regions: 8 }),
|
||||
..server("permission", b"bin/permission", Limits { pages: 96, ..SMALL })
|
||||
},
|
||||
server("session", b"bin/session", SMALL),
|
||||
];
|
||||
|
||||
/// Started for each sign-in, before anything that touches a user's files.
|
||||
/// It holds the screen (through the terminal), its keystore and randomness,
|
||||
/// and exits once it has handed over who is in and their key.
|
||||
pub const GATEKEEPER: Program = Program {
|
||||
uses: &["terminal"],
|
||||
devices: &[Device::Keystore, Device::Random],
|
||||
// A user's record is KiBs, and it handles a few at once.
|
||||
stack_pages: 12,
|
||||
// argon2's 16 MiB, its image and a little more.
|
||||
..server("gatekeeper", b"bin/gatekeeper", Limits { pages: 4096 + 160, regions: 6, ..SMALL })
|
||||
};
|
||||
|
||||
/// Started for each signed-in user, and stopped when they sign out. The
|
||||
/// permission manager is the only one that equips apps (docs/apps.md): it
|
||||
/// reaches the user's apps, their data and decisions, and the system's apps
|
||||
/// (read-only), and runs apps through init.
|
||||
pub const SESSION: &[Program] = &[Program {
|
||||
uses: &["terminal", "display"],
|
||||
storage: &[users(b"", true), disk(b"apps", false)],
|
||||
// Room for a few apps at a time.
|
||||
spawns: Some(Limits { pages: 512, endpoints: 8, notifications: 8, regions: 8 }),
|
||||
..server("permission", b"bin/permission", Limits { pages: 96, ..SMALL })
|
||||
}];
|
||||
|
||||
/// Run on the test image after the SMP self-test, instead of the desktop:
|
||||
/// spawns through init, then reports and halts.
|
||||
pub const TESTS: &[Program] = &[Program {
|
||||
serves: false,
|
||||
// The permission manager, to run an app the way the shell does.
|
||||
uses: &["permission"],
|
||||
storage: Some(Scope { path: b"", write: true }),
|
||||
storage: &[disk(b"", true), users(b"home", true)],
|
||||
spawns: Some(Limits { pages: 128, endpoints: 2, notifications: 2, regions: 2 }),
|
||||
system_control: true,
|
||||
..server("spawntest", b"bin/spawntest", SMALL)
|
||||
}];
|
||||
|
||||
/// The desktop; the test image runs the SMP self-test instead. The shell
|
||||
/// holds the owner's files and asks the permission manager to run apps;
|
||||
/// it holds nothing to give them beyond that.
|
||||
/// holds the user's files and asks the permission manager to run apps;
|
||||
/// it holds nothing to give them beyond that, and may sign the user out.
|
||||
pub const DESKTOP: &[Program] = &[Program {
|
||||
serves: false,
|
||||
uses: &["terminal", "permission"],
|
||||
storage: Some(Scope { path: b"home", write: true }),
|
||||
storage: &[users(b"home", true)],
|
||||
signs_out: true,
|
||||
..server("shell", b"bin/shell", SMALL)
|
||||
}];
|
||||
|
||||
@@ -39,7 +39,7 @@ fn start_spinner(storage: &mut Storage, scratch: &mut [u8], page: CapSlot, words
|
||||
for w in words {
|
||||
w.store(0, Ordering::SeqCst);
|
||||
}
|
||||
let spinner = crate::load::load_binary(storage, scratch, b"bin/smptest", crate::manifest::SMALL).ok_or("cannot load bin/smptest")?;
|
||||
let spinner = crate::load::load_binary(storage, scratch, b"bin/smptest", crate::manifest::SMALL, crate::load::STACK_PAGES).ok_or("cannot load bin/smptest")?;
|
||||
let rw = PermissionSet::empty()
|
||||
.with(Permission::Read)
|
||||
.with(Permission::Write)
|
||||
|
||||
163
crates/init/src/session.rs
Normal file
163
crates/init/src/session.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,17 @@ impl Spawner {
|
||||
}
|
||||
}
|
||||
|
||||
impl Spawner {
|
||||
/// Sign-out: every client goes, and every child is stopped.
|
||||
pub(crate) fn clear(&mut self) {
|
||||
for child in self.children.iter_mut().filter_map(Option::take) {
|
||||
let _ = syscall::kill(child.process);
|
||||
syscall::drop_cap(child.process);
|
||||
}
|
||||
self.allowances = [None; MAX_CLIENTS];
|
||||
}
|
||||
}
|
||||
|
||||
/// Where init maps a spawn request's file grant while it reads it.
|
||||
const PROGRAM_BUF_VA: VirtualAddress = VirtualAddress::from_raw(0x01A0_0000);
|
||||
|
||||
@@ -115,7 +126,7 @@ impl Launcher<'_> {
|
||||
// The grant is the file: its path inside it is empty.
|
||||
let found = file.open(b"").and_then(|h| file.close(h)).is_ok();
|
||||
let (_, scratch) = self.storage();
|
||||
let loaded = found.then(|| load::load_binary(&mut file, scratch, b"", r.limits)).flatten();
|
||||
let loaded = found.then(|| load::load_binary(&mut file, scratch, b"", r.limits, load::STACK_PAGES)).flatten();
|
||||
file.detach();
|
||||
let Some(loaded) = loaded else {
|
||||
return refused(if found { reply::ERROR } else { reply::NOT_FOUND });
|
||||
@@ -170,6 +181,12 @@ impl Server for Launcher<'_> {
|
||||
}
|
||||
|
||||
fn handle_call(&mut self, badge: usize, words: [usize; 4], cap: Option<CapSlot>) -> ([usize; 4], Option<libkos::syscall::Give>) {
|
||||
if badge == types::spawn::SESSION_BADGE {
|
||||
if words[0] == types::spawn::SIGN_OUT {
|
||||
crate::session::sign_out(self);
|
||||
}
|
||||
return ([MALFORMED, 0, 0, 0], None);
|
||||
}
|
||||
match (Request::decode(words), cap) {
|
||||
(Some(r), Some(binary)) => {
|
||||
let (reply, process) = self.spawn(badge, &r, binary);
|
||||
|
||||
@@ -141,6 +141,30 @@ impl Storage {
|
||||
self.with_path(op::RMDIR, path, reply::OK).map(|_| ())
|
||||
}
|
||||
|
||||
/// Init only, while nobody is signed in: removes the tree at `path`
|
||||
/// (in `users/`) without its key.
|
||||
pub fn purge(&mut self, path: &[u8]) -> Result<()> {
|
||||
self.with_path(op::PURGE, path, reply::OK).map(|_| ())
|
||||
}
|
||||
|
||||
/// Init only: whose areas the key about to be sent opens (see
|
||||
/// `types::user`).
|
||||
pub fn set_user(&mut self, id: u64, guest: bool) -> Result<()> {
|
||||
self.expect([op::SET_USER, id as usize, usize::from(guest), 0], reply::OK).map(|_| ())
|
||||
}
|
||||
|
||||
/// Init only: half (`half` 0 or 1) of the master key, or with `seed` of
|
||||
/// the seed for storage's randomness.
|
||||
pub fn set_half(&mut self, seed: bool, half: usize, words: [usize; 2]) -> Result<()> {
|
||||
let opcode = if seed { op::SET_SEED } else { op::SET_KEY };
|
||||
self.expect([opcode, half, words[0], words[1]], reply::OK).map(|_| ())
|
||||
}
|
||||
|
||||
/// Init only: the user signed out; every grant and file is gone.
|
||||
pub fn lock(&mut self) -> Result<()> {
|
||||
self.expect([op::LOCK, 0, 0, 0], reply::OK).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn close(&mut self, handle: FileHandle) -> Result<()> {
|
||||
self.expect([op::CLOSE, handle.0 as usize, 0, 0], reply::OK).map(|_| ())
|
||||
}
|
||||
|
||||
@@ -94,16 +94,21 @@ pub struct Unlocked {
|
||||
pub removed: [Option<UserId>; SLOTS],
|
||||
}
|
||||
|
||||
// Each step keeps its records and buffers (KiBs) to itself: inlined into
|
||||
// `run` together they would overflow the 16 KiB stack init gives a program.
|
||||
|
||||
/// The keystore's newest record, and where the next write goes.
|
||||
#[inline(never)]
|
||||
fn load(ks: &mut impl Keystore) -> Result<(Option<Record>, usize), Failure> {
|
||||
let (mut a, mut b) = ([0u8; RECORD_SIZE], [0u8; RECORD_SIZE]);
|
||||
let (mut a, mut b) = (alloc::boxed::Box::new([0u8; RECORD_SIZE]), alloc::boxed::Box::new([0u8; RECORD_SIZE]));
|
||||
if !ks.read(0, &mut a) || !ks.read(1, &mut b) {
|
||||
return Err(Failure::Device);
|
||||
}
|
||||
let copies = Copies { copies: [&a, &b] };
|
||||
let copies = Copies { copies: [&a[..], &b[..]] };
|
||||
Ok((copies.newest().map(|(r, _)| r), copies.next_copy()))
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
fn store(ks: &mut impl Keystore, copy: usize, r: &Record) -> Result<(), Failure> {
|
||||
if ks.write(copy, &r.encode()) { Ok(()) } else { Err(Failure::Device) }
|
||||
}
|
||||
@@ -117,6 +122,7 @@ fn save(ks: &mut impl Keystore, r: &Record) -> Result<(), Failure> {
|
||||
/// Destroys every slot and the directory: noise over both copies, twice,
|
||||
/// then blank (the device then looks never set up). Marked as wiping
|
||||
/// first, so a power cut part way is finished on the next boot.
|
||||
#[inline(never)]
|
||||
fn wipe(ks: &mut impl Keystore, record: &Record, next: usize, rng: &mut impl Random) -> Result<(), Failure> {
|
||||
let first = record.wiping(rng).ok_or(Failure::Random)?;
|
||||
let second = first.wiping(rng).ok_or(Failure::Random)?;
|
||||
@@ -175,6 +181,7 @@ fn ask_passcodes(prompt: &mut impl Prompt, real: &mut Secret, duress: &mut Secre
|
||||
}
|
||||
|
||||
/// A device never set up (or wiped): its first user, an owner.
|
||||
#[inline(never)]
|
||||
fn setup(ks: &mut impl Keystore, rng: &mut impl Random, prompt: &mut impl Prompt, params: KdfParams, memory: &mut [Block]) -> Result<Unlocked, Failure> {
|
||||
// After a wipe nothing earlier may show: it must look like a new device.
|
||||
prompt.clear();
|
||||
@@ -190,6 +197,7 @@ fn setup(ks: &mut impl Keystore, rng: &mut impl Random, prompt: &mut impl Prompt
|
||||
}
|
||||
|
||||
/// What the owner may change at the lock screen, after signing in.
|
||||
#[inline(never)]
|
||||
fn manage(ks: &mut impl Keystore, rng: &mut impl Random, prompt: &mut impl Prompt, memory: &mut [Block], session: &Session, removed: &mut [Option<UserId>; SLOTS]) -> Result<(), Failure> {
|
||||
loop {
|
||||
let (Some(record), _) = load(ks)? else { return Err(Failure::Device) };
|
||||
|
||||
@@ -22,18 +22,20 @@ use types::address::VirtualAddress;
|
||||
use types::bounded::CapSlot;
|
||||
|
||||
/// Where init's manifest put them: its own endpoint, then `uses`, then
|
||||
/// storage, then the spawn endpoint.
|
||||
/// storage (the user's folder, the system's apps), then the spawn endpoint.
|
||||
const EP: CapSlot = CapSlot::const_new(0);
|
||||
const TERMINAL: CapSlot = CapSlot::const_new(1);
|
||||
const DISPLAY: CapSlot = CapSlot::const_new(2);
|
||||
const STORAGE: CapSlot = CapSlot::const_new(3);
|
||||
const INIT: CapSlot = CapSlot::const_new(4);
|
||||
const USER: CapSlot = CapSlot::const_new(3);
|
||||
const SYSTEM: CapSlot = CapSlot::const_new(4);
|
||||
const INIT: CapSlot = CapSlot::const_new(5);
|
||||
|
||||
const STORAGE_BUF_VA: VirtualAddress = VirtualAddress::from_raw(0x0180_0000);
|
||||
const USER_BUF_VA: VirtualAddress = VirtualAddress::from_raw(0x0180_0000);
|
||||
const SOURCE_BUF_VA: VirtualAddress = VirtualAddress::from_raw(0x0190_0000);
|
||||
const SYSTEM_BUF_VA: VirtualAddress = VirtualAddress::from_raw(0x01B0_0000);
|
||||
|
||||
pub fn run() -> ! {
|
||||
let Ok(fs) = Storage::attach(STORAGE, STORAGE_BUF_VA) else {
|
||||
let (Ok(user), Ok(system)) = (Storage::attach(USER, USER_BUF_VA), Storage::attach(SYSTEM, SYSTEM_BUF_VA)) else {
|
||||
libkos::println!("permission: no storage, exiting");
|
||||
syscall::exit();
|
||||
};
|
||||
@@ -41,7 +43,7 @@ pub fn run() -> ! {
|
||||
libkos::println!("permission: no notification, exiting");
|
||||
syscall::exit();
|
||||
};
|
||||
let store = store::DiskStore::new(fs, SOURCE_BUF_VA);
|
||||
let store = store::DiskStore::new(user, system, SOURCE_BUF_VA);
|
||||
let consent = consent::TerminalConsent { terminal: TERMINAL };
|
||||
let mut srv = server::PermissionServer::new(store, consent, INIT, Some(DISPLAY), deaths);
|
||||
libkos::println!("permission: serving");
|
||||
|
||||
@@ -19,7 +19,7 @@ use types::start_arg::StartArg;
|
||||
|
||||
use crate::consent::{CapabilityNames, TerminalConsent, version};
|
||||
use crate::policy::{self, Refusal, Store};
|
||||
use crate::store::{self, DiskStore, PATH_BUF};
|
||||
use crate::store::DiskStore;
|
||||
|
||||
/// What each app gets to run in, out of the permission manager's allowance.
|
||||
const PROGRAM: Limits = Limits { pages: 128, endpoints: 1, notifications: 1, regions: 1 };
|
||||
@@ -164,9 +164,7 @@ impl PermissionServer {
|
||||
let plan = policy::plan_launch(&mut self.store, command, available).map_err(refused)?;
|
||||
let index = self.running.iter().position(Option::is_none).ok_or(code(reply::BUSY))?;
|
||||
|
||||
let mut p = [0u8; PATH_BUF];
|
||||
let binary = store::binary_path(&mut p, &plan.app).ok_or(code(reply::FAILED))?;
|
||||
let file = self.store.storage().grant(binary, false).map_err(|_| code(reply::FAILED))?;
|
||||
let file = self.store.binary_grant(&plan.app).ok_or(code(reply::FAILED))?;
|
||||
let spawned = libkos::spawn::spawn(self.init, file, PROGRAM);
|
||||
let _ = libkos::fs::release(file);
|
||||
let child = spawned.map_err(|e| match e {
|
||||
@@ -176,7 +174,7 @@ impl PermissionServer {
|
||||
|
||||
let call = PermissionSet::empty().with(Permission::Execute);
|
||||
let data = match plan.granted.contains(Capability::Storage) {
|
||||
true => store::data_path(&mut p, &plan.app.id).and_then(|d| self.store.storage().grant(d, true).ok()),
|
||||
true => self.store.data_grant(&plan.app.id),
|
||||
false => None,
|
||||
};
|
||||
let staged = (badge == self.staged.badge).then_some(&self.staged);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
//! The `Store` port on the storage server: apps in `/apps/<id>/`, their
|
||||
//! data in `/data/<id>/`, the owner's decisions in `/system/permissions/`.
|
||||
//! The `Store` port on the storage server, through two grants: the
|
||||
//! signed-in user's folder (their apps in `apps/<id>/`, each app's data in
|
||||
//! `data/<id>/`, their decisions in `permissions/`) and, read-only, the
|
||||
//! system image's apps (`/apps/<id>/`), which every user shares.
|
||||
|
||||
use libkos::fs::Storage;
|
||||
use manifest::{AppId, Decision, MAX_DECISION_TEXT, Manifest};
|
||||
@@ -10,18 +12,18 @@ use crate::policy::{MAX_MANIFEST, Store};
|
||||
|
||||
const APPS: &str = "apps";
|
||||
const DATA: &str = "data";
|
||||
const DECISIONS: &str = "system/permissions";
|
||||
const DECISIONS: &str = "permissions";
|
||||
const MANIFEST: &str = "manifest.toml";
|
||||
/// Apps `find` looks through.
|
||||
pub const MAX_APPS: usize = 32;
|
||||
const LONGEST_PATH: usize = 160;
|
||||
const COPY_CHUNK: usize = 4096;
|
||||
|
||||
/// `parts` joined with `/`, in `buf`.
|
||||
/// `parts` (the empty ones skipped) joined with `/`, in `buf`.
|
||||
fn path<'a>(buf: &'a mut [u8; LONGEST_PATH], parts: &[&str]) -> Option<&'a [u8]> {
|
||||
let mut len = 0;
|
||||
for (i, p) in parts.iter().enumerate() {
|
||||
let sep = usize::from(i > 0);
|
||||
for p in parts.iter().filter(|p| !p.is_empty()) {
|
||||
let sep = usize::from(len > 0);
|
||||
let end = len + sep + p.len();
|
||||
let room = buf.get_mut(len..end)?;
|
||||
room[..sep].copy_from_slice(&b"/"[..sep]);
|
||||
@@ -63,29 +65,69 @@ fn copy(src: &mut Storage, from: &[u8], dst: &mut Storage, to: &[u8]) -> bool {
|
||||
ok
|
||||
}
|
||||
|
||||
/// Where an app is installed: on the system image, or by the user.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Shelf {
|
||||
System,
|
||||
User,
|
||||
}
|
||||
|
||||
pub struct DiskStore {
|
||||
fs: Storage,
|
||||
/// The signed-in user's folder.
|
||||
user: Storage,
|
||||
/// The system image's apps, read-only.
|
||||
system: Storage,
|
||||
/// Where an offered app folder's grant is mapped while it is read.
|
||||
source_va: VirtualAddress,
|
||||
}
|
||||
|
||||
impl DiskStore {
|
||||
pub fn new(fs: Storage, source_va: VirtualAddress) -> Self {
|
||||
Self { fs, source_va }
|
||||
pub fn new(user: Storage, system: Storage, source_va: VirtualAddress) -> Self {
|
||||
Self { user, system, source_va }
|
||||
}
|
||||
|
||||
/// Storage itself, for grants on the app's behalf.
|
||||
pub fn storage(&mut self) -> &mut Storage {
|
||||
&mut self.fs
|
||||
/// The storage holding `shelf`, and its apps' folder in it.
|
||||
fn on(&mut self, shelf: Shelf) -> (&mut Storage, &'static str) {
|
||||
match shelf {
|
||||
Shelf::System => (&mut self.system, ""),
|
||||
Shelf::User => (&mut self.user, APPS),
|
||||
}
|
||||
}
|
||||
|
||||
/// The ids of the installed apps (folder names in `/apps`).
|
||||
/// The ids of the installed apps (folder names), the system's first.
|
||||
pub fn ids(&mut self, mut each: impl FnMut(AppId)) {
|
||||
let _ = self.fs.list_dir(APPS.as_bytes(), |e| {
|
||||
if let Some(id) = core::str::from_utf8(e.name()).ok().and_then(AppId::new) {
|
||||
each(id);
|
||||
}
|
||||
});
|
||||
for shelf in [Shelf::System, Shelf::User] {
|
||||
let (fs, apps) = self.on(shelf);
|
||||
let _ = fs.list_dir(apps.as_bytes(), |e| {
|
||||
if let Some(id) = core::str::from_utf8(e.name()).ok().and_then(AppId::new) {
|
||||
each(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Which shelf holds `id`'s manifest; the system's wins, so a user
|
||||
/// cannot stand in for a system app.
|
||||
fn shelf(&mut self, id: &AppId) -> Option<Shelf> {
|
||||
[Shelf::System, Shelf::User].into_iter().find(|shelf| {
|
||||
let (fs, apps) = self.on(*shelf);
|
||||
let mut p = [0u8; LONGEST_PATH];
|
||||
path(&mut p, &[apps, id.as_str(), MANIFEST]).is_some_and(|p| fs.open(p).and_then(|h| fs.close(h)).is_ok())
|
||||
})
|
||||
}
|
||||
|
||||
/// A read-only grant to `app`'s binary, for init to load it from.
|
||||
pub fn binary_grant(&mut self, app: &Manifest) -> Option<CapSlot> {
|
||||
let shelf = self.shelf(&app.id)?;
|
||||
let (fs, apps) = self.on(shelf);
|
||||
let mut p = [0u8; LONGEST_PATH];
|
||||
fs.grant(path(&mut p, &[apps, app.id.as_str(), app.binary.as_str()])?, false).ok()
|
||||
}
|
||||
|
||||
/// A read-write grant to `id`'s own data folder, for the app.
|
||||
pub fn data_grant(&mut self, id: &AppId) -> Option<CapSlot> {
|
||||
let mut p = [0u8; LONGEST_PATH];
|
||||
self.user.grant(path(&mut p, &[DATA, id.as_str()])?, true).ok()
|
||||
}
|
||||
|
||||
fn with_source<T>(&self, source: CapSlot, f: impl FnOnce(&mut Storage) -> T) -> Option<T> {
|
||||
@@ -108,9 +150,7 @@ impl Store for DiskStore {
|
||||
}
|
||||
|
||||
fn is_installed(&mut self, id: &AppId) -> bool {
|
||||
let mut p = [0u8; LONGEST_PATH];
|
||||
let Some(p) = path(&mut p, &[APPS, id.as_str(), MANIFEST]) else { return false };
|
||||
self.fs.open(p).and_then(|h| self.fs.close(h)).is_ok()
|
||||
self.shelf(id).is_some()
|
||||
}
|
||||
|
||||
fn install(&mut self, source: &CapSlot, app: &Manifest, data_folder: bool) -> bool {
|
||||
@@ -125,8 +165,8 @@ impl Store for DiskStore {
|
||||
};
|
||||
// There already if an earlier attempt failed part way; an app
|
||||
// counts as installed only once its manifest is in.
|
||||
let _ = self.fs.mkdir(folder);
|
||||
let fs = &mut self.fs;
|
||||
let _ = self.user.mkdir(folder);
|
||||
let fs = &mut self.user;
|
||||
let copied = Storage::attach(*source, self.source_va).ok().map(|mut src| {
|
||||
// The binary first: an app folder with a manifest is installed.
|
||||
let ok = copy(&mut src, app.binary.as_str().as_bytes(), fs, to_binary)
|
||||
@@ -139,14 +179,16 @@ impl Store for DiskStore {
|
||||
}
|
||||
let mut data = [0u8; LONGEST_PATH];
|
||||
// Kept from an earlier install if there is one: the app's data.
|
||||
!data_folder || path(&mut data, &[DATA, id]).is_some_and(|d| self.fs.mkdir(d).is_ok() || self.fs.list_dir(d, |_| {}).is_ok())
|
||||
!data_folder || path(&mut data, &[DATA, id]).is_some_and(|d| self.user.mkdir(d).is_ok() || self.user.list_dir(d, |_| {}).is_ok())
|
||||
}
|
||||
|
||||
fn manifest(&mut self, id: &AppId) -> Option<Manifest> {
|
||||
let shelf = self.shelf(id)?;
|
||||
let (fs, apps) = self.on(shelf);
|
||||
let mut p = [0u8; LONGEST_PATH];
|
||||
let p = path(&mut p, &[APPS, id.as_str(), MANIFEST])?;
|
||||
let p = path(&mut p, &[apps, id.as_str(), MANIFEST])?;
|
||||
let mut text = [0u8; MAX_MANIFEST];
|
||||
let m = Manifest::parse(read_text(&mut self.fs, p, &mut text)?).ok()?;
|
||||
let m = Manifest::parse(read_text(fs, p, &mut text)?).ok()?;
|
||||
// A manifest in the wrong folder is no app.
|
||||
(m.id == *id).then_some(m)
|
||||
}
|
||||
@@ -167,7 +209,7 @@ impl Store for DiskStore {
|
||||
let mut p = [0u8; LONGEST_PATH];
|
||||
let p = path(&mut p, &[DECISIONS, id.as_str()])?;
|
||||
let mut text = [0u8; MAX_DECISION_TEXT];
|
||||
Decision::parse(read_text(&mut self.fs, p, &mut text)?).ok()
|
||||
Decision::parse(read_text(&mut self.user, p, &mut text)?).ok()
|
||||
}
|
||||
|
||||
fn record(&mut self, id: &AppId, decision: &Decision) -> bool {
|
||||
@@ -176,21 +218,10 @@ impl Store for DiskStore {
|
||||
let mut text = [0u8; MAX_DECISION_TEXT];
|
||||
let n = decision.write(&mut text);
|
||||
// Replaced whole: there is no truncate.
|
||||
let _ = self.fs.delete(p);
|
||||
let Ok(h) = self.fs.create(p) else { return false };
|
||||
let written = self.fs.write(h, &text[..n]) == Ok(n);
|
||||
let _ = self.fs.close(h);
|
||||
let _ = self.user.delete(p);
|
||||
let Ok(h) = self.user.create(p) else { return false };
|
||||
let written = self.user.write(h, &text[..n]) == Ok(n);
|
||||
let _ = self.user.close(h);
|
||||
written
|
||||
}
|
||||
}
|
||||
|
||||
/// The path of app `id`'s binary and its data folder, for grants.
|
||||
pub fn binary_path<'a>(buf: &'a mut [u8; LONGEST_PATH], app: &Manifest) -> Option<&'a [u8]> {
|
||||
path(buf, &[APPS, app.id.as_str(), app.binary.as_str()])
|
||||
}
|
||||
|
||||
pub fn data_path<'a>(buf: &'a mut [u8; LONGEST_PATH], id: &AppId) -> Option<&'a [u8]> {
|
||||
path(buf, &[DATA, id.as_str()])
|
||||
}
|
||||
|
||||
pub const PATH_BUF: usize = LONGEST_PATH;
|
||||
|
||||
@@ -139,4 +139,14 @@ impl FileSystem for MockFileSystem {
|
||||
fn rmdir(&mut self, _path: &[u8]) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Flat: a tree is the files named under it.
|
||||
fn purge(&mut self, path: &[u8]) -> bool {
|
||||
for slot in self.files.iter_mut() {
|
||||
if slot.as_ref().is_some_and(|f| f.name.as_bytes().starts_with(path)) {
|
||||
*slot = None;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
//! The encrypted areas (see the vision's Encryption note), over any file
|
||||
//! system: each app's `data/<id>/`, the owner's `home/`, and `system/` are
|
||||
//! kept as `filecrypt` files under their own keys, derived from the
|
||||
//! master key; the rest (programs, `factory/`) is plaintext. Before the key
|
||||
//! arrives the encrypted areas refuse everything, and a plaintext file in
|
||||
//! one is refused too: nothing there is trusted unless it was sealed.
|
||||
//! The encrypted areas (see the vision's Encryption and Users notes), over
|
||||
//! any file system. Each user's folder `users/<id>/` holds their areas:
|
||||
//! `home/`, `permissions/`, and per app `apps/<app>/` and `data/<app>/`,
|
||||
//! each kept as `filecrypt` files under its own key, derived from that
|
||||
//! user's master key. Only the signed-in user's areas open; anyone else's
|
||||
//! are refused (and noise to their key anyway). The rest (programs,
|
||||
//! `factory/`) is plaintext. Signed out, every area refuses everything, and
|
||||
//! a plaintext file in one is refused too: nothing there is trusted unless
|
||||
//! it was sealed.
|
||||
|
||||
use filecrypt::{CHUNK, Drbg, HEADER, Header, Key, OVERHEAD, PLAIN_CHUNK, area_key, chunk_offset, chunks, plain_len};
|
||||
|
||||
@@ -22,15 +25,28 @@ enum Area<'a> {
|
||||
Neither,
|
||||
}
|
||||
|
||||
fn area(path: &[u8]) -> Area<'_> {
|
||||
/// Where `path` falls, and whose it is: the user folder's name.
|
||||
fn area(path: &[u8]) -> (Area<'_>, Option<&[u8]>) {
|
||||
let path = path.strip_prefix(b"/").unwrap_or(path);
|
||||
let mut parts = path.splitn(3, |b| *b == b'/');
|
||||
match (parts.next(), parts.next(), parts.next()) {
|
||||
(Some(b"home" | b"system"), _, _) => Area::Sealed(&path[..path.iter().position(|b| *b == b'/').unwrap_or(path.len())]),
|
||||
(Some(b"data"), Some(id), Some(rest)) if !id.is_empty() && !rest.is_empty() => Area::Sealed(&path[..5 + id.len()]),
|
||||
(Some(b"data"), _, _) => Area::Neither,
|
||||
_ => Area::Plain,
|
||||
let mut parts = path.splitn(5, |b| *b == b'/');
|
||||
let (users, user, kind, app, rest) = (parts.next(), parts.next(), parts.next(), parts.next(), parts.next());
|
||||
if users != Some(b"users") {
|
||||
return (Area::Plain, None);
|
||||
}
|
||||
// `users/` itself holds only the folders, which anyone may list.
|
||||
let Some(user) = user else { return (Area::Plain, None) };
|
||||
if user.is_empty() {
|
||||
return (Area::Neither, None);
|
||||
}
|
||||
// `users/<id>/` up to and including `kind`, and the app's folder.
|
||||
let upto = |n: usize| &path[..n];
|
||||
let base = 6 + user.len() + 1;
|
||||
let area = match (kind, app, rest) {
|
||||
(Some(k @ (b"home" | b"permissions")), _, _) => Area::Sealed(upto(base + k.len())),
|
||||
(Some(k @ (b"apps" | b"data")), Some(a), Some(r)) if !a.is_empty() && !r.is_empty() => Area::Sealed(upto(base + k.len() + 1 + a.len())),
|
||||
_ => Area::Neither,
|
||||
};
|
||||
(area, Some(user))
|
||||
}
|
||||
|
||||
/// An open encrypted file.
|
||||
@@ -44,15 +60,20 @@ struct Open {
|
||||
|
||||
pub struct Encrypted<F: FileSystem> {
|
||||
inner: F,
|
||||
/// The master key and randomness, once the gatekeeper has unlocked.
|
||||
/// The signed-in user's master key and folder name, and randomness.
|
||||
master: Option<Key>,
|
||||
user: [u8; USER_NAME],
|
||||
user_len: usize,
|
||||
rng: Option<Drbg>,
|
||||
open: [Option<Open>; MAX_HANDLES],
|
||||
}
|
||||
|
||||
/// A user folder's name: 16 hex digits, or `guest`.
|
||||
pub const USER_NAME: usize = 16;
|
||||
|
||||
impl<F: FileSystem> Encrypted<F> {
|
||||
pub fn new(inner: F) -> Self {
|
||||
Self { inner, master: None, rng: None, open: [const { None }; MAX_HANDLES] }
|
||||
Self { inner, master: None, user: [0; USER_NAME], user_len: 0, rng: None, open: [const { None }; MAX_HANDLES] }
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &F {
|
||||
@@ -68,21 +89,27 @@ impl<F: FileSystem> Encrypted<F> {
|
||||
self.inner
|
||||
}
|
||||
|
||||
/// The key of `path`'s area, if it is encrypted and the store unlocked:
|
||||
/// Whether `user`'s folder is the signed-in user's.
|
||||
fn signed_in(&self, user: Option<&[u8]>) -> bool {
|
||||
self.master.is_some() && user == Some(&self.user[..self.user_len])
|
||||
}
|
||||
|
||||
/// The key of `path`'s area, if it is encrypted and its user signed in:
|
||||
/// `Ok(None)` for a plaintext path, `Err` for one it must refuse.
|
||||
fn key_for(&self, path: &[u8]) -> Result<Option<Key>, ()> {
|
||||
match area(path) {
|
||||
Area::Plain => Ok(None),
|
||||
Area::Neither => Err(()),
|
||||
Area::Sealed(name) => self.master.as_ref().map(|m| Some(area_key(m, name))).ok_or(()),
|
||||
(Area::Plain, _) => Ok(None),
|
||||
(Area::Sealed(name), user) if self.signed_in(user) => Ok(self.master.as_ref().map(|m| area_key(m, name))),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `path` may be used as a directory (made, listed, removed).
|
||||
/// Whether `path` may be used as a directory (made, listed, removed):
|
||||
/// anywhere plaintext, and in the signed-in user's own folder.
|
||||
fn dir_allowed(&self, path: &[u8]) -> bool {
|
||||
match area(path) {
|
||||
Area::Plain => true,
|
||||
_ => self.master.is_some(),
|
||||
(Area::Plain, _) => true,
|
||||
(_, user) => self.signed_in(user),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,13 +142,33 @@ impl<F: FileSystem> FileSystem for Encrypted<F> {
|
||||
self.inner.writable()
|
||||
}
|
||||
|
||||
fn unlock(&mut self, master: &[u8; 32], seed: &[u8; 32]) {
|
||||
if self.master.is_none() {
|
||||
fn unlock(&mut self, master: &[u8; 32], seed: &[u8; 32], user: &[u8]) {
|
||||
if self.master.is_none() && !user.is_empty() && user.len() <= USER_NAME {
|
||||
self.master = Some(Key(*master));
|
||||
self.rng = Some(Drbg::new(*seed));
|
||||
self.user[..user.len()].copy_from_slice(user);
|
||||
self.user_len = user.len();
|
||||
}
|
||||
}
|
||||
|
||||
fn lock(&mut self) {
|
||||
for (i, slot) in self.open.iter_mut().enumerate() {
|
||||
if slot.take().is_some() {
|
||||
self.inner.close(FileHandle::new(i as u16));
|
||||
}
|
||||
}
|
||||
self.master = None;
|
||||
self.rng = None;
|
||||
self.user = [0; USER_NAME];
|
||||
self.user_len = 0;
|
||||
}
|
||||
|
||||
/// Removes a whole tree without opening anything in it: a removed
|
||||
/// user's or a guest's files go though no key for them is held.
|
||||
fn purge(&mut self, path: &[u8]) -> bool {
|
||||
crate::filesystem::remove_tree(&mut self.inner, path)
|
||||
}
|
||||
|
||||
fn open(&mut self, path: &[u8]) -> Option<FileHandle> {
|
||||
let key = self.key_for(path).ok()?;
|
||||
let h = self.inner.open(path)?;
|
||||
@@ -242,8 +289,8 @@ impl<F: FileSystem> FileSystem for Encrypted<F> {
|
||||
}
|
||||
let n = self.inner.list_dir(path, entries)?;
|
||||
// In an encrypted area a file's size is what it holds, not its disk size.
|
||||
// (Listing an app's own folder, `data/<id>`, shows its sealed files.)
|
||||
let sealed = area(path) != Area::Plain;
|
||||
// In a user's folder, a file's size is what it holds.
|
||||
let sealed = area(path).0 != Area::Plain;
|
||||
for e in entries[..n].iter_mut().filter(|e| sealed && !e.is_dir) {
|
||||
e.size = plain_len(e.size).unwrap_or(0);
|
||||
}
|
||||
@@ -265,7 +312,7 @@ impl<F: FileSystem> FileSystem for Encrypted<F> {
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn area_of(path: &[u8]) -> &'static str {
|
||||
match area(path) {
|
||||
match area(path).0 {
|
||||
Area::Plain => "plain",
|
||||
Area::Sealed(_) => "sealed",
|
||||
Area::Neither => "neither",
|
||||
|
||||
@@ -78,12 +78,55 @@ pub trait FileSystem {
|
||||
fn seek(&mut self, handle: FileHandle, offset: usize) -> bool;
|
||||
/// Removes the directory at `path`, which must be empty.
|
||||
fn rmdir(&mut self, path: &[u8]) -> bool;
|
||||
/// The master key and a fresh random seed, once the owner is in; only
|
||||
/// an encrypting file system has a use for them.
|
||||
fn unlock(&mut self, _master: &[u8; 32], _seed: &[u8; 32]) {}
|
||||
/// The signed-in user's master key, a fresh random seed, and the name
|
||||
/// of their folder; only an encrypting file system has a use for them.
|
||||
fn unlock(&mut self, _master: &[u8; 32], _seed: &[u8; 32], _user: &[u8]) {}
|
||||
/// The user signed out: their key is forgotten.
|
||||
fn lock(&mut self) {}
|
||||
/// Removes the tree at `path`, whatever is in it.
|
||||
fn purge(&mut self, path: &[u8]) -> bool {
|
||||
remove_tree(self, path)
|
||||
}
|
||||
/// Whether anything can be written at all; the server refuses
|
||||
/// writing requests up front when not.
|
||||
fn writable(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes everything under `path` and `path` itself, a directory or a
|
||||
/// file; true if nothing is left there.
|
||||
pub fn remove_tree<F: FileSystem + ?Sized>(fs: &mut F, path: &[u8]) -> bool {
|
||||
if fs.delete(path) {
|
||||
return true;
|
||||
}
|
||||
const BATCH: usize = 16;
|
||||
const LONGEST: usize = 255;
|
||||
loop {
|
||||
let mut entries = [DirEntry::empty(); BATCH];
|
||||
let Some(n) = fs.list_dir(path, &mut entries) else {
|
||||
// Neither a file nor a directory: nothing there.
|
||||
return true;
|
||||
};
|
||||
let children = entries[..n].iter().filter(|e| e.name.as_bytes() != b"." && e.name.as_bytes() != b"..");
|
||||
let mut any = false;
|
||||
for e in children {
|
||||
any = true;
|
||||
let mut child = [0u8; LONGEST];
|
||||
let name = e.name.as_bytes();
|
||||
let len = path.len() + 1 + name.len();
|
||||
if len > LONGEST {
|
||||
return false;
|
||||
}
|
||||
child[..path.len()].copy_from_slice(path);
|
||||
child[path.len()] = b'/';
|
||||
child[path.len() + 1..len].copy_from_slice(name);
|
||||
if !remove_tree(fs, &child[..len]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if !any {
|
||||
return fs.rmdir(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,16 @@ impl Grants {
|
||||
Some(cap)
|
||||
}
|
||||
|
||||
/// The grants init made directly, into `out`; how many.
|
||||
pub fn roots(&self, out: &mut [usize; MAX_GRANTS]) -> usize {
|
||||
let mut n = 0;
|
||||
for g in self.grants.iter().flatten().filter(|g| g.parent == 0) {
|
||||
out[n] = g.badge;
|
||||
n += 1;
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
/// Forgets `badge` and every grant narrowed from it, transitively,
|
||||
/// calling `gone` with each one's badge and capability.
|
||||
pub fn release(&mut self, badge: usize, mut gone: impl FnMut(usize, CapSlot)) {
|
||||
|
||||
@@ -30,6 +30,10 @@ pub enum StorageRequest {
|
||||
/// Half of the seed for randomness.
|
||||
SetSeed { half: usize, words: [usize; 2] },
|
||||
Rmdir { path_len: usize },
|
||||
/// Whose areas the key will open.
|
||||
SetUser { id: u64, guest: bool },
|
||||
Lock,
|
||||
Purge { path_len: usize },
|
||||
/// A capability to the path (`path_len` bytes of the buffer), inside
|
||||
/// the caller's scope.
|
||||
Grant {
|
||||
@@ -82,6 +86,9 @@ impl Encode for StorageRequest {
|
||||
SetKey { half, words } => [op::SET_KEY, half, words[0], words[1]],
|
||||
SetSeed { half, words } => [op::SET_SEED, half, words[0], words[1]],
|
||||
Rmdir { path_len } => [op::RMDIR, path_len, 0, 0],
|
||||
SetUser { id, guest } => [op::SET_USER, id as usize, usize::from(guest), 0],
|
||||
Lock => [op::LOCK, 0, 0, 0],
|
||||
Purge { path_len } => [op::PURGE, path_len, 0, 0],
|
||||
Grant { path_len, write } => [op::GRANT, path_len, usize::from(write), 0],
|
||||
Release => [op::RELEASE, 0, 0, 0],
|
||||
}
|
||||
@@ -106,6 +113,9 @@ impl Decode for StorageRequest {
|
||||
op::SET_KEY => SetKey { half: w[1], words: [w[2], w[3]] },
|
||||
op::SET_SEED => SetSeed { half: w[1], words: [w[2], w[3]] },
|
||||
op::RMDIR => Rmdir { path_len: w[1] },
|
||||
op::SET_USER => SetUser { id: w[1] as u64, guest: w[2] != 0 },
|
||||
op::LOCK => Lock,
|
||||
op::PURGE => Purge { path_len: w[1] },
|
||||
op::GRANT => Grant {
|
||||
path_len: w[1],
|
||||
write: w[2] != 0,
|
||||
|
||||
@@ -7,6 +7,8 @@ use crate::grants::Grants;
|
||||
use crate::protocol::{DIR_ENTRY_SIZE, DIR_NAME_SIZE, MAX_PATH, StorageRequest, StorageResponse};
|
||||
use crate::scope::Scope;
|
||||
|
||||
mod session;
|
||||
|
||||
const MAX_CLIENTS: usize = 8;
|
||||
/// Handles at or past this are never valid (the file systems hand out
|
||||
/// fewer), so ownership fits a small table.
|
||||
@@ -54,6 +56,8 @@ pub struct StorageServer<F: FileSystem, H: Host> {
|
||||
/// init; handed to the file system (and forgotten here) once all in.
|
||||
key: [Option<[usize; 2]>; 2],
|
||||
seed: [Option<[usize; 2]>; 2],
|
||||
/// Whose areas they open, said first.
|
||||
user: Option<types::user::UserFolder>,
|
||||
unlocked: bool,
|
||||
}
|
||||
|
||||
@@ -68,6 +72,7 @@ impl<F: FileSystem, H: Host> StorageServer<F, H> {
|
||||
log: None,
|
||||
key: [None; 2],
|
||||
seed: [None; 2],
|
||||
user: None,
|
||||
unlocked: false,
|
||||
}
|
||||
}
|
||||
@@ -91,27 +96,6 @@ impl<F: FileSystem, H: Host> StorageServer<F, H> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Only the unbadged holder (init), each half only once: nobody can
|
||||
/// replace the key or the seed later.
|
||||
fn set_half(&mut self, badge: usize, seed: bool, half: usize, words: [usize; 2]) -> StorageResponse {
|
||||
let halves = if seed { &mut self.seed } else { &mut self.key };
|
||||
match (badge, self.unlocked, halves.get_mut(half)) {
|
||||
(0, false, Some(slot @ None)) => *slot = Some(words),
|
||||
_ => return StorageResponse::Error,
|
||||
}
|
||||
if let ([Some(k0), Some(k1)], [Some(s0), Some(s1)]) = (self.key, self.seed) {
|
||||
let bytes = |w: [usize; 4]| -> [u8; 32] { core::array::from_fn(|i| w[i / 8].to_le_bytes()[i % 8]) };
|
||||
let (mut master, mut seed) = (bytes([k0[0], k0[1], k1[0], k1[1]]), bytes([s0[0], s0[1], s1[0], s1[1]]));
|
||||
self.fs.unlock(&master, &seed);
|
||||
master.fill(0);
|
||||
seed.fill(0);
|
||||
self.key = [None; 2];
|
||||
self.seed = [None; 2];
|
||||
self.unlocked = true;
|
||||
}
|
||||
StorageResponse::Ok
|
||||
}
|
||||
|
||||
/// Whether the encrypted areas are open: the key and seed are in.
|
||||
pub fn unlocked(&self) -> bool {
|
||||
self.unlocked
|
||||
@@ -300,7 +284,7 @@ impl<F: FileSystem, H: Host> StorageServer<F, H> {
|
||||
None => R::NotFound,
|
||||
};
|
||||
let response = match request {
|
||||
Attach | SetLog | SetKey { .. } | SetSeed { .. } | Release => R::Error,
|
||||
Attach | SetLog | SetKey { .. } | SetSeed { .. } | SetUser { .. } | Lock | Purge { .. } | Release => R::Error,
|
||||
Grant { path_len, write } => {
|
||||
let narrower = scope.narrow(&buf[sent(path_len)], write);
|
||||
return match narrower.and_then(|s| grants.add(badge, s, |b| host.mint(b))) {
|
||||
@@ -412,6 +396,9 @@ impl<F: FileSystem, H: Host> Server for StorageServer<F, H> {
|
||||
Some(StorageRequest::SetLog) => (self.set_log(badge, cap).encode(), None),
|
||||
Some(StorageRequest::SetKey { half, words }) => (self.set_half(badge, false, half, words).encode(), None),
|
||||
Some(StorageRequest::SetSeed { half, words }) => (self.set_half(badge, true, half, words).encode(), None),
|
||||
Some(StorageRequest::SetUser { id, guest }) => (self.set_user(badge, id, guest).encode(), None),
|
||||
Some(StorageRequest::Lock) => (self.lock(badge).encode(), None),
|
||||
Some(StorageRequest::Purge { path_len }) => (self.purge(badge, path_len).encode(), None),
|
||||
// A badge storage never issued, or one it has taken back.
|
||||
Some(request) => match self.grants.scope(badge) {
|
||||
None => (StorageResponse::Denied.encode(), None),
|
||||
|
||||
88
crates/servers/storage-server/src/server/session.rs
Normal file
88
crates/servers/storage-server/src/server/session.rs
Normal 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 }
|
||||
}
|
||||
}
|
||||
@@ -74,11 +74,25 @@ impl FileSystem for Memory {
|
||||
let (path, _) = self.open.get(&h.raw())?;
|
||||
Some(self.files[path.as_str()].len())
|
||||
}
|
||||
/// The direct children: files, and folders made or implied by a file.
|
||||
fn list_dir(&mut self, path: &[u8], entries: &mut [DirEntry]) -> Option<usize> {
|
||||
let prefix = std::format!("{}/", key(path));
|
||||
let dir = key(path);
|
||||
let prefix = std::format!("{dir}/");
|
||||
if !self.dirs.contains(&dir) && !self.files.keys().any(|k| k.starts_with(&prefix)) {
|
||||
return None;
|
||||
}
|
||||
let mut seen: Vec<String> = Vec::new();
|
||||
let mut n = 0;
|
||||
for (name, data) in self.files.iter().filter(|(k, _)| k.starts_with(&prefix)) {
|
||||
entries[n] = DirEntry { name: FileName::from_bytes(name[prefix.len()..].as_bytes()), size: data.len(), is_dir: false };
|
||||
let names = self.files.keys().map(|k| (k, true)).chain(self.dirs.iter().map(|d| (d, false)));
|
||||
for (name, file) in names.filter(|(k, _)| k.starts_with(&prefix)) {
|
||||
let rest = &name[prefix.len()..];
|
||||
let (child, deeper) = rest.split_once('/').map_or((rest, false), |(c, _)| (c, true));
|
||||
if seen.iter().any(|s| s == child) || n == entries.len() {
|
||||
continue;
|
||||
}
|
||||
seen.push(child.to_string());
|
||||
let size = if file && !deeper { self.files[name.as_str()].len() } else { 0 };
|
||||
entries[n] = DirEntry { name: FileName::from_bytes(child.as_bytes()), size, is_dir: deeper || !file };
|
||||
n += 1;
|
||||
}
|
||||
Some(n)
|
||||
@@ -87,14 +101,22 @@ impl FileSystem for Memory {
|
||||
self.files.remove(&key(path)).is_some()
|
||||
}
|
||||
fn mkdir(&mut self, path: &[u8]) -> bool {
|
||||
self.dirs.push(key(path));
|
||||
let k = key(path);
|
||||
if !self.dirs.contains(&k) {
|
||||
self.dirs.push(k);
|
||||
}
|
||||
true
|
||||
}
|
||||
/// Only an empty folder.
|
||||
fn rmdir(&mut self, path: &[u8]) -> bool {
|
||||
let k = key(path);
|
||||
let before = self.dirs.len();
|
||||
self.dirs.retain(|d| *d != k);
|
||||
self.dirs.len() < before
|
||||
let prefix = std::format!("{k}/");
|
||||
let empty = !self.files.keys().chain(self.dirs.iter()).any(|f| f.starts_with(&prefix));
|
||||
let was = self.dirs.contains(&k) || !empty;
|
||||
if empty {
|
||||
self.dirs.retain(|d| *d != k);
|
||||
}
|
||||
empty && was
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +124,7 @@ const MASTER: [u8; 32] = [9; 32];
|
||||
|
||||
fn unlocked() -> Encrypted<Memory> {
|
||||
let mut fs = Encrypted::new(Memory::default());
|
||||
fs.unlock(&MASTER, &[3; 32]);
|
||||
fs.unlock(&MASTER, &[3; 32], b"u1");
|
||||
fs
|
||||
}
|
||||
|
||||
@@ -129,17 +151,22 @@ fn on_disk(fs: &Encrypted<Memory>, path: &str) -> Vec<u8> {
|
||||
#[test]
|
||||
fn areas_are_told_by_path() {
|
||||
for (p, a) in [
|
||||
(&b"home/readme.txt"[..], "sealed"),
|
||||
(b"/home", "sealed"),
|
||||
(b"system/permissions/kos.snake", "sealed"),
|
||||
(b"data/kos.notes/a", "sealed"),
|
||||
(b"data/kos.notes", "neither"),
|
||||
(b"data", "neither"),
|
||||
(b"data/", "neither"),
|
||||
(&b"users/u1/home/readme.txt"[..], "sealed"),
|
||||
(b"/users/u1/home", "sealed"),
|
||||
(b"users/u1/permissions/kos.snake", "sealed"),
|
||||
(b"users/u1/data/kos.notes/a", "sealed"),
|
||||
(b"users/u1/apps/kos.notes/notes", "sealed"),
|
||||
(b"users/u1/data/kos.notes", "neither"),
|
||||
(b"users/u1/apps", "neither"),
|
||||
(b"users/u1/loose", "neither"),
|
||||
(b"users/u1", "neither"),
|
||||
(b"users//home", "neither"),
|
||||
(b"users", "plain"),
|
||||
(b"bin/shell", "plain"),
|
||||
(b"apps/kos.cat/cat", "plain"),
|
||||
(b"factory/home/readme.txt", "plain"),
|
||||
(b"homework/x", "plain"),
|
||||
(b"home/x", "plain"),
|
||||
(b"usersx/u1/home/x", "plain"),
|
||||
(b"selftest.txt", "plain"),
|
||||
] {
|
||||
assert_eq!(area_of(p), a, "{}", String::from_utf8_lossy(p));
|
||||
@@ -150,9 +177,9 @@ fn areas_are_told_by_path() {
|
||||
fn sealed_files_read_back_and_are_noise_on_disk() {
|
||||
let mut fs = unlocked();
|
||||
let text = b"Dear diary, nobody else may read this.";
|
||||
put(&mut fs, b"home/diary.txt", text);
|
||||
assert_eq!(get(&mut fs, b"home/diary.txt").unwrap(), text);
|
||||
let disk = on_disk(&fs, "home/diary.txt");
|
||||
put(&mut fs, b"users/u1/home/diary.txt", text);
|
||||
assert_eq!(get(&mut fs, b"users/u1/home/diary.txt").unwrap(), text);
|
||||
let disk = on_disk(&fs, "users/u1/home/diary.txt");
|
||||
assert!(disk.starts_with(b"KOSFILE1"));
|
||||
assert!(!disk.windows(5).any(|w| w == b"diary"), "the words are not on the disk");
|
||||
assert_eq!(disk.len(), filecrypt::disk_len(text.len()));
|
||||
@@ -170,37 +197,37 @@ fn plaintext_areas_stay_plaintext() {
|
||||
fn big_files_and_writes_across_chunks_round_trip() {
|
||||
let mut fs = unlocked();
|
||||
let big: Vec<u8> = (0..3 * filecrypt::PLAIN_CHUNK + 123).map(|i| (i % 251) as u8).collect();
|
||||
put(&mut fs, b"data/kos.notes/big", &big);
|
||||
assert_eq!(get(&mut fs, b"data/kos.notes/big").unwrap(), big);
|
||||
put(&mut fs, b"users/u1/data/kos.notes/big", &big);
|
||||
assert_eq!(get(&mut fs, b"users/u1/data/kos.notes/big").unwrap(), big);
|
||||
// Overwrite a stretch spanning two chunks in the middle.
|
||||
let h = fs.open(b"data/kos.notes/big").unwrap();
|
||||
let h = fs.open(b"users/u1/data/kos.notes/big").unwrap();
|
||||
let at = filecrypt::PLAIN_CHUNK - 10;
|
||||
assert!(fs.seek(h, at));
|
||||
assert_eq!(fs.write(h, &[0xAA; 30]), Some(30));
|
||||
fs.close(h);
|
||||
let mut expect = big.clone();
|
||||
expect[at..at + 30].fill(0xAA);
|
||||
assert_eq!(get(&mut fs, b"data/kos.notes/big").unwrap(), expect);
|
||||
assert_eq!(get(&mut fs, b"users/u1/data/kos.notes/big").unwrap(), expect);
|
||||
// Grow exactly from a chunk boundary: the old last chunk stops being last.
|
||||
let mut fs = unlocked();
|
||||
let full = vec![7u8; filecrypt::PLAIN_CHUNK];
|
||||
put(&mut fs, b"home/f", &full);
|
||||
let h = fs.open(b"home/f").unwrap();
|
||||
put(&mut fs, b"users/u1/home/f", &full);
|
||||
let h = fs.open(b"users/u1/home/f").unwrap();
|
||||
assert!(fs.seek(h, filecrypt::PLAIN_CHUNK));
|
||||
assert_eq!(fs.write(h, b"more"), Some(4));
|
||||
fs.close(h);
|
||||
let mut expect = full.clone();
|
||||
expect.extend_from_slice(b"more");
|
||||
assert_eq!(get(&mut fs, b"home/f").unwrap(), expect);
|
||||
assert_eq!(get(&mut fs, b"users/u1/home/f").unwrap(), expect);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_app_cannot_read_anothers_file_even_moved_into_its_folder() {
|
||||
let mut fs = unlocked();
|
||||
put(&mut fs, b"data/kos.notes/secret", b"notes only");
|
||||
let stolen = on_disk(&fs, "data/kos.notes/secret");
|
||||
fs.inner_mut_for_test().files.insert("data/kos.chat/secret".to_string(), stolen);
|
||||
let h = fs.open(b"data/kos.chat/secret").expect("it looks like a sealed file");
|
||||
put(&mut fs, b"users/u1/data/kos.notes/secret", b"notes only");
|
||||
let stolen = on_disk(&fs, "users/u1/data/kos.notes/secret");
|
||||
fs.inner_mut_for_test().files.insert("users/u1/data/kos.chat/secret".to_string(), stolen);
|
||||
let h = fs.open(b"users/u1/data/kos.chat/secret").expect("it looks like a sealed file");
|
||||
let mut out = [0u8; 64];
|
||||
assert_eq!(fs.read(h, &mut out), None, "but it opens only under kos.notes' key");
|
||||
}
|
||||
@@ -208,68 +235,117 @@ fn one_app_cannot_read_anothers_file_even_moved_into_its_folder() {
|
||||
#[test]
|
||||
fn locked_the_areas_refuse_everything() {
|
||||
let mut fs = unlocked();
|
||||
put(&mut fs, b"home/x", b"y");
|
||||
put(&mut fs, b"users/u1/home/x", b"y");
|
||||
let mut locked = Encrypted::new(fs.into_inner());
|
||||
assert!(locked.open(b"home/x").is_none());
|
||||
assert!(locked.create(b"home/z").is_none());
|
||||
assert!(!locked.delete(b"home/x"));
|
||||
assert!(!locked.mkdir(b"data/kos.notes"));
|
||||
assert!(locked.list_dir(b"home", &mut [DirEntry::empty(); 4]).is_none());
|
||||
assert!(locked.open(b"users/u1/home/x").is_none());
|
||||
assert!(locked.create(b"users/u1/home/z").is_none());
|
||||
assert!(!locked.delete(b"users/u1/home/x"));
|
||||
assert!(!locked.mkdir(b"users/u1/data/kos.notes"));
|
||||
assert!(locked.list_dir(b"users/u1/home", &mut [DirEntry::empty(); 4]).is_none());
|
||||
assert!(locked.open(b"bin/anything").is_none(), "(absent) but plaintext areas are not refused");
|
||||
// Another master key (a wiped and re-set-up device) reads noise.
|
||||
let mut other = Encrypted::new(locked.into_inner());
|
||||
other.unlock(&[1; 32], &[2; 32]);
|
||||
let h = other.open(b"home/x").unwrap();
|
||||
other.unlock(&[1; 32], &[2; 32], b"u1");
|
||||
let h = other.open(b"users/u1/home/x").unwrap();
|
||||
assert_eq!(other.read(h, &mut [0u8; 8]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plaintext_planted_in_an_encrypted_area_is_refused() {
|
||||
let mut fs = unlocked();
|
||||
fs.inner_mut_for_test().files.insert("system/permissions/kos.evil".to_string(), b"[decision]\ngranted = [\"network\"]\n".to_vec());
|
||||
assert!(fs.open(b"system/permissions/kos.evil").is_none());
|
||||
fs.inner_mut_for_test().files.insert("users/u1/permissions/kos.evil".to_string(), b"[decision]\ngranted = [\"network\"]\n".to_vec());
|
||||
assert!(fs.open(b"users/u1/permissions/kos.evil").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tampering_and_truncation_are_caught() {
|
||||
let mut fs = unlocked();
|
||||
let data = vec![5u8; 2 * filecrypt::PLAIN_CHUNK + 50];
|
||||
put(&mut fs, b"home/t", &data);
|
||||
let good = on_disk(&fs, "home/t");
|
||||
put(&mut fs, b"users/u1/home/t", &data);
|
||||
let good = on_disk(&fs, "users/u1/home/t");
|
||||
// One flipped byte.
|
||||
let mut bent = good.clone();
|
||||
bent[100] ^= 1;
|
||||
fs.inner_mut_for_test().files.insert("home/t".to_string(), bent);
|
||||
assert_eq!(get(&mut fs, b"home/t"), None);
|
||||
fs.inner_mut_for_test().files.insert("users/u1/home/t".to_string(), bent);
|
||||
assert_eq!(get(&mut fs, b"users/u1/home/t"), None);
|
||||
// Cut at a chunk boundary: the remaining last chunk was not sealed as last.
|
||||
let cut = good[..filecrypt::chunk_offset(2)].to_vec();
|
||||
fs.inner_mut_for_test().files.insert("home/t".to_string(), cut);
|
||||
assert_eq!(get(&mut fs, b"home/t"), None);
|
||||
fs.inner_mut_for_test().files.insert("users/u1/home/t".to_string(), cut);
|
||||
assert_eq!(get(&mut fs, b"users/u1/home/t"), None);
|
||||
// Chunks swapped.
|
||||
let (a, b) = (filecrypt::chunk_offset(0), filecrypt::chunk_offset(1));
|
||||
let mut swapped = good.clone();
|
||||
swapped[a..b].copy_from_slice(&good[b..b + filecrypt::CHUNK]);
|
||||
swapped[b..b + filecrypt::CHUNK].copy_from_slice(&good[a..b]);
|
||||
fs.inner_mut_for_test().files.insert("home/t".to_string(), swapped);
|
||||
assert_eq!(get(&mut fs, b"home/t"), None);
|
||||
fs.inner_mut_for_test().files.insert("users/u1/home/t".to_string(), swapped);
|
||||
assert_eq!(get(&mut fs, b"users/u1/home/t"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listings_show_what_files_hold() {
|
||||
let mut fs = unlocked();
|
||||
put(&mut fs, b"home/a", b"12345");
|
||||
put(&mut fs, b"users/u1/home/a", b"12345");
|
||||
let mut entries = [DirEntry::empty(); 4];
|
||||
let n = fs.list_dir(b"home", &mut entries).unwrap();
|
||||
let n = fs.list_dir(b"users/u1/home", &mut entries).unwrap();
|
||||
assert_eq!((n, entries[0].size), (1, 5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn files_cannot_live_loose_in_data() {
|
||||
fn files_cannot_live_loose_in_a_users_folder() {
|
||||
let mut fs = unlocked();
|
||||
assert!(fs.create(b"data/loose").is_none());
|
||||
assert!(fs.mkdir(b"data/kos.notes"), "an app's folder is fine");
|
||||
assert!(fs.create(b"users/u1/data/loose").is_none());
|
||||
assert!(fs.create(b"users/u1/loose").is_none());
|
||||
assert!(fs.create(b"users/loose").is_none());
|
||||
assert!(fs.mkdir(b"users/u1/data/kos.notes"), "an app's folder is fine");
|
||||
let mut entries = [DirEntry::empty(); 2];
|
||||
put(&mut fs, b"data/kos.notes/a", b"xyz");
|
||||
let n = fs.list_dir(b"data/kos.notes", &mut entries).unwrap();
|
||||
put(&mut fs, b"users/u1/data/kos.notes/a", b"xyz");
|
||||
let n = fs.list_dir(b"users/u1/data/kos.notes", &mut entries).unwrap();
|
||||
assert_eq!((n, entries[0].size), (1, 3), "listing the folder shows what its files hold");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_the_signed_in_users_folder_opens() {
|
||||
let mut fs = unlocked();
|
||||
put(&mut fs, b"users/u1/home/mine", b"u1's");
|
||||
let mut inner = fs.into_inner();
|
||||
let theirs = inner.files["users/u1/home/mine"].clone();
|
||||
let mut u2 = Encrypted::new(std::mem::take(&mut inner));
|
||||
u2.unlock(&MASTER, &[3; 32], b"u2");
|
||||
assert!(u2.open(b"users/u1/home/mine").is_none(), "not someone else's, even under the same key");
|
||||
assert!(u2.create(b"users/u1/home/planted").is_none());
|
||||
assert!(!u2.mkdir(b"users/u1/home/sub"));
|
||||
assert!(u2.list_dir(b"users/u1/home", &mut [DirEntry::empty(); 4]).is_none());
|
||||
assert!(!u2.delete(b"users/u1/home/mine"));
|
||||
// Moved into u2's own home, it is u1's area key that sealed it.
|
||||
u2.inner_mut_for_test().files.insert("users/u2/home/mine".to_string(), theirs);
|
||||
let h = u2.open(b"users/u2/home/mine").unwrap();
|
||||
assert_eq!(u2.read(h, &mut [0u8; 8]), None);
|
||||
assert!(u2.list_dir(b"users", &mut [DirEntry::empty(); 4]).is_some(), "the folders themselves are no secret");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn locking_forgets_the_key_and_closes_sealed_files() {
|
||||
let mut fs = unlocked();
|
||||
put(&mut fs, b"users/u1/home/a", b"abc");
|
||||
let h = fs.open(b"users/u1/home/a").unwrap();
|
||||
fs.lock();
|
||||
assert_eq!(fs.read(h, &mut [0u8; 4]), None, "the open file went with the key");
|
||||
assert!(fs.open(b"users/u1/home/a").is_none());
|
||||
fs.unlock(&MASTER, &[3; 32], b"u1");
|
||||
assert_eq!(get(&mut fs, b"users/u1/home/a").unwrap(), b"abc", "and comes back with it");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn purging_removes_a_tree_without_its_key() {
|
||||
let mut fs = unlocked();
|
||||
put(&mut fs, b"users/u1/home/a", b"1");
|
||||
put(&mut fs, b"users/u1/data/kos.notes/b", b"2");
|
||||
assert!(fs.mkdir(b"users/u1/home/empty"));
|
||||
fs.lock();
|
||||
fs.inner_mut_for_test().files.insert("users/u2x/home/c".to_string(), b"3".to_vec());
|
||||
assert!(fs.purge(b"users/u1"));
|
||||
let left: Vec<&String> = fs.inner().files.keys().collect();
|
||||
assert_eq!(left, ["users/u2x/home/c"]);
|
||||
assert!(!fs.inner().dirs.iter().any(|d| d.starts_with("users/u1")), "folders too");
|
||||
assert!(fs.purge(b"users/nobody"), "nothing there is nothing to remove");
|
||||
}
|
||||
|
||||
@@ -224,17 +224,19 @@ fn sealed_files_on_real_ext4_read_back_and_pass_e2fsck() {
|
||||
use crate::encrypted::Encrypted;
|
||||
let Some((dir, img)) = setup("sealed") else { return };
|
||||
let mut fs = Encrypted::new(Ext4::mount(FileDisk(img)).expect("mount"));
|
||||
fs.unlock(&[4; 32], &[5; 32]);
|
||||
assert!(fs.mkdir(b"/home"));
|
||||
fs.unlock(&[4; 32], &[5; 32], b"u1");
|
||||
for dir in [&b"/users"[..], b"/users/u1", b"/users/u1/home"] {
|
||||
assert!(fs.mkdir(dir));
|
||||
}
|
||||
let text: Vec<u8> = b"the quick brown fox ".iter().copied().cycle().take(3 * filecrypt::PLAIN_CHUNK + 99).collect();
|
||||
let h = fs.create(b"/home/fox.txt").expect("create");
|
||||
let h = fs.create(b"/users/u1/home/fox.txt").expect("create");
|
||||
assert_eq!(fs.write(h, &text), Some(text.len()));
|
||||
assert!(fs.seek(h, 5000));
|
||||
assert_eq!(fs.write(h, b"LAZY DOG"), Some(8));
|
||||
fs.close(h);
|
||||
let mut expect = text.clone();
|
||||
expect[5000..5008].copy_from_slice(b"LAZY DOG");
|
||||
let h = fs.open(b"/home/fox.txt").expect("open");
|
||||
let h = fs.open(b"/users/u1/home/fox.txt").expect("open");
|
||||
let mut back = std::vec![0u8; expect.len()];
|
||||
assert_eq!(fs.read(h, &mut back), Some(expect.len()));
|
||||
fs.close(h);
|
||||
@@ -244,3 +246,29 @@ fn sealed_files_on_real_ext4_read_back_and_pass_e2fsck() {
|
||||
let (clean, report) = fsck_clean(&dir, &disk);
|
||||
assert!(clean, "e2fsck: {report}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn purging_a_users_tree_on_real_ext4_passes_e2fsck() {
|
||||
use crate::encrypted::Encrypted;
|
||||
let Some((dir, img)) = setup("purge") else { return };
|
||||
let mut fs = Encrypted::new(Ext4::mount(FileDisk(img)).expect("mount"));
|
||||
fs.unlock(&[4; 32], &[5; 32], b"u1");
|
||||
for d in [&b"/users"[..], b"/users/u1", b"/users/u1/home", b"/users/u1/home/deep", b"/users/u1/data", b"/users/u1/data/kos.notes"] {
|
||||
assert!(fs.mkdir(d));
|
||||
}
|
||||
// More files than one listing batch holds.
|
||||
for i in 0..20u8 {
|
||||
let name = std::format!("/users/u1/home/deep/f{i}");
|
||||
let h = fs.create(name.as_bytes()).expect("create");
|
||||
assert_eq!(fs.write(h, &[i; 100]), Some(100));
|
||||
fs.close(h);
|
||||
}
|
||||
let h = fs.create(b"/users/u1/data/kos.notes/n").expect("create");
|
||||
fs.close(h);
|
||||
assert!(!fs.delete(b"/users/u1/home"), "a folder is not deleted as a file");
|
||||
fs.lock();
|
||||
assert!(fs.purge(b"/users/u1"));
|
||||
assert!(fs.list_dir(b"/users", &mut [crate::filesystem::DirEntry::empty(); 4]).is_some_and(|n| n <= 2), "only . and ..");
|
||||
let (clean, report) = fsck_clean(&dir, &fs.into_inner().into_device().0);
|
||||
assert!(clean, "e2fsck: {report}");
|
||||
}
|
||||
|
||||
@@ -479,6 +479,11 @@ fn only_init_unlocks_and_only_once() {
|
||||
let set = |s: &mut Srv, badge, req| call(s, badge, req).0;
|
||||
let key = |half| StorageRequest::SetKey { half, words: [1, 2] };
|
||||
let seed = |half| StorageRequest::SetSeed { half, words: [3, 4] };
|
||||
let user = StorageRequest::SetUser { id: 7, guest: false };
|
||||
assert_eq!(set(&mut s, 0, key(0)), StorageResponse::Error, "not before saying whose");
|
||||
assert_eq!(set(&mut s, 5, user), StorageResponse::Error, "a client cannot say");
|
||||
assert_eq!(set(&mut s, 0, user), StorageResponse::Ok);
|
||||
assert_eq!(set(&mut s, 0, user), StorageResponse::Error, "once");
|
||||
assert_eq!(set(&mut s, 5, key(0)), StorageResponse::Error, "a client cannot");
|
||||
assert_eq!(set(&mut s, 0, key(0)), StorageResponse::Ok);
|
||||
assert_eq!(set(&mut s, 0, key(0)), StorageResponse::Error, "nor can a half be replaced");
|
||||
@@ -490,3 +495,40 @@ fn only_init_unlocks_and_only_once() {
|
||||
assert!(s.unlocked());
|
||||
assert_eq!(set(&mut s, 0, key(0)), StorageResponse::Error, "once unlocked, never again");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn locking_takes_every_grant_back_and_lets_the_next_user_in() {
|
||||
let mut s = server();
|
||||
let set = |s: &mut Srv, badge, req| call(s, badge, req).0;
|
||||
let init = Client::attach(&mut s, 0);
|
||||
let shell = init.narrowed(&mut s, b"", true);
|
||||
let app = shell.narrowed(&mut s, b"", false);
|
||||
for req in [StorageRequest::SetUser { id: 1, guest: false }, StorageRequest::SetKey { half: 0, words: [1, 2] }, StorageRequest::SetKey { half: 1, words: [1, 2] }, StorageRequest::SetSeed { half: 0, words: [1, 2] }, StorageRequest::SetSeed { half: 1, words: [1, 2] }] {
|
||||
assert_eq!(set(&mut s, 0, req), StorageResponse::Ok);
|
||||
}
|
||||
assert!(s.unlocked());
|
||||
assert_eq!(set(&mut s, shell.badge, StorageRequest::Lock), StorageResponse::Error, "only init signs out");
|
||||
assert_eq!(set(&mut s, 0, StorageRequest::Lock), StorageResponse::Ok);
|
||||
assert!(!s.unlocked());
|
||||
let revoked: Vec<usize> = s.host().revoked.iter().map(|c| badge_of(*c)).collect();
|
||||
assert_eq!(revoked, [shell.badge, app.badge]);
|
||||
assert_eq!(set(&mut s, shell.badge, StorageRequest::FileSize { handle: crate::filesystem::FileHandle::new(0) }), StorageResponse::Denied);
|
||||
assert_eq!(set(&mut s, 0, StorageRequest::SetUser { id: 2, guest: true }), StorageResponse::Ok, "someone else may sign in");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_init_purges_only_users_and_only_while_locked() {
|
||||
let mut s = server();
|
||||
let init = Client::attach(&mut s, 0);
|
||||
let shell = init.narrowed(&mut s, b"", true);
|
||||
let purge = |s: &mut Srv, badge: usize, path: &[u8]| {
|
||||
init.put(s, path);
|
||||
call(s, badge, StorageRequest::Purge { path_len: path.len() }).0
|
||||
};
|
||||
assert_eq!(purge(&mut s, shell.badge, b"users/x"), StorageResponse::Error);
|
||||
assert_eq!(purge(&mut s, 0, b"bin"), StorageResponse::Denied);
|
||||
assert_eq!(purge(&mut s, 0, b"users/../bin"), StorageResponse::Denied);
|
||||
assert_eq!(purge(&mut s, 0, b"users/"), StorageResponse::Denied);
|
||||
assert_eq!(purge(&mut s, 0, b"users/x"), StorageResponse::Ok);
|
||||
assert_eq!(purge(&mut s, 0, b"users"), StorageResponse::Ok);
|
||||
}
|
||||
|
||||
@@ -103,6 +103,10 @@ impl server::Console for Screen {
|
||||
fn interrupt(&mut self, notification: CapSlot) {
|
||||
let _ = syscall::signal(notification);
|
||||
}
|
||||
|
||||
fn forget(&mut self, notification: CapSlot) {
|
||||
syscall::drop_cap(notification);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run() -> ! {
|
||||
|
||||
@@ -24,6 +24,8 @@ pub trait Console {
|
||||
/// Takes input focus back from whoever took it, and clears the screen.
|
||||
fn take_focus(&mut self);
|
||||
fn interrupt(&mut self, notification: CapSlot);
|
||||
/// Lets go of a notification it was given.
|
||||
fn forget(&mut self, notification: CapSlot);
|
||||
}
|
||||
|
||||
pub const BACKSPACE: u8 = 8;
|
||||
@@ -232,7 +234,7 @@ impl<C: Console> Server for TerminalServer<C> {
|
||||
Done
|
||||
}
|
||||
|
||||
fn handle_call(&mut self, _badge: usize, words: [usize; 4], cap: Option<CapSlot>) -> ([usize; 4], Option<libkos::syscall::Give>) {
|
||||
fn handle_call(&mut self, badge: usize, words: [usize; 4], cap: Option<CapSlot>) -> ([usize; 4], Option<libkos::syscall::Give>) {
|
||||
let reply = match words[0] {
|
||||
types::terminal::READ => self.read(false),
|
||||
types::terminal::READ_SECRET => self.read(true),
|
||||
@@ -252,6 +254,13 @@ impl<C: Console> Server for TerminalServer<C> {
|
||||
}
|
||||
_ => [MALFORMED, 0, 0, 0],
|
||||
},
|
||||
types::terminal::SIGNED_OUT if badge == 0 => {
|
||||
if let Some(n) = self.on_interrupt.take() {
|
||||
self.console.forget(n);
|
||||
}
|
||||
[0; 4]
|
||||
}
|
||||
types::terminal::SIGNED_OUT => [MALFORMED, 0, 0, 0],
|
||||
types::terminal::CLEAR => {
|
||||
self.handle(Request::Clear);
|
||||
[0; 4]
|
||||
|
||||
@@ -20,6 +20,7 @@ struct Fake {
|
||||
/// Every row drawn, with how many cells.
|
||||
rows_drawn: Vec<(usize, usize)>,
|
||||
interrupted: Vec<CapSlot>,
|
||||
forgotten: Vec<CapSlot>,
|
||||
}
|
||||
|
||||
impl Console for Fake {
|
||||
@@ -48,6 +49,9 @@ impl Console for Fake {
|
||||
fn interrupt(&mut self, notification: CapSlot) {
|
||||
self.interrupted.push(notification);
|
||||
}
|
||||
fn forget(&mut self, notification: CapSlot) {
|
||||
self.forgotten.push(notification);
|
||||
}
|
||||
}
|
||||
|
||||
fn term(keys: &[u8]) -> TerminalServer<Fake> {
|
||||
@@ -186,3 +190,17 @@ fn a_secret_line_shows_stars_and_is_forgotten() {
|
||||
assert_eq!(&t.grid().row(0)[..8], b"*******\x20");
|
||||
assert!(!t.grid().row(0).windows(7).any(|w| w == b"hunter2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_init_lets_the_next_session_register_for_ctrl_c() {
|
||||
use server_framework::MALFORMED;
|
||||
let mut t = term(&[]);
|
||||
assert_eq!(on_interrupt(&mut t, 7), 0);
|
||||
assert_eq!(on_interrupt(&mut t, 8), MALFORMED, "an app cannot take Ctrl+C over");
|
||||
let signed_out = |t: &mut TerminalServer<Fake>, badge| t.handle_call(badge, [types::terminal::SIGNED_OUT, 0, 0, 0], None).0[0];
|
||||
assert_eq!(signed_out(&mut t, 1), MALFORMED, "nor say the user signed out");
|
||||
assert_eq!(on_interrupt(&mut t, 8), MALFORMED);
|
||||
assert_eq!(signed_out(&mut t, 0), 0);
|
||||
assert_eq!(t.console().forgotten, [CapSlot::new(7).unwrap()]);
|
||||
assert_eq!(on_interrupt(&mut t, 9), 0, "the next shell may");
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//!
|
||||
//! app-image install <root> <manifest.toml> <binary>
|
||||
//! an installed, pre-approved app: /apps/<id>/, and (under factory/,
|
||||
//! which init seeds the encrypted areas from) its decision granting
|
||||
//! which init seeds each user's folder from) its decision granting
|
||||
//! everything it asks for, and data/<id>/ if that includes storage
|
||||
//! app-image stage <folder> <manifest.toml> <binary>
|
||||
//! an app folder the owner can install later (`install <folder>`)
|
||||
@@ -37,7 +37,7 @@ fn install(root: &Path, manifest: &Path, binary: &Path) -> Result<(), String> {
|
||||
};
|
||||
let mut out = [0u8; MAX_DECISION_TEXT];
|
||||
let n = decision.write(&mut out);
|
||||
let decisions = root.join("factory/system/permissions");
|
||||
let decisions = root.join("factory/permissions");
|
||||
fs::create_dir_all(&decisions).map_err(|e| e.to_string())?;
|
||||
fs::write(decisions.join(m.id.as_str()), &out[..n]).map_err(|e| e.to_string())?;
|
||||
if decision.granted.contains(Capability::Storage) {
|
||||
|
||||
@@ -24,3 +24,4 @@ pub mod storage;
|
||||
pub mod syscall_abi;
|
||||
pub mod terminal;
|
||||
pub mod time;
|
||||
pub mod user;
|
||||
|
||||
@@ -17,6 +17,12 @@ pub const STACK_TOP: usize = 0x10000;
|
||||
/// A slot word meaning "none".
|
||||
pub const NONE: usize = usize::MAX;
|
||||
|
||||
/// Sent one way (no reply: the sender is about to be stopped) through the
|
||||
/// endpoint copy init gives a session's shell: the user signs out.
|
||||
pub const SIGN_OUT: usize = 1;
|
||||
/// The badge on that copy; spawn clients' badges are small indices.
|
||||
pub const SESSION_BADGE: usize = 0x5E55;
|
||||
|
||||
pub mod reply {
|
||||
pub const OK: usize = 0;
|
||||
/// The grant names no file init can read.
|
||||
|
||||
@@ -41,6 +41,17 @@ pub mod op {
|
||||
pub const SET_SEED: usize = 14;
|
||||
/// Removes the (empty) directory at the path in the buffer.
|
||||
pub const RMDIR: usize = 15;
|
||||
/// Whose areas the key opens (init only, before the key): word 1 the
|
||||
/// user id as the gatekeeper sent it, word 2 non-zero for a guest. See
|
||||
/// `types::user`.
|
||||
pub const SET_USER: usize = 16;
|
||||
/// The user signed out (init only): every grant is taken back, every
|
||||
/// file closed, the key forgotten; a new user, key and seed may follow.
|
||||
pub const LOCK: usize = 17;
|
||||
/// Removes the tree at the path in the buffer, which must be in
|
||||
/// `users/`, without its key (init only, while locked): a removed
|
||||
/// user's or a guest's files.
|
||||
pub const PURGE: usize = 18;
|
||||
}
|
||||
|
||||
/// Reply tags (word 0).
|
||||
|
||||
@@ -12,8 +12,11 @@ pub const READ_SECRET: usize = 14;
|
||||
/// that drew on the screen itself.
|
||||
pub const REDRAW: usize = 12;
|
||||
/// Carries a notification the terminal signals on Ctrl+C while no line is
|
||||
/// being read. Accepted once.
|
||||
/// being read. Accepted once a session (see `SIGNED_OUT`).
|
||||
pub const ON_INTERRUPT: usize = 13;
|
||||
/// From the unbadged holder (init) only, at sign-out: forgets who
|
||||
/// `ON_INTERRUPT` signals, so the next session's shell may register.
|
||||
pub const SIGNED_OUT: usize = 15;
|
||||
/// What Ctrl+C types.
|
||||
pub const CTRL_C: u8 = 3;
|
||||
|
||||
|
||||
45
crates/types/src/user.rs
Normal file
45
crates/types/src/user.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
59
docs/apps.md
59
docs/apps.md
@@ -1,12 +1,12 @@
|
||||
# Apps, manifests and the permission manager
|
||||
|
||||
How a program gets its authority. The kernel only knows capabilities; this
|
||||
is the userspace layer that decides who holds which, on the owner's behalf
|
||||
is the userspace layer that decides who holds which, on the user's behalf
|
||||
(see the vision's Capability Management and App Manifest notes).
|
||||
|
||||
## Apps
|
||||
|
||||
Every program the owner runs is an **app**: a manifest plus a binary.
|
||||
Every program a user runs is an **app**: a manifest plus a binary.
|
||||
An app is identified by its **id** (`kos.snake`), never by a file name or a
|
||||
process id. The id is what decisions, the app's data folder and (later) its
|
||||
encryption keys hang off.
|
||||
@@ -18,12 +18,12 @@ starts them from its own table, with exactly the capabilities listed there.
|
||||
|
||||
An app's capabilities have exactly two sources:
|
||||
|
||||
1. **Its manifest and the owner's consent.** Services the app declared
|
||||
1. **Its manifest and the user's consent.** Services the app declared
|
||||
(`display`, `storage`, …), granted only by the permission manager, only
|
||||
as far as the owner approved.
|
||||
as far as the user approved.
|
||||
2. **Its launcher.** Whoever starts an app may pass along what it already
|
||||
holds: the terminal as its output, a read-only grant to the current
|
||||
folder, a file the owner picked. A launcher can only give what it has.
|
||||
folder, a file the user picked. A launcher can only give what it has.
|
||||
|
||||
`cat` declares nothing and gets only what the shell passes it. Snake
|
||||
declares `display`. Nothing gets anything "just in case".
|
||||
@@ -31,22 +31,27 @@ declares `display`. Nothing gets anything "just in case".
|
||||
## Disk layout
|
||||
|
||||
```
|
||||
/home/ the owner's files; the shell's `/` (encrypted)
|
||||
/apps/<id>/ an installed app: manifest.toml and its binary
|
||||
/data/<id>/ the app's private folder (its `storage`) (encrypted, its own key)
|
||||
/system/permissions/ the owner's decisions, one file per app id (encrypted)
|
||||
/bin/ system servers and the shell (init starts these)
|
||||
/factory/ plaintext starter content the encrypted areas are
|
||||
seeded from with a new master key
|
||||
/apps/<id>/ a system app: manifest.toml and its binary, shared by every user
|
||||
/users/<user>/ one user's folder (encrypted, under their own key)
|
||||
home/ their files; the shell's `/`
|
||||
apps/<id>/ an app they installed
|
||||
data/<id>/ an app's private folder (its `storage`, its own key)
|
||||
permissions/<id> their decisions, one file per app id
|
||||
/bin/ system servers and the shell (init starts these)
|
||||
/factory/ plaintext starter content a user's folder is
|
||||
seeded from (home/, permissions/, data/)
|
||||
```
|
||||
|
||||
Only the permission manager reaches `/apps`, `/data` and `/system`. The
|
||||
shell's storage grant is `/home`, so neither it nor anything it runs can
|
||||
read another app's data or rewrite a decision.
|
||||
Users each decide for themselves, even about the system's apps, and what
|
||||
one installs the others do not see. An app id a system app has cannot be
|
||||
installed. Only the signed-in user's folder opens. Only the permission
|
||||
manager reaches their `apps/`, `data/` and `permissions/` (and `/apps`,
|
||||
read-only). The shell's storage grant is their `home/`, so neither it nor
|
||||
anything it runs can read an app's data or rewrite a decision.
|
||||
|
||||
## Manifest
|
||||
|
||||
`/apps/<id>/manifest.toml`, a strict subset of TOML:
|
||||
`apps/<id>/manifest.toml`, a strict subset of TOML:
|
||||
|
||||
```toml
|
||||
[app]
|
||||
@@ -60,7 +65,7 @@ icon = "icon.png" # optional: a file in the app's folder
|
||||
|
||||
[capabilities]
|
||||
required = ["display"] # cannot work without; denied = not installed
|
||||
optional = [] # works without; the owner decides each
|
||||
optional = [] # works without; the user decides each
|
||||
|
||||
[appearance]
|
||||
accent = "#e91e63" # optional: checked for format here, contrast by K-UI
|
||||
@@ -79,29 +84,29 @@ optional one is simply not granted.
|
||||
| Capability | What the app gets |
|
||||
|---|---|
|
||||
| `display` | the display server: the screen, and the keyboard while in focus |
|
||||
| `storage` | `/data/<id>/`, read-write (its own folder only) |
|
||||
| `storage` | the user's `data/<id>/`, read-write (its own folder only) |
|
||||
|
||||
## Decisions
|
||||
|
||||
`/system/permissions/<id>`, the same TOML subset:
|
||||
The user's `permissions/<id>`, the same TOML subset:
|
||||
|
||||
```toml
|
||||
[decision]
|
||||
granted = ["display"]
|
||||
```
|
||||
|
||||
Written by the permission manager when the owner decides; apps on the
|
||||
Written by the permission manager when the user decides; apps on the
|
||||
system image ship with their decisions already recorded under
|
||||
`factory/system/` (pre-approved by whoever built the image), and init
|
||||
seeds them into the encrypted `system/` on a fresh setup. Changing one later is the owner's call, through
|
||||
the permission manager.
|
||||
`factory/permissions/` (pre-approved by whoever built the image), and init
|
||||
seeds them into each new user's `permissions/`. Changing one later is that
|
||||
user's call, through the permission manager.
|
||||
|
||||
## Permission manager
|
||||
|
||||
A system server with no hardware. Its logic (checking manifests, deciding
|
||||
what an app gets, keeping decisions) is pure and generic over two ports:
|
||||
|
||||
- `Consent`: asks the owner. Today: a yes/no prompt on the terminal.
|
||||
- `Consent`: asks the user. Today: a yes/no prompt on the terminal.
|
||||
- `Store`: reads manifests and decisions, writes decisions, installs files.
|
||||
Today: the storage server.
|
||||
|
||||
@@ -113,9 +118,9 @@ From a folder the launcher passes as a read-only grant:
|
||||
|
||||
1. Read and check its `manifest.toml`; refuse if the id is installed.
|
||||
2. Required capabilities this device lacks: refuse.
|
||||
3. Ask the owner about the required ones together (no = not installed),
|
||||
3. Ask the user about the required ones together (no = not installed),
|
||||
then each optional one.
|
||||
4. Copy the binary and manifest to `/apps/<id>/`, create `/data/<id>/` if
|
||||
4. Copy the binary and manifest to the user's `apps/<id>/`, create `data/<id>/` if
|
||||
`storage` was granted, record the decision.
|
||||
|
||||
### Launch
|
||||
@@ -136,7 +141,7 @@ permission manager:
|
||||
|
||||
### Revoke
|
||||
|
||||
The owner withdraws a capability: the decision changes and the next launch
|
||||
The user withdraws a capability: the decision changes and the next launch
|
||||
gets less. A running app that held it is **stopped**: all its access dies
|
||||
at once. (Endpoint capabilities like `display` are copies the permission
|
||||
manager cannot take back from one app alone yet; once servers issue
|
||||
|
||||
49
docs/ipc.md
49
docs/ipc.md
@@ -91,22 +91,37 @@ and budget listed there. Storage, started before the log server, receives
|
||||
its log endpoint from init later (`SET_LOG`, accepted only from the
|
||||
unbadged holder of its endpoint).
|
||||
|
||||
Nothing that touches user data starts before the owner is authenticated.
|
||||
Nothing that touches a user's files starts before someone has signed in.
|
||||
Init starts the log, input, display and terminal servers, then the
|
||||
gatekeeper (`types::gatekeeper`), handing it only the terminal, the keystore
|
||||
(a small disk of its own, told from the data disk by its size) and the
|
||||
virtio-rng device. Init calls `UNLOCK`, which returns once the owner is in,
|
||||
then `KEY`, whose reply is the master key; init passes it to storage
|
||||
(`SET_KEY`, from the unbadged holder only, each half once), and a fresh
|
||||
32-byte seed (`SEED` → `SET_SEED`) for storage's randomness this boot.
|
||||
Storage's encrypted areas (`home/`, `system/`, each app's `data/<id>/`;
|
||||
crate `filecrypt`, layer `storage_server::encrypted`) open once both are
|
||||
in. With a new key (a fresh device, or one just wiped: `UNLOCK`'s word 1)
|
||||
init empties those areas and seeds them from the plaintext `factory/`;
|
||||
an area that is missing is seeded too. Only then does init start the
|
||||
permission manager and the desktop. If the gatekeeper cannot
|
||||
unlock, init stops there. The gatekeeper exits, and its capabilities go
|
||||
with it.
|
||||
virtio-rng device. Init calls `UNLOCK`, which returns once someone is in:
|
||||
`[OK, flags, user id, role]`, a guest (`GUEST`) or a new keystore
|
||||
(`FRESH`) among the flags. Then `KEY` (their master key), `SEED` (32 fresh
|
||||
random bytes for storage this sign-in), `NAME`, and `REMOVED` until `DONE`
|
||||
(users the owner removed just now). The gatekeeper exits.
|
||||
|
||||
Each user's files are in `users/<id>/` (`types::user`: the id in hex, or
|
||||
`guest`): `home/`, `permissions/`, and per app `apps/<id>/` and
|
||||
`data/<id>/`, each sealed under its own key derived from that user's
|
||||
master key (crate `filecrypt`, layer `storage_server::encrypted`); the rest
|
||||
of the disk is plaintext. While storage is still locked, init removes what
|
||||
must go (`PURGE`, init only, only in `users/`, only while locked): every
|
||||
user's folder on a new keystore, a removed user's, the last guest's. Then
|
||||
it tells storage who is in (`SET_USER`) and hands over the key and seed
|
||||
(`SET_KEY`, `SET_SEED`, a half per call, each once a sign-in; init only).
|
||||
Only the signed-in user's folder opens; anyone else's is refused. Init
|
||||
seeds whatever area of theirs is missing from the plaintext `factory/`,
|
||||
then starts the session: the permission manager (the user's folder, and
|
||||
`apps/` read-only) and the desktop. If the gatekeeper cannot unlock, init
|
||||
stops there.
|
||||
|
||||
Signing out (`types::spawn::SIGN_OUT`, sent one way through the copy of
|
||||
init's endpoint the shell holds) stops everything the session ran (the
|
||||
shell, the permission manager, every app), tells the terminal
|
||||
(`SIGNED_OUT`, init only), locks storage (`LOCK`, init only: every grant
|
||||
taken back, every file closed, the key forgotten), removes a guest's
|
||||
files, and runs the gatekeeper again.
|
||||
|
||||
A program's `uses` endpoints arrive as badged copies with `Execute` and
|
||||
`Delegate`: the badge (unique per copy) tells the server which client is
|
||||
@@ -135,12 +150,12 @@ log endpoint is the terminal prints there with `println!`: standard output
|
||||
is whatever text endpoint a program was given. `READ` (`types::terminal`)
|
||||
blocks until a line is typed and returns it in chunks, the last ending in
|
||||
`\n`. Ctrl+C there drops the line. Keys typed while no line is read are
|
||||
kept for the next one, except Ctrl+C, which signals the notification its
|
||||
first `ON_INTERRUPT` caller gave. `REDRAW` repaints the screen and takes
|
||||
kept for the next one, except Ctrl+C, which signals the notification the
|
||||
session's first `ON_INTERRUPT` caller (its shell) gave. `REDRAW` repaints the screen and takes
|
||||
input focus back.
|
||||
|
||||
The desktop is the shell, given the terminal, the permission manager and
|
||||
`/home` (its `/`). Everything the owner runs is an app (docs/apps.md): a
|
||||
The desktop is the shell, given the terminal, the permission manager,
|
||||
the user's `home/` (its `/`) and signing out. Everything the owner runs is an app (docs/apps.md): a
|
||||
command that is not a built-in asks the permission manager (`types::apps`,
|
||||
client `libkos::apps`) to run it, passing the terminal as its output and a
|
||||
read-only grant to the current folder (`cd`, `pwd`). The permission manager
|
||||
|
||||
@@ -37,9 +37,9 @@ staged() {
|
||||
fi
|
||||
}
|
||||
|
||||
# home/, system/ and data/ are encrypted, so not made here: init seeds them
|
||||
# from factory/ (plaintext) once the gatekeeper has the key.
|
||||
mkdir -p "${ROOT}/bin" "${ROOT}/apps" "${ROOT}/factory/home" "${ROOT}/factory/system/permissions" "${ROOT}/factory/data"
|
||||
# Each user's folder in users/ is encrypted, so not filled here: init seeds
|
||||
# a new user's areas from factory/ (plaintext) once they are signed in.
|
||||
mkdir -p "${ROOT}/bin" "${ROOT}/apps" "${ROOT}/users" "${ROOT}/factory/home" "${ROOT}/factory/permissions" "${ROOT}/factory/data" "${ROOT}/factory/apps"
|
||||
# System servers and the shell: init starts these.
|
||||
copy display-qemu display
|
||||
copy input-qemu input
|
||||
@@ -55,7 +55,7 @@ app ls
|
||||
app sleep
|
||||
app snake
|
||||
staged counter inbox/counter
|
||||
# The owner's first files.
|
||||
# A user's first files.
|
||||
cp -r disk/home/. "${ROOT}/factory/home/"
|
||||
# Only the test image: its presence makes init run the self-test.
|
||||
if [ -n "${SMPTEST:-}" ]; then
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#!/bin/bash
|
||||
# Boots IMAGE headless and waits for the self-test (init's SMP checks, then
|
||||
# spawntest) to report; the machine halts itself when done. Twice, on one
|
||||
# fresh keystore: the first boot sets a passcode up at the gatekeeper, the
|
||||
# second gets it wrong once and then right. Exit 0 only if both unlock and
|
||||
# both say "SMPTEST PASS".
|
||||
# fresh keystore: the first boot sets up its first user at the gatekeeper,
|
||||
# the second gets the passcode wrong once and then right. Exit 0 only if
|
||||
# both sign in and both say "SMPTEST PASS".
|
||||
set -uo pipefail
|
||||
|
||||
IMAGE="${1:?usage: test-qemu.sh IMAGE}"
|
||||
@@ -40,18 +40,20 @@ boot() {
|
||||
return $s
|
||||
}
|
||||
|
||||
# First boot: a new passcode (twice), no duress passcode (twice empty).
|
||||
boot 1234 1234 "" ""
|
||||
# First boot: the first user's name, a passcode (twice), no duress
|
||||
# passcode (twice empty).
|
||||
boot tester 1234 1234 "" ""
|
||||
status=$?
|
||||
first_ok=0
|
||||
grep -aq "the owner is in" "${LOG}" && grep -aq "SMPTEST PASS" "${LOG}" && first_ok=1
|
||||
echo "test-qemu: first boot: passcode set up: $([ ${first_ok} -eq 1 ] && echo ok || echo FAIL)"
|
||||
# Second boot: the keystore remembers; a wrong guess, then the passcode.
|
||||
boot 9999 1234
|
||||
grep -aq "init: tester is in" "${LOG}" && grep -aq "SMPTEST PASS" "${LOG}" && first_ok=1
|
||||
echo "test-qemu: first boot: first user set up: $([ ${first_ok} -eq 1 ] && echo ok || echo FAIL)"
|
||||
# Second boot: the keystore remembers; a wrong guess, then the passcode,
|
||||
# then on past the menu.
|
||||
boot tester 9999 tester 1234 ""
|
||||
[ ${status} -eq 0 ] && status=$?
|
||||
second_ok=0
|
||||
grep -aq "the owner is in" "${LOG}" && grep -aq "from the last boot reads back: ok" "${LOG}" && second_ok=1
|
||||
echo "test-qemu: second boot: wrong passcode refused, right one unlocks, sealed file kept: $([ ${second_ok} -eq 1 ] && echo ok || echo FAIL)"
|
||||
grep -aq "init: tester is in" "${LOG}" && grep -aq "from the last boot reads back: ok" "${LOG}" && second_ok=1
|
||||
echo "test-qemu: second boot: wrong passcode refused, right one signs in, sealed file kept: $([ ${second_ok} -eq 1 ] && echo ok || echo FAIL)"
|
||||
rm -f "${KEYS}" "${MON}"
|
||||
|
||||
grep -a "smptest:\|spawntest:\|Hello\|SMPTEST" "${LOG}"
|
||||
@@ -62,10 +64,18 @@ if command -v e2fsck >/dev/null && command -v debugfs >/dev/null; then
|
||||
echo "test-qemu: e2fsck rejects the disk:"; cat "${LOG}.fsck"; disk_ok=0
|
||||
elif [ "$(debugfs -R 'cat /selftest.txt' "${IMAGE}" 2>/dev/null)" != "written by k-os on ext4" ]; then
|
||||
echo "test-qemu: /selftest.txt is not on the disk"; disk_ok=0
|
||||
elif debugfs -R 'cat /home/readme.txt' "${IMAGE}" 2>/dev/null | grep -aq "Welcome"; then
|
||||
echo "test-qemu: /home/readme.txt is plaintext on the disk"; disk_ok=0
|
||||
elif ! debugfs -R 'cat /home/readme.txt' "${IMAGE}" 2>/dev/null | head -c 8 | grep -aq "KOSFILE1"; then
|
||||
echo "test-qemu: /home/readme.txt is not a sealed file"; disk_ok=0
|
||||
fi
|
||||
# The one user's folder: users/<16 hex digits>.
|
||||
user="$(debugfs -R 'ls /users' "${IMAGE}" 2>/dev/null | grep -aoE '[0-9a-f]{16}' | head -1)"
|
||||
readme="/users/${user}/home/readme.txt"
|
||||
if [ "${disk_ok}" -eq 0 ]; then
|
||||
:
|
||||
elif [ -z "${user}" ]; then
|
||||
echo "test-qemu: no user's folder on the disk"; disk_ok=0
|
||||
elif debugfs -R "cat ${readme}" "${IMAGE}" 2>/dev/null | grep -aq "Welcome"; then
|
||||
echo "test-qemu: ${readme} is plaintext on the disk"; disk_ok=0
|
||||
elif ! debugfs -R "cat ${readme}" "${IMAGE}" 2>/dev/null | head -c 8 | grep -aq "KOSFILE1"; then
|
||||
echo "test-qemu: ${readme} is not a sealed file"; disk_ok=0
|
||||
else
|
||||
echo "test-qemu: e2fsck clean, file on disk, home sealed"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user