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:
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 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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
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 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 { path_len } => {
|
||||
if fs.mkdir(&buf[path(path_len)]) { R::Ok } else { R::Error }
|
||||
Mkdir { .. } => {
|
||||
if fs.mkdir(path) {
|
||||
R::Ok
|
||||
} else {
|
||||
R::Error
|
||||
}
|
||||
}
|
||||
};
|
||||
(response, None)
|
||||
@@ -203,17 +343,21 @@ fn write_entry(entry: &DirEntry, record: &mut [u8]) {
|
||||
let n = name.len().min(DIR_NAME_SIZE);
|
||||
record[..n].copy_from_slice(&name[..n]);
|
||||
record[DIR_NAME_SIZE..DIR_NAME_SIZE + 8].copy_from_slice(&(entry.size as u64).to_le_bytes());
|
||||
let flags = if entry.is_dir { types::storage::DIR_FLAG_DIRECTORY } else { 0 };
|
||||
let flags = if entry.is_dir {
|
||||
types::storage::DIR_FLAG_DIRECTORY
|
||||
} else {
|
||||
0
|
||||
};
|
||||
record[DIR_NAME_SIZE + 8..DIR_ENTRY_SIZE].copy_from_slice(&flags.to_le_bytes());
|
||||
}
|
||||
|
||||
impl<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);
|
||||
(response.encode(), cap)
|
||||
}
|
||||
// A badge storage never issued, or one it has taken back.
|
||||
Some(request) => match self.grants.scope(badge) {
|
||||
None => (StorageResponse::Denied.encode(), None),
|
||||
Some(scope) => {
|
||||
let (response, cap) = self.serve(badge, scope, request);
|
||||
(response.encode(), cap)
|
||||
}
|
||||
},
|
||||
None => ([MALFORMED, 0, 0, 0], None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod ext4;
|
||||
mod ext4_real;
|
||||
mod protocol;
|
||||
mod scope;
|
||||
mod server;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
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::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
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user