From 909b3f05032dfc246ff9438823f4171378e4cda6 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Tue, 27 Oct 2026 12:42:37 +0100 Subject: [PATCH] feat(storage): full file protocol over per-client shared region (badged clients, owned handles, clamped lengths, ReadOnly on unwritable disk); init loads via libkos::fs; drop boot shared buffer --- crates/adapter-riscv/src/devices/mod.rs | 28 +- crates/adapter-riscv/src/main.rs | 15 +- crates/drivers/block/virtio/src/lib.rs | 3 + crates/init/src/lib.rs | 90 +++--- crates/init/src/main.rs | 4 +- crates/libkos/src/fs.rs | 234 ++++++++++----- crates/ports/block-device/src/lib.rs | 4 + crates/server-framework/src/lib.rs | 2 +- crates/server-framework/src/server/mod.rs | 31 +- .../storage-server/src/adapters/mock_fs.rs | 12 + .../servers/storage-server/src/filesystem.rs | 5 + crates/servers/storage-server/src/fs/fat16.rs | 14 +- crates/servers/storage-server/src/lib.rs | 95 ++----- crates/servers/storage-server/src/protocol.rs | 181 ++++++------ crates/servers/storage-server/src/server.rs | 266 +++++++++++++----- .../storage-server/src/tests/protocol.rs | 94 +++---- .../storage-server/src/tests/server.rs | 211 ++++++++++---- crates/types/src/lib.rs | 1 + crates/types/src/storage.rs | 43 +++ 19 files changed, 827 insertions(+), 506 deletions(-) create mode 100644 crates/types/src/storage.rs diff --git a/crates/adapter-riscv/src/devices/mod.rs b/crates/adapter-riscv/src/devices/mod.rs index 08efee7..f8ec8ad 100644 --- a/crates/adapter-riscv/src/devices/mod.rs +++ b/crates/adapter-riscv/src/devices/mod.rs @@ -1,12 +1,13 @@ // What boot still sets up for userspace, all through the kernel's mapping -// API: a buffer init and storage share, the display's page quota, and the -// BootInfo page that tells init where its device capabilities are. Drivers -// set up their own devices with the capabilities init hands them. +// API: the display's page quota and the BootInfo page that tells init +// where its device capabilities are. Drivers set up their own devices +// with the capabilities init hands them; processes share memory through +// regions they create. use crate::hw::uart::Uart; use types::address::VirtualAddress; use types::bootinfo::{BOOT_INFO_VA, BootDevice, BootInfo, MAGIC, MAX_BOOT_DEVICES, NO_SLOT}; -use types::bounded::{CapSlot, PageCount}; +use types::bounded::CapSlot; use types::id::ProcessId; use types::mapping::PagePerms; @@ -17,25 +18,6 @@ fn kern() -> &'static mut kernel::Kernel { unsafe { &mut *crate::kernel_ptr() } } -fn pages(uart: &Uart, n: usize) -> PageCount { - u16::try_from(n) - .ok() - .and_then(PageCount::new) - .unwrap_or_else(|| crate::halt(uart, "bad page count")) -} - -/// Shares a fresh region between `owner` and `other` at the same `va`. -pub(crate) fn shared_buffer(uart: &Uart, owner: ProcessId, other: ProcessId, va: usize, n: usize) { - let id = kern() - .create_pool_region(owner, pages(uart, n)) - .unwrap_or_else(|_| crate::halt(uart, "shared region failed")); - for pid in [owner, other] { - kern() - .map_region(id, pid, VirtualAddress::from_raw(va), PagePerms::ReadWrite) - .unwrap_or_else(|_| crate::halt(uart, "shared map failed")); - } -} - /// Lets the display server hold a framebuffer. pub(crate) fn display_quota(uart: &Uart, display: ProcessId) { kern() diff --git a/crates/adapter-riscv/src/main.rs b/crates/adapter-riscv/src/main.rs index bf7af7b..ec2187a 100644 --- a/crates/adapter-riscv/src/main.rs +++ b/crates/adapter-riscv/src/main.rs @@ -106,9 +106,6 @@ boot_page_table: "# ); -const SHARED_BUF_VA: usize = 0x30000; -const SHARED_BUF_PAGES: usize = 16; - #[unsafe(no_mangle)] extern "C" fn kmain(hart: usize) -> ! { let uart = Uart::new(); @@ -121,10 +118,7 @@ extern "C" fn kmain(hart: usize) -> ! { let mut blk = boot::hardware::probe_block_device(&uart).unwrap_or_else(|| halt(&uart, "no block device")); - let (init, init_thread) = boot::loader::load_init(&uart, &mut blk); - if let Some(ctx) = kern_mut().thread_context_mut(init_thread) { - ctx.registers_mut()[10] = SHARED_BUF_VA; - } + let (init, _) = boot::loader::load_init(&uart, &mut blk); let storage = boot::loader::load_server(&uart, &mut blk, b"storage.elf", "storage"); let display = boot::loader::load_server(&uart, &mut blk, b"display.elf", "display"); let input = boot::loader::load_server(&uart, &mut blk, b"input.elf", "input"); @@ -133,13 +127,6 @@ extern "C" fn kmain(hart: usize) -> ! { kern_mut() .register_board(init.pid, &board) .unwrap_or_else(|_| halt(&uart, "board registration failed")); - devices::shared_buffer( - &uart, - init.pid, - storage.pid, - SHARED_BUF_VA, - SHARED_BUF_PAGES, - ); devices::display_quota(&uart, display.pid); boot::caps::grant_all(&uart, init.pid, storage.pid, display.pid, input.pid); // After grant_all, so the slots init already relies on stay put. diff --git a/crates/drivers/block/virtio/src/lib.rs b/crates/drivers/block/virtio/src/lib.rs index af0ad72..6f03388 100644 --- a/crates/drivers/block/virtio/src/lib.rs +++ b/crates/drivers/block/virtio/src/lib.rs @@ -76,4 +76,7 @@ impl BlockDevice for VirtioBlockDevice { } fn write_sector(&mut self, _sector: SectorId, _buf: &[u8]) -> bool { false } + + /// Writing is not implemented yet. + fn writable(&self) -> bool { false } } diff --git a/crates/init/src/lib.rs b/crates/init/src/lib.rs index 2810887..d0137a6 100644 --- a/crates/init/src/lib.rs +++ b/crates/init/src/lib.rs @@ -7,26 +7,33 @@ mod devices; use types::address::{EntryPoint, StackPointer, VirtualAddress}; use types::bounded::{CapSlot, PageCount}; use types::permission::{Permission, PermissionSet}; +use libkos::fs::Storage; use libkos::syscall; const PAGE_SIZE: usize = 4096; const USER_CODE_BASE: usize = 0x10000; const USER_STACK_TOP: StackPointer = StackPointer::from_raw(USER_CODE_BASE); const USER_STACK_PAGES: usize = 4; -const CMD_READ_FILE: usize = 1; +/// Where a program file is read to before it is loaded (16 pages: the +/// largest program init will load). +const SCRATCH_VA: usize = 0x30000; +const SCRATCH_PAGES: u16 = 16; +/// Where the buffer storage shares with init is mapped. +const STORAGE_BUF_VA: usize = 0x0180_0000; const CREATE_CAP: CapSlot = CapSlot::const_new(0); const STORAGE_EP: CapSlot = CapSlot::const_new(2); const DISPLAY_EP: CapSlot = CapSlot::const_new(3); const INPUT_EP: CapSlot = CapSlot::const_new(5); -fn request_file(shared_buf: *mut u8, filename: &[u8]) -> usize { - unsafe { - for (i, &b) in filename.iter().enumerate() { - core::ptr::write_volatile(shared_buf.add(i), b); - } - } - syscall::request(STORAGE_EP, [CMD_READ_FILE, filename.len(), 0, 0]).map_or(0, |r| r[0]) +/// Reads all of `filename` into `scratch`; its size, or `None` if it is +/// missing, unreadable or larger than `scratch`. +fn read_file(storage: &mut Storage, scratch: &mut [u8], filename: &[u8]) -> Option { + let handle = storage.open(filename).ok()?; + let size = storage.file_size(handle).ok().filter(|s| *s <= scratch.len()); + let read = size.and_then(|s| storage.read(handle, &mut scratch[..s]).ok()); + let _ = storage.close(handle); + read.filter(|r| Some(*r) == size) } struct LoadedProcess { @@ -34,13 +41,9 @@ struct LoadedProcess { entry_point: EntryPoint, } -fn load_binary(shared_buf: *mut u8, filename: &[u8]) -> Option { - let file_size = request_file(shared_buf, filename); - if file_size == 0 { - return None; - } - - let elf_data = unsafe { core::slice::from_raw_parts(shared_buf as *const u8, file_size) }; +fn load_binary(storage: &mut Storage, scratch: &mut [u8], filename: &[u8]) -> Option { + let file_size = read_file(storage, scratch, filename)?; + let elf_data = &scratch[..file_size]; let elf_info = match elf::parse(elf_data) { Ok(info) => info, Err(_) => return None, @@ -63,16 +66,15 @@ fn load_binary(shared_buf: *mut u8, filename: &[u8]) -> Option { } let copy_len = segment.file_size.min(segment.mem_size); - if copy_len > 0 { - let src = segment.file_offset; - if src + copy_len <= file_size { - unsafe { - core::ptr::copy_nonoverlapping( - shared_buf.add(src), - local_ptr.add(offset_in_page), - copy_len, - ); - } + let src = segment + .file_offset + .checked_add(copy_len) + .and_then(|end| elf_data.get(segment.file_offset..end)); + if let Some(src) = src.filter(|_| copy_len > 0) { + // SAFETY: the pages just requested hold seg_pages * PAGE_SIZE + // bytes, which covers offset_in_page + copy_len. + unsafe { + core::ptr::copy_nonoverlapping(src.as_ptr(), local_ptr.add(offset_in_page), copy_len); } } @@ -107,9 +109,7 @@ const SERVERS: &[ServerInfo] = &[ ServerInfo { filename: b"session.elf", name: "session" }, ]; -pub fn run(shared_buf_va: usize) -> ! { - let shared_buf = shared_buf_va as *mut u8; - +pub fn run() -> ! { libkos::println!("init: started (pid={:#x})", syscall::get_process_id().raw()); // Drivers wait for their device before serving, so this comes first: @@ -124,6 +124,15 @@ pub fn run(shared_buf_va: usize) -> ! { libkos::println!("init: {}: {}", name, if given { "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"); + idle(); + }; + let Some(scratch) = scratch() else { + libkos::println!("init: no memory to load programs into"); + idle(); + }; + let ipc_perms = PermissionSet::empty() .with(Permission::Read) .with(Permission::Execute); @@ -131,7 +140,7 @@ pub fn run(shared_buf_va: usize) -> ! { for server in SERVERS { libkos::print!("init: loading {}... ", server.name); - let loaded = match load_binary(shared_buf, server.filename) { + let loaded = match load_binary(&mut storage, scratch, server.filename) { Some(l) => l, None => { libkos::println!("SKIP"); @@ -148,7 +157,7 @@ pub fn run(shared_buf_va: usize) -> ! { } libkos::print!("init: loading snake... "); - if let Some(snake) = load_binary(shared_buf, b"snake.elf") { + if let Some(snake) = load_binary(&mut storage, scratch, b"snake.elf") { // A client only calls: Execute, never Read, so it cannot pose as // the server and take other clients' requests. let client = PermissionSet::empty().with(Permission::Execute); @@ -162,8 +171,25 @@ pub fn run(shared_buf_va: usize) -> ! { libkos::println!("init: all loaded, supervising"); - // Nothing to supervise yet: block on a notification nobody signals - // rather than spin a core on Yield. + idle(); +} + +/// A region of init's own to read program files into. +fn scratch() -> Option<&'static mut [u8]> { + let region = syscall::create_region(PageCount::new(SCRATCH_PAGES)?).ok()?; + syscall::map_region( + region, + VirtualAddress::from_raw(SCRATCH_VA), + types::mapping::PagePerms::ReadWrite, + ) + .ok()?; + // SAFETY: the region is mapped read-write at SCRATCH_VA for good. + Some(unsafe { core::slice::from_raw_parts_mut(SCRATCH_VA as *mut u8, SCRATCH_PAGES as usize * PAGE_SIZE) }) +} + +/// Nothing to supervise yet: block on a notification nobody signals +/// rather than spin a core on Yield. +fn idle() -> ! { let idle = syscall::create_notification(); loop { match idle { diff --git a/crates/init/src/main.rs b/crates/init/src/main.rs index df64c5e..4c30f0b 100644 --- a/crates/init/src/main.rs +++ b/crates/init/src/main.rs @@ -4,6 +4,6 @@ extern crate libkos; #[unsafe(no_mangle)] -extern "C" fn main(shared_buf: usize) { - init::run(shared_buf); +extern "C" fn main() { + init::run(); } diff --git a/crates/libkos/src/fs.rs b/crates/libkos/src/fs.rs index fbfa0f2..6563659 100644 --- a/crates/libkos/src/fs.rs +++ b/crates/libkos/src/fs.rs @@ -1,5 +1,12 @@ +//! The storage client (protocol: `storage_server::protocol`, wire numbers: +//! `types::storage`). Attach once; paths and data then go through the +//! buffer the server shares with this process. + use crate::syscall; +use types::address::VirtualAddress; use types::bounded::CapSlot; +use types::mapping::PagePerms; +use types::storage::{DIR_ENTRY_SIZE, DIR_FLAG_DIRECTORY, DIR_NAME_SIZE, MAX_PATH, op, reply}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct FileHandle(u16); @@ -10,78 +17,167 @@ impl FileHandle { } } -fn pack_path(path: &[u8]) -> (usize, usize) { - let mut w0 = [0u8; 8]; - let mut w1 = [0u8; 8]; - for (i, &b) in path.iter().enumerate() { - if i < 8 { - w0[i] = b; - } else if i < 16 { - w1[i - 8] = b; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FsError { + NotFound, + /// The disk cannot be written. + ReadOnly, + /// A path longer than the server accepts. + PathTooLong, + /// The server refused, or the call itself failed. + Failed, +} + +pub type Result = core::result::Result; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DirEntry { + pub name: [u8; DIR_NAME_SIZE], + pub name_len: usize, + pub size: u64, + pub is_dir: bool, +} + +impl DirEntry { + pub fn name(&self) -> &[u8] { + &self.name[..self.name_len] + } +} + +/// A connection to the storage server. +pub struct Storage { + ep: CapSlot, + buf: &'static mut [u8], +} + +impl Storage { + /// Attaches to the server behind `ep`, mapping the buffer it shares + /// at `va` (which must be free for `BUFFER_SIZE` bytes). + pub fn attach(ep: CapSlot, va: usize) -> Result { + let reply = syscall::call(ep, [op::ATTACH, 0, 0, 0], None).map_err(|_| FsError::Failed)?; + let [tag, size, _, _] = reply.words(); + let cap = match reply { + syscall::Received::Message { cap, .. } | syscall::Received::Call { cap, .. } => cap, + syscall::Received::Signal { .. } => None, + }; + let (true, Some(region)) = (tag == reply::ATTACHED, cap) else { + return Err(FsError::Failed); + }; + syscall::map_region(region, VirtualAddress::from_raw(va), PagePerms::ReadWrite) + .map_err(|_| FsError::Failed)?; + // SAFETY: the region the server made, `size` bytes, is now mapped + // read-write at `va` and stays so while this process holds it. + let buf = unsafe { core::slice::from_raw_parts_mut(va as *mut u8, size) }; + Ok(Self { ep, buf }) + } + + fn call(&self, words: [usize; 4]) -> Result<[usize; 4]> { + let r = syscall::request(self.ep, words).map_err(|_| FsError::Failed)?; + match r[0] { + reply::NOT_FOUND => Err(FsError::NotFound), + reply::READ_ONLY => Err(FsError::ReadOnly), + reply::ERROR | reply::NOT_ATTACHED => Err(FsError::Failed), + _ => Ok(r), } } - (usize::from_le_bytes(w0), usize::from_le_bytes(w1)) -} -/// A failed call reads as an error reply (tag `usize::MAX`). -fn ipc(ep: CapSlot, w0: usize, w1: usize, w2: usize, w3: usize) -> (usize, usize, usize, usize) { - let r = syscall::request(ep, [w0, w1, w2, w3]).unwrap_or([usize::MAX, 0, 0, 0]); - (r[0], r[1], r[2], r[3]) -} + fn expect(&self, words: [usize; 4], tag: usize) -> Result { + let r = self.call(words)?; + if r[0] == tag { Ok(r[1]) } else { Err(FsError::Failed) } + } -pub fn open(ep: CapSlot, path: &[u8]) -> Option { - let (p0, p1) = pack_path(path); - let (tag, handle, _, _) = ipc(ep, 0, p0, p1, 0); - if tag == 0 { - Some(FileHandle(handle as u16)) - } else { - None + /// Puts `path` in the buffer; returns its length. + fn put_path(&mut self, path: &[u8]) -> Result { + if path.len() > MAX_PATH || path.len() > self.buf.len() { + return Err(FsError::PathTooLong); + } + self.buf[..path.len()].copy_from_slice(path); + Ok(path.len()) + } + + fn with_path(&mut self, opcode: usize, path: &[u8], tag: usize) -> Result { + let len = self.put_path(path)?; + self.expect([opcode, len, 0, 0], tag) + } + + pub fn open(&mut self, path: &[u8]) -> Result { + self.with_path(op::OPEN, path, reply::HANDLE).map(|h| FileHandle(h as u16)) + } + + pub fn create(&mut self, path: &[u8]) -> Result { + self.with_path(op::CREATE, path, reply::HANDLE).map(|h| FileHandle(h as u16)) + } + + pub fn delete(&mut self, path: &[u8]) -> Result<()> { + self.with_path(op::DELETE, path, reply::OK).map(|_| ()) + } + + pub fn mkdir(&mut self, path: &[u8]) -> Result<()> { + self.with_path(op::MKDIR, path, reply::OK).map(|_| ()) + } + + pub fn close(&mut self, handle: FileHandle) -> Result<()> { + self.expect([op::CLOSE, handle.0 as usize, 0, 0], reply::OK).map(|_| ()) + } + + pub fn file_size(&mut self, handle: FileHandle) -> Result { + self.expect([op::FILE_SIZE, handle.0 as usize, 0, 0], reply::SIZE) + } + + /// Reads from the current position into `out`, as many round trips as + /// it takes; returns how much was read (less than `out.len()` at the + /// end of the file). + pub fn read(&mut self, handle: FileHandle, out: &mut [u8]) -> Result { + let mut done = 0; + while done < out.len() { + let want = (out.len() - done).min(self.buf.len()); + let got = self.expect([op::READ, handle.0 as usize, want, 0], reply::DATA)?; + let got = got.min(want); + out[done..done + got].copy_from_slice(&self.buf[..got]); + done += got; + if got < want { + break; + } + } + Ok(done) + } + + /// Writes all of `data` at the current position; returns how much the + /// server took (less only if it ran out of room). + pub fn write(&mut self, handle: FileHandle, data: &[u8]) -> Result { + let mut done = 0; + while done < data.len() { + let chunk = (data.len() - done).min(self.buf.len()); + self.buf[..chunk].copy_from_slice(&data[done..done + chunk]); + let took = self.expect([op::WRITE, handle.0 as usize, chunk, 0], reply::WRITTEN)?; + done += took.min(chunk); + if took < chunk { + break; + } + } + Ok(done) + } + + /// Calls `f` with every entry of the directory at `path`. + pub fn list_dir(&mut self, path: &[u8], mut f: impl FnMut(&DirEntry)) -> Result { + let count = self.with_path(op::LIST_DIR, path, reply::DIR_ENTRIES)?; + let count = count.min(self.buf.len() / DIR_ENTRY_SIZE); + for record in self.buf.chunks(DIR_ENTRY_SIZE).take(count) { + let mut name = [0u8; DIR_NAME_SIZE]; + name.copy_from_slice(&record[..DIR_NAME_SIZE]); + let name_len = name.iter().position(|&b| b == 0).unwrap_or(DIR_NAME_SIZE); + let word = |at: usize| { + let mut w = [0u8; 8]; + w.copy_from_slice(&record[at..at + 8]); + u64::from_le_bytes(w) + }; + f(&DirEntry { + name, + name_len, + size: word(DIR_NAME_SIZE), + is_dir: word(DIR_NAME_SIZE + 8) & DIR_FLAG_DIRECTORY != 0, + }); + } + Ok(count) } } - -pub fn read(ep: CapSlot, handle: FileHandle, max_bytes: usize) -> Option { - let (tag, bytes_read, _, _) = ipc(ep, 1, handle.0 as usize, max_bytes, 0); - if tag == 1 { Some(bytes_read) } else { None } -} - -pub fn write(ep: CapSlot, handle: FileHandle, data_word: usize) -> Option { - let (tag, bytes_written, _, _) = ipc(ep, 2, handle.0 as usize, data_word, 0); - if tag == 2 { Some(bytes_written) } else { None } -} - -pub fn close(ep: CapSlot, handle: FileHandle) { - ipc(ep, 3, handle.0 as usize, 0, 0); -} - -pub fn list_dir(ep: CapSlot, path: &[u8]) -> Option { - let (p0, p1) = pack_path(path); - let (tag, count, _, _) = ipc(ep, 4, p0, p1, 0); - if tag == 4 { Some(count) } else { None } -} - -pub fn create(ep: CapSlot, path: &[u8]) -> Option { - let (p0, p1) = pack_path(path); - let (tag, handle, _, _) = ipc(ep, 5, p0, p1, 0); - if tag == 0 { - Some(FileHandle(handle as u16)) - } else { - None - } -} - -pub fn delete(ep: CapSlot, path: &[u8]) -> bool { - let (p0, p1) = pack_path(path); - let (tag, _, _, _) = ipc(ep, 6, p0, p1, 0); - tag == 3 -} - -pub fn file_size(ep: CapSlot, handle: FileHandle) -> Option { - let (tag, size, _, _) = ipc(ep, 7, handle.0 as usize, 0, 0); - if tag == 5 { Some(size) } else { None } -} - -pub fn mkdir(ep: CapSlot, path: &[u8]) -> bool { - let (p0, p1) = pack_path(path); - let (tag, _, _, _) = ipc(ep, 8, p0, p1, 0); - tag == 3 -} diff --git a/crates/ports/block-device/src/lib.rs b/crates/ports/block-device/src/lib.rs index fa4ae11..cf7cc6b 100644 --- a/crates/ports/block-device/src/lib.rs +++ b/crates/ports/block-device/src/lib.rs @@ -18,4 +18,8 @@ pub trait BlockDevice { fn sector_count(&self) -> usize; fn read_sector(&mut self, sector: SectorId, buf: &mut [u8]) -> bool; fn write_sector(&mut self, sector: SectorId, buf: &[u8]) -> bool; + /// Whether `write_sector` can succeed at all. + fn writable(&self) -> bool { + true + } } diff --git a/crates/server-framework/src/lib.rs b/crates/server-framework/src/lib.rs index 2603465..400a8d9 100644 --- a/crates/server-framework/src/lib.rs +++ b/crates/server-framework/src/lib.rs @@ -6,4 +6,4 @@ pub mod server; pub use codec::{Decode, Encode}; pub use executor::block_on; -pub use server::{serve, Server}; +pub use server::{MALFORMED, Server, serve}; diff --git a/crates/server-framework/src/server/mod.rs b/crates/server-framework/src/server/mod.rs index f82035f..eb93036 100644 --- a/crates/server-framework/src/server/mod.rs +++ b/crates/server-framework/src/server/mod.rs @@ -18,6 +18,22 @@ pub trait Server { /// server's device interrupt). `bits` are the signalled bits. fn signal(&mut self, _bits: usize) {} + /// Handles one message, seeing who sent it (`badge`) and any + /// capability it carried; returns the reply and a capability to send + /// back with it. The default decodes the words and calls `handle`; + /// servers that track clients or pass capabilities override it. + fn handle_call( + &mut self, + _badge: usize, + words: [usize; 4], + _cap: Option, + ) -> ([usize; 4], Option) { + match Self::Request::decode(words) { + Some(request) => (self.handle(request).encode(), None), + None => ([MALFORMED, 0, 0, 0], None), + } + } + /// Serves `endpoint` until receiving on it fails (it was destroyed or /// the capability revoked), then returns why. Every `Call` gets a /// reply, `[MALFORMED, ..]` if it does not decode; a one-way `Send` is @@ -31,17 +47,12 @@ pub trait Server { Err(e) => return e, }; next = match message { - Received::Call { words, .. } => { - let reply = match Self::Request::decode(words) { - Some(request) => self.handle(request).encode(), - None => [MALFORMED, 0, 0, 0], - }; - reply_recv(endpoint, reply, None) + Received::Call { badge, words, cap } => { + let (reply, give) = self.handle_call(badge, words, cap); + reply_recv(endpoint, reply, give) } - Received::Message { words, .. } => { - if let Some(request) = Self::Request::decode(words) { - let _ = self.handle(request); - } + Received::Message { badge, words, cap } => { + let _ = self.handle_call(badge, words, cap); receive_msg(endpoint) } Received::Signal { bits } => { diff --git a/crates/servers/storage-server/src/adapters/mock_fs.rs b/crates/servers/storage-server/src/adapters/mock_fs.rs index 088ddbe..442aa28 100644 --- a/crates/servers/storage-server/src/adapters/mock_fs.rs +++ b/crates/servers/storage-server/src/adapters/mock_fs.rs @@ -24,6 +24,7 @@ impl FileEntry { pub struct MockFileSystem { files: [Option; MAX_FILES], count: usize, + read_only: bool, } impl MockFileSystem { @@ -31,9 +32,16 @@ impl MockFileSystem { Self { files: [const { None }; MAX_FILES], count: 0, + read_only: false, } } + /// The same file system, refusing all writes. + pub fn read_only(mut self) -> Self { + self.read_only = true; + self + } + fn find_by_name(&self, path: &[u8]) -> Option { for i in 0..MAX_FILES { if let Some(entry) = &self.files[i] { @@ -47,6 +55,10 @@ impl MockFileSystem { } impl FileSystem for MockFileSystem { + fn writable(&self) -> bool { + !self.read_only + } + fn open(&mut self, path: &[u8]) -> Option { let idx = self.find_by_name(path)?; if let Some(entry) = &mut self.files[idx] { diff --git a/crates/servers/storage-server/src/filesystem.rs b/crates/servers/storage-server/src/filesystem.rs index 554f269..13b9619 100644 --- a/crates/servers/storage-server/src/filesystem.rs +++ b/crates/servers/storage-server/src/filesystem.rs @@ -74,4 +74,9 @@ pub trait FileSystem { fn delete(&mut self, path: &[u8]) -> bool; fn file_size(&mut self, handle: FileHandle) -> Option; fn mkdir(&mut self, path: &[u8]) -> bool; + /// Whether anything can be written at all; the server refuses + /// writing requests up front when not. + fn writable(&self) -> bool { + true + } } diff --git a/crates/servers/storage-server/src/fs/fat16.rs b/crates/servers/storage-server/src/fs/fat16.rs index 814697c..bbd1237 100644 --- a/crates/servers/storage-server/src/fs/fat16.rs +++ b/crates/servers/storage-server/src/fs/fat16.rs @@ -358,13 +358,17 @@ impl FileSystem for Fat16 { break; }; let sector = base + sector_in_cluster as u32; - self.device - .read_sector(SectorId::new(sector), &mut sector_buf); + if !self.device.read_sector(SectorId::new(sector), &mut sector_buf) { + break; + } let chunk = (SECTOR_SIZE - byte_in_sector).min(buf.len() - written); sector_buf[byte_in_sector..byte_in_sector + chunk] .copy_from_slice(&buf[written..written + chunk]); - self.device.write_sector(SectorId::new(sector), §or_buf); + // Bytes count as written only once they reached the disk. + if !self.device.write_sector(SectorId::new(sector), §or_buf) { + break; + } written += chunk; pos += chunk; @@ -479,6 +483,10 @@ impl FileSystem for Fat16 { fn mkdir(&mut self, _path: &[u8]) -> bool { false } + + fn writable(&self) -> bool { + self.device.writable() + } } fn to_fat_name(name: &[u8]) -> [u8; 11] { diff --git a/crates/servers/storage-server/src/lib.rs b/crates/servers/storage-server/src/lib.rs index 677822f..3a42d55 100644 --- a/crates/servers/storage-server/src/lib.rs +++ b/crates/servers/storage-server/src/lib.rs @@ -13,19 +13,33 @@ pub mod server; mod tests; use libkos::syscall; +use types::address::VirtualAddress; +use types::bounded::{CapSlot, PageCount}; +use types::mapping::PagePerms; -const CMD_READ_FILE: usize = 1; -const SHARED_BUF_VA: usize = 0x30000; -const SHARED_BUF_SIZE: usize = 64 * 1024; +/// Client buffers sit high in storage's window, clear of its heap. +const BUFFERS_VA: usize = 0x0100_0000; -fn shared_buf() -> &'static mut [u8] { - unsafe { core::slice::from_raw_parts_mut(SHARED_BUF_VA as *mut u8, SHARED_BUF_SIZE) } +/// 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; + +impl server::Buffers for RegionBuffers { + fn create(&mut self, client: usize) -> Option<(&'static mut [u8], Option)> { + let va = BUFFERS_VA + client * protocol::BUFFER_SIZE; + let region = syscall::create_region(PageCount::new(protocol::BUFFER_PAGES as u16)?).ok()?; + syscall::map_region(region, VirtualAddress::from_raw(va), PagePerms::ReadWrite).ok()?; + // SAFETY: the region is mapped read-write at `va` for as long as + // the server holds `region`, which it never drops. + let buf = unsafe { core::slice::from_raw_parts_mut(va as *mut u8, protocol::BUFFER_SIZE) }; + Some((buf, Some(region))) + } } pub fn run() -> ! { libkos::println!("storage: started"); - let ep = types::bounded::CapSlot::new(0).unwrap(); + let ep = CapSlot::new(0).unwrap(); // Init hands over the disk before asking for anything. let Some(device) = libkos::device::receive_device(ep) .mmio @@ -34,72 +48,11 @@ pub fn run() -> ! { libkos::println!("storage: no block device, exiting"); syscall::exit(); }; - let fat16 = fs::fat16::Fat16::mount(device); - if fat16.is_none() { + let Some(fat16) = fs::fat16::Fat16::mount(device) else { libkos::println!("storage: FAT16 mount failed, exiting"); syscall::exit(); - } - let mut fat16 = fat16.unwrap(); - libkos::println!("storage: FAT16 mounted"); - - - let mut next = syscall::receive_msg(ep); - loop { - let message = match next { - Ok(m) => m, - Err(e) => { - libkos::println!("storage: endpoint unusable ({:?}), exiting", e); - syscall::exit(); - } - }; - let [w0, w1, _, _] = message.words(); - let reply = match w0 { - CMD_READ_FILE => { - let filename_len = w1; - let buf = shared_buf(); - let filename_len = filename_len.min(buf.len()); - - let mut name_buf = [0u8; 32]; - let name_len = filename_len.min(32); - name_buf[..name_len].copy_from_slice(&buf[..name_len]); - - let name = core::str::from_utf8(&name_buf[..name_len]).unwrap_or("?"); - libkos::print!("storage: read {}...", name); - - let file_size = read_entire_file(&mut fat16, &name_buf[..name_len], buf); - - if file_size > 0 { - libkos::println!("ok"); - } else { - libkos::println!("NOT FOUND"); - } - - [file_size, 0, 0, 0] - } - _ => [0, 0, 0, 0], - }; - next = match message { - syscall::Received::Call { .. } => syscall::reply_recv(ep, reply, None), - _ => syscall::receive_msg(ep), - }; - } -} - -fn read_entire_file( - fat: &mut fs::fat16::Fat16, - filename: &[u8], - out: &mut [u8], -) -> usize { - use filesystem::FileSystem; - - let handle = match fat.open(filename) { - Some(h) => h, - None => return 0, }; - - let size = fat.file_size(handle).unwrap_or(0); - let to_read = size.min(out.len()); - let read = fat.read(handle, &mut out[..to_read]).unwrap_or(0); - fat.close(handle); - read + libkos::println!("storage: FAT16 mounted"); + let mut srv = server::StorageServer::new(fat16, RegionBuffers); + server_framework::serve(&mut srv, ep) } diff --git a/crates/servers/storage-server/src/protocol.rs b/crates/servers/storage-server/src/protocol.rs index 8b87c49..87f0f2a 100644 --- a/crates/servers/storage-server/src/protocol.rs +++ b/crates/servers/storage-server/src/protocol.rs @@ -1,126 +1,129 @@ +//! The storage protocol. A client first `Attach`es: the server makes it a +//! buffer region and replies with a capability to it, which the client +//! maps. Paths, file data and directory listings then travel through that +//! buffer; the message words carry only lengths and handles, and the +//! server clamps every length to the buffer. Clients are told apart by +//! their endpoint badge, and a handle only works for the client that +//! opened it. + use server_framework::{Decode, Encode}; use crate::filesystem::FileHandle; +pub use types::storage::{BUFFER_PAGES, BUFFER_SIZE, DIR_ENTRY_SIZE, DIR_NAME_SIZE, MAX_PATH}; +use types::storage::{op, reply}; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct PackedPath { - word0: usize, - word1: usize, -} - -impl PackedPath { - pub fn from_bytes(src: &[u8]) -> Self { - let mut w0 = [0u8; 8]; - let mut w1 = [0u8; 8]; - for (i, &b) in src.iter().enumerate() { - if i < 8 { w0[i] = b; } - else if i < 16 { w1[i - 8] = b; } - } - Self { - word0: usize::from_le_bytes(w0), - word1: usize::from_le_bytes(w1), - } - } - - pub fn from_words(word0: usize, word1: usize) -> Self { - Self { word0, word1 } - } - - pub fn to_bytes(&self) -> [u8; 16] { - let mut buf = [0u8; 16]; - buf[..8].copy_from_slice(&self.word0.to_le_bytes()); - buf[8..16].copy_from_slice(&self.word1.to_le_bytes()); - buf - } - - pub fn word0(&self) -> usize { self.word0 } - pub fn word1(&self) -> usize { self.word1 } -} - -#[derive(Debug, PartialEq)] pub enum StorageRequest { - Open { path: PackedPath }, - Read { handle: FileHandle, max_bytes: usize }, - Write { handle: FileHandle, data_word: usize }, + Attach, + /// The path is the first `path_len` bytes of the buffer. + Open { path_len: usize }, + /// Reads up to `len` bytes into the buffer. + Read { handle: FileHandle, len: usize }, + /// Writes the first `len` bytes of the buffer. + Write { handle: FileHandle, len: usize }, Close { handle: FileHandle }, - ListDir { path: PackedPath }, - Create { path: PackedPath }, - Delete { path: PackedPath }, FileSize { handle: FileHandle }, - Mkdir { path: PackedPath }, + /// Fills the buffer with `DIR_ENTRY_SIZE` records. + ListDir { path_len: usize }, + Create { path_len: usize }, + Delete { path_len: usize }, + Mkdir { path_len: usize }, } -#[derive(Debug, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum StorageResponse { + /// Carries the buffer capability; `size` bytes are usable. + Attached { size: usize }, Handle(FileHandle), - Data { bytes_read: usize }, - Written { bytes_written: usize }, + Data { bytes: usize }, + Written { bytes: usize }, Ok, DirEntries { count: usize }, Size(usize), NotFound, Error, + /// The disk cannot be written. + ReadOnly, + /// The client has not attached a buffer yet. + NotAttached, +} + +fn handle(word: usize) -> Option { + u16::try_from(word).ok().map(FileHandle::new) } impl Encode for StorageRequest { fn encode(&self) -> [usize; 4] { - match self { - StorageRequest::Open { path } => [0, path.word0(), path.word1(), 0], - StorageRequest::Read { handle, max_bytes } => [1, handle.raw() as usize, *max_bytes, 0], - StorageRequest::Write { handle, data_word } => [2, handle.raw() as usize, *data_word, 0], - StorageRequest::Close { handle } => [3, handle.raw() as usize, 0, 0], - StorageRequest::ListDir { path } => [4, path.word0(), path.word1(), 0], - StorageRequest::Create { path } => [5, path.word0(), path.word1(), 0], - StorageRequest::Delete { path } => [6, path.word0(), path.word1(), 0], - StorageRequest::FileSize { handle } => [7, handle.raw() as usize, 0, 0], - StorageRequest::Mkdir { path } => [8, path.word0(), path.word1(), 0], + use StorageRequest::*; + match *self { + Attach => [op::ATTACH, 0, 0, 0], + Open { path_len } => [op::OPEN, path_len, 0, 0], + Read { handle, len } => [op::READ, handle.raw() as usize, len, 0], + Write { handle, len } => [op::WRITE, handle.raw() as usize, len, 0], + Close { handle } => [op::CLOSE, handle.raw() as usize, 0, 0], + FileSize { handle } => [op::FILE_SIZE, handle.raw() as usize, 0, 0], + ListDir { path_len } => [op::LIST_DIR, path_len, 0, 0], + Create { path_len } => [op::CREATE, path_len, 0, 0], + Delete { path_len } => [op::DELETE, path_len, 0, 0], + Mkdir { path_len } => [op::MKDIR, path_len, 0, 0], } } } impl Decode for StorageRequest { - fn decode(words: [usize; 4]) -> Option { - match words[0] { - 0 => Some(StorageRequest::Open { path: PackedPath::from_words(words[1], words[2]) }), - 1 => Some(StorageRequest::Read { handle: FileHandle::new(words[1] as u16), max_bytes: words[2] }), - 2 => Some(StorageRequest::Write { handle: FileHandle::new(words[1] as u16), data_word: words[2] }), - 3 => Some(StorageRequest::Close { handle: FileHandle::new(words[1] as u16) }), - 4 => Some(StorageRequest::ListDir { path: PackedPath::from_words(words[1], words[2]) }), - 5 => Some(StorageRequest::Create { path: PackedPath::from_words(words[1], words[2]) }), - 6 => Some(StorageRequest::Delete { path: PackedPath::from_words(words[1], words[2]) }), - 7 => Some(StorageRequest::FileSize { handle: FileHandle::new(words[1] as u16) }), - 8 => Some(StorageRequest::Mkdir { path: PackedPath::from_words(words[1], words[2]) }), - _ => None, - } + fn decode(w: [usize; 4]) -> Option { + use StorageRequest::*; + Some(match w[0] { + op::ATTACH => Attach, + op::OPEN => Open { path_len: w[1] }, + op::READ => Read { handle: handle(w[1])?, len: w[2] }, + op::WRITE => Write { handle: handle(w[1])?, len: w[2] }, + op::CLOSE => Close { handle: handle(w[1])? }, + op::FILE_SIZE => FileSize { handle: handle(w[1])? }, + op::LIST_DIR => ListDir { path_len: w[1] }, + op::CREATE => Create { path_len: w[1] }, + op::DELETE => Delete { path_len: w[1] }, + op::MKDIR => Mkdir { path_len: w[1] }, + _ => return None, + }) } } impl Encode for StorageResponse { fn encode(&self) -> [usize; 4] { - match self { - StorageResponse::Handle(h) => [0, h.raw() as usize, 0, 0], - StorageResponse::Data { bytes_read } => [1, *bytes_read, 0, 0], - StorageResponse::Written { bytes_written } => [2, *bytes_written, 0, 0], - StorageResponse::Ok => [3, 0, 0, 0], - StorageResponse::DirEntries { count } => [4, *count, 0, 0], - StorageResponse::Size(s) => [5, *s, 0, 0], - StorageResponse::NotFound => [6, 0, 0, 0], - StorageResponse::Error => [7, 0, 0, 0], + use StorageResponse::*; + match *self { + Attached { size } => [reply::ATTACHED, size, 0, 0], + Handle(h) => [reply::HANDLE, h.raw() as usize, 0, 0], + Data { bytes } => [reply::DATA, bytes, 0, 0], + Written { bytes } => [reply::WRITTEN, bytes, 0, 0], + Ok => [reply::OK, 0, 0, 0], + DirEntries { count } => [reply::DIR_ENTRIES, count, 0, 0], + Size(s) => [reply::SIZE, s, 0, 0], + NotFound => [reply::NOT_FOUND, 0, 0, 0], + Error => [reply::ERROR, 0, 0, 0], + ReadOnly => [reply::READ_ONLY, 0, 0, 0], + NotAttached => [reply::NOT_ATTACHED, 0, 0, 0], } } } impl Decode for StorageResponse { - fn decode(words: [usize; 4]) -> Option { - match words[0] { - 0 => Some(StorageResponse::Handle(FileHandle::new(words[1] as u16))), - 1 => Some(StorageResponse::Data { bytes_read: words[1] }), - 2 => Some(StorageResponse::Written { bytes_written: words[1] }), - 3 => Some(StorageResponse::Ok), - 4 => Some(StorageResponse::DirEntries { count: words[1] }), - 5 => Some(StorageResponse::Size(words[1])), - 6 => Some(StorageResponse::NotFound), - 7 => Some(StorageResponse::Error), - _ => None, - } + fn decode(w: [usize; 4]) -> Option { + use StorageResponse::*; + Some(match w[0] { + reply::ATTACHED => Attached { size: w[1] }, + reply::HANDLE => Handle(handle(w[1])?), + reply::DATA => Data { bytes: w[1] }, + reply::WRITTEN => Written { bytes: w[1] }, + reply::OK => Ok, + reply::DIR_ENTRIES => DirEntries { count: w[1] }, + reply::SIZE => Size(w[1]), + reply::NOT_FOUND => NotFound, + reply::ERROR => Error, + reply::READ_ONLY => ReadOnly, + reply::NOT_ATTACHED => NotAttached, + _ => return None, + }) } } diff --git a/crates/servers/storage-server/src/server.rs b/crates/servers/storage-server/src/server.rs index e838935..0ecf5dc 100644 --- a/crates/servers/storage-server/src/server.rs +++ b/crates/servers/storage-server/src/server.rs @@ -1,96 +1,208 @@ -use server_framework::Server; +use server_framework::{Decode, Encode, MALFORMED, Server}; +use types::bounded::CapSlot; -use crate::filesystem::{FileSystem, DirEntry}; -use crate::protocol::{StorageRequest, StorageResponse}; +use crate::filesystem::{DirEntry, FileHandle, FileSystem}; +use crate::protocol::{ + DIR_ENTRY_SIZE, DIR_NAME_SIZE, MAX_PATH, StorageRequest, StorageResponse, +}; -pub struct StorageServer { - fs: F, +const MAX_CLIENTS: usize = 8; +/// Handles at or past this are never valid (the file systems hand out +/// fewer), so ownership fits a small table. +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 { + /// 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)>; } -impl StorageServer { - pub fn new(fs: F) -> Self { - Self { fs } +struct Client { + badge: usize, + buf: &'static mut [u8], +} + +pub struct StorageServer { + fs: F, + buffers: B, + clients: [Option; MAX_CLIENTS], + /// The badge of the client that opened each handle. + owners: [Option; MAX_HANDLES], +} + +impl StorageServer { + pub fn new(fs: F, buffers: B) -> Self { + Self { + fs, + buffers, + clients: [const { None }; MAX_CLIENTS], + owners: [None; MAX_HANDLES], + } } pub fn filesystem(&self) -> &F { &self.fs } + + #[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) + } + + fn attach(&mut self, badge: usize) -> (StorageResponse, Option) { + if self.client(badge).is_some() { + return (StorageResponse::Error, None); + } + let Some(index) = self.clients.iter().position(Option::is_none) else { + return (StorageResponse::Error, None); + }; + let Some((buf, cap)) = self.buffers.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 }); + } + (StorageResponse::Attached { size }, cap) + } + + fn owns(&self, badge: usize, handle: FileHandle) -> bool { + self.owners.get(handle.raw() as usize).copied().flatten() == Some(badge) + } + + /// Serves one request from `badge`. + fn serve(&mut self, badge: usize, request: StorageRequest) -> (StorageResponse, Option) { + use StorageRequest::*; + use StorageResponse as R; + if request == Attach { + return self.attach(badge); + } + let handle_ok = match request { + Read { handle, .. } | Write { handle, .. } | Close { handle } | FileSize { handle } => { + self.owns(badge, handle) + } + _ => true, + }; + if !handle_ok { + return (R::Error, None); + } + 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 { + return (R::NotAttached, None); + }; + let buf = &mut *client.buf; + let room = buf.len(); + let path = |len: usize| ..len.min(MAX_PATH).min(room); + let opened = |owners: &mut [Option; MAX_HANDLES], h: Option| match h { + Some(h) => match owners.get_mut(h.raw() as usize) { + Some(owner) => { + *owner = Some(badge); + R::Handle(h) + } + // A handle the table cannot track is one we cannot police. + None => R::Error, + }, + None => R::NotFound, + }; + let response = match request { + Attach => R::Error, + Open { path_len } => { + let h = fs.open(&buf[path(path_len)]); + opened(owners, h) + } + Create { path_len } => match fs.create(&buf[path(path_len)]) { + None => R::Error, + h => opened(owners, h), + }, + Read { handle, len } => match fs.read(handle, &mut buf[..len.min(room)]) { + Some(bytes) => R::Data { bytes }, + None => R::Error, + }, + Write { handle, len } => match fs.write(handle, &buf[..len.min(room)]) { + Some(bytes) => R::Written { bytes }, + None => R::Error, + }, + Close { handle } => { + fs.close(handle); + if let Some(owner) = owners.get_mut(handle.raw() as usize) { + *owner = None; + } + R::Ok + } + FileSize { handle } => match fs.file_size(handle) { + 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); + 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]) { + Some(count) => { + for (entry, record) in entries.iter().take(count).zip(buf.chunks_mut(DIR_ENTRY_SIZE)) { + write_entry(entry, record); + } + R::DirEntries { count } + } + None => R::NotFound, + } + } + Delete { path_len } => { + if fs.delete(&buf[path(path_len)]) { R::Ok } else { R::NotFound } + } + Mkdir { path_len } => { + if fs.mkdir(&buf[path(path_len)]) { R::Ok } else { R::Error } + } + }; + (response, None) + } } -impl Server for StorageServer { +fn write_entry(entry: &DirEntry, record: &mut [u8]) { + record.fill(0); + let name = entry.name.as_bytes(); + 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 }; + record[DIR_NAME_SIZE + 8..DIR_ENTRY_SIZE].copy_from_slice(&flags.to_le_bytes()); +} + +impl Server for StorageServer { type Request = StorageRequest; type Response = StorageResponse; + /// Without a badge: as the unbadged client. fn handle(&mut self, request: StorageRequest) -> StorageResponse { - match request { - StorageRequest::Open { path } => { - let buf = path.to_bytes(); - let len = buf.iter().position(|&b| b == 0).unwrap_or(16); - match self.fs.open(&buf[..len]) { - Some(h) => StorageResponse::Handle(h), - None => StorageResponse::NotFound, - } - } - StorageRequest::Read { handle, max_bytes } => { - let mut buf = [0u8; 512]; - let read_len = max_bytes.min(buf.len()); - match self.fs.read(handle, &mut buf[..read_len]) { - Some(n) => StorageResponse::Data { bytes_read: n }, - None => StorageResponse::Error, - } - } - StorageRequest::Write { handle, data_word } => { - let data = data_word.to_le_bytes(); - match self.fs.write(handle, &data) { - Some(n) => StorageResponse::Written { bytes_written: n }, - None => StorageResponse::Error, - } - } - StorageRequest::Close { handle } => { - self.fs.close(handle); - StorageResponse::Ok - } - StorageRequest::ListDir { path } => { - let buf = path.to_bytes(); - let len = buf.iter().position(|&b| b == 0).unwrap_or(16); - let mut entries = [DirEntry::empty(); 16]; - match self.fs.list_dir(&buf[..len], &mut entries) { - Some(count) => StorageResponse::DirEntries { count }, - None => StorageResponse::NotFound, - } - } - StorageRequest::Create { path } => { - let buf = path.to_bytes(); - let len = buf.iter().position(|&b| b == 0).unwrap_or(16); - match self.fs.create(&buf[..len]) { - Some(h) => StorageResponse::Handle(h), - None => StorageResponse::Error, - } - } - StorageRequest::Delete { path } => { - let buf = path.to_bytes(); - let len = buf.iter().position(|&b| b == 0).unwrap_or(16); - if self.fs.delete(&buf[..len]) { - StorageResponse::Ok - } else { - StorageResponse::NotFound - } - } - StorageRequest::FileSize { handle } => { - match self.fs.file_size(handle) { - Some(size) => StorageResponse::Size(size), - None => StorageResponse::Error, - } - } - StorageRequest::Mkdir { path } => { - let buf = path.to_bytes(); - let len = buf.iter().position(|&b| b == 0).unwrap_or(16); - if self.fs.mkdir(&buf[..len]) { - StorageResponse::Ok - } else { - StorageResponse::Error - } + self.serve(0, request).0 + } + + fn handle_call( + &mut self, + badge: usize, + words: [usize; 4], + _cap: Option, + ) -> ([usize; 4], Option) { + match StorageRequest::decode(words) { + Some(request) => { + let (response, cap) = self.serve(badge, request); + (response.encode(), cap) } + None => ([MALFORMED, 0, 0, 0], None), } } } diff --git a/crates/servers/storage-server/src/tests/protocol.rs b/crates/servers/storage-server/src/tests/protocol.rs index 17f43f8..bb7c947 100644 --- a/crates/servers/storage-server/src/tests/protocol.rs +++ b/crates/servers/storage-server/src/tests/protocol.rs @@ -3,78 +3,58 @@ use crate::protocol::*; use server_framework::{Decode, Encode}; #[test] -fn open_roundtrip() { - let path = PackedPath::from_bytes(b"test.txt"); - let req = StorageRequest::Open { path }; - assert_eq!(StorageRequest::decode(req.encode()), Some(req)); +fn every_request_roundtrips() { + let h = FileHandle::new(3); + for req in [ + StorageRequest::Attach, + StorageRequest::Open { path_len: 9 }, + StorageRequest::Read { handle: h, len: 100 }, + StorageRequest::Write { handle: h, len: 7 }, + StorageRequest::Close { handle: h }, + StorageRequest::FileSize { handle: h }, + StorageRequest::ListDir { path_len: 1 }, + StorageRequest::Create { path_len: 5 }, + StorageRequest::Delete { path_len: 5 }, + StorageRequest::Mkdir { path_len: 4 }, + ] { + assert_eq!(StorageRequest::decode(req.encode()), Some(req)); + } } #[test] -fn read_roundtrip() { - let req = StorageRequest::Read { handle: FileHandle::new(3), max_bytes: 512 }; - assert_eq!(StorageRequest::decode(req.encode()), Some(req)); -} - -#[test] -fn write_roundtrip() { - let req = StorageRequest::Write { handle: FileHandle::new(1), data_word: 42 }; - assert_eq!(StorageRequest::decode(req.encode()), Some(req)); -} - -#[test] -fn close_roundtrip() { - let req = StorageRequest::Close { handle: FileHandle::new(5) }; - assert_eq!(StorageRequest::decode(req.encode()), Some(req)); -} - -#[test] -fn file_size_roundtrip() { - let req = StorageRequest::FileSize { handle: FileHandle::new(2) }; - assert_eq!(StorageRequest::decode(req.encode()), Some(req)); -} - -#[test] -fn handle_response_roundtrip() { - let resp = StorageResponse::Handle(FileHandle::new(7)); - assert_eq!(StorageResponse::decode(resp.encode()), Some(resp)); -} - -#[test] -fn data_response_roundtrip() { - let resp = StorageResponse::Data { bytes_read: 256 }; - assert_eq!(StorageResponse::decode(resp.encode()), Some(resp)); -} - -#[test] -fn all_response_variants() { - let variants = [ +fn every_response_roundtrips() { + for resp in [ + StorageResponse::Attached { size: BUFFER_SIZE }, + StorageResponse::Handle(FileHandle::new(2)), + StorageResponse::Data { bytes: 512 }, + StorageResponse::Written { bytes: 8 }, StorageResponse::Ok, + StorageResponse::DirEntries { count: 3 }, + StorageResponse::Size(4096), StorageResponse::NotFound, StorageResponse::Error, - StorageResponse::Size(1024), - StorageResponse::DirEntries { count: 3 }, - ]; - for resp in variants { + StorageResponse::ReadOnly, + StorageResponse::NotAttached, + ] { assert_eq!(StorageResponse::decode(resp.encode()), Some(resp)); } } #[test] -fn mkdir_roundtrip() { - let path = PackedPath::from_bytes(b"mydir"); - let req = StorageRequest::Mkdir { path }; - assert_eq!(StorageRequest::decode(req.encode()), Some(req)); +fn handles_that_do_not_fit_are_rejected_not_truncated() { + assert_eq!(StorageRequest::decode([2, 0x1_0003, 10, 0]), None); + assert_eq!(StorageRequest::decode([4, usize::MAX, 0, 0]), None); } #[test] -fn invalid_request_returns_none() { - assert!(StorageRequest::decode([99, 0, 0, 0]).is_none()); +fn unknown_opcodes_do_not_decode() { + assert_eq!(StorageRequest::decode([99, 0, 0, 0]), None); + assert_eq!(StorageRequest::decode([usize::MAX, 0, 0, 0]), None); } #[test] -fn packed_path_roundtrip() { - let path = PackedPath::from_bytes(b"hello"); - let buf = path.to_bytes(); - assert_eq!(&buf[..5], b"hello"); - assert_eq!(buf[5], 0); +fn wire_numbers_are_the_shared_ones() { + use types::storage::{op, reply}; + assert_eq!(StorageRequest::Read { handle: FileHandle::new(0), len: 0 }.encode()[0], op::READ); + assert_eq!(StorageResponse::ReadOnly.encode()[0], reply::READ_ONLY); } diff --git a/crates/servers/storage-server/src/tests/server.rs b/crates/servers/storage-server/src/tests/server.rs index 2343c23..8c7d8fc 100644 --- a/crates/servers/storage-server/src/tests/server.rs +++ b/crates/servers/storage-server/src/tests/server.rs @@ -1,87 +1,182 @@ +extern crate std; + use crate::adapters::mock_fs::MockFileSystem; +use crate::filesystem::FileHandle; use crate::protocol::*; -use crate::server::StorageServer; -use server_framework::Server; +use crate::server::{Buffers, StorageServer}; +use server_framework::{Decode, Encode, MALFORMED, Server}; +use std::boxed::Box; +use std::vec; +use types::bounded::CapSlot; -fn test_server() -> StorageServer { - StorageServer::new(MockFileSystem::new()) +/// Plain memory instead of regions; the "capability" is a fixed slot. +struct TestBuffers; + +impl Buffers for TestBuffers { + fn create(&mut self, _client: usize) -> Option<(&'static mut [u8], Option)> { + Some((Box::leak(vec![0u8; 4096].into_boxed_slice()), CapSlot::new(9))) + } } -fn path(s: &[u8]) -> PackedPath { - PackedPath::from_bytes(s) +type Srv = StorageServer; + +fn server() -> Srv { + StorageServer::new(MockFileSystem::new(), TestBuffers) +} + +/// One request from `badge`, returning the decoded reply and any cap. +fn call(s: &mut Srv, badge: usize, req: StorageRequest) -> (StorageResponse, Option) { + let (words, cap) = s.handle_call(badge, req.encode(), None); + (StorageResponse::decode(words).expect("valid reply"), cap) +} + +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, CapSlot::new(9), "the reply carries the buffer"); + Self { 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 { + s.client_buffer_for_test(self.badge).unwrap()[..len].to_vec() + } + + fn create(&self, s: &mut Srv, path: &[u8]) -> FileHandle { + self.put(s, path); + match call(s, self.badge, StorageRequest::Create { path_len: path.len() }).0 { + StorageResponse::Handle(h) => h, + other => panic!("create: {other:?}"), + } + } } #[test] -fn create_and_open() { - let mut server = test_server(); - - let resp = server.handle(StorageRequest::Create { path: path(b"test.txt") }); - assert!(matches!(resp, StorageResponse::Handle(_))); - - let resp = server.handle(StorageRequest::Open { path: path(b"test.txt") }); - assert!(matches!(resp, StorageResponse::Handle(_))); +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 open_nonexistent_returns_not_found() { - let mut server = test_server(); - let resp = server.handle(StorageRequest::Open { path: path(b"nope.txt") }); - assert_eq!(resp, StorageResponse::NotFound); +fn a_client_attaches_once() { + let mut s = server(); + Client::attach(&mut s, 0); + assert_eq!(call(&mut s, 0, StorageRequest::Attach).0, StorageResponse::Error); } #[test] -fn write_then_read() { - let mut server = test_server(); +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); - let handle = match server.handle(StorageRequest::Create { path: path(b"data") }) { - StorageResponse::Handle(h) => h, - _ => panic!("expected handle"), + c.put(&mut s, b"note.txt"); + let StorageResponse::Handle(h) = call(&mut s, 0, StorageRequest::Open { path_len: 8 }).0 else { + panic!("open"); }; - - let resp = server.handle(StorageRequest::Write { handle, data_word: 0xDEADBEEF }); - assert!(matches!(resp, StorageResponse::Written { bytes_written: 8 })); - - let resp = server.handle(StorageRequest::FileSize { handle }); - assert_eq!(resp, StorageResponse::Size(8)); + 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 delete_file() { - let mut server = test_server(); - - server.handle(StorageRequest::Create { path: path(b"del.txt") }); - let resp = server.handle(StorageRequest::Delete { path: path(b"del.txt") }); - assert_eq!(resp, StorageResponse::Ok); - - let resp = server.handle(StorageRequest::Open { path: path(b"del.txt") }); - assert_eq!(resp, StorageResponse::NotFound); +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 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, 2, req).0, StorageResponse::Error, "{req:?}"); + } + assert_eq!(call(&mut s, 1, StorageRequest::FileSize { handle: h }).0, StorageResponse::Size(0)); } #[test] -fn list_dir() { - let mut server = test_server(); - - server.handle(StorageRequest::Create { path: path(b"a.txt") }); - server.handle(StorageRequest::Create { path: path(b"b.txt") }); - - let resp = server.handle(StorageRequest::ListDir { path: path(b"/") }); - assert_eq!(resp, StorageResponse::DirEntries { count: 2 }); +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 mkdir_returns_ok() { - let mut server = test_server(); - let resp = server.handle(StorageRequest::Mkdir { path: path(b"subdir") }); - assert_eq!(resp, StorageResponse::Ok); +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::Error); + } } #[test] -fn close_succeeds() { - let mut server = test_server(); - let handle = match server.handle(StorageRequest::Create { path: path(b"c.txt") }) { - StorageResponse::Handle(h) => h, - _ => panic!("expected handle"), - }; - let resp = server.handle(StorageRequest::Close { handle }); - assert_eq!(resp, StorageResponse::Ok); +fn a_read_only_disk_refuses_every_write_up_front() { + let mut s = StorageServer::new(MockFileSystem::new().read_only(), TestBuffers); + 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::Error, + _ => 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)); } diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 175987f..304ea69 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -12,5 +12,6 @@ pub mod mapping; pub mod permission; #[cfg(kani)] pub mod proofs; +pub mod storage; pub mod syscall_abi; pub mod time; diff --git a/crates/types/src/storage.rs b/crates/types/src/storage.rs new file mode 100644 index 0000000..455e045 --- /dev/null +++ b/crates/types/src/storage.rs @@ -0,0 +1,43 @@ +//! The storage protocol's wire numbers, shared by the server +//! (`storage_server::protocol`) and the client (`libkos::fs`) so the two +//! cannot drift apart. + +/// Size of each client's buffer. +pub const BUFFER_PAGES: usize = 16; +pub const BUFFER_SIZE: usize = BUFFER_PAGES * 4096; +/// Longest path the server will look at. +pub const MAX_PATH: usize = 255; +/// One `ListDir` record: the name (NUL padded), then the size and flags +/// as little-endian u64s. Flag bit 0: a directory. +pub const DIR_ENTRY_SIZE: usize = 48; +pub const DIR_NAME_SIZE: usize = 32; +pub const DIR_FLAG_DIRECTORY: u64 = 1; + +/// Request opcodes (word 0). +pub mod op { + pub const ATTACH: usize = 0; + pub const OPEN: usize = 1; + pub const READ: usize = 2; + pub const WRITE: usize = 3; + pub const CLOSE: usize = 4; + pub const FILE_SIZE: usize = 5; + pub const LIST_DIR: usize = 6; + pub const CREATE: usize = 7; + pub const DELETE: usize = 8; + pub const MKDIR: usize = 9; +} + +/// Reply tags (word 0). +pub mod reply { + pub const ATTACHED: usize = 0; + pub const HANDLE: usize = 1; + pub const DATA: usize = 2; + pub const WRITTEN: usize = 3; + pub const OK: usize = 4; + pub const DIR_ENTRIES: usize = 5; + pub const SIZE: usize = 6; + pub const NOT_FOUND: usize = 7; + pub const ERROR: usize = 8; + pub const READ_ONLY: usize = 9; + pub const NOT_ATTACHED: usize = 10; +}