From 1fc5a54f06845992c9077df0a2ee8767215dce5c Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Tue, 27 Oct 2026 20:20:34 +0100 Subject: [PATCH] feat(storage): scoped grants (subtree, ro/rw), narrow-only, release revokes all holders; IPC KEEP_DELEGATE opt-in; shell cd/pwd, programs get ro cwd --- crates/adapter-riscv/src/boot/caps.rs | 4 +- crates/apps/cat/src/main.rs | 1 + crates/apps/ls/src/main.rs | 8 +- crates/apps/shell/src/main.rs | 115 +++++- crates/apps/spawntest/src/main.rs | 50 ++- crates/bins/display-qemu/src/main.rs | 2 +- crates/domain/src/message/mod.rs | 17 +- crates/init/src/launch.rs | 14 +- crates/init/src/lib.rs | 23 +- crates/init/src/spawn.rs | 7 +- crates/kernel/src/abi.rs | 11 +- crates/kernel/src/proofs/abi.rs | 8 +- crates/kernel/src/proofs/revocation.rs | 2 +- crates/kernel/src/syscall/ipc/endpoint.rs | 22 +- crates/kernel/src/tests/ipc.rs | 61 ++++ crates/libkos/src/device.rs | 2 +- crates/libkos/src/display.rs | 6 +- crates/libkos/src/fs.rs | 27 ++ crates/libkos/src/syscall/ipc.rs | 10 +- crates/server-framework/src/server/mod.rs | 9 +- crates/servers/display-server/src/server.rs | 2 +- crates/servers/input-server/src/server.rs | 2 +- crates/servers/storage-server/src/grants.rs | 94 +++++ crates/servers/storage-server/src/lib.rs | 37 +- crates/servers/storage-server/src/protocol.rs | 21 ++ crates/servers/storage-server/src/scope.rs | 71 ++++ crates/servers/storage-server/src/server.rs | 270 ++++++++++---- .../servers/storage-server/src/tests/mod.rs | 1 + .../storage-server/src/tests/protocol.rs | 11 + .../servers/storage-server/src/tests/scope.rs | 46 +++ .../storage-server/src/tests/server.rs | 329 ++++++++++++++++-- crates/servers/terminal-server/src/server.rs | 2 +- crates/types/src/proofs/abi.rs | 18 + crates/types/src/storage.rs | 10 + crates/types/src/syscall_abi.rs | 55 ++- disk/docs/shell.txt | 9 +- disk/readme.txt | 4 +- docs/ipc.md | 26 +- 38 files changed, 1237 insertions(+), 170 deletions(-) create mode 100644 crates/servers/storage-server/src/grants.rs create mode 100644 crates/servers/storage-server/src/scope.rs create mode 100644 crates/servers/storage-server/src/tests/scope.rs diff --git a/crates/adapter-riscv/src/boot/caps.rs b/crates/adapter-riscv/src/boot/caps.rs index 2945356..02176e0 100644 --- a/crates/adapter-riscv/src/boot/caps.rs +++ b/crates/adapter-riscv/src/boot/caps.rs @@ -14,9 +14,11 @@ pub fn grant_all(uart: &Uart, init_pid: ProcessId, storage_pid: ProcessId) { .with(types::permission::Permission::Execute) .with(types::permission::Permission::Delegate); + // Delegate so storage can mint the badged copies it grants. let ipc_perms = types::permission::PermissionSet::empty() .with(types::permission::Permission::Read) - .with(types::permission::Permission::Execute); + .with(types::permission::Permission::Execute) + .with(types::permission::Permission::Delegate); let storage_ep = kern() .create_endpoint(storage_pid) diff --git a/crates/apps/cat/src/main.rs b/crates/apps/cat/src/main.rs index 99e9d29..d82fa68 100644 --- a/crates/apps/cat/src/main.rs +++ b/crates/apps/cat/src/main.rs @@ -36,6 +36,7 @@ extern "C" fn main() { match cat(&mut fs, path) { Ok(()) => {} Err(FsError::NotFound) => println!("cat: {}: no such file", path), + Err(FsError::Denied) => println!("cat: {}: outside what it was given", path), Err(_) => println!("cat: {}: cannot read", path), } } diff --git a/crates/apps/ls/src/main.rs b/crates/apps/ls/src/main.rs index fd6ff2a..2aafade 100644 --- a/crates/apps/ls/src/main.rs +++ b/crates/apps/ls/src/main.rs @@ -5,7 +5,7 @@ extern crate libkos; -use libkos::fs::Storage; +use libkos::fs::{FsError, Storage}; use libkos::println; /// Where storage's buffer is mapped. @@ -29,7 +29,9 @@ extern "C" fn main() { println!("{:<24} {:>8}", name, e.size); } }); - if listed.is_err() { - println!("ls: {}: no such directory", dir); + match listed { + Ok(_) => {} + Err(FsError::Denied) => println!("ls: {}: outside what it was given", dir), + Err(_) => println!("ls: {}: no such directory", dir), } } diff --git a/crates/apps/shell/src/main.rs b/crates/apps/shell/src/main.rs index 5367873..b3d6a86 100644 --- a/crates/apps/shell/src/main.rs +++ b/crates/apps/shell/src/main.rs @@ -1,12 +1,14 @@ //! The shell: a few built-ins, and anything else is a program on disk. //! `name args...` runs `name.elf` through init, handing it the terminal as -//! its output and the storage server, and nothing else; then waits for it. +//! its output and read-only storage rooted at the current directory, and +//! nothing else; then waits for it and takes the storage back. #![no_std] #![no_main] extern crate libkos; +use libkos::fs::Storage; use libkos::spawn::{SpawnError, spawn}; use libkos::{print, println, syscall}; use types::bounded::CapSlot; @@ -22,9 +24,76 @@ const INIT: CapSlot = CapSlot::const_new(2); /// What each program gets, out of the shell's allowance. const PROGRAM: Limits = Limits { pages: 128, endpoints: 1, notifications: 1, regions: 1 }; const MAX_ARGS: usize = 16; +/// Where storage's buffer is mapped. +const STORAGE_BUF_VA: usize = 0x0180_0000; -/// Runs `args[0].elf` with `args` and waits for it to finish. -fn run_program(args: &[&str], done: CapSlot) { +/// The current directory, as a path on disk: no leading or trailing `/`. +struct Cwd { + path: [u8; types::storage::MAX_PATH], + len: usize, +} + +impl Cwd { + fn as_bytes(&self) -> &[u8] { + &self.path[..self.len] + } + + /// Where `cd to` from here leads: `/` starts at the root, `..` goes up + /// (the shell holds the whole disk, so that is its to resolve). + fn join(&self, to: &str) -> Option { + let mut next = match to.starts_with('/') { + true => Cwd { + path: [0; types::storage::MAX_PATH], + len: 0, + }, + false => Cwd { + path: self.path, + len: self.len, + }, + }; + for part in to.split('/') { + match part { + "" | "." => {} + ".." => { + next.len = next + .as_bytes() + .iter() + .rposition(|b| *b == b'/') + .unwrap_or(0) + } + name => { + let sep = usize::from(next.len > 0); + let end = next.len + sep + name.len(); + let room = next.path.get_mut(next.len..end)?; + room[..sep].copy_from_slice(&b"/"[..sep]); + room[sep..].copy_from_slice(name.as_bytes()); + next.len = end; + } + } + } + Some(next) + } +} + +impl core::fmt::Display for Cwd { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!( + f, + "/{}", + core::str::from_utf8(self.as_bytes()).unwrap_or("?") + ) + } +} + +struct Shell { + fs: Storage, + cwd: Cwd, + done: CapSlot, +} + +/// Runs `args[0].elf` with `args` in the current directory, and waits +/// for it to finish. +fn run_program(sh: &mut Shell, args: &[&str]) { let mut file = [0u8; types::spawn::MAX_NAME]; let name = args[0].as_bytes(); let Some(file) = file.get_mut(..name.len() + 4).map(|f| { @@ -42,23 +111,34 @@ fn run_program(args: &[&str], done: CapSlot) { Err(SpawnError::Failed) => return println!("shell: {}: cannot run", args[0]), }; let call = PermissionSet::empty().with(Permission::Execute); + let grant = sh.fs.grant(sh.cwd.as_bytes(), false).ok(); let arg = StartArg { log: child.give(TERMINAL, call), - storage: child.give(STORAGE, call), + storage: grant.and_then(|g| child.give(g, call)), args: child.set_args(args), console: None, }; - let watched = syscall::watch_death(child.process, done).is_ok(); + let watched = syscall::watch_death(child.process, sh.done).is_ok(); if child.start_with(arg) && watched { - let _ = syscall::wait(done); + let _ = syscall::wait(sh.done); } else { let _ = syscall::kill(child.process); println!("shell: {}: cannot start", args[0]); } syscall::drop_cap(child.process); + if let Some(g) = grant { + let _ = libkos::fs::release(g); + } } -fn run(line: &str, done: CapSlot) { +fn cd(sh: &mut Shell, to: &str) { + match sh.cwd.join(to) { + Some(next) if sh.fs.list_dir(next.as_bytes(), |_| {}).is_ok() => sh.cwd = next, + _ => println!("cd: {}: no such directory", to), + } +} + +fn run(sh: &mut Shell, line: &str) { let mut args = [""; MAX_ARGS]; let mut n = 0; for w in line.split_ascii_whitespace().take(MAX_ARGS) { @@ -69,12 +149,15 @@ fn run(line: &str, done: CapSlot) { match args.first().copied() { None => {} Some("help") => { - println!("built-ins: help, echo , clear"); + println!("built-ins: help, echo , clear, cd , pwd"); println!("anything else runs a program from the disk: try ls, cat readme.txt"); + println!("programs see the current directory, read-only, and nothing above it"); } + Some("cd") => cd(sh, args.get(1).copied().unwrap_or("/")), + Some("pwd") => println!("{}", sh.cwd), Some("echo") => println!("{}", Joined(&args[1..])), Some("clear") => libkos::terminal::clear(TERMINAL), - Some(_) => run_program(args, done), + Some(_) => run_program(sh, args), } } @@ -101,13 +184,25 @@ extern "C" fn main() { println!("shell: no notification to wait on"); return; }; + let Ok(fs) = Storage::attach(STORAGE, STORAGE_BUF_VA) else { + println!("shell: no storage"); + return; + }; + let mut sh = Shell { + fs, + cwd: Cwd { + path: [0; types::storage::MAX_PATH], + len: 0, + }, + done, + }; println!("k-os shell. Type 'help'."); let mut buf = [0u8; 256]; loop { print!("$ "); let n = libkos::terminal::read_line(TERMINAL, &mut buf); match core::str::from_utf8(&buf[..n]) { - Ok(line) => run(line.trim_end_matches('\n'), done), + Ok(line) => run(&mut sh, line.trim_end_matches('\n')), Err(_) => println!("shell: not text"), } } diff --git a/crates/apps/spawntest/src/main.rs b/crates/apps/spawntest/src/main.rs index dd34022..000e638 100644 --- a/crates/apps/spawntest/src/main.rs +++ b/crates/apps/spawntest/src/main.rs @@ -7,6 +7,7 @@ extern crate libkos; +use libkos::fs::{FsError, Storage}; use libkos::spawn::{SpawnError, spawn}; use libkos::syscall; use types::bounded::CapSlot; @@ -17,8 +18,9 @@ use types::budget::Limits; const STORAGE: CapSlot = CapSlot::const_new(0); const INIT: CapSlot = CapSlot::const_new(1); const CONTROL: CapSlot = CapSlot::const_new(2); -/// Where storage's buffer is mapped. +/// Where storage's buffers are mapped: the whole disk's, a narrowed one's. const STORAGE_BUF_VA: usize = 0x0180_0000; +const NARROW_BUF_VA: usize = 0x0190_0000; /// Read back from the disk image by scripts/test-qemu.sh. const NOTE: &[u8] = b"written by k-os on ext4\n"; @@ -26,9 +28,14 @@ const NOTE: &[u8] = b"written by k-os on ext4\n"; /// the first has died and its limits came back. const HELLO: Limits = Limits { pages: 80, endpoints: 0, notifications: 0, regions: 0 }; -/// Spawns hello, runs it, and waits for it to die. -fn run_hello(died: CapSlot) -> Result<(), &'static str> { +/// Spawns hello, runs it, and waits for it to die; hands it `grant` +/// first, if there is one. +fn run_hello(died: CapSlot, grant: Option) -> Result<(), &'static str> { let child = spawn(INIT, b"hello.elf", HELLO).map_err(|_| "init would not spawn hello")?; + let call = types::permission::PermissionSet::empty().with(types::permission::Permission::Execute); + if grant.is_some_and(|g| child.give(g, call).is_none()) { + return Err("a storage grant could not be passed on"); + } syscall::watch_death(child.process, died).map_err(|_| "cannot watch hello")?; if !child.start() { return Err("cannot start hello"); @@ -38,8 +45,7 @@ fn run_hello(died: CapSlot) -> Result<(), &'static str> { } /// A file written through storage, which the host then checks with e2fsck. -fn write_a_file() -> Result<(), &'static str> { - let mut fs = libkos::fs::Storage::attach(STORAGE, STORAGE_BUF_VA).map_err(|_| "cannot attach to storage")?; +fn write_a_file(fs: &mut Storage) -> Result<(), &'static str> { let h = fs.create(b"selftest.txt").map_err(|_| "cannot create a file")?; if fs.write(h, NOTE) != Ok(NOTE.len()) { return Err("short write"); @@ -55,12 +61,40 @@ fn write_a_file() -> Result<(), &'static str> { Ok(()) } +/// A read-only grant to one directory reaches nothing else and changes +/// nothing, can be passed on, and is gone everywhere once released. +fn narrow_storage(fs: &mut Storage, died: CapSlot) -> Result<(), &'static str> { + let grant = fs.grant(b"lost+found", false).map_err(|_| "storage refused a grant")?; + let mut inner = Storage::attach(grant, NARROW_BUF_VA).map_err(|_| "cannot attach through the grant")?; + inner.list_dir(b"", |_| {}).map_err(|_| "cannot list its own directory")?; + if inner.open(b"../selftest.txt").err() != Some(FsError::Denied) { + return Err("a grant reached outside its directory"); + } + if inner.create(b"x").err() != Some(FsError::Denied) { + return Err("a read-only grant created a file"); + } + let widened = inner.grant(b"", true).and_then(|g| Storage::attach(g, NARROW_BUF_VA + 0x10_0000)); + if widened.and_then(|mut w| w.create(b"x")).is_ok() { + return Err("a grant was widened to write"); + } + run_hello(died, Some(grant))?; + libkos::fs::release(grant).map_err(|_| "release failed")?; + if inner.list_dir(b"", |_| {}).is_ok() { + return Err("a released grant still works"); + } + Ok(()) +} + fn check() -> Result<(), &'static str> { - write_a_file()?; + 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"); let died = syscall::create_notification().map_err(|_| "no notification")?; - run_hello(died)?; + narrow_storage(&mut fs, died)?; + libkos::println!("spawntest: storage grants stay in scope, pass on, release: ok"); + + run_hello(died, None)?; libkos::println!("spawntest: spawned program runs and its death is heard: ok"); let greedy = Limits { pages: 200, ..HELLO }; @@ -73,7 +107,7 @@ fn check() -> Result<(), &'static str> { } libkos::println!("spawntest: allowance and missing files refused: ok"); - run_hello(died)?; + run_hello(died, None)?; libkos::println!("spawntest: allowance comes back when a child dies: ok"); Ok(()) } diff --git a/crates/bins/display-qemu/src/main.rs b/crates/bins/display-qemu/src/main.rs index 9de47ef..e5d8c39 100644 --- a/crates/bins/display-qemu/src/main.rs +++ b/crates/bins/display-qemu/src/main.rs @@ -30,7 +30,7 @@ fn subscribe_to_input() -> Option { libkos::syscall::bind_notification(notify).ok()?; let signal = PermissionSet::empty().with(Permission::Write).with(Permission::Delegate); let copy = libkos::syscall::mint(notify, Badge::NONE, signal).ok()?; - let sent = libkos::syscall::call(INPUT, [types::input::SUBSCRIBE, 0, 0, 0], Some(copy)); + let sent = libkos::syscall::call(INPUT, [types::input::SUBSCRIBE, 0, 0, 0], Some(copy.into())); libkos::syscall::drop_cap(copy); sent.ok().map(|_| INPUT) } diff --git a/crates/domain/src/message/mod.rs b/crates/domain/src/message/mod.rs index 25dedfb..7032bd1 100644 --- a/crates/domain/src/message/mod.rs +++ b/crates/domain/src/message/mod.rs @@ -11,6 +11,8 @@ pub const MAX_MESSAGE_WORDS: usize = 4; pub struct Message { words: [usize; MAX_MESSAGE_WORDS], cap: Option, + /// The receiver may pass `cap` on (see `syscall_abi::KEEP_DELEGATE`). + keep_delegate: bool, } impl Message { @@ -19,7 +21,11 @@ impl Message { } pub fn from_words(words: [usize; MAX_MESSAGE_WORDS]) -> Self { - Self { words, cap: None } + Self { + words, + cap: None, + keep_delegate: false, + } } pub fn with_cap(mut self, slot: CapSlot) -> Self { @@ -39,4 +45,13 @@ impl Message { pub fn cap(&self) -> Option { self.cap } + + pub fn keeping_delegate(mut self) -> Self { + self.keep_delegate = true; + self + } + + pub fn keeps_delegate(&self) -> bool { + self.keep_delegate + } } diff --git a/crates/init/src/launch.rs b/crates/init/src/launch.rs index 18fc6e9..2d22a0b 100644 --- a/crates/init/src/launch.rs +++ b/crates/init/src/launch.rs @@ -78,10 +78,16 @@ impl<'a> Launcher<'a> { // and take other clients' requests. for name in p.uses { let ep = self.endpoint(name).ok_or("needs a server that is not running")?; - let badge = Badge::new(self.next_badge); - self.next_badge += 1; - let copy = syscall::mint(ep, badge, perms(&[Permission::Execute, Permission::Delegate])) - .map_err(|_| "cannot mint an endpoint")?; + let copy = match *name { + // Storage issues its own badges, each naming a scope. + "storage" => self.storage.grant(b"", true).map_err(|_| "storage refused a grant")?, + _ => { + let badge = Badge::new(self.next_badge); + self.next_badge += 1; + syscall::mint(ep, badge, perms(&[Permission::Execute, Permission::Delegate])) + .map_err(|_| "cannot mint an endpoint")? + } + }; let given = syscall::delegate(copy, child, perms(&[Permission::Execute, Permission::Delegate])); syscall::drop_cap(copy); given.ok_or("cannot give an endpoint")?; diff --git a/crates/init/src/lib.rs b/crates/init/src/lib.rs index e24ba2d..92af958 100644 --- a/crates/init/src/lib.rs +++ b/crates/init/src/lib.rs @@ -26,8 +26,12 @@ pub fn run() -> ! { // Storage waits for its disk before serving, so this comes first. let found = devices::discover(); - let disk = libkos::device::give_device(STORAGE_EP, found.disk).is_ok() && found.disk.mmio.is_some(); - libkos::println!("init: disk: {}", if disk { "handed to its driver" } else { "none" }); + let disk = + libkos::device::give_device(STORAGE_EP, found.disk).is_ok() && found.disk.mmio.is_some(); + libkos::println!( + "init: disk: {}", + if disk { "handed to its driver" } else { "none" } + ); let Ok(mut storage) = Storage::attach(STORAGE_EP, STORAGE_BUF_VA) else { libkos::println!("init: cannot reach storage, nothing to load"); @@ -53,7 +57,10 @@ 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.open(b"smptest.elf").is_ok_and(|h| storage.close(h).is_ok()) { + if storage + .open(b"smptest.elf") + .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); @@ -83,9 +90,15 @@ fn give_storage_the_log() { let Some(log) = libkos::start::log() else { return; }; - let call_only = PermissionSet::empty().with(Permission::Execute).with(Permission::Delegate); + let call_only = PermissionSet::empty() + .with(Permission::Execute) + .with(Permission::Delegate); if let Ok(copy) = syscall::mint(log, Badge::NONE, call_only) { - let _ = syscall::call(STORAGE_EP, [types::storage::op::SET_LOG, 0, 0, 0], Some(copy)); + let _ = syscall::call( + STORAGE_EP, + [types::storage::op::SET_LOG, 0, 0, 0], + Some(copy.into()), + ); syscall::drop_cap(copy); } } diff --git a/crates/init/src/spawn.rs b/crates/init/src/spawn.rs index e9e4e0e..9d3ecfa 100644 --- a/crates/init/src/spawn.rs +++ b/crates/init/src/spawn.rs @@ -165,9 +165,12 @@ impl Server for Launcher<'_> { Reply(refused(reply::ERROR).0) } - fn handle_call(&mut self, badge: usize, words: [usize; 4], _cap: Option) -> ([usize; 4], Option) { + fn handle_call(&mut self, badge: usize, words: [usize; 4], _cap: Option) -> ([usize; 4], Option) { match Request::decode(words) { - Some(r) => self.spawn(badge, &r), + Some(r) => { + let (reply, process) = self.spawn(badge, &r); + (reply, process.map(Into::into)) + } None => ([MALFORMED, 0, 0, 0], None), } } diff --git a/crates/kernel/src/abi.rs b/crates/kernel/src/abi.rs index 896970f..edc4232 100644 --- a/crates/kernel/src/abi.rs +++ b/crates/kernel/src/abi.rs @@ -28,9 +28,16 @@ fn permissions(r: usize) -> Option { /// The message in `a1..a5`. fn message(a: &[usize; 6]) -> Option { let msg = Message::from_words([a[1], a[2], a[3], a[4]]); - Some(match reg_slot(a[5])? { + Some(match reg_give(a[5])? { None => msg, - Some(cap) => msg.with_cap(cap), + Some(Give { + slot, + keep_delegate: false, + }) => msg.with_cap(slot), + Some(Give { + slot, + keep_delegate: true, + }) => msg.with_cap(slot).keeping_delegate(), }) } diff --git a/crates/kernel/src/proofs/abi.rs b/crates/kernel/src/proofs/abi.rs index f131ba0..e4103bb 100644 --- a/crates/kernel/src/proofs/abi.rs +++ b/crates/kernel/src/proofs/abi.rs @@ -39,12 +39,18 @@ fn send_roundtrips() { } else { None }; - let regs = message_regs(ep.value() as usize, words, cap); + let keep: bool = kani::any(); + let give = cap.map(|slot| Give { + slot, + keep_delegate: keep, + }); + let regs = message_regs(ep.value() as usize, words, give); match decode(SYS_SEND, ®s) { Some(Syscall::Send { endpoint, message }) => { assert!(endpoint == ep); assert!(*message.words() == words); assert!(message.cap() == cap); + assert!(message.keeps_delegate() == (keep && cap.is_some())); } _ => panic!("valid send did not decode"), } diff --git a/crates/kernel/src/proofs/revocation.rs b/crates/kernel/src/proofs/revocation.rs index b3515ef..e8d51d6 100644 --- a/crates/kernel/src/proofs/revocation.rs +++ b/crates/kernel/src/proofs/revocation.rs @@ -3,7 +3,7 @@ use domain::capability::{Capability, CapabilityTable}; use domain::resource::Resource; use types::generation::Generation; use types::id::EndpointId; -use types::permission::{Permission, PermissionSet}; +use types::permission::PermissionSet; #[kani::proof] fn revoke_invalidates_cap_slot() { diff --git a/crates/kernel/src/syscall/ipc/endpoint.rs b/crates/kernel/src/syscall/ipc/endpoint.rs index 41356ae..575da67 100644 --- a/crates/kernel/src/syscall/ipc/endpoint.rs +++ b/crates/kernel/src/syscall/ipc/endpoint.rs @@ -38,23 +38,23 @@ impl Kernel

{ pub(super) fn transfer_cap( &mut self, from: ProcessId, - slot: Option, + message: &Message, grant: bool, to: ProcessId, ) -> Option { - let slot = slot?; + let slot = message.cap()?; if !grant { return None; } // The receiver gets the capability but not the right to pass it - // on; a sender that wants that uses Delegate with explicit rights. + // on, unless the sender says so; it can only give what it holds. let source = self.check_cap(from, slot, Permission::Delegate).ok()?; - let copy = Capability::new( - *source.resource(), - source.permissions().without(Permission::Delegate), - *source.generation(), - ) - .with_badge(source.badge()); + let permissions = match message.keeps_delegate() { + true => *source.permissions(), + false => source.permissions().without(Permission::Delegate), + }; + let copy = Capability::new(*source.resource(), permissions, *source.generation()) + .with_badge(source.badge()); let parent = self.cap_ref(from, slot); self.insert_derived(to, copy, parent).ok() } @@ -71,7 +71,7 @@ impl Kernel

{ let from = self.thread_process(sender); let to = self.thread_process(receiver); let cap = match (from, to) { - (Some(from), Some(to)) => self.transfer_cap(from, out.message.cap(), out.grant, to), + (Some(from), Some(to)) => self.transfer_cap(from, &out.message, out.grant, to), _ => None, }; if kind == DeliveryKind::Call { @@ -234,7 +234,7 @@ impl Kernel

{ } let to = self.thread_process(caller.tid); // A reply may always carry a capability the server can delegate. - let cap = to.and_then(|to| self.transfer_cap(pid, message.cap(), true, to)); + let cap = to.and_then(|to| self.transfer_cap(pid, &message, true, to)); let delivery = Delivery { kind: DeliveryKind::Message, badge: 0, diff --git a/crates/kernel/src/tests/ipc.rs b/crates/kernel/src/tests/ipc.rs index a8c1f76..8454c92 100644 --- a/crates/kernel/src/tests/ipc.rs +++ b/crates/kernel/src/tests/ipc.rs @@ -917,6 +917,67 @@ fn a_received_cap_cannot_be_passed_on() { )); } +/// What `b` holds in `slot`, by permission. +fn rights_of(t: &Ipc, pid: ProcessId, slot: CapSlot) -> PermissionSet { + *t.k.processes + .get(pid) + .unwrap() + .capabilities() + .get(slot) + .unwrap() + .permissions() +} + +#[test] +fn a_sender_can_let_the_receiver_pass_a_cap_on() { + let mut t = Ipc::new(); + let (a, b, ta, tb, a_send, b_recv) = (t.a, t.b, t.ta, t.tb, t.a_send, t.b_recv); + let other = t.k.create_endpoint(a).unwrap(); + let gift = + t.k.grant_cap_for_test(a, Resource::Endpoint(other), perms(SEND_GRANT)); + t.run( + ta, + Syscall::Send { + endpoint: a_send, + message: msg([0; 4]).with_cap(gift).keeping_delegate(), + }, + ); + let d = delivered(&t.run(tb, Syscall::Receive { endpoint: b_recv })).unwrap(); + let got = d.cap.expect("the cap arrives"); + assert_eq!( + rights_of(&t, b, got), + rights_of(&t, a, gift), + "with exactly the sender's rights" + ); + let mint = t.run( + tb, + Syscall::Mint { + slot: got, + badge: Badge::new(1), + permissions: perms(SEND), + }, + ); + assert!(error(&mint).is_none(), "so it can derive from it"); +} + +#[test] +fn keeping_delegate_cannot_give_what_the_sender_lacks() { + let mut t = Ipc::new(); + let (a, ta, tb, a_send, b_recv) = (t.a, t.ta, t.tb, t.a_send, t.b_recv); + let other = t.k.create_endpoint(a).unwrap(); + let gift = + t.k.grant_cap_for_test(a, Resource::Endpoint(other), perms(SEND)); + t.run( + ta, + Syscall::Send { + endpoint: a_send, + message: msg([0; 4]).with_cap(gift).keeping_delegate(), + }, + ); + let d = delivered(&t.run(tb, Syscall::Receive { endpoint: b_recv })).unwrap(); + assert_eq!(d.cap, None, "a cap without Delegate never travels"); +} + #[test] fn an_irq_has_one_owner_until_it_dies() { let mut t = Ipc::new(); diff --git a/crates/libkos/src/device.rs b/crates/libkos/src/device.rs index 6c27620..aae3cb6 100644 --- a/crates/libkos/src/device.rs +++ b/crates/libkos/src/device.rs @@ -59,7 +59,7 @@ pub fn receive_device(endpoint: CapSlot) -> Device { pub fn give_device(endpoint: CapSlot, device: Device) -> Result<()> { for (what, cap) in [(MMIO, device.mmio), (IRQ, device.irq)] { if let Some(cap) = cap { - syscall::call(endpoint, [SETUP, what, device.kind, 0], Some(cap))?; + syscall::call(endpoint, [SETUP, what, device.kind, 0], Some(cap.into()))?; } } syscall::call(endpoint, [SETUP, DONE, 0, 0], None).map(|_| ()) diff --git a/crates/libkos/src/display.rs b/crates/libkos/src/display.rs index 3465832..8df300b 100644 --- a/crates/libkos/src/display.rs +++ b/crates/libkos/src/display.rs @@ -60,7 +60,11 @@ pub fn subscribe_input(ep: CapSlot) -> Option { // Signal only (Delegate just to send it; the transfer strips it). let signal = PermissionSet::empty().with(Permission::Write).with(Permission::Delegate); let copy = syscall::mint(notify, Badge::NONE, signal).ok()?; - let sent = syscall::call(ep, [types::display::input::SUBSCRIBE, 0, 0, 0], Some(copy)); + let sent = syscall::call( + ep, + [types::display::input::SUBSCRIBE, 0, 0, 0], + Some(copy.into()), + ); syscall::drop_cap(copy); (sent.ok()?.words()[0] == 0).then_some(notify) } diff --git a/crates/libkos/src/fs.rs b/crates/libkos/src/fs.rs index 6563659..bbf7940 100644 --- a/crates/libkos/src/fs.rs +++ b/crates/libkos/src/fs.rs @@ -24,6 +24,8 @@ pub enum FsError { ReadOnly, /// A path longer than the server accepts. PathTooLong, + /// Outside this connection's scope, or beyond its rights. + Denied, /// The server refused, or the call itself failed. Failed, } @@ -76,6 +78,7 @@ impl Storage { match r[0] { reply::NOT_FOUND => Err(FsError::NotFound), reply::READ_ONLY => Err(FsError::ReadOnly), + reply::DENIED => Err(FsError::Denied), reply::ERROR | reply::NOT_ATTACHED => Err(FsError::Failed), _ => Ok(r), } @@ -100,6 +103,20 @@ impl Storage { self.expect([opcode, len, 0, 0], tag) } + /// A storage capability to `path` inside this connection's scope, + /// writable only if `write` and this one is. It can be passed on; + /// `release` takes it back. + pub fn grant(&mut self, path: &[u8], write: bool) -> Result { + let len = self.put_path(path)?; + let reply = syscall::call(self.ep, [op::GRANT, len, usize::from(write), 0], None) + .map_err(|_| FsError::Failed)?; + match (reply.words()[0], reply) { + (reply::GRANTED, syscall::Received::Message { cap: Some(cap), .. }) => Ok(cap), + (reply::DENIED, _) => Err(FsError::Denied), + _ => Err(FsError::Failed), + } + } + pub fn open(&mut self, path: &[u8]) -> Result { self.with_path(op::OPEN, path, reply::HANDLE).map(|h| FileHandle(h as u16)) } @@ -181,3 +198,13 @@ impl Storage { Ok(count) } } + +/// Takes back `grant` and everything granted from it, from every holder; +/// the slot is empty afterwards. +pub fn release(grant: CapSlot) -> Result<()> { + match syscall::request(grant, [op::RELEASE, 0, 0, 0]) { + Ok([reply::OK, ..]) => Ok(()), + Ok([reply::DENIED, ..]) => Err(FsError::Denied), + _ => Err(FsError::Failed), + } +} diff --git a/crates/libkos/src/syscall/ipc.rs b/crates/libkos/src/syscall/ipc.rs index 9f997e3..8c72f55 100644 --- a/crates/libkos/src/syscall/ipc.rs +++ b/crates/libkos/src/syscall/ipc.rs @@ -4,6 +4,8 @@ use super::{call0, raw, slot_of, Error, Result}; use types::bounded::CapSlot; use types::syscall_abi::*; +pub use types::syscall_abi::Give; + /// What a receiving call got. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Received { @@ -48,7 +50,7 @@ pub fn create_endpoint() -> Option { slot_of(call0(SYS_CREATE_ENDPOINT)).ok() } -pub fn send_msg(endpoint: CapSlot, words: [usize; 4], cap: Option) -> Result<()> { +pub fn send_msg(endpoint: CapSlot, words: [usize; 4], cap: Option) -> Result<()> { raw( SYS_SEND, message_regs(endpoint.value() as usize, words, cap), @@ -61,7 +63,7 @@ pub fn receive_msg(endpoint: CapSlot) -> Result { } /// Sends and waits for the reply. -pub fn call(endpoint: CapSlot, words: [usize; 4], cap: Option) -> Result { +pub fn call(endpoint: CapSlot, words: [usize; 4], cap: Option) -> Result { raw( SYS_CALL, message_regs(endpoint.value() as usize, words, cap), @@ -80,12 +82,12 @@ pub fn request(endpoint: CapSlot, words: [usize; 4]) -> Result<[usize; 4]> { } /// Answers the last `Call` this thread received. -pub fn reply(words: [usize; 4], cap: Option) -> Result<()> { +pub fn reply(words: [usize; 4], cap: Option) -> Result<()> { raw(SYS_REPLY, message_regs(0, words, cap)).map(|_| ()) } /// Replies to the pending `Call` (if any), then receives the next message. -pub fn reply_recv(endpoint: CapSlot, words: [usize; 4], cap: Option) -> Result { +pub fn reply_recv(endpoint: CapSlot, words: [usize; 4], cap: Option) -> Result { raw( SYS_REPLY_RECV, message_regs(endpoint.value() as usize, words, cap), diff --git a/crates/server-framework/src/server/mod.rs b/crates/server-framework/src/server/mod.rs index eb93036..4cb7dfc 100644 --- a/crates/server-framework/src/server/mod.rs +++ b/crates/server-framework/src/server/mod.rs @@ -2,6 +2,7 @@ mod tests; use crate::codec::{Decode, Encode}; +use libkos::syscall::Give; use types::bounded::CapSlot; /// The reply to a request that does not decode. No protocol uses this @@ -27,7 +28,7 @@ pub trait Server { _badge: usize, words: [usize; 4], _cap: Option, - ) -> ([usize; 4], Option) { + ) -> ([usize; 4], Option) { match Self::Request::decode(words) { Some(request) => (self.handle(request).encode(), None), None => ([MALFORMED, 0, 0, 0], None), @@ -68,6 +69,10 @@ pub trait Server { /// has nobody left to serve, so it says why and exits. pub fn serve(server: &mut S, endpoint: CapSlot) -> ! { let error = server.run(endpoint); - libkos::println!("server: endpoint {} unusable ({:?}), exiting", endpoint.value(), error); + libkos::println!( + "server: endpoint {} unusable ({:?}), exiting", + endpoint.value(), + error + ); libkos::syscall::exit() } diff --git a/crates/servers/display-server/src/server.rs b/crates/servers/display-server/src/server.rs index b56dcaa..f12ec24 100644 --- a/crates/servers/display-server/src/server.rs +++ b/crates/servers/display-server/src/server.rs @@ -57,7 +57,7 @@ impl Server for DisplayServer { type Request = DisplayRequest; type Response = DisplayResponse; - fn handle_call(&mut self, badge: usize, words: [usize; 4], cap: Option) -> ([usize; 4], Option) { + fn handle_call(&mut self, badge: usize, words: [usize; 4], cap: Option) -> ([usize; 4], Option) { use types::display::input::{POLL, SUBSCRIBE}; match (words[0], cap) { (SUBSCRIBE, Some(notify)) => { diff --git a/crates/servers/input-server/src/server.rs b/crates/servers/input-server/src/server.rs index 72e3078..38b1575 100644 --- a/crates/servers/input-server/src/server.rs +++ b/crates/servers/input-server/src/server.rs @@ -88,7 +88,7 @@ impl Server for InputServer { } } - fn handle_call(&mut self, _badge: usize, words: [usize; 4], cap: Option) -> ([usize; 4], Option) { + fn handle_call(&mut self, _badge: usize, words: [usize; 4], cap: Option) -> ([usize; 4], Option) { if words[0] == types::input::SUBSCRIBE { self.subscriber = cap; return ([0; 4], None); diff --git a/crates/servers/storage-server/src/grants.rs b/crates/servers/storage-server/src/grants.rs new file mode 100644 index 0000000..73aceb8 --- /dev/null +++ b/crates/servers/storage-server/src/grants.rs @@ -0,0 +1,94 @@ +//! Who holds which scope. Storage mints every badged copy of its own +//! endpoint, so a badge it does not know (never issued, or released) is +//! no authority at all. + +use types::bounded::CapSlot; + +use crate::scope::Scope; + +pub const MAX_GRANTS: usize = 16; + +#[derive(Debug, Clone, Copy)] +struct Grant { + badge: usize, + scope: Scope, + /// The badge it was narrowed from; releasing that releases this. + parent: usize, + /// Storage's own copy, which every copy handed out derives from: + /// revoking it takes the grant back from everyone. + cap: CapSlot, +} + +pub struct Grants { + grants: [Option; MAX_GRANTS], + /// Badges are never reused, so a released one stays dead. + next_badge: usize, +} + +impl Grants { + pub const fn new() -> Self { + Self { + grants: [None; MAX_GRANTS], + next_badge: 1, + } + } + + pub fn scope(&self, badge: usize) -> Option { + match badge { + 0 => Some(Scope::EVERYTHING), + b => self + .grants + .iter() + .flatten() + .find(|g| g.badge == b) + .map(|g| g.scope), + } + } + + /// Records `scope` under a fresh badge, which `mint` turns into a + /// capability; returns that capability. + pub fn add( + &mut self, + parent: usize, + scope: Scope, + mint: impl FnOnce(usize) -> Option, + ) -> Option { + let free = self.grants.iter_mut().find(|g| g.is_none())?; + let badge = self.next_badge; + let cap = mint(badge)?; + self.next_badge += 1; + *free = Some(Grant { + badge, + scope, + parent, + cap, + }); + Some(cap) + } + + /// 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)) { + if badge == 0 { + return; + } + let mut doomed = [0usize; MAX_GRANTS]; + doomed[0] = badge; + let mut n = 1; + let mut i = 0; + while i < n { + let b = doomed[i]; + i += 1; + for g in &mut self.grants { + let Some(grant) = g else { continue }; + if grant.badge == b { + gone(grant.badge, grant.cap); + *g = None; + } else if grant.parent == b && n < MAX_GRANTS { + doomed[n] = grant.badge; + n += 1; + } + } + } + } +} diff --git a/crates/servers/storage-server/src/lib.rs b/crates/servers/storage-server/src/lib.rs index f3cdeff..d9ac3ff 100644 --- a/crates/servers/storage-server/src/lib.rs +++ b/crates/servers/storage-server/src/lib.rs @@ -5,7 +5,9 @@ extern crate alloc; pub mod adapters; pub mod filesystem; pub mod fs; +pub mod grants; pub mod protocol; +pub mod scope; pub mod server; #[cfg(test)] @@ -14,17 +16,22 @@ mod tests; use libkos::syscall; use types::address::VirtualAddress; +use types::badge::Badge; use types::bounded::{CapSlot, PageCount}; use types::mapping::PagePerms; +use types::permission::{Permission, PermissionSet}; /// Client buffers sit high in storage's window, clear of its heap. const BUFFERS_VA: usize = 0x0100_0000; -/// Each client's buffer is a fresh region, mapped here and handed to the -/// client, which maps the same pages in its own space. -struct RegionBuffers; +const EP: CapSlot = CapSlot::const_new(0); -impl server::Buffers for RegionBuffers { +/// Each client's buffer is a region, mapped here and handed to the +/// client, which maps the same pages in its own space. Grants are badged +/// copies of storage's endpoint. +struct Kernel; + +impl server::Host for Kernel { fn create(&mut self, client: usize) -> Option<(&'static mut [u8], Option)> { let va = BUFFERS_VA + client * protocol::BUFFER_SIZE; let region = syscall::create_region(PageCount::new(protocol::BUFFER_PAGES as u16)?).ok()?; @@ -34,12 +41,30 @@ impl server::Buffers for RegionBuffers { let buf = unsafe { core::slice::from_raw_parts_mut(va as *mut u8, protocol::BUFFER_SIZE) }; Some((buf, Some(region))) } + + fn reclaim(&mut self, buffer: CapSlot) { + syscall::revoke(buffer); + } + + /// Call it and pass it on; never Read, which would let the holder + /// take other clients' requests. + fn mint(&mut self, badge: usize) -> Option { + let perms = PermissionSet::empty() + .with(Permission::Execute) + .with(Permission::Delegate); + syscall::mint(EP, Badge::new(badge), perms).ok() + } + + fn revoke(&mut self, grant: CapSlot) { + syscall::revoke(grant); + syscall::drop_cap(grant); + } } pub fn run() -> ! { libkos::println!("storage: started"); - let ep = CapSlot::new(0).unwrap(); + let ep = EP; // Init hands over the disk before asking for anything. let Some(device) = libkos::device::receive_device(ep) .mmio @@ -52,6 +77,6 @@ pub fn run() -> ! { libkos::println!("storage: no ext4 filesystem it can read, exiting"); syscall::exit(); }; - let mut srv = server::StorageServer::new(ext4, RegionBuffers); + let mut srv = server::StorageServer::new(ext4, Kernel); server_framework::serve(&mut srv, ep) } diff --git a/crates/servers/storage-server/src/protocol.rs b/crates/servers/storage-server/src/protocol.rs index 0d5e4c0..10039b6 100644 --- a/crates/servers/storage-server/src/protocol.rs +++ b/crates/servers/storage-server/src/protocol.rs @@ -25,6 +25,13 @@ pub enum StorageRequest { Mkdir { path_len: usize }, /// Carries the endpoint to print to. SetLog, + /// A capability to the path (`path_len` bytes of the buffer), inside + /// the caller's scope. + Grant { + path_len: usize, + write: bool, + }, + Release, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -43,6 +50,9 @@ pub enum StorageResponse { ReadOnly, /// The client has not attached a buffer yet. NotAttached, + /// Carries the new capability. + Granted, + Denied, } fn handle(word: usize) -> Option { @@ -64,6 +74,8 @@ impl Encode for StorageRequest { Delete { path_len } => [op::DELETE, path_len, 0, 0], Mkdir { path_len } => [op::MKDIR, path_len, 0, 0], SetLog => [op::SET_LOG, 0, 0, 0], + Grant { path_len, write } => [op::GRANT, path_len, usize::from(write), 0], + Release => [op::RELEASE, 0, 0, 0], } } } @@ -83,6 +95,11 @@ impl Decode for StorageRequest { op::DELETE => Delete { path_len: w[1] }, op::MKDIR => Mkdir { path_len: w[1] }, op::SET_LOG => SetLog, + op::GRANT => Grant { + path_len: w[1], + write: w[2] != 0, + }, + op::RELEASE => Release, _ => return None, }) } @@ -103,6 +120,8 @@ impl Encode for StorageResponse { Error => [reply::ERROR, 0, 0, 0], ReadOnly => [reply::READ_ONLY, 0, 0, 0], NotAttached => [reply::NOT_ATTACHED, 0, 0, 0], + Granted => [reply::GRANTED, 0, 0, 0], + Denied => [reply::DENIED, 0, 0, 0], } } } @@ -122,6 +141,8 @@ impl Decode for StorageResponse { reply::ERROR => Error, reply::READ_ONLY => ReadOnly, reply::NOT_ATTACHED => NotAttached, + reply::GRANTED => Granted, + reply::DENIED => Denied, _ => return None, }) } diff --git a/crates/servers/storage-server/src/scope.rs b/crates/servers/storage-server/src/scope.rs new file mode 100644 index 0000000..6030fbb --- /dev/null +++ b/crates/servers/storage-server/src/scope.rs @@ -0,0 +1,71 @@ +//! What one storage capability reaches: a subtree (or a single file) and +//! whether it may change it. Every path a client sends is resolved inside +//! its scope, so no path names anything outside it. + +use crate::protocol::MAX_PATH; + +/// A path on disk, without leading, trailing or doubled slashes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DiskPath { + bytes: [u8; MAX_PATH], + len: usize, +} + +impl DiskPath { + const ROOT: Self = Self { + bytes: [0; MAX_PATH], + len: 0, + }; + + pub fn as_bytes(&self) -> &[u8] { + &self.bytes[..self.len] + } + + /// Appends `relative` one component at a time. `..` is refused, not + /// resolved: the file system would follow it out of the scope. + fn join(mut self, relative: &[u8]) -> Option { + for part in relative.split(|b| *b == b'/') { + match part { + b"" | b"." => continue, + b".." => return None, + _ => {} + } + let sep = usize::from(self.len > 0); + let end = self.len + sep + part.len(); + let room = self.bytes.get_mut(self.len..end)?; + room[..sep].copy_from_slice(&b"/"[..sep]); + room[sep..].copy_from_slice(part); + self.len = end; + } + Some(self) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Scope { + root: DiskPath, + pub write: bool, +} + +impl Scope { + /// The whole disk, read-write: what the unbadged holder (init) has. + pub const EVERYTHING: Self = Self { + root: DiskPath::ROOT, + write: true, + }; + + /// `path`, taken relative to this scope's root, as a path on disk; + /// `None` if it would leave the scope or is too long. + pub fn resolve(&self, path: &[u8]) -> Option { + self.root.join(path) + } + + /// A scope inside this one: never wider, and writable only if both + /// this one is and `write` asks for it. + pub fn narrow(&self, path: &[u8], write: bool) -> Option { + Some(Self { + root: self.resolve(path)?, + write: self.write && write, + }) + } +} diff --git a/crates/servers/storage-server/src/server.rs b/crates/servers/storage-server/src/server.rs index 839a0ab..ca0ff20 100644 --- a/crates/servers/storage-server/src/server.rs +++ b/crates/servers/storage-server/src/server.rs @@ -1,10 +1,11 @@ +use libkos::syscall::Give; use server_framework::{Decode, Encode, MALFORMED, Server}; use types::bounded::CapSlot; use crate::filesystem::{DirEntry, FileHandle, FileSystem}; -use crate::protocol::{ - DIR_ENTRY_SIZE, DIR_NAME_SIZE, MAX_PATH, StorageRequest, StorageResponse, -}; +use crate::grants::Grants; +use crate::protocol::{DIR_ENTRY_SIZE, DIR_NAME_SIZE, MAX_PATH, StorageRequest, StorageResponse}; +use crate::scope::Scope; const MAX_CLIENTS: usize = 8; /// Handles at or past this are never valid (the file systems hand out @@ -12,35 +13,51 @@ const MAX_CLIENTS: usize = 8; const MAX_HANDLES: usize = 16; const MAX_LISTED: usize = 64; -/// Where client buffers come from: a fresh region per client in the -/// real server, plain memory in tests. -pub trait Buffers { +/// What the server needs from the kernel: regions for client buffers +/// and badged copies of its endpoint for grants. Syscalls in the real +/// server, plain memory in tests. +pub trait Host { /// A buffer for a new client and the capability that lets the client /// map it, if there is one to send. fn create(&mut self, client: usize) -> Option<(&'static mut [u8], Option)>; + /// Unmaps a buffer from whoever it was sent to, so it can be reused. + fn reclaim(&mut self, buffer: CapSlot); + /// A copy of the server's endpoint carrying `badge`. + fn mint(&mut self, badge: usize) -> Option; + /// Deletes `grant` and every copy made from it, in every process. + fn revoke(&mut self, grant: CapSlot); } struct Client { - badge: usize, + /// Who the buffer belongs to now; `None` once its grant is released. + badge: Option, buf: &'static mut [u8], /// What maps `buf`, sent again when the same client attaches again. cap: Option, } -pub struct StorageServer { +#[derive(Debug, Clone, Copy)] +struct Owner { + badge: usize, + /// Opened through a scope that may write. + write: bool, +} + +pub struct StorageServer { fs: F, - buffers: B, + host: H, + grants: Grants, clients: [Option; MAX_CLIENTS], - /// The badge of the client that opened each handle. - owners: [Option; MAX_HANDLES], + owners: [Option; MAX_HANDLES], log: Option, } -impl StorageServer { - pub fn new(fs: F, buffers: B) -> Self { +impl StorageServer { + pub fn new(fs: F, host: H) -> Self { Self { fs, - buffers, + host, + grants: Grants::new(), clients: [const { None }; MAX_CLIENTS], owners: [None; MAX_HANDLES], log: None, @@ -70,70 +87,176 @@ impl StorageServer { &self.fs } + #[cfg(test)] + pub fn host(&self) -> &H { + &self.host + } + #[cfg(test)] pub fn client_buffer_for_test(&mut self, badge: usize) -> Option<&mut [u8]> { self.client(badge).map(|c| &mut *c.buf) } fn client(&mut self, badge: usize) -> Option<&mut Client> { - self.clients.iter_mut().flatten().find(|c| c.badge == badge) + self.clients + .iter_mut() + .flatten() + .find(|c| c.badge == Some(badge)) } - /// A client attaching again gets its buffer again: the programs a shell - /// runs one after another share its badge. - fn attach(&mut self, badge: usize) -> (StorageResponse, Option) { + /// A client attaching again gets its buffer again; a new one gets a + /// buffer a released grant left behind, or a fresh one. + fn attach(&mut self, badge: usize) -> (StorageResponse, Option) { if let Some(c) = self.client(badge) { - return (StorageResponse::Attached { size: c.buf.len() }, c.cap); + return ( + StorageResponse::Attached { size: c.buf.len() }, + c.cap.map(Give::from), + ); + } + if let Some(c) = self + .clients + .iter_mut() + .flatten() + .find(|c| c.badge.is_none()) + { + c.badge = Some(badge); + return ( + StorageResponse::Attached { size: c.buf.len() }, + c.cap.map(Give::from), + ); } let Some(index) = self.clients.iter().position(Option::is_none) else { return (StorageResponse::Error, None); }; - let Some((buf, cap)) = self.buffers.create(index) else { + let Some((buf, cap)) = self.host.create(index) else { return (StorageResponse::Error, None); }; let size = buf.len(); if let Some(slot) = self.clients.get_mut(index) { - *slot = Some(Client { badge, buf, cap }); + *slot = Some(Client { + badge: Some(badge), + buf, + cap, + }); } - (StorageResponse::Attached { size }, cap) + (StorageResponse::Attached { size }, cap.map(Give::from)) } - fn owns(&self, badge: usize, handle: FileHandle) -> bool { - self.owners.get(handle.raw() as usize).copied().flatten() == Some(badge) + /// Takes `badge`'s grant back from everyone, with everything granted + /// from it: their capabilities, open files and buffers. + fn release(&mut self, badge: usize) -> StorageResponse { + if badge == 0 { + return StorageResponse::Error; + } + let Self { + fs, + host, + grants, + clients, + owners, + .. + } = self; + grants.release(badge, |gone, cap| { + host.revoke(cap); + for (raw, owner) in owners.iter_mut().enumerate() { + if owner.is_some_and(|o| o.badge == gone) { + fs.close(FileHandle::new(raw as u16)); + *owner = None; + } + } + for c in clients + .iter_mut() + .flatten() + .filter(|c| c.badge == Some(gone)) + { + if let Some(buffer) = c.cap { + host.reclaim(buffer); + } + c.buf.fill(0); + c.badge = None; + } + }); + StorageResponse::Ok } - /// Serves one request from `badge`. - fn serve(&mut self, badge: usize, request: StorageRequest) -> (StorageResponse, Option) { + fn owner(&self, handle: FileHandle) -> Option { + self.owners.get(handle.raw() as usize).copied().flatten() + } + + /// Serves one request from `badge`, which holds `scope`. + fn serve( + &mut self, + badge: usize, + scope: Scope, + request: StorageRequest, + ) -> (StorageResponse, Option) { use StorageRequest::*; use StorageResponse as R; - if request == Attach { - return self.attach(badge); + match request { + Attach => return self.attach(badge), + Release => return (self.release(badge), None), + _ => {} } - let handle_ok = match request { - Read { handle, .. } | Write { handle, .. } | Close { handle } | FileSize { handle } => { - self.owns(badge, handle) + let allowed = match request { + Read { handle, .. } | Close { handle } | FileSize { handle } => { + self.owner(handle).is_some_and(|o| o.badge == badge) } + Write { handle, .. } => self + .owner(handle) + .is_some_and(|o| o.badge == badge && o.write), + Create { .. } | Delete { .. } | Mkdir { .. } => scope.write, _ => true, }; - if !handle_ok { - return (R::Error, None); + if !allowed { + return (R::Denied, None); } - let mutates = matches!(request, Write { .. } | Create { .. } | Delete { .. } | Mkdir { .. }); + let mutates = matches!( + request, + Write { .. } | Create { .. } | Delete { .. } | Mkdir { .. } + ); if mutates && !self.fs.writable() { return (R::ReadOnly, None); } // Split borrows: the client's buffer, the file system, the owners. - let Self { fs, clients, owners, .. } = self; - let Some(client) = clients.iter_mut().flatten().find(|c| c.badge == badge) else { + let Self { + fs, + host, + grants, + clients, + owners, + .. + } = self; + let Some(client) = clients + .iter_mut() + .flatten() + .find(|c| c.badge == Some(badge)) + else { return (R::NotAttached, None); }; let buf = &mut *client.buf; let room = buf.len(); - let path = |len: usize| ..len.min(MAX_PATH).min(room); - let opened = |owners: &mut [Option; MAX_HANDLES], h: Option| match h { + let sent = |len: usize| ..len.min(MAX_PATH).min(room); + // The client's path, as a path on disk inside its scope. + let path_len = match request { + Open { path_len } + | Create { path_len } + | Delete { path_len } + | Mkdir { path_len } + | ListDir { path_len } => Some(path_len), + _ => None, + }; + let disk = match path_len.map(|len| scope.resolve(&buf[sent(len)])) { + Some(None) => return (R::Denied, None), + resolved => resolved.flatten(), + }; + let path = disk.as_ref().map_or(&[][..], |p| p.as_bytes()); + let opened = |owners: &mut [Option; MAX_HANDLES], h: Option| match h { Some(h) => match owners.get_mut(h.raw() as usize) { Some(owner) => { - *owner = Some(badge); + *owner = Some(Owner { + badge, + write: scope.write, + }); R::Handle(h) } // A handle the table cannot track is one we cannot police. @@ -142,12 +265,20 @@ impl StorageServer { None => R::NotFound, }; let response = match request { - Attach | SetLog => R::Error, - Open { path_len } => { - let h = fs.open(&buf[path(path_len)]); + Attach | SetLog | 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))) { + // The holder may pass it on: that is what it asked for. + Some(cap) => (R::Granted, Some(Give::keeping_delegate(cap))), + None => (R::Denied, None), + }; + } + Open { .. } => { + let h = fs.open(path); opened(owners, h) } - Create { path_len } => match fs.create(&buf[path(path_len)]) { + Create { .. } => match fs.create(path) { None => R::Error, h => opened(owners, h), }, @@ -170,15 +301,16 @@ impl StorageServer { Some(size) => R::Size(size), None => R::Error, }, - ListDir { path_len } => { - let mut name = [0u8; MAX_PATH]; - let p = &buf[path(path_len)]; - name[..p.len()].copy_from_slice(p); + ListDir { .. } => { let mut entries = [DirEntry::empty(); MAX_LISTED]; let room = (buf.len() / DIR_ENTRY_SIZE).min(MAX_LISTED); - match fs.list_dir(&name[..p.len()], &mut entries[..room]) { + match fs.list_dir(path, &mut entries[..room]) { Some(count) => { - for (entry, record) in entries.iter().take(count).zip(buf.chunks_mut(DIR_ENTRY_SIZE)) { + for (entry, record) in entries + .iter() + .take(count) + .zip(buf.chunks_mut(DIR_ENTRY_SIZE)) + { write_entry(entry, record); } R::DirEntries { count } @@ -186,11 +318,19 @@ impl StorageServer { None => R::NotFound, } } - Delete { path_len } => { - if fs.delete(&buf[path(path_len)]) { R::Ok } else { R::NotFound } + Delete { .. } => { + if fs.delete(path) { + R::Ok + } else { + R::NotFound + } } - Mkdir { path_len } => { - if fs.mkdir(&buf[path(path_len)]) { R::Ok } else { R::Error } + Mkdir { .. } => { + if fs.mkdir(path) { + R::Ok + } else { + R::Error + } } }; (response, None) @@ -203,17 +343,21 @@ fn write_entry(entry: &DirEntry, record: &mut [u8]) { let n = name.len().min(DIR_NAME_SIZE); record[..n].copy_from_slice(&name[..n]); record[DIR_NAME_SIZE..DIR_NAME_SIZE + 8].copy_from_slice(&(entry.size as u64).to_le_bytes()); - let flags = if entry.is_dir { types::storage::DIR_FLAG_DIRECTORY } else { 0 }; + let flags = if entry.is_dir { + types::storage::DIR_FLAG_DIRECTORY + } else { + 0 + }; record[DIR_NAME_SIZE + 8..DIR_ENTRY_SIZE].copy_from_slice(&flags.to_le_bytes()); } -impl Server for StorageServer { +impl Server for StorageServer { type Request = StorageRequest; type Response = StorageResponse; /// Without a badge: as the unbadged client. fn handle(&mut self, request: StorageRequest) -> StorageResponse { - self.serve(0, request).0 + self.serve(0, Scope::EVERYTHING, request).0 } fn handle_call( @@ -221,13 +365,17 @@ impl Server for StorageServer { badge: usize, words: [usize; 4], cap: Option, - ) -> ([usize; 4], Option) { + ) -> ([usize; 4], Option) { match StorageRequest::decode(words) { Some(StorageRequest::SetLog) => (self.set_log(badge, cap).encode(), None), - Some(request) => { - let (response, cap) = self.serve(badge, request); - (response.encode(), cap) - } + // A badge storage never issued, or one it has taken back. + Some(request) => match self.grants.scope(badge) { + None => (StorageResponse::Denied.encode(), None), + Some(scope) => { + let (response, cap) = self.serve(badge, scope, request); + (response.encode(), cap) + } + }, None => ([MALFORMED, 0, 0, 0], None), } } diff --git a/crates/servers/storage-server/src/tests/mod.rs b/crates/servers/storage-server/src/tests/mod.rs index 24766d0..8e2d350 100644 --- a/crates/servers/storage-server/src/tests/mod.rs +++ b/crates/servers/storage-server/src/tests/mod.rs @@ -1,4 +1,5 @@ mod ext4; mod ext4_real; mod protocol; +mod scope; mod server; diff --git a/crates/servers/storage-server/src/tests/protocol.rs b/crates/servers/storage-server/src/tests/protocol.rs index bb7c947..209db2c 100644 --- a/crates/servers/storage-server/src/tests/protocol.rs +++ b/crates/servers/storage-server/src/tests/protocol.rs @@ -16,6 +16,15 @@ fn every_request_roundtrips() { StorageRequest::Create { path_len: 5 }, StorageRequest::Delete { path_len: 5 }, StorageRequest::Mkdir { path_len: 4 }, + StorageRequest::Grant { + path_len: 4, + write: true, + }, + StorageRequest::Grant { + path_len: 0, + write: false, + }, + StorageRequest::Release, ] { assert_eq!(StorageRequest::decode(req.encode()), Some(req)); } @@ -35,6 +44,8 @@ fn every_response_roundtrips() { StorageResponse::Error, StorageResponse::ReadOnly, StorageResponse::NotAttached, + StorageResponse::Granted, + StorageResponse::Denied, ] { assert_eq!(StorageResponse::decode(resp.encode()), Some(resp)); } diff --git a/crates/servers/storage-server/src/tests/scope.rs b/crates/servers/storage-server/src/tests/scope.rs new file mode 100644 index 0000000..dccbcff --- /dev/null +++ b/crates/servers/storage-server/src/tests/scope.rs @@ -0,0 +1,46 @@ +extern crate std; + +use crate::protocol::MAX_PATH; +use crate::scope::Scope; + +fn resolved(scope: &Scope, path: &[u8]) -> Option> { + scope.resolve(path).map(|p| p.as_bytes().to_vec()) +} + +#[test] +fn paths_are_joined_one_component_at_a_time() { + let docs = Scope::EVERYTHING.narrow(b"/docs/", true).unwrap(); + assert_eq!( + resolved(&docs, b"a//b/./c/").as_deref(), + Some(&b"docs/a/b/c"[..]) + ); + assert_eq!(resolved(&docs, b"").as_deref(), Some(&b"docs"[..])); + assert_eq!( + resolved(&Scope::EVERYTHING, b"/").as_deref(), + Some(&b""[..]) + ); +} + +#[test] +fn dot_dot_is_refused_anywhere() { + for path in [b"..".as_slice(), b"a/..", b"a/../b", b"../.."] { + assert_eq!(resolved(&Scope::EVERYTHING, path), None, "{path:?}"); + } + // A name that merely starts with dots is a name. + assert!(resolved(&Scope::EVERYTHING, b"...").is_some()); +} + +#[test] +fn a_path_too_long_for_the_disk_is_refused() { + let long = [b'a'; MAX_PATH]; + assert!(Scope::EVERYTHING.resolve(&long).is_some()); + let docs = Scope::EVERYTHING.narrow(b"docs", true).unwrap(); + assert!(docs.resolve(&long).is_none()); +} + +#[test] +fn narrowing_never_adds_write() { + let ro = Scope::EVERYTHING.narrow(b"", false).unwrap(); + assert!(!ro.narrow(b"x", true).unwrap().write); + assert!(Scope::EVERYTHING.narrow(b"x", true).unwrap().write); +} diff --git a/crates/servers/storage-server/src/tests/server.rs b/crates/servers/storage-server/src/tests/server.rs index 1b6f1d5..3c1cc7d 100644 --- a/crates/servers/storage-server/src/tests/server.rs +++ b/crates/servers/storage-server/src/tests/server.rs @@ -3,31 +3,59 @@ extern crate std; use crate::adapters::mock_fs::MockFileSystem; use crate::filesystem::FileHandle; use crate::protocol::*; -use crate::server::{Buffers, StorageServer}; +use crate::server::{Host, StorageServer}; use server_framework::{Decode, Encode, MALFORMED, Server}; use std::boxed::Box; use std::vec; +use std::vec::Vec; use types::bounded::CapSlot; -/// Plain memory instead of regions; the "capability" is a fixed slot. -struct TestBuffers; +/// Plain memory instead of regions; the buffer "capability" is a fixed +/// slot, a grant's is its badge past `GRANT_SLOTS`. +#[derive(Default)] +struct TestHost { + reclaimed: Vec, + revoked: Vec, +} -impl Buffers for TestBuffers { +const BUFFER: Option = Some(CapSlot::const_new(9)); +const GRANT_SLOTS: usize = 32; + +fn badge_of(grant: CapSlot) -> usize { + grant.value() as usize - GRANT_SLOTS +} + +impl Host for TestHost { fn create(&mut self, _client: usize) -> Option<(&'static mut [u8], Option)> { - Some((Box::leak(vec![0u8; 4096].into_boxed_slice()), CapSlot::new(9))) + Some((Box::leak(vec![0u8; 4096].into_boxed_slice()), BUFFER)) + } + + fn reclaim(&mut self, buffer: CapSlot) { + self.reclaimed.push(buffer); + } + + fn mint(&mut self, badge: usize) -> Option { + CapSlot::new(u8::try_from(GRANT_SLOTS + badge).ok()?) + } + + fn revoke(&mut self, grant: CapSlot) { + self.revoked.push(grant); } } -type Srv = StorageServer; +type Srv = StorageServer; fn server() -> Srv { - StorageServer::new(MockFileSystem::new(), TestBuffers) + StorageServer::new(MockFileSystem::new(), TestHost::default()) } /// One request from `badge`, returning the decoded reply and any cap. fn call(s: &mut Srv, badge: usize, req: StorageRequest) -> (StorageResponse, Option) { let (words, cap) = s.handle_call(badge, req.encode(), None); - (StorageResponse::decode(words).expect("valid reply"), cap) + ( + StorageResponse::decode(words).expect("valid reply"), + cap.map(|g| g.slot), + ) } struct Client { @@ -38,10 +66,37 @@ impl Client { fn attach(s: &mut Srv, badge: usize) -> Self { let (resp, cap) = call(s, badge, StorageRequest::Attach); assert_eq!(resp, StorageResponse::Attached { size: 4096 }); - assert_eq!(cap, CapSlot::new(9), "the reply carries the buffer"); + assert_eq!(cap, BUFFER, "the reply carries the buffer"); Self { badge } } + /// The badge of a new grant to `path`, narrowed from this client's. + fn grant(&self, s: &mut Srv, path: &[u8], write: bool) -> usize { + self.put(s, path); + let (words, cap) = s.handle_call( + self.badge, + StorageRequest::Grant { + path_len: path.len(), + write, + } + .encode(), + None, + ); + assert_eq!( + StorageResponse::decode(words), + Some(StorageResponse::Granted) + ); + let cap = cap.expect("the reply carries the grant"); + assert!(cap.keep_delegate, "a grant can be passed on"); + badge_of(cap.slot) + } + + /// Attaches through a new grant to `path`. + fn narrowed(&self, s: &mut Srv, path: &[u8], write: bool) -> Client { + let badge = self.grant(s, path, write); + Client::attach(s, badge) + } + fn put(&self, s: &mut Srv, bytes: &[u8]) { s.client_buffer_for_test(self.badge).unwrap()[..bytes.len()].copy_from_slice(bytes); } @@ -50,13 +105,26 @@ impl Client { s.client_buffer_for_test(self.badge).unwrap()[..len].to_vec() } - fn create(&self, s: &mut Srv, path: &[u8]) -> FileHandle { + fn with_path( + &self, + s: &mut Srv, + path: &[u8], + req: fn(usize) -> StorageRequest, + ) -> StorageResponse { self.put(s, path); - match call(s, self.badge, StorageRequest::Create { path_len: path.len() }).0 { + call(s, self.badge, req(path.len())).0 + } + + fn create(&self, s: &mut Srv, path: &[u8]) -> FileHandle { + match self.with_path(s, path, |path_len| StorageRequest::Create { path_len }) { StorageResponse::Handle(h) => h, other => panic!("create: {other:?}"), } } + + fn open(&self, s: &mut Srv, path: &[u8]) -> StorageResponse { + self.with_path(s, path, |path_len| StorageRequest::Open { path_len }) + } } #[test] @@ -69,11 +137,12 @@ fn nothing_works_before_attaching() { #[test] fn attaching_again_hands_back_the_same_buffer() { let mut s = server(); + let root = Client::attach(&mut s, 0); let (first, cap) = call(&mut s, 0, StorageRequest::Attach); assert_eq!(call(&mut s, 0, StorageRequest::Attach), (first, cap)); // Not a new client: the other seven places are still free. - for badge in 1..8 { - assert!(matches!(call(&mut s, badge, StorageRequest::Attach).0, StorageResponse::Attached { .. })); + for _ in 1..8 { + root.narrowed(&mut s, b"", true); } } @@ -87,13 +156,19 @@ fn data_goes_through_the_buffer_both_ways() { call(&mut s, 0, StorageRequest::Write { handle: h, len: 14 }).0, StorageResponse::Written { bytes: 14 } ); - assert_eq!(call(&mut s, 0, StorageRequest::Close { handle: h }).0, StorageResponse::Ok); + assert_eq!( + call(&mut s, 0, StorageRequest::Close { handle: h }).0, + StorageResponse::Ok + ); c.put(&mut s, b"note.txt"); let StorageResponse::Handle(h) = call(&mut s, 0, StorageRequest::Open { path_len: 8 }).0 else { panic!("open"); }; - assert_eq!(call(&mut s, 0, StorageRequest::FileSize { handle: h }).0, StorageResponse::Size(14)); + assert_eq!( + call(&mut s, 0, StorageRequest::FileSize { handle: h }).0, + StorageResponse::Size(14) + ); c.put(&mut s, &[0; 32]); assert_eq!( call(&mut s, 0, StorageRequest::Read { handle: h, len: 64 }).0, @@ -105,8 +180,9 @@ fn data_goes_through_the_buffer_both_ways() { #[test] fn a_handle_works_only_for_the_client_that_opened_it() { let mut s = server(); - let a = Client::attach(&mut s, 1); - Client::attach(&mut s, 2); + let root = Client::attach(&mut s, 0); + let a = root.narrowed(&mut s, b"", true); + let b = root.narrowed(&mut s, b"", true); let h = a.create(&mut s, b"mine"); for req in [ StorageRequest::Read { handle: h, len: 4 }, @@ -114,9 +190,16 @@ fn a_handle_works_only_for_the_client_that_opened_it() { StorageRequest::FileSize { handle: h }, StorageRequest::Close { handle: h }, ] { - assert_eq!(call(&mut s, 2, req).0, StorageResponse::Error, "{req:?}"); + assert_eq!( + call(&mut s, b.badge, req).0, + StorageResponse::Denied, + "{req:?}" + ); } - assert_eq!(call(&mut s, 1, StorageRequest::FileSize { handle: h }).0, StorageResponse::Size(0)); + assert_eq!( + call(&mut s, a.badge, StorageRequest::FileSize { handle: h }).0, + StorageResponse::Size(0) + ); } #[test] @@ -125,10 +208,22 @@ fn hostile_lengths_are_clamped_to_the_buffer() { let c = Client::attach(&mut s, 0); let h = c.create(&mut s, b"x"); for len in [4097, usize::MAX] { - assert!(matches!(call(&mut s, 0, StorageRequest::Write { handle: h, len }).0, StorageResponse::Written { .. })); - assert!(matches!(call(&mut s, 0, StorageRequest::Read { handle: h, len }).0, StorageResponse::Data { .. })); - assert_eq!(call(&mut s, 0, StorageRequest::Open { path_len: len }).0, StorageResponse::NotFound); - assert!(matches!(call(&mut s, 0, StorageRequest::ListDir { path_len: len }).0, StorageResponse::DirEntries { .. })); + assert!(matches!( + call(&mut s, 0, StorageRequest::Write { handle: h, len }).0, + StorageResponse::Written { .. } + )); + assert!(matches!( + call(&mut s, 0, StorageRequest::Read { handle: h, len }).0, + StorageResponse::Data { .. } + )); + assert_eq!( + call(&mut s, 0, StorageRequest::Open { path_len: len }).0, + StorageResponse::NotFound + ); + assert!(matches!( + call(&mut s, 0, StorageRequest::ListDir { path_len: len }).0, + StorageResponse::DirEntries { .. } + )); } } @@ -138,24 +233,30 @@ fn handles_never_opened_are_refused() { Client::attach(&mut s, 0); for raw in [0, 15, 16, u16::MAX] { let h = FileHandle::new(raw); - assert_eq!(call(&mut s, 0, StorageRequest::Read { handle: h, len: 1 }).0, StorageResponse::Error); + assert_eq!( + call(&mut s, 0, StorageRequest::Read { handle: h, len: 1 }).0, + StorageResponse::Denied + ); } } #[test] fn a_read_only_disk_refuses_every_write_up_front() { - let mut s = StorageServer::new(MockFileSystem::new().read_only(), TestBuffers); + let mut s = StorageServer::new(MockFileSystem::new().read_only(), TestHost::default()); let c = Client::attach(&mut s, 0); c.put(&mut s, b"f"); for req in [ StorageRequest::Create { path_len: 1 }, StorageRequest::Delete { path_len: 1 }, StorageRequest::Mkdir { path_len: 1 }, - StorageRequest::Write { handle: FileHandle::new(0), len: 1 }, + StorageRequest::Write { + handle: FileHandle::new(0), + len: 1, + }, ] { let expect = match req { // A handle nobody opened is refused before the disk is asked. - StorageRequest::Write { .. } => StorageResponse::Error, + StorageRequest::Write { .. } => StorageResponse::Denied, _ => StorageResponse::ReadOnly, }; assert_eq!(call(&mut s, 0, req).0, expect, "{req:?}"); @@ -171,7 +272,10 @@ fn listings_are_records_in_the_buffer() { call(&mut s, 0, StorageRequest::Close { handle: h }); } c.put(&mut s, b"/"); - assert_eq!(call(&mut s, 0, StorageRequest::ListDir { path_len: 1 }).0, StorageResponse::DirEntries { count: 2 }); + assert_eq!( + call(&mut s, 0, StorageRequest::ListDir { path_len: 1 }).0, + StorageResponse::DirEntries { count: 2 } + ); let records = c.get(&mut s, 2 * DIR_ENTRY_SIZE); assert_eq!(&records[..5], b"a.txt"); assert_eq!(records[5], 0); @@ -199,3 +303,172 @@ fn only_the_unbadged_holder_sets_the_log_and_only_once() { assert_eq!(s.log(), log); assert_eq!(set(&mut s, 0), StorageResponse::Error); } + +#[test] +fn a_badge_storage_never_issued_is_no_authority() { + let mut s = server(); + for req in [ + StorageRequest::Attach, + StorageRequest::Open { path_len: 1 }, + StorageRequest::Release, + ] { + assert_eq!(call(&mut s, 5, req).0, StorageResponse::Denied, "{req:?}"); + } +} + +#[test] +fn paths_resolve_inside_the_scope() { + let mut s = server(); + let root = Client::attach(&mut s, 0); + let h = root.create(&mut s, b"docs/a.txt"); + call(&mut s, 0, StorageRequest::Close { handle: h }); + let h = root.create(&mut s, b"secret"); + call(&mut s, 0, StorageRequest::Close { handle: h }); + + let docs = root.narrowed(&mut s, b"docs", false); + assert!(matches!( + docs.open(&mut s, b"a.txt"), + StorageResponse::Handle(_) + )); + assert!( + matches!(docs.open(&mut s, b"/a.txt"), StorageResponse::Handle(_)), + "/ is the scope's root" + ); + assert_eq!( + docs.open(&mut s, b"secret"), + StorageResponse::NotFound, + "only docs/secret, which is not there" + ); + for escape in [ + b"../secret".as_slice(), + b"./../secret", + b"x/../../secret", + b"..", + ] { + assert_eq!( + docs.open(&mut s, escape), + StorageResponse::Denied, + "{escape:?}" + ); + } +} + +#[test] +fn a_read_only_scope_changes_nothing() { + let mut s = server(); + let root = Client::attach(&mut s, 0); + let h = root.create(&mut s, b"f"); + call(&mut s, 0, StorageRequest::Close { handle: h }); + let ro = root.narrowed(&mut s, b"", false); + for req in [ + |path_len| StorageRequest::Create { path_len }, + |path_len| StorageRequest::Delete { path_len }, + |path_len| StorageRequest::Mkdir { path_len }, + ] { + assert_eq!(ro.with_path(&mut s, b"f", req), StorageResponse::Denied); + } + // It can open the file, but the handle cannot write. + let StorageResponse::Handle(h) = ro.open(&mut s, b"f") else { + panic!("open") + }; + assert_eq!( + call( + &mut s, + ro.badge, + StorageRequest::Write { handle: h, len: 1 } + ) + .0, + StorageResponse::Denied + ); + assert!(matches!( + call(&mut s, ro.badge, StorageRequest::Read { handle: h, len: 1 }).0, + StorageResponse::Data { .. } + )); +} + +#[test] +fn a_grant_is_never_wider_than_its_holder() { + let mut s = server(); + let root = Client::attach(&mut s, 0); + let docs = root.narrowed(&mut s, b"docs", false); + // Asking for write from a read-only scope yields read-only. + let sub = docs.narrowed(&mut s, b"sub", true); + assert_eq!( + sub.with_path(&mut s, b"x", |path_len| StorageRequest::Create { path_len }), + StorageResponse::Denied + ); + docs.put(&mut s, b"../"); + assert_eq!( + call( + &mut s, + docs.badge, + StorageRequest::Grant { + path_len: 3, + write: false + } + ) + .0, + StorageResponse::Denied + ); +} + +#[test] +fn releasing_takes_back_the_grant_and_everything_from_it() { + let mut s = server(); + let root = Client::attach(&mut s, 0); + let f = root.create(&mut s, b"f"); + call(&mut s, 0, StorageRequest::Close { handle: f }); + let shell = root.narrowed(&mut s, b"", true); + let program = shell.narrowed(&mut s, b"", false); + let grandchild = program.grant(&mut s, b"", false); + let sibling = root.narrowed(&mut s, b"", true); + let StorageResponse::Handle(h) = program.open(&mut s, b"f") else { + panic!("open") + }; + + program.put(&mut s, b"leftover"); + assert_eq!( + call(&mut s, program.badge, StorageRequest::Release).0, + StorageResponse::Ok + ); + for badge in [program.badge, grandchild] { + assert_eq!( + call(&mut s, badge, StorageRequest::Attach).0, + StorageResponse::Denied + ); + } + assert_eq!( + call( + &mut s, + program.badge, + StorageRequest::FileSize { handle: h } + ) + .0, + StorageResponse::Denied + ); + // The kernel takes the capabilities back, and the buffer is unmapped. + let revoked: Vec = s.host().revoked.iter().map(|c| badge_of(*c)).collect(); + assert_eq!(revoked, [program.badge, grandchild]); + assert_eq!(s.host().reclaimed, [BUFFER.unwrap()]); + // The rest are untouched, and the freed buffer is reused, wiped. + assert!(matches!( + shell.open(&mut s, b"f"), + StorageResponse::Handle(_) + )); + assert!(matches!( + sibling.open(&mut s, b"f"), + StorageResponse::Handle(_) + )); + let next = shell.narrowed(&mut s, b"", false); + assert_eq!(next.get(&mut s, 8), [0; 8]); +} + +#[test] +fn the_unbadged_holder_cannot_be_released() { + let mut s = server(); + Client::attach(&mut s, 0); + assert_eq!( + call(&mut s, 0, StorageRequest::Release).0, + StorageResponse::Error + ); +} diff --git a/crates/servers/terminal-server/src/server.rs b/crates/servers/terminal-server/src/server.rs index f4e8a61..d4c5092 100644 --- a/crates/servers/terminal-server/src/server.rs +++ b/crates/servers/terminal-server/src/server.rs @@ -148,7 +148,7 @@ impl Server for TerminalServer { Done } - fn handle_call(&mut self, _badge: usize, words: [usize; 4], _cap: Option) -> ([usize; 4], Option) { + fn handle_call(&mut self, _badge: usize, words: [usize; 4], _cap: Option) -> ([usize; 4], Option) { let reply = match words[0] { types::terminal::READ => self.read(), types::terminal::CLEAR => { diff --git a/crates/types/src/proofs/abi.rs b/crates/types/src/proofs/abi.rs index d206d16..28c6a44 100644 --- a/crates/types/src/proofs/abi.rs +++ b/crates/types/src/proofs/abi.rs @@ -15,6 +15,24 @@ fn optional_slots_roundtrip() { assert!(reg_slot(slot_reg(s)) == Some(s)); } +#[kani::proof] +fn gives_roundtrip() { + let g = any_slot().map(|slot| Give { + slot, + keep_delegate: kani::any(), + }); + assert!(reg_give(give_reg(g)) == Some(g)); +} + +/// The flag never turns one slot into another. +#[kani::proof] +fn a_give_register_names_the_slot_it_carries() { + let r: usize = kani::any(); + if let Some(Some(g)) = reg_give(r) { + assert!(g.slot.value() as usize == r & !KEEP_DELEGATE); + } +} + #[kani::proof] fn every_register_decodes_to_none_or_a_valid_slot() { let r: usize = kani::any(); diff --git a/crates/types/src/storage.rs b/crates/types/src/storage.rs index 4ecf308..e2b0a0b 100644 --- a/crates/types/src/storage.rs +++ b/crates/types/src/storage.rs @@ -27,6 +27,12 @@ pub mod op { pub const MKDIR: usize = 9; /// Carries the log endpoint; only from the unbadged holder (init). pub const SET_LOG: usize = 10; + /// A capability to the path in the buffer, inside the caller's scope; + /// word 2 non-zero asks for write (only if the caller has it). + pub const GRANT: usize = 11; + /// Takes back the grant this is called through, and everything + /// granted from it, from every holder. + pub const RELEASE: usize = 12; } /// Reply tags (word 0). @@ -42,4 +48,8 @@ pub mod reply { pub const ERROR: usize = 8; pub const READ_ONLY: usize = 9; pub const NOT_ATTACHED: usize = 10; + /// Carries the new capability. + pub const GRANTED: usize = 11; + /// Outside the caller's scope, beyond its rights, or no grant at all. + pub const DENIED: usize = 12; } diff --git a/crates/types/src/syscall_abi.rs b/crates/types/src/syscall_abi.rs index 8dd3787..4d58fb9 100644 --- a/crates/types/src/syscall_abi.rs +++ b/crates/types/src/syscall_abi.rs @@ -52,6 +52,10 @@ pub const PERMS_READ_EXECUTE: usize = 2; /// "No capability" in a capability argument or result register. pub const NONE: usize = usize::MAX; +/// Set in a message's capability register: the receiver may pass the +/// capability on (it keeps `Delegate`). Without it the receiver may not. +pub const KEEP_DELEGATE: usize = 1 << 8; + /// What a receiving syscall got, returned in `a7`. pub const KIND_MESSAGE: usize = 0; pub const KIND_CALL: usize = 1; @@ -115,6 +119,53 @@ pub fn reg_slot(reg: usize) -> Option> { } } +/// A capability sent with a message. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Give { + pub slot: CapSlot, + /// The receiver gets it with `Delegate`, so it can pass it on; only + /// if the sender's own copy has `Delegate` to give. + pub keep_delegate: bool, +} + +impl Give { + pub const fn keeping_delegate(slot: CapSlot) -> Self { + Self { + slot, + keep_delegate: true, + } + } +} + +impl From for Give { + fn from(slot: CapSlot) -> Self { + Self { + slot, + keep_delegate: false, + } + } +} + +pub fn give_reg(give: Option) -> usize { + match give { + None => NONE, + Some(g) => g.slot.value() as usize | if g.keep_delegate { KEEP_DELEGATE } else { 0 }, + } +} + +/// Decodes a message's capability register: `Some(None)` for `NONE`, +/// `None` for anything but a valid slot, optionally with `KEEP_DELEGATE`. +pub fn reg_give(reg: usize) -> Option> { + if reg == NONE { + return Some(None); + } + let slot = reg_slot(reg & !KEEP_DELEGATE)??; + Some(Some(Give { + slot, + keep_delegate: reg & KEEP_DELEGATE != 0, + })) +} + pub fn perms_reg(perms: PagePerms) -> usize { match perms { PagePerms::ReadOnly => PERMS_READ_ONLY, @@ -133,14 +184,14 @@ pub fn reg_perms(reg: usize) -> Option { } /// Argument registers `a0..a5` of a message-carrying call. -pub fn message_regs(target: usize, words: [usize; 4], cap: Option) -> [usize; 6] { +pub fn message_regs(target: usize, words: [usize; 4], cap: Option) -> [usize; 6] { [ target, words[0], words[1], words[2], words[3], - slot_reg(cap), + give_reg(cap), ] } diff --git a/disk/docs/shell.txt b/disk/docs/shell.txt index d46c82e..bcee5d6 100644 --- a/disk/docs/shell.txt +++ b/disk/docs/shell.txt @@ -1,7 +1,8 @@ The shell runs a program by asking init to load it (init alone may create processes), then gives the program the terminal as its output -and the storage server, writes its arguments onto a page it moves -into the program, starts it and waits for it to die. +and a read-only storage grant to the current directory, writes its +arguments onto a page it moves into the program, starts it and waits +for it to die. Then it takes the grant back. -Built-ins: help, echo, clear. Anything else is a program on disk: -`name args...` runs name.elf. +Built-ins: help, echo, clear, cd, pwd. Anything else is a program on +disk: `name args...` runs name.elf. diff --git a/disk/readme.txt b/disk/readme.txt index 49b736d..39cc748 100644 --- a/disk/readme.txt +++ b/disk/readme.txt @@ -2,6 +2,6 @@ Welcome to k-os. Every program here runs with only the capabilities it was given: this file reached your screen because the shell handed `cat` the -terminal and the storage server, and nothing else. +terminal and read-only access to this directory, and nothing else. -Try: ls ls docs cat docs/shell.txt +Try: ls cd docs cat shell.txt cat ../readme.txt diff --git a/docs/ipc.md b/docs/ipc.md index 461e55c..a55cb32 100644 --- a/docs/ipc.md +++ b/docs/ipc.md @@ -95,6 +95,17 @@ A program's `uses` endpoints arrive as badged copies with `Execute` and calling, and `Delegate` lets the client send capabilities with its calls and hand its access to programs it starts. +Storage is the exception: it mints every badged copy of its own endpoint, +and each badge names a *scope*, a subtree (or one file) and whether it may +write. For `uses: ["storage"]` init asks it for the whole disk, read-write. +Any holder can ask for a narrower one (`GRANT(path, write)`: inside its own +scope, writable only if it is) and gets it with `Delegate` kept, so it can +pass it on. Paths resolve inside the scope; `..` is refused. `RELEASE`, +called through a grant, takes it and everything granted from it back from +every holder (storage revokes its own copy, so the kernel deletes the +rest), closes their files and unmaps their buffers. Badge 0 (init) is the +whole disk; a badge storage did not issue, or took back, is `DENIED`. + Input reaches apps through the display server, the input server's only client. It subscribes to the input server with a notification, drains it when signalled, and passes events on to the app in focus: the last one to @@ -107,10 +118,10 @@ 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`. The desktop is the shell, given the terminal, storage and a spawn allowance. A command that is not a built-in runs `name.elf`: the shell -spawns it, gives it call-only copies of the terminal (as its log, so its -output) and of storage, writes its arguments page, starts it and waits for -its death. The programs share the shell's storage badge; storage hands a -client that attaches again the same buffer, so they run one at a time. +spawns it, gives it a call-only copy of the terminal (as its log, so its +output) and a read-only storage grant to the shell's current directory +(`cd`, `pwd`), writes its arguments page, starts it, waits for its death +and releases the grant. Only init holds `ProcessCreate`. Everyone else runs programs through init's spawn service (`types::spawn`, client `libkos::spawn`), which init serves @@ -159,8 +170,11 @@ A message is 4 words and at most one capability. The capability is transferred only if the sender's endpoint capability has `Delegate` and the transferred capability itself has `Delegate`; it is **copied** (the sender keeps its own) with the same badge and permissions minus `Delegate`: the -receiver can use it but not pass it on. To let a process pass a capability -on, give it with the `Delegate` syscall and explicit rights. If the receiver's +receiver can use it but not pass it on. A sender that means the receiver +to pass it on sets `KEEP_DELEGATE` in the capability register +(`syscall_abi::Give`): the copy then keeps exactly the sender's rights, +`Delegate` included. Or it gives the capability with the `Delegate` syscall +and explicit rights. If the receiver's table is full, or the capability is no longer valid at delivery time, the message is delivered without it.