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
This commit is contained in:
@@ -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::Execute)
|
||||||
.with(types::permission::Permission::Delegate);
|
.with(types::permission::Permission::Delegate);
|
||||||
|
|
||||||
|
// Delegate so storage can mint the badged copies it grants.
|
||||||
let ipc_perms = types::permission::PermissionSet::empty()
|
let ipc_perms = types::permission::PermissionSet::empty()
|
||||||
.with(types::permission::Permission::Read)
|
.with(types::permission::Permission::Read)
|
||||||
.with(types::permission::Permission::Execute);
|
.with(types::permission::Permission::Execute)
|
||||||
|
.with(types::permission::Permission::Delegate);
|
||||||
|
|
||||||
let storage_ep = kern()
|
let storage_ep = kern()
|
||||||
.create_endpoint(storage_pid)
|
.create_endpoint(storage_pid)
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ extern "C" fn main() {
|
|||||||
match cat(&mut fs, path) {
|
match cat(&mut fs, path) {
|
||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
Err(FsError::NotFound) => println!("cat: {}: no such file", path),
|
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),
|
Err(_) => println!("cat: {}: cannot read", path),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
extern crate libkos;
|
extern crate libkos;
|
||||||
|
|
||||||
use libkos::fs::Storage;
|
use libkos::fs::{FsError, Storage};
|
||||||
use libkos::println;
|
use libkos::println;
|
||||||
|
|
||||||
/// Where storage's buffer is mapped.
|
/// Where storage's buffer is mapped.
|
||||||
@@ -29,7 +29,9 @@ extern "C" fn main() {
|
|||||||
println!("{:<24} {:>8}", name, e.size);
|
println!("{:<24} {:>8}", name, e.size);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if listed.is_err() {
|
match listed {
|
||||||
println!("ls: {}: no such directory", dir);
|
Ok(_) => {}
|
||||||
|
Err(FsError::Denied) => println!("ls: {}: outside what it was given", dir),
|
||||||
|
Err(_) => println!("ls: {}: no such directory", dir),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
//! The shell: a few built-ins, and anything else is a program on disk.
|
//! 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
|
//! `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_std]
|
||||||
#![no_main]
|
#![no_main]
|
||||||
|
|
||||||
extern crate libkos;
|
extern crate libkos;
|
||||||
|
|
||||||
|
use libkos::fs::Storage;
|
||||||
use libkos::spawn::{SpawnError, spawn};
|
use libkos::spawn::{SpawnError, spawn};
|
||||||
use libkos::{print, println, syscall};
|
use libkos::{print, println, syscall};
|
||||||
use types::bounded::CapSlot;
|
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.
|
/// What each program gets, out of the shell's allowance.
|
||||||
const PROGRAM: Limits = Limits { pages: 128, endpoints: 1, notifications: 1, regions: 1 };
|
const PROGRAM: Limits = Limits { pages: 128, endpoints: 1, notifications: 1, regions: 1 };
|
||||||
const MAX_ARGS: usize = 16;
|
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.
|
/// The current directory, as a path on disk: no leading or trailing `/`.
|
||||||
fn run_program(args: &[&str], done: CapSlot) {
|
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<Cwd> {
|
||||||
|
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 mut file = [0u8; types::spawn::MAX_NAME];
|
||||||
let name = args[0].as_bytes();
|
let name = args[0].as_bytes();
|
||||||
let Some(file) = file.get_mut(..name.len() + 4).map(|f| {
|
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]),
|
Err(SpawnError::Failed) => return println!("shell: {}: cannot run", args[0]),
|
||||||
};
|
};
|
||||||
let call = PermissionSet::empty().with(Permission::Execute);
|
let call = PermissionSet::empty().with(Permission::Execute);
|
||||||
|
let grant = sh.fs.grant(sh.cwd.as_bytes(), false).ok();
|
||||||
let arg = StartArg {
|
let arg = StartArg {
|
||||||
log: child.give(TERMINAL, call),
|
log: child.give(TERMINAL, call),
|
||||||
storage: child.give(STORAGE, call),
|
storage: grant.and_then(|g| child.give(g, call)),
|
||||||
args: child.set_args(args),
|
args: child.set_args(args),
|
||||||
console: None,
|
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 {
|
if child.start_with(arg) && watched {
|
||||||
let _ = syscall::wait(done);
|
let _ = syscall::wait(sh.done);
|
||||||
} else {
|
} else {
|
||||||
let _ = syscall::kill(child.process);
|
let _ = syscall::kill(child.process);
|
||||||
println!("shell: {}: cannot start", args[0]);
|
println!("shell: {}: cannot start", args[0]);
|
||||||
}
|
}
|
||||||
syscall::drop_cap(child.process);
|
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 args = [""; MAX_ARGS];
|
||||||
let mut n = 0;
|
let mut n = 0;
|
||||||
for w in line.split_ascii_whitespace().take(MAX_ARGS) {
|
for w in line.split_ascii_whitespace().take(MAX_ARGS) {
|
||||||
@@ -69,12 +149,15 @@ fn run(line: &str, done: CapSlot) {
|
|||||||
match args.first().copied() {
|
match args.first().copied() {
|
||||||
None => {}
|
None => {}
|
||||||
Some("help") => {
|
Some("help") => {
|
||||||
println!("built-ins: help, echo <text>, clear");
|
println!("built-ins: help, echo <text>, clear, cd <dir>, pwd");
|
||||||
println!("anything else runs a program from the disk: try ls, cat readme.txt");
|
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("echo") => println!("{}", Joined(&args[1..])),
|
||||||
Some("clear") => libkos::terminal::clear(TERMINAL),
|
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");
|
println!("shell: no notification to wait on");
|
||||||
return;
|
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'.");
|
println!("k-os shell. Type 'help'.");
|
||||||
let mut buf = [0u8; 256];
|
let mut buf = [0u8; 256];
|
||||||
loop {
|
loop {
|
||||||
print!("$ ");
|
print!("$ ");
|
||||||
let n = libkos::terminal::read_line(TERMINAL, &mut buf);
|
let n = libkos::terminal::read_line(TERMINAL, &mut buf);
|
||||||
match core::str::from_utf8(&buf[..n]) {
|
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"),
|
Err(_) => println!("shell: not text"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
extern crate libkos;
|
extern crate libkos;
|
||||||
|
|
||||||
|
use libkos::fs::{FsError, Storage};
|
||||||
use libkos::spawn::{SpawnError, spawn};
|
use libkos::spawn::{SpawnError, spawn};
|
||||||
use libkos::syscall;
|
use libkos::syscall;
|
||||||
use types::bounded::CapSlot;
|
use types::bounded::CapSlot;
|
||||||
@@ -17,8 +18,9 @@ use types::budget::Limits;
|
|||||||
const STORAGE: CapSlot = CapSlot::const_new(0);
|
const STORAGE: CapSlot = CapSlot::const_new(0);
|
||||||
const INIT: CapSlot = CapSlot::const_new(1);
|
const INIT: CapSlot = CapSlot::const_new(1);
|
||||||
const CONTROL: CapSlot = CapSlot::const_new(2);
|
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 STORAGE_BUF_VA: usize = 0x0180_0000;
|
||||||
|
const NARROW_BUF_VA: usize = 0x0190_0000;
|
||||||
/// Read back from the disk image by scripts/test-qemu.sh.
|
/// Read back from the disk image by scripts/test-qemu.sh.
|
||||||
const NOTE: &[u8] = b"written by k-os on ext4\n";
|
const NOTE: &[u8] = b"written by k-os on ext4\n";
|
||||||
|
|
||||||
@@ -26,9 +28,14 @@ const NOTE: &[u8] = b"written by k-os on ext4\n";
|
|||||||
/// the first has died and its limits came back.
|
/// the first has died and its limits came back.
|
||||||
const HELLO: Limits = Limits { pages: 80, endpoints: 0, notifications: 0, regions: 0 };
|
const HELLO: Limits = Limits { pages: 80, endpoints: 0, notifications: 0, regions: 0 };
|
||||||
|
|
||||||
/// Spawns hello, runs it, and waits for it to die.
|
/// Spawns hello, runs it, and waits for it to die; hands it `grant`
|
||||||
fn run_hello(died: CapSlot) -> Result<(), &'static str> {
|
/// first, if there is one.
|
||||||
|
fn run_hello(died: CapSlot, grant: Option<CapSlot>) -> Result<(), &'static str> {
|
||||||
let child = spawn(INIT, b"hello.elf", HELLO).map_err(|_| "init would not spawn hello")?;
|
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")?;
|
syscall::watch_death(child.process, died).map_err(|_| "cannot watch hello")?;
|
||||||
if !child.start() {
|
if !child.start() {
|
||||||
return Err("cannot start hello");
|
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.
|
/// A file written through storage, which the host then checks with e2fsck.
|
||||||
fn write_a_file() -> Result<(), &'static str> {
|
fn write_a_file(fs: &mut Storage) -> Result<(), &'static str> {
|
||||||
let mut fs = libkos::fs::Storage::attach(STORAGE, STORAGE_BUF_VA).map_err(|_| "cannot attach to storage")?;
|
|
||||||
let h = fs.create(b"selftest.txt").map_err(|_| "cannot create a file")?;
|
let h = fs.create(b"selftest.txt").map_err(|_| "cannot create a file")?;
|
||||||
if fs.write(h, NOTE) != Ok(NOTE.len()) {
|
if fs.write(h, NOTE) != Ok(NOTE.len()) {
|
||||||
return Err("short write");
|
return Err("short write");
|
||||||
@@ -55,12 +61,40 @@ fn write_a_file() -> Result<(), &'static str> {
|
|||||||
Ok(())
|
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> {
|
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");
|
libkos::println!("spawntest: a file written to ext4 reads back: ok");
|
||||||
|
|
||||||
let died = syscall::create_notification().map_err(|_| "no notification")?;
|
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");
|
libkos::println!("spawntest: spawned program runs and its death is heard: ok");
|
||||||
|
|
||||||
let greedy = Limits { pages: 200, ..HELLO };
|
let greedy = Limits { pages: 200, ..HELLO };
|
||||||
@@ -73,7 +107,7 @@ fn check() -> Result<(), &'static str> {
|
|||||||
}
|
}
|
||||||
libkos::println!("spawntest: allowance and missing files refused: ok");
|
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");
|
libkos::println!("spawntest: allowance comes back when a child dies: ok");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ fn subscribe_to_input() -> Option<CapSlot> {
|
|||||||
libkos::syscall::bind_notification(notify).ok()?;
|
libkos::syscall::bind_notification(notify).ok()?;
|
||||||
let signal = PermissionSet::empty().with(Permission::Write).with(Permission::Delegate);
|
let signal = PermissionSet::empty().with(Permission::Write).with(Permission::Delegate);
|
||||||
let copy = libkos::syscall::mint(notify, Badge::NONE, signal).ok()?;
|
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);
|
libkos::syscall::drop_cap(copy);
|
||||||
sent.ok().map(|_| INPUT)
|
sent.ok().map(|_| INPUT)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ pub const MAX_MESSAGE_WORDS: usize = 4;
|
|||||||
pub struct Message {
|
pub struct Message {
|
||||||
words: [usize; MAX_MESSAGE_WORDS],
|
words: [usize; MAX_MESSAGE_WORDS],
|
||||||
cap: Option<CapSlot>,
|
cap: Option<CapSlot>,
|
||||||
|
/// The receiver may pass `cap` on (see `syscall_abi::KEEP_DELEGATE`).
|
||||||
|
keep_delegate: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Message {
|
impl Message {
|
||||||
@@ -19,7 +21,11 @@ impl Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_words(words: [usize; MAX_MESSAGE_WORDS]) -> Self {
|
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 {
|
pub fn with_cap(mut self, slot: CapSlot) -> Self {
|
||||||
@@ -39,4 +45,13 @@ impl Message {
|
|||||||
pub fn cap(&self) -> Option<CapSlot> {
|
pub fn cap(&self) -> Option<CapSlot> {
|
||||||
self.cap
|
self.cap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn keeping_delegate(mut self) -> Self {
|
||||||
|
self.keep_delegate = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn keeps_delegate(&self) -> bool {
|
||||||
|
self.keep_delegate
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,10 +78,16 @@ impl<'a> Launcher<'a> {
|
|||||||
// and take other clients' requests.
|
// and take other clients' requests.
|
||||||
for name in p.uses {
|
for name in p.uses {
|
||||||
let ep = self.endpoint(name).ok_or("needs a server that is not running")?;
|
let ep = self.endpoint(name).ok_or("needs a server that is not running")?;
|
||||||
let badge = Badge::new(self.next_badge);
|
let copy = match *name {
|
||||||
self.next_badge += 1;
|
// Storage issues its own badges, each naming a scope.
|
||||||
let copy = syscall::mint(ep, badge, perms(&[Permission::Execute, Permission::Delegate]))
|
"storage" => self.storage.grant(b"", true).map_err(|_| "storage refused a grant")?,
|
||||||
.map_err(|_| "cannot mint an endpoint")?;
|
_ => {
|
||||||
|
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]));
|
let given = syscall::delegate(copy, child, perms(&[Permission::Execute, Permission::Delegate]));
|
||||||
syscall::drop_cap(copy);
|
syscall::drop_cap(copy);
|
||||||
given.ok_or("cannot give an endpoint")?;
|
given.ok_or("cannot give an endpoint")?;
|
||||||
|
|||||||
@@ -26,8 +26,12 @@ pub fn run() -> ! {
|
|||||||
|
|
||||||
// Storage waits for its disk before serving, so this comes first.
|
// Storage waits for its disk before serving, so this comes first.
|
||||||
let found = devices::discover();
|
let found = devices::discover();
|
||||||
let disk = libkos::device::give_device(STORAGE_EP, found.disk).is_ok() && found.disk.mmio.is_some();
|
let disk =
|
||||||
libkos::println!("init: disk: {}", if disk { "handed to its driver" } else { "none" });
|
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 {
|
let Ok(mut storage) = Storage::attach(STORAGE_EP, STORAGE_BUF_VA) else {
|
||||||
libkos::println!("init: cannot reach storage, nothing to load");
|
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
|
// The test image carries the SMP self-test; it and the tests run
|
||||||
// instead of the desktop, and the last one halts the machine.
|
// instead of the desktop, and the last one halts the machine.
|
||||||
let (storage, scratch) = launcher.storage();
|
let (storage, scratch) = launcher.storage();
|
||||||
if storage.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");
|
libkos::println!("init: running the SMP self-test");
|
||||||
selftest::run(storage, scratch);
|
selftest::run(storage, scratch);
|
||||||
launch_all(&mut launcher, manifest::TESTS);
|
launch_all(&mut launcher, manifest::TESTS);
|
||||||
@@ -83,9 +90,15 @@ fn give_storage_the_log() {
|
|||||||
let Some(log) = libkos::start::log() else {
|
let Some(log) = libkos::start::log() else {
|
||||||
return;
|
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) {
|
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);
|
syscall::drop_cap(copy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,9 +165,12 @@ impl Server for Launcher<'_> {
|
|||||||
Reply(refused(reply::ERROR).0)
|
Reply(refused(reply::ERROR).0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_call(&mut self, badge: usize, words: [usize; 4], _cap: Option<CapSlot>) -> ([usize; 4], Option<CapSlot>) {
|
fn handle_call(&mut self, badge: usize, words: [usize; 4], _cap: Option<CapSlot>) -> ([usize; 4], Option<libkos::syscall::Give>) {
|
||||||
match Request::decode(words) {
|
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),
|
None => ([MALFORMED, 0, 0, 0], None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,9 +28,16 @@ fn permissions(r: usize) -> Option<PermissionSet> {
|
|||||||
/// The message in `a1..a5`.
|
/// The message in `a1..a5`.
|
||||||
fn message(a: &[usize; 6]) -> Option<Message> {
|
fn message(a: &[usize; 6]) -> Option<Message> {
|
||||||
let msg = Message::from_words([a[1], a[2], a[3], a[4]]);
|
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,
|
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(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,12 +39,18 @@ fn send_roundtrips() {
|
|||||||
} else {
|
} else {
|
||||||
None
|
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) {
|
match decode(SYS_SEND, ®s) {
|
||||||
Some(Syscall::Send { endpoint, message }) => {
|
Some(Syscall::Send { endpoint, message }) => {
|
||||||
assert!(endpoint == ep);
|
assert!(endpoint == ep);
|
||||||
assert!(*message.words() == words);
|
assert!(*message.words() == words);
|
||||||
assert!(message.cap() == cap);
|
assert!(message.cap() == cap);
|
||||||
|
assert!(message.keeps_delegate() == (keep && cap.is_some()));
|
||||||
}
|
}
|
||||||
_ => panic!("valid send did not decode"),
|
_ => panic!("valid send did not decode"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use domain::capability::{Capability, CapabilityTable};
|
|||||||
use domain::resource::Resource;
|
use domain::resource::Resource;
|
||||||
use types::generation::Generation;
|
use types::generation::Generation;
|
||||||
use types::id::EndpointId;
|
use types::id::EndpointId;
|
||||||
use types::permission::{Permission, PermissionSet};
|
use types::permission::PermissionSet;
|
||||||
|
|
||||||
#[kani::proof]
|
#[kani::proof]
|
||||||
fn revoke_invalidates_cap_slot() {
|
fn revoke_invalidates_cap_slot() {
|
||||||
|
|||||||
@@ -38,23 +38,23 @@ impl<P: Platform> Kernel<P> {
|
|||||||
pub(super) fn transfer_cap(
|
pub(super) fn transfer_cap(
|
||||||
&mut self,
|
&mut self,
|
||||||
from: ProcessId,
|
from: ProcessId,
|
||||||
slot: Option<CapSlot>,
|
message: &Message,
|
||||||
grant: bool,
|
grant: bool,
|
||||||
to: ProcessId,
|
to: ProcessId,
|
||||||
) -> Option<CapSlot> {
|
) -> Option<CapSlot> {
|
||||||
let slot = slot?;
|
let slot = message.cap()?;
|
||||||
if !grant {
|
if !grant {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
// The receiver gets the capability but not the right to pass it
|
// 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 source = self.check_cap(from, slot, Permission::Delegate).ok()?;
|
||||||
let copy = Capability::new(
|
let permissions = match message.keeps_delegate() {
|
||||||
*source.resource(),
|
true => *source.permissions(),
|
||||||
source.permissions().without(Permission::Delegate),
|
false => source.permissions().without(Permission::Delegate),
|
||||||
*source.generation(),
|
};
|
||||||
)
|
let copy = Capability::new(*source.resource(), permissions, *source.generation())
|
||||||
.with_badge(source.badge());
|
.with_badge(source.badge());
|
||||||
let parent = self.cap_ref(from, slot);
|
let parent = self.cap_ref(from, slot);
|
||||||
self.insert_derived(to, copy, parent).ok()
|
self.insert_derived(to, copy, parent).ok()
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,7 @@ impl<P: Platform> Kernel<P> {
|
|||||||
let from = self.thread_process(sender);
|
let from = self.thread_process(sender);
|
||||||
let to = self.thread_process(receiver);
|
let to = self.thread_process(receiver);
|
||||||
let cap = match (from, to) {
|
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,
|
_ => None,
|
||||||
};
|
};
|
||||||
if kind == DeliveryKind::Call {
|
if kind == DeliveryKind::Call {
|
||||||
@@ -234,7 +234,7 @@ impl<P: Platform> Kernel<P> {
|
|||||||
}
|
}
|
||||||
let to = self.thread_process(caller.tid);
|
let to = self.thread_process(caller.tid);
|
||||||
// A reply may always carry a capability the server can delegate.
|
// 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 {
|
let delivery = Delivery {
|
||||||
kind: DeliveryKind::Message,
|
kind: DeliveryKind::Message,
|
||||||
badge: 0,
|
badge: 0,
|
||||||
|
|||||||
@@ -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]
|
#[test]
|
||||||
fn an_irq_has_one_owner_until_it_dies() {
|
fn an_irq_has_one_owner_until_it_dies() {
|
||||||
let mut t = Ipc::new();
|
let mut t = Ipc::new();
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ pub fn receive_device(endpoint: CapSlot) -> Device {
|
|||||||
pub fn give_device(endpoint: CapSlot, device: Device) -> Result<()> {
|
pub fn give_device(endpoint: CapSlot, device: Device) -> Result<()> {
|
||||||
for (what, cap) in [(MMIO, device.mmio), (IRQ, device.irq)] {
|
for (what, cap) in [(MMIO, device.mmio), (IRQ, device.irq)] {
|
||||||
if let Some(cap) = cap {
|
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(|_| ())
|
syscall::call(endpoint, [SETUP, DONE, 0, 0], None).map(|_| ())
|
||||||
|
|||||||
@@ -60,7 +60,11 @@ pub fn subscribe_input(ep: CapSlot) -> Option<CapSlot> {
|
|||||||
// Signal only (Delegate just to send it; the transfer strips it).
|
// Signal only (Delegate just to send it; the transfer strips it).
|
||||||
let signal = PermissionSet::empty().with(Permission::Write).with(Permission::Delegate);
|
let signal = PermissionSet::empty().with(Permission::Write).with(Permission::Delegate);
|
||||||
let copy = syscall::mint(notify, Badge::NONE, signal).ok()?;
|
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);
|
syscall::drop_cap(copy);
|
||||||
(sent.ok()?.words()[0] == 0).then_some(notify)
|
(sent.ok()?.words()[0] == 0).then_some(notify)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ pub enum FsError {
|
|||||||
ReadOnly,
|
ReadOnly,
|
||||||
/// A path longer than the server accepts.
|
/// A path longer than the server accepts.
|
||||||
PathTooLong,
|
PathTooLong,
|
||||||
|
/// Outside this connection's scope, or beyond its rights.
|
||||||
|
Denied,
|
||||||
/// The server refused, or the call itself failed.
|
/// The server refused, or the call itself failed.
|
||||||
Failed,
|
Failed,
|
||||||
}
|
}
|
||||||
@@ -76,6 +78,7 @@ impl Storage {
|
|||||||
match r[0] {
|
match r[0] {
|
||||||
reply::NOT_FOUND => Err(FsError::NotFound),
|
reply::NOT_FOUND => Err(FsError::NotFound),
|
||||||
reply::READ_ONLY => Err(FsError::ReadOnly),
|
reply::READ_ONLY => Err(FsError::ReadOnly),
|
||||||
|
reply::DENIED => Err(FsError::Denied),
|
||||||
reply::ERROR | reply::NOT_ATTACHED => Err(FsError::Failed),
|
reply::ERROR | reply::NOT_ATTACHED => Err(FsError::Failed),
|
||||||
_ => Ok(r),
|
_ => Ok(r),
|
||||||
}
|
}
|
||||||
@@ -100,6 +103,20 @@ impl Storage {
|
|||||||
self.expect([opcode, len, 0, 0], tag)
|
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<CapSlot> {
|
||||||
|
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<FileHandle> {
|
pub fn open(&mut self, path: &[u8]) -> Result<FileHandle> {
|
||||||
self.with_path(op::OPEN, path, reply::HANDLE).map(|h| FileHandle(h as u16))
|
self.with_path(op::OPEN, path, reply::HANDLE).map(|h| FileHandle(h as u16))
|
||||||
}
|
}
|
||||||
@@ -181,3 +198,13 @@ impl Storage {
|
|||||||
Ok(count)
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ use super::{call0, raw, slot_of, Error, Result};
|
|||||||
use types::bounded::CapSlot;
|
use types::bounded::CapSlot;
|
||||||
use types::syscall_abi::*;
|
use types::syscall_abi::*;
|
||||||
|
|
||||||
|
pub use types::syscall_abi::Give;
|
||||||
|
|
||||||
/// What a receiving call got.
|
/// What a receiving call got.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum Received {
|
pub enum Received {
|
||||||
@@ -48,7 +50,7 @@ pub fn create_endpoint() -> Option<CapSlot> {
|
|||||||
slot_of(call0(SYS_CREATE_ENDPOINT)).ok()
|
slot_of(call0(SYS_CREATE_ENDPOINT)).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_msg(endpoint: CapSlot, words: [usize; 4], cap: Option<CapSlot>) -> Result<()> {
|
pub fn send_msg(endpoint: CapSlot, words: [usize; 4], cap: Option<Give>) -> Result<()> {
|
||||||
raw(
|
raw(
|
||||||
SYS_SEND,
|
SYS_SEND,
|
||||||
message_regs(endpoint.value() as usize, words, cap),
|
message_regs(endpoint.value() as usize, words, cap),
|
||||||
@@ -61,7 +63,7 @@ pub fn receive_msg(endpoint: CapSlot) -> Result<Received> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Sends and waits for the reply.
|
/// Sends and waits for the reply.
|
||||||
pub fn call(endpoint: CapSlot, words: [usize; 4], cap: Option<CapSlot>) -> Result<Received> {
|
pub fn call(endpoint: CapSlot, words: [usize; 4], cap: Option<Give>) -> Result<Received> {
|
||||||
raw(
|
raw(
|
||||||
SYS_CALL,
|
SYS_CALL,
|
||||||
message_regs(endpoint.value() as usize, words, cap),
|
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.
|
/// Answers the last `Call` this thread received.
|
||||||
pub fn reply(words: [usize; 4], cap: Option<CapSlot>) -> Result<()> {
|
pub fn reply(words: [usize; 4], cap: Option<Give>) -> Result<()> {
|
||||||
raw(SYS_REPLY, message_regs(0, words, cap)).map(|_| ())
|
raw(SYS_REPLY, message_regs(0, words, cap)).map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replies to the pending `Call` (if any), then receives the next message.
|
/// Replies to the pending `Call` (if any), then receives the next message.
|
||||||
pub fn reply_recv(endpoint: CapSlot, words: [usize; 4], cap: Option<CapSlot>) -> Result<Received> {
|
pub fn reply_recv(endpoint: CapSlot, words: [usize; 4], cap: Option<Give>) -> Result<Received> {
|
||||||
raw(
|
raw(
|
||||||
SYS_REPLY_RECV,
|
SYS_REPLY_RECV,
|
||||||
message_regs(endpoint.value() as usize, words, cap),
|
message_regs(endpoint.value() as usize, words, cap),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
use crate::codec::{Decode, Encode};
|
use crate::codec::{Decode, Encode};
|
||||||
|
use libkos::syscall::Give;
|
||||||
use types::bounded::CapSlot;
|
use types::bounded::CapSlot;
|
||||||
|
|
||||||
/// The reply to a request that does not decode. No protocol uses this
|
/// The reply to a request that does not decode. No protocol uses this
|
||||||
@@ -27,7 +28,7 @@ pub trait Server {
|
|||||||
_badge: usize,
|
_badge: usize,
|
||||||
words: [usize; 4],
|
words: [usize; 4],
|
||||||
_cap: Option<CapSlot>,
|
_cap: Option<CapSlot>,
|
||||||
) -> ([usize; 4], Option<CapSlot>) {
|
) -> ([usize; 4], Option<Give>) {
|
||||||
match Self::Request::decode(words) {
|
match Self::Request::decode(words) {
|
||||||
Some(request) => (self.handle(request).encode(), None),
|
Some(request) => (self.handle(request).encode(), None),
|
||||||
None => ([MALFORMED, 0, 0, 0], 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.
|
/// has nobody left to serve, so it says why and exits.
|
||||||
pub fn serve<S: Server>(server: &mut S, endpoint: CapSlot) -> ! {
|
pub fn serve<S: Server>(server: &mut S, endpoint: CapSlot) -> ! {
|
||||||
let error = server.run(endpoint);
|
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()
|
libkos::syscall::exit()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ impl<F: Framebuffer> Server for DisplayServer<F> {
|
|||||||
type Request = DisplayRequest;
|
type Request = DisplayRequest;
|
||||||
type Response = DisplayResponse;
|
type Response = DisplayResponse;
|
||||||
|
|
||||||
fn handle_call(&mut self, badge: usize, words: [usize; 4], cap: Option<CapSlot>) -> ([usize; 4], Option<CapSlot>) {
|
fn handle_call(&mut self, badge: usize, words: [usize; 4], cap: Option<CapSlot>) -> ([usize; 4], Option<libkos::syscall::Give>) {
|
||||||
use types::display::input::{POLL, SUBSCRIBE};
|
use types::display::input::{POLL, SUBSCRIBE};
|
||||||
match (words[0], cap) {
|
match (words[0], cap) {
|
||||||
(SUBSCRIBE, Some(notify)) => {
|
(SUBSCRIBE, Some(notify)) => {
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ impl<D: InputDevice> Server for InputServer<D> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_call(&mut self, _badge: usize, words: [usize; 4], cap: Option<CapSlot>) -> ([usize; 4], Option<CapSlot>) {
|
fn handle_call(&mut self, _badge: usize, words: [usize; 4], cap: Option<CapSlot>) -> ([usize; 4], Option<libkos::syscall::Give>) {
|
||||||
if words[0] == types::input::SUBSCRIBE {
|
if words[0] == types::input::SUBSCRIBE {
|
||||||
self.subscriber = cap;
|
self.subscriber = cap;
|
||||||
return ([0; 4], None);
|
return ([0; 4], None);
|
||||||
|
|||||||
94
crates/servers/storage-server/src/grants.rs
Normal file
94
crates/servers/storage-server/src/grants.rs
Normal file
@@ -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<Grant>; 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<Scope> {
|
||||||
|
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<CapSlot>,
|
||||||
|
) -> Option<CapSlot> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,9 @@ extern crate alloc;
|
|||||||
pub mod adapters;
|
pub mod adapters;
|
||||||
pub mod filesystem;
|
pub mod filesystem;
|
||||||
pub mod fs;
|
pub mod fs;
|
||||||
|
pub mod grants;
|
||||||
pub mod protocol;
|
pub mod protocol;
|
||||||
|
pub mod scope;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -14,17 +16,22 @@ mod tests;
|
|||||||
|
|
||||||
use libkos::syscall;
|
use libkos::syscall;
|
||||||
use types::address::VirtualAddress;
|
use types::address::VirtualAddress;
|
||||||
|
use types::badge::Badge;
|
||||||
use types::bounded::{CapSlot, PageCount};
|
use types::bounded::{CapSlot, PageCount};
|
||||||
use types::mapping::PagePerms;
|
use types::mapping::PagePerms;
|
||||||
|
use types::permission::{Permission, PermissionSet};
|
||||||
|
|
||||||
/// Client buffers sit high in storage's window, clear of its heap.
|
/// Client buffers sit high in storage's window, clear of its heap.
|
||||||
const BUFFERS_VA: usize = 0x0100_0000;
|
const BUFFERS_VA: usize = 0x0100_0000;
|
||||||
|
|
||||||
/// Each client's buffer is a fresh region, mapped here and handed to the
|
const EP: CapSlot = CapSlot::const_new(0);
|
||||||
/// client, which maps the same pages in its own space.
|
|
||||||
struct RegionBuffers;
|
|
||||||
|
|
||||||
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<CapSlot>)> {
|
fn create(&mut self, client: usize) -> Option<(&'static mut [u8], Option<CapSlot>)> {
|
||||||
let va = BUFFERS_VA + client * protocol::BUFFER_SIZE;
|
let va = BUFFERS_VA + client * protocol::BUFFER_SIZE;
|
||||||
let region = syscall::create_region(PageCount::new(protocol::BUFFER_PAGES as u16)?).ok()?;
|
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) };
|
let buf = unsafe { core::slice::from_raw_parts_mut(va as *mut u8, protocol::BUFFER_SIZE) };
|
||||||
Some((buf, Some(region)))
|
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<CapSlot> {
|
||||||
|
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() -> ! {
|
pub fn run() -> ! {
|
||||||
libkos::println!("storage: started");
|
libkos::println!("storage: started");
|
||||||
|
|
||||||
let ep = CapSlot::new(0).unwrap();
|
let ep = EP;
|
||||||
// Init hands over the disk before asking for anything.
|
// Init hands over the disk before asking for anything.
|
||||||
let Some(device) = libkos::device::receive_device(ep)
|
let Some(device) = libkos::device::receive_device(ep)
|
||||||
.mmio
|
.mmio
|
||||||
@@ -52,6 +77,6 @@ pub fn run() -> ! {
|
|||||||
libkos::println!("storage: no ext4 filesystem it can read, exiting");
|
libkos::println!("storage: no ext4 filesystem it can read, exiting");
|
||||||
syscall::exit();
|
syscall::exit();
|
||||||
};
|
};
|
||||||
let mut srv = server::StorageServer::new(ext4, RegionBuffers);
|
let mut srv = server::StorageServer::new(ext4, Kernel);
|
||||||
server_framework::serve(&mut srv, ep)
|
server_framework::serve(&mut srv, ep)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,13 @@ pub enum StorageRequest {
|
|||||||
Mkdir { path_len: usize },
|
Mkdir { path_len: usize },
|
||||||
/// Carries the endpoint to print to.
|
/// Carries the endpoint to print to.
|
||||||
SetLog,
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -43,6 +50,9 @@ pub enum StorageResponse {
|
|||||||
ReadOnly,
|
ReadOnly,
|
||||||
/// The client has not attached a buffer yet.
|
/// The client has not attached a buffer yet.
|
||||||
NotAttached,
|
NotAttached,
|
||||||
|
/// Carries the new capability.
|
||||||
|
Granted,
|
||||||
|
Denied,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle(word: usize) -> Option<FileHandle> {
|
fn handle(word: usize) -> Option<FileHandle> {
|
||||||
@@ -64,6 +74,8 @@ impl Encode for StorageRequest {
|
|||||||
Delete { path_len } => [op::DELETE, path_len, 0, 0],
|
Delete { path_len } => [op::DELETE, path_len, 0, 0],
|
||||||
Mkdir { path_len } => [op::MKDIR, path_len, 0, 0],
|
Mkdir { path_len } => [op::MKDIR, path_len, 0, 0],
|
||||||
SetLog => [op::SET_LOG, 0, 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::DELETE => Delete { path_len: w[1] },
|
||||||
op::MKDIR => Mkdir { path_len: w[1] },
|
op::MKDIR => Mkdir { path_len: w[1] },
|
||||||
op::SET_LOG => SetLog,
|
op::SET_LOG => SetLog,
|
||||||
|
op::GRANT => Grant {
|
||||||
|
path_len: w[1],
|
||||||
|
write: w[2] != 0,
|
||||||
|
},
|
||||||
|
op::RELEASE => Release,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -103,6 +120,8 @@ impl Encode for StorageResponse {
|
|||||||
Error => [reply::ERROR, 0, 0, 0],
|
Error => [reply::ERROR, 0, 0, 0],
|
||||||
ReadOnly => [reply::READ_ONLY, 0, 0, 0],
|
ReadOnly => [reply::READ_ONLY, 0, 0, 0],
|
||||||
NotAttached => [reply::NOT_ATTACHED, 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::ERROR => Error,
|
||||||
reply::READ_ONLY => ReadOnly,
|
reply::READ_ONLY => ReadOnly,
|
||||||
reply::NOT_ATTACHED => NotAttached,
|
reply::NOT_ATTACHED => NotAttached,
|
||||||
|
reply::GRANTED => Granted,
|
||||||
|
reply::DENIED => Denied,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
71
crates/servers/storage-server/src/scope.rs
Normal file
71
crates/servers/storage-server/src/scope.rs
Normal file
@@ -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<Self> {
|
||||||
|
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<DiskPath> {
|
||||||
|
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<Self> {
|
||||||
|
Some(Self {
|
||||||
|
root: self.resolve(path)?,
|
||||||
|
write: self.write && write,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
|
use libkos::syscall::Give;
|
||||||
use server_framework::{Decode, Encode, MALFORMED, Server};
|
use server_framework::{Decode, Encode, MALFORMED, Server};
|
||||||
use types::bounded::CapSlot;
|
use types::bounded::CapSlot;
|
||||||
|
|
||||||
use crate::filesystem::{DirEntry, FileHandle, FileSystem};
|
use crate::filesystem::{DirEntry, FileHandle, FileSystem};
|
||||||
use crate::protocol::{
|
use crate::grants::Grants;
|
||||||
DIR_ENTRY_SIZE, DIR_NAME_SIZE, MAX_PATH, StorageRequest, StorageResponse,
|
use crate::protocol::{DIR_ENTRY_SIZE, DIR_NAME_SIZE, MAX_PATH, StorageRequest, StorageResponse};
|
||||||
};
|
use crate::scope::Scope;
|
||||||
|
|
||||||
const MAX_CLIENTS: usize = 8;
|
const MAX_CLIENTS: usize = 8;
|
||||||
/// Handles at or past this are never valid (the file systems hand out
|
/// Handles at or past this are never valid (the file systems hand out
|
||||||
@@ -12,35 +13,51 @@ const MAX_CLIENTS: usize = 8;
|
|||||||
const MAX_HANDLES: usize = 16;
|
const MAX_HANDLES: usize = 16;
|
||||||
const MAX_LISTED: usize = 64;
|
const MAX_LISTED: usize = 64;
|
||||||
|
|
||||||
/// Where client buffers come from: a fresh region per client in the
|
/// What the server needs from the kernel: regions for client buffers
|
||||||
/// real server, plain memory in tests.
|
/// and badged copies of its endpoint for grants. Syscalls in the real
|
||||||
pub trait Buffers {
|
/// server, plain memory in tests.
|
||||||
|
pub trait Host {
|
||||||
/// A buffer for a new client and the capability that lets the client
|
/// A buffer for a new client and the capability that lets the client
|
||||||
/// map it, if there is one to send.
|
/// map it, if there is one to send.
|
||||||
fn create(&mut self, client: usize) -> Option<(&'static mut [u8], Option<CapSlot>)>;
|
fn create(&mut self, client: usize) -> Option<(&'static mut [u8], Option<CapSlot>)>;
|
||||||
|
/// 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<CapSlot>;
|
||||||
|
/// Deletes `grant` and every copy made from it, in every process.
|
||||||
|
fn revoke(&mut self, grant: CapSlot);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Client {
|
struct Client {
|
||||||
badge: usize,
|
/// Who the buffer belongs to now; `None` once its grant is released.
|
||||||
|
badge: Option<usize>,
|
||||||
buf: &'static mut [u8],
|
buf: &'static mut [u8],
|
||||||
/// What maps `buf`, sent again when the same client attaches again.
|
/// What maps `buf`, sent again when the same client attaches again.
|
||||||
cap: Option<CapSlot>,
|
cap: Option<CapSlot>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct StorageServer<F: FileSystem, B: Buffers> {
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct Owner {
|
||||||
|
badge: usize,
|
||||||
|
/// Opened through a scope that may write.
|
||||||
|
write: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct StorageServer<F: FileSystem, H: Host> {
|
||||||
fs: F,
|
fs: F,
|
||||||
buffers: B,
|
host: H,
|
||||||
|
grants: Grants,
|
||||||
clients: [Option<Client>; MAX_CLIENTS],
|
clients: [Option<Client>; MAX_CLIENTS],
|
||||||
/// The badge of the client that opened each handle.
|
owners: [Option<Owner>; MAX_HANDLES],
|
||||||
owners: [Option<usize>; MAX_HANDLES],
|
|
||||||
log: Option<CapSlot>,
|
log: Option<CapSlot>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<F: FileSystem, B: Buffers> StorageServer<F, B> {
|
impl<F: FileSystem, H: Host> StorageServer<F, H> {
|
||||||
pub fn new(fs: F, buffers: B) -> Self {
|
pub fn new(fs: F, host: H) -> Self {
|
||||||
Self {
|
Self {
|
||||||
fs,
|
fs,
|
||||||
buffers,
|
host,
|
||||||
|
grants: Grants::new(),
|
||||||
clients: [const { None }; MAX_CLIENTS],
|
clients: [const { None }; MAX_CLIENTS],
|
||||||
owners: [None; MAX_HANDLES],
|
owners: [None; MAX_HANDLES],
|
||||||
log: None,
|
log: None,
|
||||||
@@ -70,70 +87,176 @@ impl<F: FileSystem, B: Buffers> StorageServer<F, B> {
|
|||||||
&self.fs
|
&self.fs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn host(&self) -> &H {
|
||||||
|
&self.host
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub fn client_buffer_for_test(&mut self, badge: usize) -> Option<&mut [u8]> {
|
pub fn client_buffer_for_test(&mut self, badge: usize) -> Option<&mut [u8]> {
|
||||||
self.client(badge).map(|c| &mut *c.buf)
|
self.client(badge).map(|c| &mut *c.buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn client(&mut self, badge: usize) -> Option<&mut Client> {
|
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
|
/// A client attaching again gets its buffer again; a new one gets a
|
||||||
/// runs one after another share its badge.
|
/// buffer a released grant left behind, or a fresh one.
|
||||||
fn attach(&mut self, badge: usize) -> (StorageResponse, Option<CapSlot>) {
|
fn attach(&mut self, badge: usize) -> (StorageResponse, Option<Give>) {
|
||||||
if let Some(c) = self.client(badge) {
|
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 {
|
let Some(index) = self.clients.iter().position(Option::is_none) else {
|
||||||
return (StorageResponse::Error, None);
|
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);
|
return (StorageResponse::Error, None);
|
||||||
};
|
};
|
||||||
let size = buf.len();
|
let size = buf.len();
|
||||||
if let Some(slot) = self.clients.get_mut(index) {
|
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 {
|
/// Takes `badge`'s grant back from everyone, with everything granted
|
||||||
self.owners.get(handle.raw() as usize).copied().flatten() == Some(badge)
|
/// 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 owner(&self, handle: FileHandle) -> Option<Owner> {
|
||||||
fn serve(&mut self, badge: usize, request: StorageRequest) -> (StorageResponse, Option<CapSlot>) {
|
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<Give>) {
|
||||||
use StorageRequest::*;
|
use StorageRequest::*;
|
||||||
use StorageResponse as R;
|
use StorageResponse as R;
|
||||||
if request == Attach {
|
match request {
|
||||||
return self.attach(badge);
|
Attach => return self.attach(badge),
|
||||||
|
Release => return (self.release(badge), None),
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
let handle_ok = match request {
|
let allowed = match request {
|
||||||
Read { handle, .. } | Write { handle, .. } | Close { handle } | FileSize { handle } => {
|
Read { handle, .. } | Close { handle } | FileSize { handle } => {
|
||||||
self.owns(badge, 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,
|
_ => true,
|
||||||
};
|
};
|
||||||
if !handle_ok {
|
if !allowed {
|
||||||
return (R::Error, None);
|
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() {
|
if mutates && !self.fs.writable() {
|
||||||
return (R::ReadOnly, None);
|
return (R::ReadOnly, None);
|
||||||
}
|
}
|
||||||
// Split borrows: the client's buffer, the file system, the owners.
|
// Split borrows: the client's buffer, the file system, the owners.
|
||||||
let Self { fs, clients, owners, .. } = self;
|
let Self {
|
||||||
let Some(client) = clients.iter_mut().flatten().find(|c| c.badge == badge) else {
|
fs,
|
||||||
|
host,
|
||||||
|
grants,
|
||||||
|
clients,
|
||||||
|
owners,
|
||||||
|
..
|
||||||
|
} = self;
|
||||||
|
let Some(client) = clients
|
||||||
|
.iter_mut()
|
||||||
|
.flatten()
|
||||||
|
.find(|c| c.badge == Some(badge))
|
||||||
|
else {
|
||||||
return (R::NotAttached, None);
|
return (R::NotAttached, None);
|
||||||
};
|
};
|
||||||
let buf = &mut *client.buf;
|
let buf = &mut *client.buf;
|
||||||
let room = buf.len();
|
let room = buf.len();
|
||||||
let path = |len: usize| ..len.min(MAX_PATH).min(room);
|
let sent = |len: usize| ..len.min(MAX_PATH).min(room);
|
||||||
let opened = |owners: &mut [Option<usize>; MAX_HANDLES], h: Option<FileHandle>| match h {
|
// 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<Owner>; MAX_HANDLES], h: Option<FileHandle>| match h {
|
||||||
Some(h) => match owners.get_mut(h.raw() as usize) {
|
Some(h) => match owners.get_mut(h.raw() as usize) {
|
||||||
Some(owner) => {
|
Some(owner) => {
|
||||||
*owner = Some(badge);
|
*owner = Some(Owner {
|
||||||
|
badge,
|
||||||
|
write: scope.write,
|
||||||
|
});
|
||||||
R::Handle(h)
|
R::Handle(h)
|
||||||
}
|
}
|
||||||
// A handle the table cannot track is one we cannot police.
|
// A handle the table cannot track is one we cannot police.
|
||||||
@@ -142,12 +265,20 @@ impl<F: FileSystem, B: Buffers> StorageServer<F, B> {
|
|||||||
None => R::NotFound,
|
None => R::NotFound,
|
||||||
};
|
};
|
||||||
let response = match request {
|
let response = match request {
|
||||||
Attach | SetLog => R::Error,
|
Attach | SetLog | Release => R::Error,
|
||||||
Open { path_len } => {
|
Grant { path_len, write } => {
|
||||||
let h = fs.open(&buf[path(path_len)]);
|
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)
|
opened(owners, h)
|
||||||
}
|
}
|
||||||
Create { path_len } => match fs.create(&buf[path(path_len)]) {
|
Create { .. } => match fs.create(path) {
|
||||||
None => R::Error,
|
None => R::Error,
|
||||||
h => opened(owners, h),
|
h => opened(owners, h),
|
||||||
},
|
},
|
||||||
@@ -170,15 +301,16 @@ impl<F: FileSystem, B: Buffers> StorageServer<F, B> {
|
|||||||
Some(size) => R::Size(size),
|
Some(size) => R::Size(size),
|
||||||
None => R::Error,
|
None => R::Error,
|
||||||
},
|
},
|
||||||
ListDir { path_len } => {
|
ListDir { .. } => {
|
||||||
let mut name = [0u8; MAX_PATH];
|
|
||||||
let p = &buf[path(path_len)];
|
|
||||||
name[..p.len()].copy_from_slice(p);
|
|
||||||
let mut entries = [DirEntry::empty(); MAX_LISTED];
|
let mut entries = [DirEntry::empty(); MAX_LISTED];
|
||||||
let room = (buf.len() / DIR_ENTRY_SIZE).min(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) => {
|
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);
|
write_entry(entry, record);
|
||||||
}
|
}
|
||||||
R::DirEntries { count }
|
R::DirEntries { count }
|
||||||
@@ -186,11 +318,19 @@ impl<F: FileSystem, B: Buffers> StorageServer<F, B> {
|
|||||||
None => R::NotFound,
|
None => R::NotFound,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Delete { path_len } => {
|
Delete { .. } => {
|
||||||
if fs.delete(&buf[path(path_len)]) { R::Ok } else { R::NotFound }
|
if fs.delete(path) {
|
||||||
|
R::Ok
|
||||||
|
} else {
|
||||||
|
R::NotFound
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Mkdir { path_len } => {
|
Mkdir { .. } => {
|
||||||
if fs.mkdir(&buf[path(path_len)]) { R::Ok } else { R::Error }
|
if fs.mkdir(path) {
|
||||||
|
R::Ok
|
||||||
|
} else {
|
||||||
|
R::Error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
(response, None)
|
(response, None)
|
||||||
@@ -203,17 +343,21 @@ fn write_entry(entry: &DirEntry, record: &mut [u8]) {
|
|||||||
let n = name.len().min(DIR_NAME_SIZE);
|
let n = name.len().min(DIR_NAME_SIZE);
|
||||||
record[..n].copy_from_slice(&name[..n]);
|
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());
|
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());
|
record[DIR_NAME_SIZE + 8..DIR_ENTRY_SIZE].copy_from_slice(&flags.to_le_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<F: FileSystem, B: Buffers> Server for StorageServer<F, B> {
|
impl<F: FileSystem, H: Host> Server for StorageServer<F, H> {
|
||||||
type Request = StorageRequest;
|
type Request = StorageRequest;
|
||||||
type Response = StorageResponse;
|
type Response = StorageResponse;
|
||||||
|
|
||||||
/// Without a badge: as the unbadged client.
|
/// Without a badge: as the unbadged client.
|
||||||
fn handle(&mut self, request: StorageRequest) -> StorageResponse {
|
fn handle(&mut self, request: StorageRequest) -> StorageResponse {
|
||||||
self.serve(0, request).0
|
self.serve(0, Scope::EVERYTHING, request).0
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_call(
|
fn handle_call(
|
||||||
@@ -221,13 +365,17 @@ impl<F: FileSystem, B: Buffers> Server for StorageServer<F, B> {
|
|||||||
badge: usize,
|
badge: usize,
|
||||||
words: [usize; 4],
|
words: [usize; 4],
|
||||||
cap: Option<CapSlot>,
|
cap: Option<CapSlot>,
|
||||||
) -> ([usize; 4], Option<CapSlot>) {
|
) -> ([usize; 4], Option<Give>) {
|
||||||
match StorageRequest::decode(words) {
|
match StorageRequest::decode(words) {
|
||||||
Some(StorageRequest::SetLog) => (self.set_log(badge, cap).encode(), None),
|
Some(StorageRequest::SetLog) => (self.set_log(badge, cap).encode(), None),
|
||||||
Some(request) => {
|
// A badge storage never issued, or one it has taken back.
|
||||||
let (response, cap) = self.serve(badge, request);
|
Some(request) => match self.grants.scope(badge) {
|
||||||
(response.encode(), cap)
|
None => (StorageResponse::Denied.encode(), None),
|
||||||
}
|
Some(scope) => {
|
||||||
|
let (response, cap) = self.serve(badge, scope, request);
|
||||||
|
(response.encode(), cap)
|
||||||
|
}
|
||||||
|
},
|
||||||
None => ([MALFORMED, 0, 0, 0], None),
|
None => ([MALFORMED, 0, 0, 0], None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
mod ext4;
|
mod ext4;
|
||||||
mod ext4_real;
|
mod ext4_real;
|
||||||
mod protocol;
|
mod protocol;
|
||||||
|
mod scope;
|
||||||
mod server;
|
mod server;
|
||||||
|
|||||||
@@ -16,6 +16,15 @@ fn every_request_roundtrips() {
|
|||||||
StorageRequest::Create { path_len: 5 },
|
StorageRequest::Create { path_len: 5 },
|
||||||
StorageRequest::Delete { path_len: 5 },
|
StorageRequest::Delete { path_len: 5 },
|
||||||
StorageRequest::Mkdir { path_len: 4 },
|
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));
|
assert_eq!(StorageRequest::decode(req.encode()), Some(req));
|
||||||
}
|
}
|
||||||
@@ -35,6 +44,8 @@ fn every_response_roundtrips() {
|
|||||||
StorageResponse::Error,
|
StorageResponse::Error,
|
||||||
StorageResponse::ReadOnly,
|
StorageResponse::ReadOnly,
|
||||||
StorageResponse::NotAttached,
|
StorageResponse::NotAttached,
|
||||||
|
StorageResponse::Granted,
|
||||||
|
StorageResponse::Denied,
|
||||||
] {
|
] {
|
||||||
assert_eq!(StorageResponse::decode(resp.encode()), Some(resp));
|
assert_eq!(StorageResponse::decode(resp.encode()), Some(resp));
|
||||||
}
|
}
|
||||||
|
|||||||
46
crates/servers/storage-server/src/tests/scope.rs
Normal file
46
crates/servers/storage-server/src/tests/scope.rs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
extern crate std;
|
||||||
|
|
||||||
|
use crate::protocol::MAX_PATH;
|
||||||
|
use crate::scope::Scope;
|
||||||
|
|
||||||
|
fn resolved(scope: &Scope, path: &[u8]) -> Option<std::vec::Vec<u8>> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -3,31 +3,59 @@ extern crate std;
|
|||||||
use crate::adapters::mock_fs::MockFileSystem;
|
use crate::adapters::mock_fs::MockFileSystem;
|
||||||
use crate::filesystem::FileHandle;
|
use crate::filesystem::FileHandle;
|
||||||
use crate::protocol::*;
|
use crate::protocol::*;
|
||||||
use crate::server::{Buffers, StorageServer};
|
use crate::server::{Host, StorageServer};
|
||||||
use server_framework::{Decode, Encode, MALFORMED, Server};
|
use server_framework::{Decode, Encode, MALFORMED, Server};
|
||||||
use std::boxed::Box;
|
use std::boxed::Box;
|
||||||
use std::vec;
|
use std::vec;
|
||||||
|
use std::vec::Vec;
|
||||||
use types::bounded::CapSlot;
|
use types::bounded::CapSlot;
|
||||||
|
|
||||||
/// Plain memory instead of regions; the "capability" is a fixed slot.
|
/// Plain memory instead of regions; the buffer "capability" is a fixed
|
||||||
struct TestBuffers;
|
/// slot, a grant's is its badge past `GRANT_SLOTS`.
|
||||||
|
#[derive(Default)]
|
||||||
|
struct TestHost {
|
||||||
|
reclaimed: Vec<CapSlot>,
|
||||||
|
revoked: Vec<CapSlot>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Buffers for TestBuffers {
|
const BUFFER: Option<CapSlot> = 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<CapSlot>)> {
|
fn create(&mut self, _client: usize) -> Option<(&'static mut [u8], Option<CapSlot>)> {
|
||||||
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> {
|
||||||
|
CapSlot::new(u8::try_from(GRANT_SLOTS + badge).ok()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn revoke(&mut self, grant: CapSlot) {
|
||||||
|
self.revoked.push(grant);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Srv = StorageServer<MockFileSystem, TestBuffers>;
|
type Srv = StorageServer<MockFileSystem, TestHost>;
|
||||||
|
|
||||||
fn server() -> Srv {
|
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.
|
/// One request from `badge`, returning the decoded reply and any cap.
|
||||||
fn call(s: &mut Srv, badge: usize, req: StorageRequest) -> (StorageResponse, Option<CapSlot>) {
|
fn call(s: &mut Srv, badge: usize, req: StorageRequest) -> (StorageResponse, Option<CapSlot>) {
|
||||||
let (words, cap) = s.handle_call(badge, req.encode(), None);
|
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 {
|
struct Client {
|
||||||
@@ -38,10 +66,37 @@ impl Client {
|
|||||||
fn attach(s: &mut Srv, badge: usize) -> Self {
|
fn attach(s: &mut Srv, badge: usize) -> Self {
|
||||||
let (resp, cap) = call(s, badge, StorageRequest::Attach);
|
let (resp, cap) = call(s, badge, StorageRequest::Attach);
|
||||||
assert_eq!(resp, StorageResponse::Attached { size: 4096 });
|
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 }
|
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]) {
|
fn put(&self, s: &mut Srv, bytes: &[u8]) {
|
||||||
s.client_buffer_for_test(self.badge).unwrap()[..bytes.len()].copy_from_slice(bytes);
|
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()
|
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);
|
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,
|
StorageResponse::Handle(h) => h,
|
||||||
other => panic!("create: {other:?}"),
|
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]
|
#[test]
|
||||||
@@ -69,11 +137,12 @@ fn nothing_works_before_attaching() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn attaching_again_hands_back_the_same_buffer() {
|
fn attaching_again_hands_back_the_same_buffer() {
|
||||||
let mut s = server();
|
let mut s = server();
|
||||||
|
let root = Client::attach(&mut s, 0);
|
||||||
let (first, cap) = call(&mut s, 0, StorageRequest::Attach);
|
let (first, cap) = call(&mut s, 0, StorageRequest::Attach);
|
||||||
assert_eq!(call(&mut s, 0, StorageRequest::Attach), (first, cap));
|
assert_eq!(call(&mut s, 0, StorageRequest::Attach), (first, cap));
|
||||||
// Not a new client: the other seven places are still free.
|
// Not a new client: the other seven places are still free.
|
||||||
for badge in 1..8 {
|
for _ in 1..8 {
|
||||||
assert!(matches!(call(&mut s, badge, StorageRequest::Attach).0, StorageResponse::Attached { .. }));
|
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,
|
call(&mut s, 0, StorageRequest::Write { handle: h, len: 14 }).0,
|
||||||
StorageResponse::Written { bytes: 14 }
|
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");
|
c.put(&mut s, b"note.txt");
|
||||||
let StorageResponse::Handle(h) = call(&mut s, 0, StorageRequest::Open { path_len: 8 }).0 else {
|
let StorageResponse::Handle(h) = call(&mut s, 0, StorageRequest::Open { path_len: 8 }).0 else {
|
||||||
panic!("open");
|
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]);
|
c.put(&mut s, &[0; 32]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
call(&mut s, 0, StorageRequest::Read { handle: h, len: 64 }).0,
|
call(&mut s, 0, StorageRequest::Read { handle: h, len: 64 }).0,
|
||||||
@@ -105,8 +180,9 @@ fn data_goes_through_the_buffer_both_ways() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn a_handle_works_only_for_the_client_that_opened_it() {
|
fn a_handle_works_only_for_the_client_that_opened_it() {
|
||||||
let mut s = server();
|
let mut s = server();
|
||||||
let a = Client::attach(&mut s, 1);
|
let root = Client::attach(&mut s, 0);
|
||||||
Client::attach(&mut s, 2);
|
let a = root.narrowed(&mut s, b"", true);
|
||||||
|
let b = root.narrowed(&mut s, b"", true);
|
||||||
let h = a.create(&mut s, b"mine");
|
let h = a.create(&mut s, b"mine");
|
||||||
for req in [
|
for req in [
|
||||||
StorageRequest::Read { handle: h, len: 4 },
|
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::FileSize { handle: h },
|
||||||
StorageRequest::Close { 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]
|
#[test]
|
||||||
@@ -125,10 +208,22 @@ fn hostile_lengths_are_clamped_to_the_buffer() {
|
|||||||
let c = Client::attach(&mut s, 0);
|
let c = Client::attach(&mut s, 0);
|
||||||
let h = c.create(&mut s, b"x");
|
let h = c.create(&mut s, b"x");
|
||||||
for len in [4097, usize::MAX] {
|
for len in [4097, usize::MAX] {
|
||||||
assert!(matches!(call(&mut s, 0, StorageRequest::Write { handle: h, len }).0, StorageResponse::Written { .. }));
|
assert!(matches!(
|
||||||
assert!(matches!(call(&mut s, 0, StorageRequest::Read { handle: h, len }).0, StorageResponse::Data { .. }));
|
call(&mut s, 0, StorageRequest::Write { handle: h, len }).0,
|
||||||
assert_eq!(call(&mut s, 0, StorageRequest::Open { path_len: len }).0, StorageResponse::NotFound);
|
StorageResponse::Written { .. }
|
||||||
assert!(matches!(call(&mut s, 0, StorageRequest::ListDir { path_len: len }).0, StorageResponse::DirEntries { .. }));
|
));
|
||||||
|
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);
|
Client::attach(&mut s, 0);
|
||||||
for raw in [0, 15, 16, u16::MAX] {
|
for raw in [0, 15, 16, u16::MAX] {
|
||||||
let h = FileHandle::new(raw);
|
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]
|
#[test]
|
||||||
fn a_read_only_disk_refuses_every_write_up_front() {
|
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);
|
let c = Client::attach(&mut s, 0);
|
||||||
c.put(&mut s, b"f");
|
c.put(&mut s, b"f");
|
||||||
for req in [
|
for req in [
|
||||||
StorageRequest::Create { path_len: 1 },
|
StorageRequest::Create { path_len: 1 },
|
||||||
StorageRequest::Delete { path_len: 1 },
|
StorageRequest::Delete { path_len: 1 },
|
||||||
StorageRequest::Mkdir { 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 {
|
let expect = match req {
|
||||||
// A handle nobody opened is refused before the disk is asked.
|
// A handle nobody opened is refused before the disk is asked.
|
||||||
StorageRequest::Write { .. } => StorageResponse::Error,
|
StorageRequest::Write { .. } => StorageResponse::Denied,
|
||||||
_ => StorageResponse::ReadOnly,
|
_ => StorageResponse::ReadOnly,
|
||||||
};
|
};
|
||||||
assert_eq!(call(&mut s, 0, req).0, expect, "{req:?}");
|
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 });
|
call(&mut s, 0, StorageRequest::Close { handle: h });
|
||||||
}
|
}
|
||||||
c.put(&mut s, b"/");
|
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);
|
let records = c.get(&mut s, 2 * DIR_ENTRY_SIZE);
|
||||||
assert_eq!(&records[..5], b"a.txt");
|
assert_eq!(&records[..5], b"a.txt");
|
||||||
assert_eq!(records[5], 0);
|
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!(s.log(), log);
|
||||||
assert_eq!(set(&mut s, 0), StorageResponse::Error);
|
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<usize> = 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ impl<C: Console> Server for TerminalServer<C> {
|
|||||||
Done
|
Done
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_call(&mut self, _badge: usize, words: [usize; 4], _cap: Option<CapSlot>) -> ([usize; 4], Option<CapSlot>) {
|
fn handle_call(&mut self, _badge: usize, words: [usize; 4], _cap: Option<CapSlot>) -> ([usize; 4], Option<libkos::syscall::Give>) {
|
||||||
let reply = match words[0] {
|
let reply = match words[0] {
|
||||||
types::terminal::READ => self.read(),
|
types::terminal::READ => self.read(),
|
||||||
types::terminal::CLEAR => {
|
types::terminal::CLEAR => {
|
||||||
|
|||||||
@@ -15,6 +15,24 @@ fn optional_slots_roundtrip() {
|
|||||||
assert!(reg_slot(slot_reg(s)) == Some(s));
|
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]
|
#[kani::proof]
|
||||||
fn every_register_decodes_to_none_or_a_valid_slot() {
|
fn every_register_decodes_to_none_or_a_valid_slot() {
|
||||||
let r: usize = kani::any();
|
let r: usize = kani::any();
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ pub mod op {
|
|||||||
pub const MKDIR: usize = 9;
|
pub const MKDIR: usize = 9;
|
||||||
/// Carries the log endpoint; only from the unbadged holder (init).
|
/// Carries the log endpoint; only from the unbadged holder (init).
|
||||||
pub const SET_LOG: usize = 10;
|
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).
|
/// Reply tags (word 0).
|
||||||
@@ -42,4 +48,8 @@ pub mod reply {
|
|||||||
pub const ERROR: usize = 8;
|
pub const ERROR: usize = 8;
|
||||||
pub const READ_ONLY: usize = 9;
|
pub const READ_ONLY: usize = 9;
|
||||||
pub const NOT_ATTACHED: usize = 10;
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ pub const PERMS_READ_EXECUTE: usize = 2;
|
|||||||
/// "No capability" in a capability argument or result register.
|
/// "No capability" in a capability argument or result register.
|
||||||
pub const NONE: usize = usize::MAX;
|
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`.
|
/// What a receiving syscall got, returned in `a7`.
|
||||||
pub const KIND_MESSAGE: usize = 0;
|
pub const KIND_MESSAGE: usize = 0;
|
||||||
pub const KIND_CALL: usize = 1;
|
pub const KIND_CALL: usize = 1;
|
||||||
@@ -115,6 +119,53 @@ pub fn reg_slot(reg: usize) -> Option<Option<CapSlot>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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<CapSlot> for Give {
|
||||||
|
fn from(slot: CapSlot) -> Self {
|
||||||
|
Self {
|
||||||
|
slot,
|
||||||
|
keep_delegate: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn give_reg(give: Option<Give>) -> 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<Option<Give>> {
|
||||||
|
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 {
|
pub fn perms_reg(perms: PagePerms) -> usize {
|
||||||
match perms {
|
match perms {
|
||||||
PagePerms::ReadOnly => PERMS_READ_ONLY,
|
PagePerms::ReadOnly => PERMS_READ_ONLY,
|
||||||
@@ -133,14 +184,14 @@ pub fn reg_perms(reg: usize) -> Option<PagePerms> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Argument registers `a0..a5` of a message-carrying call.
|
/// Argument registers `a0..a5` of a message-carrying call.
|
||||||
pub fn message_regs(target: usize, words: [usize; 4], cap: Option<CapSlot>) -> [usize; 6] {
|
pub fn message_regs(target: usize, words: [usize; 4], cap: Option<Give>) -> [usize; 6] {
|
||||||
[
|
[
|
||||||
target,
|
target,
|
||||||
words[0],
|
words[0],
|
||||||
words[1],
|
words[1],
|
||||||
words[2],
|
words[2],
|
||||||
words[3],
|
words[3],
|
||||||
slot_reg(cap),
|
give_reg(cap),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
The shell runs a program by asking init to load it (init alone may
|
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
|
create processes), then gives the program the terminal as its output
|
||||||
and the storage server, writes its arguments onto a page it moves
|
and a read-only storage grant to the current directory, writes its
|
||||||
into the program, starts it and waits for it to die.
|
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:
|
Built-ins: help, echo, clear, cd, pwd. Anything else is a program on
|
||||||
`name args...` runs name.elf.
|
disk: `name args...` runs name.elf.
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ Welcome to k-os.
|
|||||||
|
|
||||||
Every program here runs with only the capabilities it was given:
|
Every program here runs with only the capabilities it was given:
|
||||||
this file reached your screen because the shell handed `cat` the
|
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
|
||||||
|
|||||||
26
docs/ipc.md
26
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
|
calling, and `Delegate` lets the client send capabilities with its calls
|
||||||
and hand its access to programs it starts.
|
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
|
Input reaches apps through the display server, the input server's only
|
||||||
client. It subscribes to the input server with a notification, drains it
|
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
|
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
|
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
|
`\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
|
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
|
spawns it, gives it a call-only copy of the terminal (as its log, so its
|
||||||
output) and of storage, writes its arguments page, starts it and waits for
|
output) and a read-only storage grant to the shell's current directory
|
||||||
its death. The programs share the shell's storage badge; storage hands a
|
(`cd`, `pwd`), writes its arguments page, starts it, waits for its death
|
||||||
client that attaches again the same buffer, so they run one at a time.
|
and releases the grant.
|
||||||
|
|
||||||
Only init holds `ProcessCreate`. Everyone else runs programs through init's
|
Only init holds `ProcessCreate`. Everyone else runs programs through init's
|
||||||
spawn service (`types::spawn`, client `libkos::spawn`), which init serves
|
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 only if the sender's endpoint capability has `Delegate` and the
|
||||||
transferred capability itself has `Delegate`; it is **copied** (the sender
|
transferred capability itself has `Delegate`; it is **copied** (the sender
|
||||||
keeps its own) with the same badge and permissions minus `Delegate`: the
|
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
|
receiver can use it but not pass it on. A sender that means the receiver
|
||||||
on, give it with the `Delegate` syscall and explicit rights. If the receiver's
|
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
|
table is full, or the capability is no longer valid at delivery time, the
|
||||||
message is delivered without it.
|
message is delivered without it.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user