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:
2026-10-27 20:20:34 +01:00
parent c603a578c0
commit 1fc5a54f06
38 changed files with 1237 additions and 170 deletions

View File

@@ -14,9 +14,11 @@ pub fn grant_all(uart: &Uart, init_pid: ProcessId, storage_pid: ProcessId) {
.with(types::permission::Permission::Execute)
.with(types::permission::Permission::Delegate);
// Delegate so storage can mint the badged copies it grants.
let ipc_perms = types::permission::PermissionSet::empty()
.with(types::permission::Permission::Read)
.with(types::permission::Permission::Execute);
.with(types::permission::Permission::Execute)
.with(types::permission::Permission::Delegate);
let storage_ep = kern()
.create_endpoint(storage_pid)

View File

@@ -36,6 +36,7 @@ extern "C" fn main() {
match cat(&mut fs, path) {
Ok(()) => {}
Err(FsError::NotFound) => println!("cat: {}: no such file", path),
Err(FsError::Denied) => println!("cat: {}: outside what it was given", path),
Err(_) => println!("cat: {}: cannot read", path),
}
}

View File

@@ -5,7 +5,7 @@
extern crate libkos;
use libkos::fs::Storage;
use libkos::fs::{FsError, Storage};
use libkos::println;
/// Where storage's buffer is mapped.
@@ -29,7 +29,9 @@ extern "C" fn main() {
println!("{:<24} {:>8}", name, e.size);
}
});
if listed.is_err() {
println!("ls: {}: no such directory", dir);
match listed {
Ok(_) => {}
Err(FsError::Denied) => println!("ls: {}: outside what it was given", dir),
Err(_) => println!("ls: {}: no such directory", dir),
}
}

View File

@@ -1,12 +1,14 @@
//! The shell: a few built-ins, and anything else is a program on disk.
//! `name args...` runs `name.elf` through init, handing it the terminal as
//! its output and the storage server, and nothing else; then waits for it.
//! its output and read-only storage rooted at the current directory, and
//! nothing else; then waits for it and takes the storage back.
#![no_std]
#![no_main]
extern crate libkos;
use libkos::fs::Storage;
use libkos::spawn::{SpawnError, spawn};
use libkos::{print, println, syscall};
use types::bounded::CapSlot;
@@ -22,9 +24,76 @@ const INIT: CapSlot = CapSlot::const_new(2);
/// What each program gets, out of the shell's allowance.
const PROGRAM: Limits = Limits { pages: 128, endpoints: 1, notifications: 1, regions: 1 };
const MAX_ARGS: usize = 16;
/// Where storage's buffer is mapped.
const STORAGE_BUF_VA: usize = 0x0180_0000;
/// Runs `args[0].elf` with `args` and waits for it to finish.
fn run_program(args: &[&str], done: CapSlot) {
/// The current directory, as a path on disk: no leading or trailing `/`.
struct Cwd {
path: [u8; types::storage::MAX_PATH],
len: usize,
}
impl Cwd {
fn as_bytes(&self) -> &[u8] {
&self.path[..self.len]
}
/// Where `cd to` from here leads: `/` starts at the root, `..` goes up
/// (the shell holds the whole disk, so that is its to resolve).
fn join(&self, to: &str) -> Option<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 name = args[0].as_bytes();
let Some(file) = file.get_mut(..name.len() + 4).map(|f| {
@@ -42,23 +111,34 @@ fn run_program(args: &[&str], done: CapSlot) {
Err(SpawnError::Failed) => return println!("shell: {}: cannot run", args[0]),
};
let call = PermissionSet::empty().with(Permission::Execute);
let grant = sh.fs.grant(sh.cwd.as_bytes(), false).ok();
let arg = StartArg {
log: child.give(TERMINAL, call),
storage: child.give(STORAGE, call),
storage: grant.and_then(|g| child.give(g, call)),
args: child.set_args(args),
console: None,
};
let watched = syscall::watch_death(child.process, done).is_ok();
let watched = syscall::watch_death(child.process, sh.done).is_ok();
if child.start_with(arg) && watched {
let _ = syscall::wait(done);
let _ = syscall::wait(sh.done);
} else {
let _ = syscall::kill(child.process);
println!("shell: {}: cannot start", args[0]);
}
syscall::drop_cap(child.process);
if let Some(g) = grant {
let _ = libkos::fs::release(g);
}
}
fn run(line: &str, done: CapSlot) {
fn cd(sh: &mut Shell, to: &str) {
match sh.cwd.join(to) {
Some(next) if sh.fs.list_dir(next.as_bytes(), |_| {}).is_ok() => sh.cwd = next,
_ => println!("cd: {}: no such directory", to),
}
}
fn run(sh: &mut Shell, line: &str) {
let mut args = [""; MAX_ARGS];
let mut n = 0;
for w in line.split_ascii_whitespace().take(MAX_ARGS) {
@@ -69,12 +149,15 @@ fn run(line: &str, done: CapSlot) {
match args.first().copied() {
None => {}
Some("help") => {
println!("built-ins: help, echo <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!("programs see the current directory, read-only, and nothing above it");
}
Some("cd") => cd(sh, args.get(1).copied().unwrap_or("/")),
Some("pwd") => println!("{}", sh.cwd),
Some("echo") => println!("{}", Joined(&args[1..])),
Some("clear") => libkos::terminal::clear(TERMINAL),
Some(_) => run_program(args, done),
Some(_) => run_program(sh, args),
}
}
@@ -101,13 +184,25 @@ extern "C" fn main() {
println!("shell: no notification to wait on");
return;
};
let Ok(fs) = Storage::attach(STORAGE, STORAGE_BUF_VA) else {
println!("shell: no storage");
return;
};
let mut sh = Shell {
fs,
cwd: Cwd {
path: [0; types::storage::MAX_PATH],
len: 0,
},
done,
};
println!("k-os shell. Type 'help'.");
let mut buf = [0u8; 256];
loop {
print!("$ ");
let n = libkos::terminal::read_line(TERMINAL, &mut buf);
match core::str::from_utf8(&buf[..n]) {
Ok(line) => run(line.trim_end_matches('\n'), done),
Ok(line) => run(&mut sh, line.trim_end_matches('\n')),
Err(_) => println!("shell: not text"),
}
}

View File

@@ -7,6 +7,7 @@
extern crate libkos;
use libkos::fs::{FsError, Storage};
use libkos::spawn::{SpawnError, spawn};
use libkos::syscall;
use types::bounded::CapSlot;
@@ -17,8 +18,9 @@ use types::budget::Limits;
const STORAGE: CapSlot = CapSlot::const_new(0);
const INIT: CapSlot = CapSlot::const_new(1);
const CONTROL: CapSlot = CapSlot::const_new(2);
/// Where storage's buffer is mapped.
/// Where storage's buffers are mapped: the whole disk's, a narrowed one's.
const STORAGE_BUF_VA: usize = 0x0180_0000;
const NARROW_BUF_VA: usize = 0x0190_0000;
/// Read back from the disk image by scripts/test-qemu.sh.
const NOTE: &[u8] = b"written by k-os on ext4\n";
@@ -26,9 +28,14 @@ const NOTE: &[u8] = b"written by k-os on ext4\n";
/// the first has died and its limits came back.
const HELLO: Limits = Limits { pages: 80, endpoints: 0, notifications: 0, regions: 0 };
/// Spawns hello, runs it, and waits for it to die.
fn run_hello(died: CapSlot) -> Result<(), &'static str> {
/// Spawns hello, runs it, and waits for it to die; hands it `grant`
/// first, if there is one.
fn run_hello(died: CapSlot, grant: Option<CapSlot>) -> Result<(), &'static str> {
let child = spawn(INIT, b"hello.elf", HELLO).map_err(|_| "init would not spawn hello")?;
let call = types::permission::PermissionSet::empty().with(types::permission::Permission::Execute);
if grant.is_some_and(|g| child.give(g, call).is_none()) {
return Err("a storage grant could not be passed on");
}
syscall::watch_death(child.process, died).map_err(|_| "cannot watch hello")?;
if !child.start() {
return Err("cannot start hello");
@@ -38,8 +45,7 @@ fn run_hello(died: CapSlot) -> Result<(), &'static str> {
}
/// A file written through storage, which the host then checks with e2fsck.
fn write_a_file() -> Result<(), &'static str> {
let mut fs = libkos::fs::Storage::attach(STORAGE, STORAGE_BUF_VA).map_err(|_| "cannot attach to storage")?;
fn write_a_file(fs: &mut Storage) -> Result<(), &'static str> {
let h = fs.create(b"selftest.txt").map_err(|_| "cannot create a file")?;
if fs.write(h, NOTE) != Ok(NOTE.len()) {
return Err("short write");
@@ -55,12 +61,40 @@ fn write_a_file() -> Result<(), &'static str> {
Ok(())
}
/// A read-only grant to one directory reaches nothing else and changes
/// nothing, can be passed on, and is gone everywhere once released.
fn narrow_storage(fs: &mut Storage, died: CapSlot) -> Result<(), &'static str> {
let grant = fs.grant(b"lost+found", false).map_err(|_| "storage refused a grant")?;
let mut inner = Storage::attach(grant, NARROW_BUF_VA).map_err(|_| "cannot attach through the grant")?;
inner.list_dir(b"", |_| {}).map_err(|_| "cannot list its own directory")?;
if inner.open(b"../selftest.txt").err() != Some(FsError::Denied) {
return Err("a grant reached outside its directory");
}
if inner.create(b"x").err() != Some(FsError::Denied) {
return Err("a read-only grant created a file");
}
let widened = inner.grant(b"", true).and_then(|g| Storage::attach(g, NARROW_BUF_VA + 0x10_0000));
if widened.and_then(|mut w| w.create(b"x")).is_ok() {
return Err("a grant was widened to write");
}
run_hello(died, Some(grant))?;
libkos::fs::release(grant).map_err(|_| "release failed")?;
if inner.list_dir(b"", |_| {}).is_ok() {
return Err("a released grant still works");
}
Ok(())
}
fn check() -> Result<(), &'static str> {
write_a_file()?;
let mut fs = Storage::attach(STORAGE, STORAGE_BUF_VA).map_err(|_| "cannot attach to storage")?;
write_a_file(&mut fs)?;
libkos::println!("spawntest: a file written to ext4 reads back: ok");
let died = syscall::create_notification().map_err(|_| "no notification")?;
run_hello(died)?;
narrow_storage(&mut fs, died)?;
libkos::println!("spawntest: storage grants stay in scope, pass on, release: ok");
run_hello(died, None)?;
libkos::println!("spawntest: spawned program runs and its death is heard: ok");
let greedy = Limits { pages: 200, ..HELLO };
@@ -73,7 +107,7 @@ fn check() -> Result<(), &'static str> {
}
libkos::println!("spawntest: allowance and missing files refused: ok");
run_hello(died)?;
run_hello(died, None)?;
libkos::println!("spawntest: allowance comes back when a child dies: ok");
Ok(())
}

View File

@@ -30,7 +30,7 @@ fn subscribe_to_input() -> Option<CapSlot> {
libkos::syscall::bind_notification(notify).ok()?;
let signal = PermissionSet::empty().with(Permission::Write).with(Permission::Delegate);
let copy = libkos::syscall::mint(notify, Badge::NONE, signal).ok()?;
let sent = libkos::syscall::call(INPUT, [types::input::SUBSCRIBE, 0, 0, 0], Some(copy));
let sent = libkos::syscall::call(INPUT, [types::input::SUBSCRIBE, 0, 0, 0], Some(copy.into()));
libkos::syscall::drop_cap(copy);
sent.ok().map(|_| INPUT)
}

View File

@@ -11,6 +11,8 @@ pub const MAX_MESSAGE_WORDS: usize = 4;
pub struct Message {
words: [usize; MAX_MESSAGE_WORDS],
cap: Option<CapSlot>,
/// The receiver may pass `cap` on (see `syscall_abi::KEEP_DELEGATE`).
keep_delegate: bool,
}
impl Message {
@@ -19,7 +21,11 @@ impl Message {
}
pub fn from_words(words: [usize; MAX_MESSAGE_WORDS]) -> Self {
Self { words, cap: None }
Self {
words,
cap: None,
keep_delegate: false,
}
}
pub fn with_cap(mut self, slot: CapSlot) -> Self {
@@ -39,4 +45,13 @@ impl Message {
pub fn cap(&self) -> Option<CapSlot> {
self.cap
}
pub fn keeping_delegate(mut self) -> Self {
self.keep_delegate = true;
self
}
pub fn keeps_delegate(&self) -> bool {
self.keep_delegate
}
}

View File

@@ -78,10 +78,16 @@ impl<'a> Launcher<'a> {
// and take other clients' requests.
for name in p.uses {
let ep = self.endpoint(name).ok_or("needs a server that is not running")?;
let copy = match *name {
// Storage issues its own badges, each naming a scope.
"storage" => self.storage.grant(b"", true).map_err(|_| "storage refused a grant")?,
_ => {
let badge = Badge::new(self.next_badge);
self.next_badge += 1;
let copy = syscall::mint(ep, badge, perms(&[Permission::Execute, Permission::Delegate]))
.map_err(|_| "cannot mint an endpoint")?;
syscall::mint(ep, badge, perms(&[Permission::Execute, Permission::Delegate]))
.map_err(|_| "cannot mint an endpoint")?
}
};
let given = syscall::delegate(copy, child, perms(&[Permission::Execute, Permission::Delegate]));
syscall::drop_cap(copy);
given.ok_or("cannot give an endpoint")?;

View File

@@ -26,8 +26,12 @@ pub fn run() -> ! {
// Storage waits for its disk before serving, so this comes first.
let found = devices::discover();
let disk = libkos::device::give_device(STORAGE_EP, found.disk).is_ok() && found.disk.mmio.is_some();
libkos::println!("init: disk: {}", if disk { "handed to its driver" } else { "none" });
let disk =
libkos::device::give_device(STORAGE_EP, found.disk).is_ok() && found.disk.mmio.is_some();
libkos::println!(
"init: disk: {}",
if disk { "handed to its driver" } else { "none" }
);
let Ok(mut storage) = Storage::attach(STORAGE_EP, STORAGE_BUF_VA) else {
libkos::println!("init: cannot reach storage, nothing to load");
@@ -53,7 +57,10 @@ pub fn run() -> ! {
// The test image carries the SMP self-test; it and the tests run
// instead of the desktop, and the last one halts the machine.
let (storage, scratch) = launcher.storage();
if storage.open(b"smptest.elf").is_ok_and(|h| storage.close(h).is_ok()) {
if storage
.open(b"smptest.elf")
.is_ok_and(|h| storage.close(h).is_ok())
{
libkos::println!("init: running the SMP self-test");
selftest::run(storage, scratch);
launch_all(&mut launcher, manifest::TESTS);
@@ -83,9 +90,15 @@ fn give_storage_the_log() {
let Some(log) = libkos::start::log() else {
return;
};
let call_only = PermissionSet::empty().with(Permission::Execute).with(Permission::Delegate);
let call_only = PermissionSet::empty()
.with(Permission::Execute)
.with(Permission::Delegate);
if let Ok(copy) = syscall::mint(log, Badge::NONE, call_only) {
let _ = syscall::call(STORAGE_EP, [types::storage::op::SET_LOG, 0, 0, 0], Some(copy));
let _ = syscall::call(
STORAGE_EP,
[types::storage::op::SET_LOG, 0, 0, 0],
Some(copy.into()),
);
syscall::drop_cap(copy);
}
}

View File

@@ -165,9 +165,12 @@ impl Server for Launcher<'_> {
Reply(refused(reply::ERROR).0)
}
fn handle_call(&mut self, badge: usize, words: [usize; 4], _cap: Option<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) {
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),
}
}

View File

@@ -28,9 +28,16 @@ fn permissions(r: usize) -> Option<PermissionSet> {
/// The message in `a1..a5`.
fn message(a: &[usize; 6]) -> Option<Message> {
let msg = Message::from_words([a[1], a[2], a[3], a[4]]);
Some(match reg_slot(a[5])? {
Some(match reg_give(a[5])? {
None => msg,
Some(cap) => msg.with_cap(cap),
Some(Give {
slot,
keep_delegate: false,
}) => msg.with_cap(slot),
Some(Give {
slot,
keep_delegate: true,
}) => msg.with_cap(slot).keeping_delegate(),
})
}

View File

@@ -39,12 +39,18 @@ fn send_roundtrips() {
} else {
None
};
let regs = message_regs(ep.value() as usize, words, cap);
let keep: bool = kani::any();
let give = cap.map(|slot| Give {
slot,
keep_delegate: keep,
});
let regs = message_regs(ep.value() as usize, words, give);
match decode(SYS_SEND, &regs) {
Some(Syscall::Send { endpoint, message }) => {
assert!(endpoint == ep);
assert!(*message.words() == words);
assert!(message.cap() == cap);
assert!(message.keeps_delegate() == (keep && cap.is_some()));
}
_ => panic!("valid send did not decode"),
}

View File

@@ -3,7 +3,7 @@ use domain::capability::{Capability, CapabilityTable};
use domain::resource::Resource;
use types::generation::Generation;
use types::id::EndpointId;
use types::permission::{Permission, PermissionSet};
use types::permission::PermissionSet;
#[kani::proof]
fn revoke_invalidates_cap_slot() {

View File

@@ -38,22 +38,22 @@ impl<P: Platform> Kernel<P> {
pub(super) fn transfer_cap(
&mut self,
from: ProcessId,
slot: Option<CapSlot>,
message: &Message,
grant: bool,
to: ProcessId,
) -> Option<CapSlot> {
let slot = slot?;
let slot = message.cap()?;
if !grant {
return None;
}
// The receiver gets the capability but not the right to pass it
// on; a sender that wants that uses Delegate with explicit rights.
// on, unless the sender says so; it can only give what it holds.
let source = self.check_cap(from, slot, Permission::Delegate).ok()?;
let copy = Capability::new(
*source.resource(),
source.permissions().without(Permission::Delegate),
*source.generation(),
)
let permissions = match message.keeps_delegate() {
true => *source.permissions(),
false => source.permissions().without(Permission::Delegate),
};
let copy = Capability::new(*source.resource(), permissions, *source.generation())
.with_badge(source.badge());
let parent = self.cap_ref(from, slot);
self.insert_derived(to, copy, parent).ok()
@@ -71,7 +71,7 @@ impl<P: Platform> Kernel<P> {
let from = self.thread_process(sender);
let to = self.thread_process(receiver);
let cap = match (from, to) {
(Some(from), Some(to)) => self.transfer_cap(from, out.message.cap(), out.grant, to),
(Some(from), Some(to)) => self.transfer_cap(from, &out.message, out.grant, to),
_ => None,
};
if kind == DeliveryKind::Call {
@@ -234,7 +234,7 @@ impl<P: Platform> Kernel<P> {
}
let to = self.thread_process(caller.tid);
// A reply may always carry a capability the server can delegate.
let cap = to.and_then(|to| self.transfer_cap(pid, message.cap(), true, to));
let cap = to.and_then(|to| self.transfer_cap(pid, &message, true, to));
let delivery = Delivery {
kind: DeliveryKind::Message,
badge: 0,

View File

@@ -917,6 +917,67 @@ fn a_received_cap_cannot_be_passed_on() {
));
}
/// What `b` holds in `slot`, by permission.
fn rights_of(t: &Ipc, pid: ProcessId, slot: CapSlot) -> PermissionSet {
*t.k.processes
.get(pid)
.unwrap()
.capabilities()
.get(slot)
.unwrap()
.permissions()
}
#[test]
fn a_sender_can_let_the_receiver_pass_a_cap_on() {
let mut t = Ipc::new();
let (a, b, ta, tb, a_send, b_recv) = (t.a, t.b, t.ta, t.tb, t.a_send, t.b_recv);
let other = t.k.create_endpoint(a).unwrap();
let gift =
t.k.grant_cap_for_test(a, Resource::Endpoint(other), perms(SEND_GRANT));
t.run(
ta,
Syscall::Send {
endpoint: a_send,
message: msg([0; 4]).with_cap(gift).keeping_delegate(),
},
);
let d = delivered(&t.run(tb, Syscall::Receive { endpoint: b_recv })).unwrap();
let got = d.cap.expect("the cap arrives");
assert_eq!(
rights_of(&t, b, got),
rights_of(&t, a, gift),
"with exactly the sender's rights"
);
let mint = t.run(
tb,
Syscall::Mint {
slot: got,
badge: Badge::new(1),
permissions: perms(SEND),
},
);
assert!(error(&mint).is_none(), "so it can derive from it");
}
#[test]
fn keeping_delegate_cannot_give_what_the_sender_lacks() {
let mut t = Ipc::new();
let (a, ta, tb, a_send, b_recv) = (t.a, t.ta, t.tb, t.a_send, t.b_recv);
let other = t.k.create_endpoint(a).unwrap();
let gift =
t.k.grant_cap_for_test(a, Resource::Endpoint(other), perms(SEND));
t.run(
ta,
Syscall::Send {
endpoint: a_send,
message: msg([0; 4]).with_cap(gift).keeping_delegate(),
},
);
let d = delivered(&t.run(tb, Syscall::Receive { endpoint: b_recv })).unwrap();
assert_eq!(d.cap, None, "a cap without Delegate never travels");
}
#[test]
fn an_irq_has_one_owner_until_it_dies() {
let mut t = Ipc::new();

View File

@@ -59,7 +59,7 @@ pub fn receive_device(endpoint: CapSlot) -> Device {
pub fn give_device(endpoint: CapSlot, device: Device) -> Result<()> {
for (what, cap) in [(MMIO, device.mmio), (IRQ, device.irq)] {
if let Some(cap) = cap {
syscall::call(endpoint, [SETUP, what, device.kind, 0], Some(cap))?;
syscall::call(endpoint, [SETUP, what, device.kind, 0], Some(cap.into()))?;
}
}
syscall::call(endpoint, [SETUP, DONE, 0, 0], None).map(|_| ())

View File

@@ -60,7 +60,11 @@ pub fn subscribe_input(ep: CapSlot) -> Option<CapSlot> {
// Signal only (Delegate just to send it; the transfer strips it).
let signal = PermissionSet::empty().with(Permission::Write).with(Permission::Delegate);
let copy = syscall::mint(notify, Badge::NONE, signal).ok()?;
let sent = syscall::call(ep, [types::display::input::SUBSCRIBE, 0, 0, 0], Some(copy));
let sent = syscall::call(
ep,
[types::display::input::SUBSCRIBE, 0, 0, 0],
Some(copy.into()),
);
syscall::drop_cap(copy);
(sent.ok()?.words()[0] == 0).then_some(notify)
}

View File

@@ -24,6 +24,8 @@ pub enum FsError {
ReadOnly,
/// A path longer than the server accepts.
PathTooLong,
/// Outside this connection's scope, or beyond its rights.
Denied,
/// The server refused, or the call itself failed.
Failed,
}
@@ -76,6 +78,7 @@ impl Storage {
match r[0] {
reply::NOT_FOUND => Err(FsError::NotFound),
reply::READ_ONLY => Err(FsError::ReadOnly),
reply::DENIED => Err(FsError::Denied),
reply::ERROR | reply::NOT_ATTACHED => Err(FsError::Failed),
_ => Ok(r),
}
@@ -100,6 +103,20 @@ impl Storage {
self.expect([opcode, len, 0, 0], tag)
}
/// A storage capability to `path` inside this connection's scope,
/// writable only if `write` and this one is. It can be passed on;
/// `release` takes it back.
pub fn grant(&mut self, path: &[u8], write: bool) -> Result<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> {
self.with_path(op::OPEN, path, reply::HANDLE).map(|h| FileHandle(h as u16))
}
@@ -181,3 +198,13 @@ impl Storage {
Ok(count)
}
}
/// Takes back `grant` and everything granted from it, from every holder;
/// the slot is empty afterwards.
pub fn release(grant: CapSlot) -> Result<()> {
match syscall::request(grant, [op::RELEASE, 0, 0, 0]) {
Ok([reply::OK, ..]) => Ok(()),
Ok([reply::DENIED, ..]) => Err(FsError::Denied),
_ => Err(FsError::Failed),
}
}

View File

@@ -4,6 +4,8 @@ use super::{call0, raw, slot_of, Error, Result};
use types::bounded::CapSlot;
use types::syscall_abi::*;
pub use types::syscall_abi::Give;
/// What a receiving call got.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Received {
@@ -48,7 +50,7 @@ pub fn create_endpoint() -> Option<CapSlot> {
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(
SYS_SEND,
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.
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(
SYS_CALL,
message_regs(endpoint.value() as usize, words, cap),
@@ -80,12 +82,12 @@ pub fn request(endpoint: CapSlot, words: [usize; 4]) -> Result<[usize; 4]> {
}
/// Answers the last `Call` this thread received.
pub fn reply(words: [usize; 4], cap: Option<CapSlot>) -> Result<()> {
pub fn reply(words: [usize; 4], cap: Option<Give>) -> Result<()> {
raw(SYS_REPLY, message_regs(0, words, cap)).map(|_| ())
}
/// Replies to the pending `Call` (if any), then receives the next message.
pub fn reply_recv(endpoint: CapSlot, words: [usize; 4], cap: Option<CapSlot>) -> Result<Received> {
pub fn reply_recv(endpoint: CapSlot, words: [usize; 4], cap: Option<Give>) -> Result<Received> {
raw(
SYS_REPLY_RECV,
message_regs(endpoint.value() as usize, words, cap),

View File

@@ -2,6 +2,7 @@
mod tests;
use crate::codec::{Decode, Encode};
use libkos::syscall::Give;
use types::bounded::CapSlot;
/// The reply to a request that does not decode. No protocol uses this
@@ -27,7 +28,7 @@ pub trait Server {
_badge: usize,
words: [usize; 4],
_cap: Option<CapSlot>,
) -> ([usize; 4], Option<CapSlot>) {
) -> ([usize; 4], Option<Give>) {
match Self::Request::decode(words) {
Some(request) => (self.handle(request).encode(), None),
None => ([MALFORMED, 0, 0, 0], None),
@@ -68,6 +69,10 @@ pub trait Server {
/// has nobody left to serve, so it says why and exits.
pub fn serve<S: Server>(server: &mut S, endpoint: CapSlot) -> ! {
let error = server.run(endpoint);
libkos::println!("server: endpoint {} unusable ({:?}), exiting", endpoint.value(), error);
libkos::println!(
"server: endpoint {} unusable ({:?}), exiting",
endpoint.value(),
error
);
libkos::syscall::exit()
}

View File

@@ -57,7 +57,7 @@ impl<F: Framebuffer> Server for DisplayServer<F> {
type Request = DisplayRequest;
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};
match (words[0], cap) {
(SUBSCRIBE, Some(notify)) => {

View File

@@ -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 {
self.subscriber = cap;
return ([0; 4], None);

View 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;
}
}
}
}
}

View File

@@ -5,7 +5,9 @@ extern crate alloc;
pub mod adapters;
pub mod filesystem;
pub mod fs;
pub mod grants;
pub mod protocol;
pub mod scope;
pub mod server;
#[cfg(test)]
@@ -14,17 +16,22 @@ mod tests;
use libkos::syscall;
use types::address::VirtualAddress;
use types::badge::Badge;
use types::bounded::{CapSlot, PageCount};
use types::mapping::PagePerms;
use types::permission::{Permission, PermissionSet};
/// Client buffers sit high in storage's window, clear of its heap.
const BUFFERS_VA: usize = 0x0100_0000;
/// Each client's buffer is a fresh region, mapped here and handed to the
/// client, which maps the same pages in its own space.
struct RegionBuffers;
const EP: CapSlot = CapSlot::const_new(0);
impl server::Buffers for RegionBuffers {
/// Each client's buffer is a region, mapped here and handed to the
/// client, which maps the same pages in its own space. Grants are badged
/// copies of storage's endpoint.
struct Kernel;
impl server::Host for Kernel {
fn create(&mut self, client: usize) -> Option<(&'static mut [u8], Option<CapSlot>)> {
let va = BUFFERS_VA + client * protocol::BUFFER_SIZE;
let region = syscall::create_region(PageCount::new(protocol::BUFFER_PAGES as u16)?).ok()?;
@@ -34,12 +41,30 @@ impl server::Buffers for RegionBuffers {
let buf = unsafe { core::slice::from_raw_parts_mut(va as *mut u8, protocol::BUFFER_SIZE) };
Some((buf, Some(region)))
}
fn reclaim(&mut self, buffer: CapSlot) {
syscall::revoke(buffer);
}
/// Call it and pass it on; never Read, which would let the holder
/// take other clients' requests.
fn mint(&mut self, badge: usize) -> Option<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() -> ! {
libkos::println!("storage: started");
let ep = CapSlot::new(0).unwrap();
let ep = EP;
// Init hands over the disk before asking for anything.
let Some(device) = libkos::device::receive_device(ep)
.mmio
@@ -52,6 +77,6 @@ pub fn run() -> ! {
libkos::println!("storage: no ext4 filesystem it can read, exiting");
syscall::exit();
};
let mut srv = server::StorageServer::new(ext4, RegionBuffers);
let mut srv = server::StorageServer::new(ext4, Kernel);
server_framework::serve(&mut srv, ep)
}

View File

@@ -25,6 +25,13 @@ pub enum StorageRequest {
Mkdir { path_len: usize },
/// Carries the endpoint to print to.
SetLog,
/// A capability to the path (`path_len` bytes of the buffer), inside
/// the caller's scope.
Grant {
path_len: usize,
write: bool,
},
Release,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -43,6 +50,9 @@ pub enum StorageResponse {
ReadOnly,
/// The client has not attached a buffer yet.
NotAttached,
/// Carries the new capability.
Granted,
Denied,
}
fn handle(word: usize) -> Option<FileHandle> {
@@ -64,6 +74,8 @@ impl Encode for StorageRequest {
Delete { path_len } => [op::DELETE, path_len, 0, 0],
Mkdir { path_len } => [op::MKDIR, path_len, 0, 0],
SetLog => [op::SET_LOG, 0, 0, 0],
Grant { path_len, write } => [op::GRANT, path_len, usize::from(write), 0],
Release => [op::RELEASE, 0, 0, 0],
}
}
}
@@ -83,6 +95,11 @@ impl Decode for StorageRequest {
op::DELETE => Delete { path_len: w[1] },
op::MKDIR => Mkdir { path_len: w[1] },
op::SET_LOG => SetLog,
op::GRANT => Grant {
path_len: w[1],
write: w[2] != 0,
},
op::RELEASE => Release,
_ => return None,
})
}
@@ -103,6 +120,8 @@ impl Encode for StorageResponse {
Error => [reply::ERROR, 0, 0, 0],
ReadOnly => [reply::READ_ONLY, 0, 0, 0],
NotAttached => [reply::NOT_ATTACHED, 0, 0, 0],
Granted => [reply::GRANTED, 0, 0, 0],
Denied => [reply::DENIED, 0, 0, 0],
}
}
}
@@ -122,6 +141,8 @@ impl Decode for StorageResponse {
reply::ERROR => Error,
reply::READ_ONLY => ReadOnly,
reply::NOT_ATTACHED => NotAttached,
reply::GRANTED => Granted,
reply::DENIED => Denied,
_ => return None,
})
}

View 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,
})
}
}

View File

@@ -1,10 +1,11 @@
use libkos::syscall::Give;
use server_framework::{Decode, Encode, MALFORMED, Server};
use types::bounded::CapSlot;
use crate::filesystem::{DirEntry, FileHandle, FileSystem};
use crate::protocol::{
DIR_ENTRY_SIZE, DIR_NAME_SIZE, MAX_PATH, StorageRequest, StorageResponse,
};
use crate::grants::Grants;
use crate::protocol::{DIR_ENTRY_SIZE, DIR_NAME_SIZE, MAX_PATH, StorageRequest, StorageResponse};
use crate::scope::Scope;
const MAX_CLIENTS: usize = 8;
/// Handles at or past this are never valid (the file systems hand out
@@ -12,35 +13,51 @@ const MAX_CLIENTS: usize = 8;
const MAX_HANDLES: usize = 16;
const MAX_LISTED: usize = 64;
/// Where client buffers come from: a fresh region per client in the
/// real server, plain memory in tests.
pub trait Buffers {
/// What the server needs from the kernel: regions for client buffers
/// and badged copies of its endpoint for grants. Syscalls in the real
/// server, plain memory in tests.
pub trait Host {
/// A buffer for a new client and the capability that lets the client
/// map it, if there is one to send.
fn create(&mut self, client: usize) -> Option<(&'static mut [u8], Option<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 {
badge: usize,
/// Who the buffer belongs to now; `None` once its grant is released.
badge: Option<usize>,
buf: &'static mut [u8],
/// What maps `buf`, sent again when the same client attaches again.
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,
buffers: B,
host: H,
grants: Grants,
clients: [Option<Client>; MAX_CLIENTS],
/// The badge of the client that opened each handle.
owners: [Option<usize>; MAX_HANDLES],
owners: [Option<Owner>; MAX_HANDLES],
log: Option<CapSlot>,
}
impl<F: FileSystem, B: Buffers> StorageServer<F, B> {
pub fn new(fs: F, buffers: B) -> Self {
impl<F: FileSystem, H: Host> StorageServer<F, H> {
pub fn new(fs: F, host: H) -> Self {
Self {
fs,
buffers,
host,
grants: Grants::new(),
clients: [const { None }; MAX_CLIENTS],
owners: [None; MAX_HANDLES],
log: None,
@@ -70,70 +87,176 @@ impl<F: FileSystem, B: Buffers> StorageServer<F, B> {
&self.fs
}
#[cfg(test)]
pub fn host(&self) -> &H {
&self.host
}
#[cfg(test)]
pub fn client_buffer_for_test(&mut self, badge: usize) -> Option<&mut [u8]> {
self.client(badge).map(|c| &mut *c.buf)
}
fn client(&mut self, badge: usize) -> Option<&mut Client> {
self.clients.iter_mut().flatten().find(|c| c.badge == badge)
self.clients
.iter_mut()
.flatten()
.find(|c| c.badge == Some(badge))
}
/// A client attaching again gets its buffer again: the programs a shell
/// runs one after another share its badge.
fn attach(&mut self, badge: usize) -> (StorageResponse, Option<CapSlot>) {
/// A client attaching again gets its buffer again; a new one gets a
/// buffer a released grant left behind, or a fresh one.
fn attach(&mut self, badge: usize) -> (StorageResponse, Option<Give>) {
if let Some(c) = self.client(badge) {
return (StorageResponse::Attached { size: c.buf.len() }, c.cap);
return (
StorageResponse::Attached { size: c.buf.len() },
c.cap.map(Give::from),
);
}
if let Some(c) = self
.clients
.iter_mut()
.flatten()
.find(|c| c.badge.is_none())
{
c.badge = Some(badge);
return (
StorageResponse::Attached { size: c.buf.len() },
c.cap.map(Give::from),
);
}
let Some(index) = self.clients.iter().position(Option::is_none) else {
return (StorageResponse::Error, None);
};
let Some((buf, cap)) = self.buffers.create(index) else {
let Some((buf, cap)) = self.host.create(index) else {
return (StorageResponse::Error, None);
};
let size = buf.len();
if let Some(slot) = self.clients.get_mut(index) {
*slot = Some(Client { badge, buf, cap });
*slot = Some(Client {
badge: Some(badge),
buf,
cap,
});
}
(StorageResponse::Attached { size }, cap)
(StorageResponse::Attached { size }, cap.map(Give::from))
}
fn owns(&self, badge: usize, handle: FileHandle) -> bool {
self.owners.get(handle.raw() as usize).copied().flatten() == Some(badge)
/// Takes `badge`'s grant back from everyone, with everything granted
/// from it: their capabilities, open files and buffers.
fn release(&mut self, badge: usize) -> StorageResponse {
if badge == 0 {
return StorageResponse::Error;
}
let Self {
fs,
host,
grants,
clients,
owners,
..
} = self;
grants.release(badge, |gone, cap| {
host.revoke(cap);
for (raw, owner) in owners.iter_mut().enumerate() {
if owner.is_some_and(|o| o.badge == gone) {
fs.close(FileHandle::new(raw as u16));
*owner = None;
}
}
for c in clients
.iter_mut()
.flatten()
.filter(|c| c.badge == Some(gone))
{
if let Some(buffer) = c.cap {
host.reclaim(buffer);
}
c.buf.fill(0);
c.badge = None;
}
});
StorageResponse::Ok
}
/// Serves one request from `badge`.
fn serve(&mut self, badge: usize, request: StorageRequest) -> (StorageResponse, Option<CapSlot>) {
fn owner(&self, handle: FileHandle) -> Option<Owner> {
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 StorageResponse as R;
if request == Attach {
return self.attach(badge);
match request {
Attach => return self.attach(badge),
Release => return (self.release(badge), None),
_ => {}
}
let handle_ok = match request {
Read { handle, .. } | Write { handle, .. } | Close { handle } | FileSize { handle } => {
self.owns(badge, handle)
let allowed = match request {
Read { handle, .. } | Close { handle } | FileSize { handle } => {
self.owner(handle).is_some_and(|o| o.badge == badge)
}
Write { handle, .. } => self
.owner(handle)
.is_some_and(|o| o.badge == badge && o.write),
Create { .. } | Delete { .. } | Mkdir { .. } => scope.write,
_ => true,
};
if !handle_ok {
return (R::Error, None);
if !allowed {
return (R::Denied, None);
}
let mutates = matches!(request, Write { .. } | Create { .. } | Delete { .. } | Mkdir { .. });
let mutates = matches!(
request,
Write { .. } | Create { .. } | Delete { .. } | Mkdir { .. }
);
if mutates && !self.fs.writable() {
return (R::ReadOnly, None);
}
// Split borrows: the client's buffer, the file system, the owners.
let Self { fs, clients, owners, .. } = self;
let Some(client) = clients.iter_mut().flatten().find(|c| c.badge == badge) else {
let Self {
fs,
host,
grants,
clients,
owners,
..
} = self;
let Some(client) = clients
.iter_mut()
.flatten()
.find(|c| c.badge == Some(badge))
else {
return (R::NotAttached, None);
};
let buf = &mut *client.buf;
let room = buf.len();
let path = |len: usize| ..len.min(MAX_PATH).min(room);
let opened = |owners: &mut [Option<usize>; MAX_HANDLES], h: Option<FileHandle>| match h {
let sent = |len: usize| ..len.min(MAX_PATH).min(room);
// The client's path, as a path on disk inside its scope.
let path_len = match request {
Open { path_len }
| Create { path_len }
| Delete { path_len }
| Mkdir { path_len }
| ListDir { path_len } => Some(path_len),
_ => None,
};
let disk = match path_len.map(|len| scope.resolve(&buf[sent(len)])) {
Some(None) => return (R::Denied, None),
resolved => resolved.flatten(),
};
let path = disk.as_ref().map_or(&[][..], |p| p.as_bytes());
let opened = |owners: &mut [Option<Owner>; MAX_HANDLES], h: Option<FileHandle>| match h {
Some(h) => match owners.get_mut(h.raw() as usize) {
Some(owner) => {
*owner = Some(badge);
*owner = Some(Owner {
badge,
write: scope.write,
});
R::Handle(h)
}
// A handle the table cannot track is one we cannot police.
@@ -142,12 +265,20 @@ impl<F: FileSystem, B: Buffers> StorageServer<F, B> {
None => R::NotFound,
};
let response = match request {
Attach | SetLog => R::Error,
Open { path_len } => {
let h = fs.open(&buf[path(path_len)]);
Attach | SetLog | Release => R::Error,
Grant { path_len, write } => {
let narrower = scope.narrow(&buf[sent(path_len)], write);
return match narrower.and_then(|s| grants.add(badge, s, |b| host.mint(b))) {
// The holder may pass it on: that is what it asked for.
Some(cap) => (R::Granted, Some(Give::keeping_delegate(cap))),
None => (R::Denied, None),
};
}
Open { .. } => {
let h = fs.open(path);
opened(owners, h)
}
Create { path_len } => match fs.create(&buf[path(path_len)]) {
Create { .. } => match fs.create(path) {
None => R::Error,
h => opened(owners, h),
},
@@ -170,15 +301,16 @@ impl<F: FileSystem, B: Buffers> StorageServer<F, B> {
Some(size) => R::Size(size),
None => R::Error,
},
ListDir { path_len } => {
let mut name = [0u8; MAX_PATH];
let p = &buf[path(path_len)];
name[..p.len()].copy_from_slice(p);
ListDir { .. } => {
let mut entries = [DirEntry::empty(); MAX_LISTED];
let room = (buf.len() / DIR_ENTRY_SIZE).min(MAX_LISTED);
match fs.list_dir(&name[..p.len()], &mut entries[..room]) {
match fs.list_dir(path, &mut entries[..room]) {
Some(count) => {
for (entry, record) in entries.iter().take(count).zip(buf.chunks_mut(DIR_ENTRY_SIZE)) {
for (entry, record) in entries
.iter()
.take(count)
.zip(buf.chunks_mut(DIR_ENTRY_SIZE))
{
write_entry(entry, record);
}
R::DirEntries { count }
@@ -186,11 +318,19 @@ impl<F: FileSystem, B: Buffers> StorageServer<F, B> {
None => R::NotFound,
}
}
Delete { path_len } => {
if fs.delete(&buf[path(path_len)]) { R::Ok } else { R::NotFound }
Delete { .. } => {
if fs.delete(path) {
R::Ok
} else {
R::NotFound
}
}
Mkdir { .. } => {
if fs.mkdir(path) {
R::Ok
} else {
R::Error
}
Mkdir { path_len } => {
if fs.mkdir(&buf[path(path_len)]) { R::Ok } else { R::Error }
}
};
(response, None)
@@ -203,17 +343,21 @@ fn write_entry(entry: &DirEntry, record: &mut [u8]) {
let n = name.len().min(DIR_NAME_SIZE);
record[..n].copy_from_slice(&name[..n]);
record[DIR_NAME_SIZE..DIR_NAME_SIZE + 8].copy_from_slice(&(entry.size as u64).to_le_bytes());
let flags = if entry.is_dir { types::storage::DIR_FLAG_DIRECTORY } else { 0 };
let flags = if entry.is_dir {
types::storage::DIR_FLAG_DIRECTORY
} else {
0
};
record[DIR_NAME_SIZE + 8..DIR_ENTRY_SIZE].copy_from_slice(&flags.to_le_bytes());
}
impl<F: FileSystem, B: Buffers> Server for StorageServer<F, B> {
impl<F: FileSystem, H: Host> Server for StorageServer<F, H> {
type Request = StorageRequest;
type Response = StorageResponse;
/// Without a badge: as the unbadged client.
fn handle(&mut self, request: StorageRequest) -> StorageResponse {
self.serve(0, request).0
self.serve(0, Scope::EVERYTHING, request).0
}
fn handle_call(
@@ -221,13 +365,17 @@ impl<F: FileSystem, B: Buffers> Server for StorageServer<F, B> {
badge: usize,
words: [usize; 4],
cap: Option<CapSlot>,
) -> ([usize; 4], Option<CapSlot>) {
) -> ([usize; 4], Option<Give>) {
match StorageRequest::decode(words) {
Some(StorageRequest::SetLog) => (self.set_log(badge, cap).encode(), None),
Some(request) => {
let (response, cap) = self.serve(badge, request);
// A badge storage never issued, or one it has taken back.
Some(request) => match self.grants.scope(badge) {
None => (StorageResponse::Denied.encode(), None),
Some(scope) => {
let (response, cap) = self.serve(badge, scope, request);
(response.encode(), cap)
}
},
None => ([MALFORMED, 0, 0, 0], None),
}
}

View File

@@ -1,4 +1,5 @@
mod ext4;
mod ext4_real;
mod protocol;
mod scope;
mod server;

View File

@@ -16,6 +16,15 @@ fn every_request_roundtrips() {
StorageRequest::Create { path_len: 5 },
StorageRequest::Delete { path_len: 5 },
StorageRequest::Mkdir { path_len: 4 },
StorageRequest::Grant {
path_len: 4,
write: true,
},
StorageRequest::Grant {
path_len: 0,
write: false,
},
StorageRequest::Release,
] {
assert_eq!(StorageRequest::decode(req.encode()), Some(req));
}
@@ -35,6 +44,8 @@ fn every_response_roundtrips() {
StorageResponse::Error,
StorageResponse::ReadOnly,
StorageResponse::NotAttached,
StorageResponse::Granted,
StorageResponse::Denied,
] {
assert_eq!(StorageResponse::decode(resp.encode()), Some(resp));
}

View 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);
}

View File

@@ -3,31 +3,59 @@ extern crate std;
use crate::adapters::mock_fs::MockFileSystem;
use crate::filesystem::FileHandle;
use crate::protocol::*;
use crate::server::{Buffers, StorageServer};
use crate::server::{Host, StorageServer};
use server_framework::{Decode, Encode, MALFORMED, Server};
use std::boxed::Box;
use std::vec;
use std::vec::Vec;
use types::bounded::CapSlot;
/// Plain memory instead of regions; the "capability" is a fixed slot.
struct TestBuffers;
/// Plain memory instead of regions; the buffer "capability" is a fixed
/// slot, a grant's is its badge past `GRANT_SLOTS`.
#[derive(Default)]
struct TestHost {
reclaimed: Vec<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>)> {
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 {
StorageServer::new(MockFileSystem::new(), TestBuffers)
StorageServer::new(MockFileSystem::new(), TestHost::default())
}
/// One request from `badge`, returning the decoded reply and any cap.
fn call(s: &mut Srv, badge: usize, req: StorageRequest) -> (StorageResponse, Option<CapSlot>) {
let (words, cap) = s.handle_call(badge, req.encode(), None);
(StorageResponse::decode(words).expect("valid reply"), cap)
(
StorageResponse::decode(words).expect("valid reply"),
cap.map(|g| g.slot),
)
}
struct Client {
@@ -38,10 +66,37 @@ impl Client {
fn attach(s: &mut Srv, badge: usize) -> Self {
let (resp, cap) = call(s, badge, StorageRequest::Attach);
assert_eq!(resp, StorageResponse::Attached { size: 4096 });
assert_eq!(cap, CapSlot::new(9), "the reply carries the buffer");
assert_eq!(cap, BUFFER, "the reply carries the buffer");
Self { badge }
}
/// The badge of a new grant to `path`, narrowed from this client's.
fn grant(&self, s: &mut Srv, path: &[u8], write: bool) -> usize {
self.put(s, path);
let (words, cap) = s.handle_call(
self.badge,
StorageRequest::Grant {
path_len: path.len(),
write,
}
.encode(),
None,
);
assert_eq!(
StorageResponse::decode(words),
Some(StorageResponse::Granted)
);
let cap = cap.expect("the reply carries the grant");
assert!(cap.keep_delegate, "a grant can be passed on");
badge_of(cap.slot)
}
/// Attaches through a new grant to `path`.
fn narrowed(&self, s: &mut Srv, path: &[u8], write: bool) -> Client {
let badge = self.grant(s, path, write);
Client::attach(s, badge)
}
fn put(&self, s: &mut Srv, bytes: &[u8]) {
s.client_buffer_for_test(self.badge).unwrap()[..bytes.len()].copy_from_slice(bytes);
}
@@ -50,13 +105,26 @@ impl Client {
s.client_buffer_for_test(self.badge).unwrap()[..len].to_vec()
}
fn create(&self, s: &mut Srv, path: &[u8]) -> FileHandle {
fn with_path(
&self,
s: &mut Srv,
path: &[u8],
req: fn(usize) -> StorageRequest,
) -> StorageResponse {
self.put(s, path);
match call(s, self.badge, StorageRequest::Create { path_len: path.len() }).0 {
call(s, self.badge, req(path.len())).0
}
fn create(&self, s: &mut Srv, path: &[u8]) -> FileHandle {
match self.with_path(s, path, |path_len| StorageRequest::Create { path_len }) {
StorageResponse::Handle(h) => h,
other => panic!("create: {other:?}"),
}
}
fn open(&self, s: &mut Srv, path: &[u8]) -> StorageResponse {
self.with_path(s, path, |path_len| StorageRequest::Open { path_len })
}
}
#[test]
@@ -69,11 +137,12 @@ fn nothing_works_before_attaching() {
#[test]
fn attaching_again_hands_back_the_same_buffer() {
let mut s = server();
let root = Client::attach(&mut s, 0);
let (first, cap) = call(&mut s, 0, StorageRequest::Attach);
assert_eq!(call(&mut s, 0, StorageRequest::Attach), (first, cap));
// Not a new client: the other seven places are still free.
for badge in 1..8 {
assert!(matches!(call(&mut s, badge, StorageRequest::Attach).0, StorageResponse::Attached { .. }));
for _ in 1..8 {
root.narrowed(&mut s, b"", true);
}
}
@@ -87,13 +156,19 @@ fn data_goes_through_the_buffer_both_ways() {
call(&mut s, 0, StorageRequest::Write { handle: h, len: 14 }).0,
StorageResponse::Written { bytes: 14 }
);
assert_eq!(call(&mut s, 0, StorageRequest::Close { handle: h }).0, StorageResponse::Ok);
assert_eq!(
call(&mut s, 0, StorageRequest::Close { handle: h }).0,
StorageResponse::Ok
);
c.put(&mut s, b"note.txt");
let StorageResponse::Handle(h) = call(&mut s, 0, StorageRequest::Open { path_len: 8 }).0 else {
panic!("open");
};
assert_eq!(call(&mut s, 0, StorageRequest::FileSize { handle: h }).0, StorageResponse::Size(14));
assert_eq!(
call(&mut s, 0, StorageRequest::FileSize { handle: h }).0,
StorageResponse::Size(14)
);
c.put(&mut s, &[0; 32]);
assert_eq!(
call(&mut s, 0, StorageRequest::Read { handle: h, len: 64 }).0,
@@ -105,8 +180,9 @@ fn data_goes_through_the_buffer_both_ways() {
#[test]
fn a_handle_works_only_for_the_client_that_opened_it() {
let mut s = server();
let a = Client::attach(&mut s, 1);
Client::attach(&mut s, 2);
let root = Client::attach(&mut s, 0);
let a = root.narrowed(&mut s, b"", true);
let b = root.narrowed(&mut s, b"", true);
let h = a.create(&mut s, b"mine");
for req in [
StorageRequest::Read { handle: h, len: 4 },
@@ -114,9 +190,16 @@ fn a_handle_works_only_for_the_client_that_opened_it() {
StorageRequest::FileSize { handle: h },
StorageRequest::Close { handle: h },
] {
assert_eq!(call(&mut s, 2, req).0, StorageResponse::Error, "{req:?}");
assert_eq!(
call(&mut s, b.badge, req).0,
StorageResponse::Denied,
"{req:?}"
);
}
assert_eq!(call(&mut s, 1, StorageRequest::FileSize { handle: h }).0, StorageResponse::Size(0));
assert_eq!(
call(&mut s, a.badge, StorageRequest::FileSize { handle: h }).0,
StorageResponse::Size(0)
);
}
#[test]
@@ -125,10 +208,22 @@ fn hostile_lengths_are_clamped_to_the_buffer() {
let c = Client::attach(&mut s, 0);
let h = c.create(&mut s, b"x");
for len in [4097, usize::MAX] {
assert!(matches!(call(&mut s, 0, StorageRequest::Write { handle: h, len }).0, StorageResponse::Written { .. }));
assert!(matches!(call(&mut s, 0, StorageRequest::Read { handle: h, len }).0, StorageResponse::Data { .. }));
assert_eq!(call(&mut s, 0, StorageRequest::Open { path_len: len }).0, StorageResponse::NotFound);
assert!(matches!(call(&mut s, 0, StorageRequest::ListDir { path_len: len }).0, StorageResponse::DirEntries { .. }));
assert!(matches!(
call(&mut s, 0, StorageRequest::Write { handle: h, len }).0,
StorageResponse::Written { .. }
));
assert!(matches!(
call(&mut s, 0, StorageRequest::Read { handle: h, len }).0,
StorageResponse::Data { .. }
));
assert_eq!(
call(&mut s, 0, StorageRequest::Open { path_len: len }).0,
StorageResponse::NotFound
);
assert!(matches!(
call(&mut s, 0, StorageRequest::ListDir { path_len: len }).0,
StorageResponse::DirEntries { .. }
));
}
}
@@ -138,24 +233,30 @@ fn handles_never_opened_are_refused() {
Client::attach(&mut s, 0);
for raw in [0, 15, 16, u16::MAX] {
let h = FileHandle::new(raw);
assert_eq!(call(&mut s, 0, StorageRequest::Read { handle: h, len: 1 }).0, StorageResponse::Error);
assert_eq!(
call(&mut s, 0, StorageRequest::Read { handle: h, len: 1 }).0,
StorageResponse::Denied
);
}
}
#[test]
fn a_read_only_disk_refuses_every_write_up_front() {
let mut s = StorageServer::new(MockFileSystem::new().read_only(), TestBuffers);
let mut s = StorageServer::new(MockFileSystem::new().read_only(), TestHost::default());
let c = Client::attach(&mut s, 0);
c.put(&mut s, b"f");
for req in [
StorageRequest::Create { path_len: 1 },
StorageRequest::Delete { path_len: 1 },
StorageRequest::Mkdir { path_len: 1 },
StorageRequest::Write { handle: FileHandle::new(0), len: 1 },
StorageRequest::Write {
handle: FileHandle::new(0),
len: 1,
},
] {
let expect = match req {
// A handle nobody opened is refused before the disk is asked.
StorageRequest::Write { .. } => StorageResponse::Error,
StorageRequest::Write { .. } => StorageResponse::Denied,
_ => StorageResponse::ReadOnly,
};
assert_eq!(call(&mut s, 0, req).0, expect, "{req:?}");
@@ -171,7 +272,10 @@ fn listings_are_records_in_the_buffer() {
call(&mut s, 0, StorageRequest::Close { handle: h });
}
c.put(&mut s, b"/");
assert_eq!(call(&mut s, 0, StorageRequest::ListDir { path_len: 1 }).0, StorageResponse::DirEntries { count: 2 });
assert_eq!(
call(&mut s, 0, StorageRequest::ListDir { path_len: 1 }).0,
StorageResponse::DirEntries { count: 2 }
);
let records = c.get(&mut s, 2 * DIR_ENTRY_SIZE);
assert_eq!(&records[..5], b"a.txt");
assert_eq!(records[5], 0);
@@ -199,3 +303,172 @@ fn only_the_unbadged_holder_sets_the_log_and_only_once() {
assert_eq!(s.log(), log);
assert_eq!(set(&mut s, 0), StorageResponse::Error);
}
#[test]
fn a_badge_storage_never_issued_is_no_authority() {
let mut s = server();
for req in [
StorageRequest::Attach,
StorageRequest::Open { path_len: 1 },
StorageRequest::Release,
] {
assert_eq!(call(&mut s, 5, req).0, StorageResponse::Denied, "{req:?}");
}
}
#[test]
fn paths_resolve_inside_the_scope() {
let mut s = server();
let root = Client::attach(&mut s, 0);
let h = root.create(&mut s, b"docs/a.txt");
call(&mut s, 0, StorageRequest::Close { handle: h });
let h = root.create(&mut s, b"secret");
call(&mut s, 0, StorageRequest::Close { handle: h });
let docs = root.narrowed(&mut s, b"docs", false);
assert!(matches!(
docs.open(&mut s, b"a.txt"),
StorageResponse::Handle(_)
));
assert!(
matches!(docs.open(&mut s, b"/a.txt"), StorageResponse::Handle(_)),
"/ is the scope's root"
);
assert_eq!(
docs.open(&mut s, b"secret"),
StorageResponse::NotFound,
"only docs/secret, which is not there"
);
for escape in [
b"../secret".as_slice(),
b"./../secret",
b"x/../../secret",
b"..",
] {
assert_eq!(
docs.open(&mut s, escape),
StorageResponse::Denied,
"{escape:?}"
);
}
}
#[test]
fn a_read_only_scope_changes_nothing() {
let mut s = server();
let root = Client::attach(&mut s, 0);
let h = root.create(&mut s, b"f");
call(&mut s, 0, StorageRequest::Close { handle: h });
let ro = root.narrowed(&mut s, b"", false);
for req in [
|path_len| StorageRequest::Create { path_len },
|path_len| StorageRequest::Delete { path_len },
|path_len| StorageRequest::Mkdir { path_len },
] {
assert_eq!(ro.with_path(&mut s, b"f", req), StorageResponse::Denied);
}
// It can open the file, but the handle cannot write.
let StorageResponse::Handle(h) = ro.open(&mut s, b"f") else {
panic!("open")
};
assert_eq!(
call(
&mut s,
ro.badge,
StorageRequest::Write { handle: h, len: 1 }
)
.0,
StorageResponse::Denied
);
assert!(matches!(
call(&mut s, ro.badge, StorageRequest::Read { handle: h, len: 1 }).0,
StorageResponse::Data { .. }
));
}
#[test]
fn a_grant_is_never_wider_than_its_holder() {
let mut s = server();
let root = Client::attach(&mut s, 0);
let docs = root.narrowed(&mut s, b"docs", false);
// Asking for write from a read-only scope yields read-only.
let sub = docs.narrowed(&mut s, b"sub", true);
assert_eq!(
sub.with_path(&mut s, b"x", |path_len| StorageRequest::Create { path_len }),
StorageResponse::Denied
);
docs.put(&mut s, b"../");
assert_eq!(
call(
&mut s,
docs.badge,
StorageRequest::Grant {
path_len: 3,
write: false
}
)
.0,
StorageResponse::Denied
);
}
#[test]
fn releasing_takes_back_the_grant_and_everything_from_it() {
let mut s = server();
let root = Client::attach(&mut s, 0);
let f = root.create(&mut s, b"f");
call(&mut s, 0, StorageRequest::Close { handle: f });
let shell = root.narrowed(&mut s, b"", true);
let program = shell.narrowed(&mut s, b"", false);
let grandchild = program.grant(&mut s, b"", false);
let sibling = root.narrowed(&mut s, b"", true);
let StorageResponse::Handle(h) = program.open(&mut s, b"f") else {
panic!("open")
};
program.put(&mut s, b"leftover");
assert_eq!(
call(&mut s, program.badge, StorageRequest::Release).0,
StorageResponse::Ok
);
for badge in [program.badge, grandchild] {
assert_eq!(
call(&mut s, badge, StorageRequest::Attach).0,
StorageResponse::Denied
);
}
assert_eq!(
call(
&mut s,
program.badge,
StorageRequest::FileSize { handle: h }
)
.0,
StorageResponse::Denied
);
// The kernel takes the capabilities back, and the buffer is unmapped.
let revoked: Vec<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
);
}

View File

@@ -148,7 +148,7 @@ impl<C: Console> Server for TerminalServer<C> {
Done
}
fn handle_call(&mut self, _badge: usize, words: [usize; 4], _cap: Option<CapSlot>) -> ([usize; 4], Option<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] {
types::terminal::READ => self.read(),
types::terminal::CLEAR => {

View File

@@ -15,6 +15,24 @@ fn optional_slots_roundtrip() {
assert!(reg_slot(slot_reg(s)) == Some(s));
}
#[kani::proof]
fn gives_roundtrip() {
let g = any_slot().map(|slot| Give {
slot,
keep_delegate: kani::any(),
});
assert!(reg_give(give_reg(g)) == Some(g));
}
/// The flag never turns one slot into another.
#[kani::proof]
fn a_give_register_names_the_slot_it_carries() {
let r: usize = kani::any();
if let Some(Some(g)) = reg_give(r) {
assert!(g.slot.value() as usize == r & !KEEP_DELEGATE);
}
}
#[kani::proof]
fn every_register_decodes_to_none_or_a_valid_slot() {
let r: usize = kani::any();

View File

@@ -27,6 +27,12 @@ pub mod op {
pub const MKDIR: usize = 9;
/// Carries the log endpoint; only from the unbadged holder (init).
pub const SET_LOG: usize = 10;
/// A capability to the path in the buffer, inside the caller's scope;
/// word 2 non-zero asks for write (only if the caller has it).
pub const GRANT: usize = 11;
/// Takes back the grant this is called through, and everything
/// granted from it, from every holder.
pub const RELEASE: usize = 12;
}
/// Reply tags (word 0).
@@ -42,4 +48,8 @@ pub mod reply {
pub const ERROR: usize = 8;
pub const READ_ONLY: usize = 9;
pub const NOT_ATTACHED: usize = 10;
/// Carries the new capability.
pub const GRANTED: usize = 11;
/// Outside the caller's scope, beyond its rights, or no grant at all.
pub const DENIED: usize = 12;
}

View File

@@ -52,6 +52,10 @@ pub const PERMS_READ_EXECUTE: usize = 2;
/// "No capability" in a capability argument or result register.
pub const NONE: usize = usize::MAX;
/// Set in a message's capability register: the receiver may pass the
/// capability on (it keeps `Delegate`). Without it the receiver may not.
pub const KEEP_DELEGATE: usize = 1 << 8;
/// What a receiving syscall got, returned in `a7`.
pub const KIND_MESSAGE: usize = 0;
pub const KIND_CALL: usize = 1;
@@ -115,6 +119,53 @@ pub fn reg_slot(reg: usize) -> Option<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 {
match perms {
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.
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,
words[0],
words[1],
words[2],
words[3],
slot_reg(cap),
give_reg(cap),
]
}

View File

@@ -1,7 +1,8 @@
The shell runs a program by asking init to load it (init alone may
create processes), then gives the program the terminal as its output
and the storage server, writes its arguments onto a page it moves
into the program, starts it and waits for it to die.
and a read-only storage grant to the current directory, writes its
arguments onto a page it moves into the program, starts it and waits
for it to die. Then it takes the grant back.
Built-ins: help, echo, clear. Anything else is a program on disk:
`name args...` runs name.elf.
Built-ins: help, echo, clear, cd, pwd. Anything else is a program on
disk: `name args...` runs name.elf.

View File

@@ -2,6 +2,6 @@ Welcome to k-os.
Every program here runs with only the capabilities it was given:
this file reached your screen because the shell handed `cat` the
terminal and the storage server, and nothing else.
terminal and read-only access to this directory, and nothing else.
Try: ls ls docs cat docs/shell.txt
Try: ls cd docs cat shell.txt cat ../readme.txt

View File

@@ -95,6 +95,17 @@ A program's `uses` endpoints arrive as badged copies with `Execute` and
calling, and `Delegate` lets the client send capabilities with its calls
and hand its access to programs it starts.
Storage is the exception: it mints every badged copy of its own endpoint,
and each badge names a *scope*, a subtree (or one file) and whether it may
write. For `uses: ["storage"]` init asks it for the whole disk, read-write.
Any holder can ask for a narrower one (`GRANT(path, write)`: inside its own
scope, writable only if it is) and gets it with `Delegate` kept, so it can
pass it on. Paths resolve inside the scope; `..` is refused. `RELEASE`,
called through a grant, takes it and everything granted from it back from
every holder (storage revokes its own copy, so the kernel deletes the
rest), closes their files and unmaps their buffers. Badge 0 (init) is the
whole disk; a badge storage did not issue, or took back, is `DENIED`.
Input reaches apps through the display server, the input server's only
client. It subscribes to the input server with a notification, drains it
when signalled, and passes events on to the app in focus: the last one to
@@ -107,10 +118,10 @@ is whatever text endpoint a program was given. `READ` (`types::terminal`)
blocks until a line is typed and returns it in chunks, the last ending in
`\n`. The desktop is the shell, given the terminal, storage and a spawn
allowance. A command that is not a built-in runs `name.elf`: the shell
spawns it, gives it call-only copies of the terminal (as its log, so its
output) and of storage, writes its arguments page, starts it and waits for
its death. The programs share the shell's storage badge; storage hands a
client that attaches again the same buffer, so they run one at a time.
spawns it, gives it a call-only copy of the terminal (as its log, so its
output) and a read-only storage grant to the shell's current directory
(`cd`, `pwd`), writes its arguments page, starts it, waits for its death
and releases the grant.
Only init holds `ProcessCreate`. Everyone else runs programs through init's
spawn service (`types::spawn`, client `libkos::spawn`), which init serves
@@ -159,8 +170,11 @@ A message is 4 words and at most one capability. The capability is
transferred only if the sender's endpoint capability has `Delegate` and the
transferred capability itself has `Delegate`; it is **copied** (the sender
keeps its own) with the same badge and permissions minus `Delegate`: the
receiver can use it but not pass it on. To let a process pass a capability
on, give it with the `Delegate` syscall and explicit rights. If the receiver's
receiver can use it but not pass it on. A sender that means the receiver
to pass it on sets `KEEP_DELEGATE` in the capability register
(`syscall_abi::Give`): the copy then keeps exactly the sender's rights,
`Delegate` included. Or it gives the capability with the `Delegate` syscall
and explicit rights. If the receiver's
table is full, or the capability is no longer valid at delivery time, the
message is delivered without it.