535 lines
17 KiB
Rust
535 lines
17 KiB
Rust
extern crate std;
|
|
|
|
use crate::adapters::mock_fs::MockFileSystem;
|
|
use crate::filesystem::FileHandle;
|
|
use crate::protocol::*;
|
|
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 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>,
|
|
}
|
|
|
|
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()), 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, TestHost>;
|
|
|
|
fn server() -> Srv {
|
|
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.map(|g| g.slot),
|
|
)
|
|
}
|
|
|
|
struct Client {
|
|
badge: usize,
|
|
}
|
|
|
|
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, 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);
|
|
}
|
|
|
|
fn get(&self, s: &mut Srv, len: usize) -> std::vec::Vec<u8> {
|
|
s.client_buffer_for_test(self.badge).unwrap()[..len].to_vec()
|
|
}
|
|
|
|
fn with_path(
|
|
&self,
|
|
s: &mut Srv,
|
|
path: &[u8],
|
|
req: fn(usize) -> StorageRequest,
|
|
) -> StorageResponse {
|
|
self.put(s, path);
|
|
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]
|
|
fn nothing_works_before_attaching() {
|
|
let mut s = server();
|
|
let (resp, _) = call(&mut s, 0, StorageRequest::Open { path_len: 3 });
|
|
assert_eq!(resp, StorageResponse::NotAttached);
|
|
}
|
|
|
|
#[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 _ in 1..8 {
|
|
root.narrowed(&mut s, b"", true);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn data_goes_through_the_buffer_both_ways() {
|
|
let mut s = server();
|
|
let c = Client::attach(&mut s, 0);
|
|
let h = c.create(&mut s, b"note.txt");
|
|
c.put(&mut s, b"hello, storage");
|
|
assert_eq!(
|
|
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
|
|
);
|
|
|
|
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)
|
|
);
|
|
c.put(&mut s, &[0; 32]);
|
|
assert_eq!(
|
|
call(&mut s, 0, StorageRequest::Read { handle: h, len: 64 }).0,
|
|
StorageResponse::Data { bytes: 14 }
|
|
);
|
|
assert_eq!(c.get(&mut s, 14), b"hello, storage");
|
|
}
|
|
|
|
#[test]
|
|
fn a_handle_works_only_for_the_client_that_opened_it() {
|
|
let mut s = server();
|
|
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 },
|
|
StorageRequest::Write { handle: h, len: 4 },
|
|
StorageRequest::FileSize { handle: h },
|
|
StorageRequest::Close { handle: h },
|
|
] {
|
|
assert_eq!(
|
|
call(&mut s, b.badge, req).0,
|
|
StorageResponse::Denied,
|
|
"{req:?}"
|
|
);
|
|
}
|
|
assert_eq!(
|
|
call(&mut s, a.badge, StorageRequest::FileSize { handle: h }).0,
|
|
StorageResponse::Size(0)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn hostile_lengths_are_clamped_to_the_buffer() {
|
|
let mut s = server();
|
|
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 { .. }
|
|
));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn handles_never_opened_are_refused() {
|
|
let mut s = server();
|
|
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::Denied
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_read_only_disk_refuses_every_write_up_front() {
|
|
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,
|
|
},
|
|
] {
|
|
let expect = match req {
|
|
// A handle nobody opened is refused before the disk is asked.
|
|
StorageRequest::Write { .. } => StorageResponse::Denied,
|
|
_ => StorageResponse::ReadOnly,
|
|
};
|
|
assert_eq!(call(&mut s, 0, req).0, expect, "{req:?}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn listings_are_records_in_the_buffer() {
|
|
let mut s = server();
|
|
let c = Client::attach(&mut s, 0);
|
|
for name in [b"a.txt".as_slice(), b"bee.bin"] {
|
|
let h = c.create(&mut s, name);
|
|
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 }
|
|
);
|
|
let records = c.get(&mut s, 2 * DIR_ENTRY_SIZE);
|
|
assert_eq!(&records[..5], b"a.txt");
|
|
assert_eq!(records[5], 0);
|
|
assert_eq!(&records[DIR_ENTRY_SIZE..DIR_ENTRY_SIZE + 7], b"bee.bin");
|
|
}
|
|
|
|
#[test]
|
|
fn garbage_gets_a_malformed_reply() {
|
|
let mut s = server();
|
|
let (words, cap) = s.handle_call(0, [99, 0, 0, 0], None);
|
|
assert_eq!((words[0], cap), (MALFORMED, None));
|
|
}
|
|
|
|
#[test]
|
|
fn only_the_unbadged_holder_sets_the_log_and_only_once() {
|
|
let mut s = server();
|
|
let log = CapSlot::new(7);
|
|
let set = |s: &mut Srv, badge| {
|
|
let (words, _) = s.handle_call(badge, StorageRequest::SetLog.encode(), log);
|
|
StorageResponse::decode(words).unwrap()
|
|
};
|
|
assert_eq!(set(&mut s, 5), StorageResponse::Error);
|
|
assert_eq!(s.log(), None);
|
|
assert_eq!(set(&mut s, 0), StorageResponse::Ok);
|
|
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
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn only_init_unlocks_and_only_once() {
|
|
let mut s = server();
|
|
let set = |s: &mut Srv, badge, req| call(s, badge, req).0;
|
|
let key = |half| StorageRequest::SetKey { half, words: [1, 2] };
|
|
let seed = |half| StorageRequest::SetSeed { half, words: [3, 4] };
|
|
let user = StorageRequest::SetUser { id: 7, guest: false };
|
|
assert_eq!(set(&mut s, 0, key(0)), StorageResponse::Error, "not before saying whose");
|
|
assert_eq!(set(&mut s, 5, user), StorageResponse::Error, "a client cannot say");
|
|
assert_eq!(set(&mut s, 0, user), StorageResponse::Ok);
|
|
assert_eq!(set(&mut s, 0, user), StorageResponse::Error, "once");
|
|
assert_eq!(set(&mut s, 5, key(0)), StorageResponse::Error, "a client cannot");
|
|
assert_eq!(set(&mut s, 0, key(0)), StorageResponse::Ok);
|
|
assert_eq!(set(&mut s, 0, key(0)), StorageResponse::Error, "nor can a half be replaced");
|
|
assert_eq!(set(&mut s, 0, key(2)), StorageResponse::Error, "two halves only");
|
|
assert_eq!(set(&mut s, 0, key(1)), StorageResponse::Ok);
|
|
assert_eq!(set(&mut s, 0, seed(0)), StorageResponse::Ok);
|
|
assert!(!s.unlocked(), "not without the whole seed");
|
|
assert_eq!(set(&mut s, 0, seed(1)), StorageResponse::Ok);
|
|
assert!(s.unlocked());
|
|
assert_eq!(set(&mut s, 0, key(0)), StorageResponse::Error, "once unlocked, never again");
|
|
}
|
|
|
|
#[test]
|
|
fn locking_takes_every_grant_back_and_lets_the_next_user_in() {
|
|
let mut s = server();
|
|
let set = |s: &mut Srv, badge, req| call(s, badge, req).0;
|
|
let init = Client::attach(&mut s, 0);
|
|
let shell = init.narrowed(&mut s, b"", true);
|
|
let app = shell.narrowed(&mut s, b"", false);
|
|
for req in [StorageRequest::SetUser { id: 1, guest: false }, StorageRequest::SetKey { half: 0, words: [1, 2] }, StorageRequest::SetKey { half: 1, words: [1, 2] }, StorageRequest::SetSeed { half: 0, words: [1, 2] }, StorageRequest::SetSeed { half: 1, words: [1, 2] }] {
|
|
assert_eq!(set(&mut s, 0, req), StorageResponse::Ok);
|
|
}
|
|
assert!(s.unlocked());
|
|
assert_eq!(set(&mut s, shell.badge, StorageRequest::Lock), StorageResponse::Error, "only init signs out");
|
|
assert_eq!(set(&mut s, 0, StorageRequest::Lock), StorageResponse::Ok);
|
|
assert!(!s.unlocked());
|
|
let revoked: Vec<usize> = s.host().revoked.iter().map(|c| badge_of(*c)).collect();
|
|
assert_eq!(revoked, [shell.badge, app.badge]);
|
|
assert_eq!(set(&mut s, shell.badge, StorageRequest::FileSize { handle: crate::filesystem::FileHandle::new(0) }), StorageResponse::Denied);
|
|
assert_eq!(set(&mut s, 0, StorageRequest::SetUser { id: 2, guest: true }), StorageResponse::Ok, "someone else may sign in");
|
|
}
|
|
|
|
#[test]
|
|
fn only_init_purges_only_users_and_only_while_locked() {
|
|
let mut s = server();
|
|
let init = Client::attach(&mut s, 0);
|
|
let shell = init.narrowed(&mut s, b"", true);
|
|
let purge = |s: &mut Srv, badge: usize, path: &[u8]| {
|
|
init.put(s, path);
|
|
call(s, badge, StorageRequest::Purge { path_len: path.len() }).0
|
|
};
|
|
assert_eq!(purge(&mut s, shell.badge, b"users/x"), StorageResponse::Error);
|
|
assert_eq!(purge(&mut s, 0, b"bin"), StorageResponse::Denied);
|
|
assert_eq!(purge(&mut s, 0, b"users/../bin"), StorageResponse::Denied);
|
|
assert_eq!(purge(&mut s, 0, b"users/"), StorageResponse::Denied);
|
|
assert_eq!(purge(&mut s, 0, b"users/x"), StorageResponse::Ok);
|
|
assert_eq!(purge(&mut s, 0, b"users"), StorageResponse::Ok);
|
|
}
|