From 0fd568cdad519e43f26f05bf0a880e928f5ce776 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Tue, 27 Oct 2026 17:09:35 +0100 Subject: [PATCH] feat(storage): ext4-only disk via mkfs.ext4 -d; drop FAT16; feature-gated mount (ro on unknown ro_compat); ext4 read spans blocks, delete clears inode, mkdir counts dir; virtio-blk writes; test-qemu e2fsck gate --- crates/adapter-riscv/build.rs | 5 +- crates/apps/spawntest/src/main.rs | 33 ++- crates/drivers/block/virtio/src/lib.rs | 16 +- crates/hw-virtio/src/block.rs | 34 +++ crates/init/src/launch.rs | 5 +- crates/init/src/lib.rs | 2 +- crates/init/src/manifest.rs | 1 + .../servers/storage-server/src/fs/ext4/bgd.rs | 3 + .../storage-server/src/fs/ext4/inode.rs | 23 ++ .../servers/storage-server/src/fs/ext4/mod.rs | 51 +++- .../storage-server/src/fs/ext4/superblock.rs | 25 ++ .../storage-server/src/fs/fat16/bpb.rs | 68 ----- .../storage-server/src/fs/fat16/dir.rs | 151 ---------- .../storage-server/src/fs/fat16/fat.rs | 64 ---- .../storage-server/src/fs/fat16/mod.rs | 275 ------------------ crates/servers/storage-server/src/fs/mod.rs | 1 - crates/servers/storage-server/src/lib.rs | 7 +- .../storage-server/src/tests/ext4_real.rs | 172 +++++++++++ .../servers/storage-server/src/tests/fat16.rs | 188 ------------ .../servers/storage-server/src/tests/mod.rs | 2 +- crates/types/src/spawn.rs | 2 +- docs/ipc.md | 5 +- scripts/make-disk-image.sh | 52 ++-- scripts/test-qemu.sh | 14 +- 24 files changed, 390 insertions(+), 809 deletions(-) delete mode 100644 crates/servers/storage-server/src/fs/fat16/bpb.rs delete mode 100644 crates/servers/storage-server/src/fs/fat16/dir.rs delete mode 100644 crates/servers/storage-server/src/fs/fat16/fat.rs delete mode 100644 crates/servers/storage-server/src/fs/fat16/mod.rs create mode 100644 crates/servers/storage-server/src/tests/ext4_real.rs delete mode 100644 crates/servers/storage-server/src/tests/fat16.rs diff --git a/crates/adapter-riscv/build.rs b/crates/adapter-riscv/build.rs index 83636f3..8784383 100644 --- a/crates/adapter-riscv/build.rs +++ b/crates/adapter-riscv/build.rs @@ -11,7 +11,10 @@ fn main() { let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); let bins = manifest.join("../../target/riscv64gc-unknown-none-elf/release"); let out = PathBuf::from(std::env::var("OUT_DIR").unwrap()); - for (var, bin) in [("KOS_INIT_ELF", "init"), ("KOS_STORAGE_ELF", "storage-qemu")] { + for (var, bin) in [ + ("KOS_INIT_ELF", "init"), + ("KOS_STORAGE_ELF", "storage-qemu"), + ] { let path = bins.join(bin); println!("cargo:rerun-if-changed={}", path.display()); let path = if path.is_file() { diff --git a/crates/apps/spawntest/src/main.rs b/crates/apps/spawntest/src/main.rs index fc18576..dd34022 100644 --- a/crates/apps/spawntest/src/main.rs +++ b/crates/apps/spawntest/src/main.rs @@ -12,9 +12,15 @@ use libkos::syscall; use types::bounded::CapSlot; use types::budget::Limits; -/// Where init put them (see init's manifest: spawn endpoint, then control). -const INIT: CapSlot = CapSlot::const_new(0); -const CONTROL: CapSlot = CapSlot::const_new(1); +/// Where init put them (see init's manifest: `uses`, the spawn endpoint, +/// then control). +const STORAGE: CapSlot = CapSlot::const_new(0); +const INIT: CapSlot = CapSlot::const_new(1); +const CONTROL: CapSlot = CapSlot::const_new(2); +/// Where storage's buffer is mapped. +const STORAGE_BUF_VA: usize = 0x0180_0000; +/// Read back from the disk image by scripts/test-qemu.sh. +const NOTE: &[u8] = b"written by k-os on ext4\n"; /// Over half the allowance (128 pages), so a second one fits only after /// the first has died and its limits came back. @@ -31,7 +37,28 @@ fn run_hello(died: CapSlot) -> Result<(), &'static str> { Ok(()) } +/// A file written through storage, which the host then checks with e2fsck. +fn write_a_file() -> Result<(), &'static str> { + let mut fs = libkos::fs::Storage::attach(STORAGE, STORAGE_BUF_VA).map_err(|_| "cannot attach to storage")?; + let h = fs.create(b"selftest.txt").map_err(|_| "cannot create a file")?; + if fs.write(h, NOTE) != Ok(NOTE.len()) { + return Err("short write"); + } + fs.close(h).map_err(|_| "close failed")?; + let h = fs.open(b"selftest.txt").map_err(|_| "cannot reopen the file")?; + let mut back = [0u8; 64]; + let n = fs.read(h, &mut back).map_err(|_| "read failed")?; + fs.close(h).map_err(|_| "close failed")?; + if &back[..n] != NOTE { + return Err("read back something else"); + } + Ok(()) +} + fn check() -> Result<(), &'static str> { + write_a_file()?; + libkos::println!("spawntest: a file written to ext4 reads back: ok"); + let died = syscall::create_notification().map_err(|_| "no notification")?; run_hello(died)?; libkos::println!("spawntest: spawned program runs and its death is heard: ok"); diff --git a/crates/drivers/block/virtio/src/lib.rs b/crates/drivers/block/virtio/src/lib.rs index 5d9c520..360f0b2 100644 --- a/crates/drivers/block/virtio/src/lib.rs +++ b/crates/drivers/block/virtio/src/lib.rs @@ -26,6 +26,7 @@ pub struct VirtioBlockDevice { mmio: UserMmio, queue: VirtioQueue, dma: DmaAddrs, + sectors: usize, } impl VirtioBlockDevice { @@ -52,20 +53,23 @@ impl VirtioBlockDevice { data_pa: PhysicalAddress::from_raw(hdr_pa + 32), data_va: VirtualAddress::from_raw(REQ_HDR_VA + 32), }; + let capacity = regs.read(block::CONFIG_CAPACITY) as u64 + | (regs.read(block::CONFIG_CAPACITY + 4) as u64) << 32; Some(Self { mmio: UserMmio, queue: VirtioQueue::new(QueueAddrs::from_base(VQUEUE_VA)), dma, + sectors: usize::try_from(capacity).ok()?, }) } } impl BlockDevice for VirtioBlockDevice { fn sector_size(&self) -> usize { SECTOR_SIZE } - fn sector_count(&self) -> usize { 0xFFFF } + fn sector_count(&self) -> usize { self.sectors } fn read_sector(&mut self, sector: SectorId, buf: &mut [u8]) -> bool { - if buf.len() < SECTOR_SIZE { return false; } + if buf.len() < SECTOR_SIZE || sector.raw() as usize >= self.sectors { return false; } let mut sector_buf = [0u8; SECTOR_SIZE]; if !block::read_sector(&self.mmio, &self.queue, &self.dma, sector.raw(), &mut sector_buf) { return false; @@ -74,8 +78,8 @@ impl BlockDevice for VirtioBlockDevice { true } - fn write_sector(&mut self, _sector: SectorId, _buf: &[u8]) -> bool { false } - - /// Writing is not implemented yet. - fn writable(&self) -> bool { false } + fn write_sector(&mut self, sector: SectorId, buf: &[u8]) -> bool { + if sector.raw() as usize >= self.sectors { return false; } + block::write_sector(&self.mmio, &self.queue, &self.dma, sector.raw(), buf) + } } diff --git a/crates/hw-virtio/src/block.rs b/crates/hw-virtio/src/block.rs index d8dfb3a..b9f5d1d 100644 --- a/crates/hw-virtio/src/block.rs +++ b/crates/hw-virtio/src/block.rs @@ -60,3 +60,37 @@ pub fn read_sector( true } + +/// Request types (virtio spec 5.2.6). +const T_OUT: u32 = 1; +/// Device configuration: the disk's size in 512-byte sectors (le64). +pub const CONFIG_CAPACITY: usize = 0x100; + +/// Writes one sector from `buf`; true once the device reports success. +pub fn write_sector( + mmio: &impl MmioAccess, + queue: &VirtioQueue, + dma: &DmaAddrs, + sector: u32, + buf: &[u8], +) -> bool { + if buf.len() < SECTOR_SIZE { return false; } + + unsafe { + core::ptr::copy_nonoverlapping(buf.as_ptr(), dma.data_va.raw() as *mut u8, SECTOR_SIZE); + let hdr = dma.header_va.raw() as *mut BlockReqHeader; + core::ptr::write_volatile(&raw mut (*hdr).req_type, T_OUT); + core::ptr::write_volatile(&raw mut (*hdr).reserved, 0); + core::ptr::write_volatile(&raw mut (*hdr).sector, sector as u64); + core::ptr::write_volatile(dma.status_va.raw() as *mut u8, 0xFF); + } + + // The device reads the data this time; only the status is written back. + let descs = [ + Descriptor { addr: dma.header_pa.raw() as u64, len: 16, writable: false }, + Descriptor { addr: dma.data_pa.raw() as u64, len: SECTOR_SIZE as u32, writable: false }, + Descriptor { addr: dma.status_pa.raw() as u64, len: 1, writable: true }, + ]; + if !queue.submit(mmio, &descs) { return false; } + unsafe { core::ptr::read_volatile(dma.status_va.raw() as *const u8) == 0 } +} diff --git a/crates/init/src/launch.rs b/crates/init/src/launch.rs index 4d29c5a..0d19515 100644 --- a/crates/init/src/launch.rs +++ b/crates/init/src/launch.rs @@ -44,7 +44,10 @@ impl<'a> Launcher<'a> { ) -> Result { let spawn_ep = syscall::create_endpoint().ok_or("no spawn endpoint")?; let deaths = syscall::create_notification().map_err(|_| "no death notification")?; - Ok(Self { storage, scratch, devices, served: [None; MAX_SERVED], next_badge: 1, spawn_ep, deaths, spawner: Spawner::new() }) + // Storage was started by boot, before init; programs may use it too. + let mut served = [None; MAX_SERVED]; + served[0] = Some(("storage", crate::STORAGE_EP)); + Ok(Self { storage, scratch, devices, served, next_badge: 1, spawn_ep, deaths, spawner: Spawner::new() }) } pub(crate) fn storage(&mut self) -> (&mut Storage, &mut [u8]) { diff --git a/crates/init/src/lib.rs b/crates/init/src/lib.rs index cc5e9df..e24ba2d 100644 --- a/crates/init/src/lib.rs +++ b/crates/init/src/lib.rs @@ -18,7 +18,7 @@ use types::permission::{Permission, PermissionSet}; /// Where the buffer storage shares with init is mapped. const STORAGE_BUF_VA: usize = 0x0180_0000; /// Boot loads storage before init and leaves its endpoint here. -const STORAGE_EP: CapSlot = CapSlot::const_new(2); +pub(crate) const STORAGE_EP: CapSlot = CapSlot::const_new(2); pub fn run() -> ! { libkos::start::set_console(devices::console()); diff --git a/crates/init/src/manifest.rs b/crates/init/src/manifest.rs index 23a8099..25ea3cc 100644 --- a/crates/init/src/manifest.rs +++ b/crates/init/src/manifest.rs @@ -70,6 +70,7 @@ pub const SERVERS: &[Program] = &[ /// spawns through init, then reports and halts. pub const TESTS: &[Program] = &[Program { serves: false, + uses: &["storage"], spawns: Some(Limits { pages: 128, endpoints: 2, notifications: 2, regions: 2 }), system_control: true, ..server("spawntest", b"spawntst.elf", SMALL) diff --git a/crates/servers/storage-server/src/fs/ext4/bgd.rs b/crates/servers/storage-server/src/fs/ext4/bgd.rs index 38ccbd6..b7eeabe 100644 --- a/crates/servers/storage-server/src/fs/ext4/bgd.rs +++ b/crates/servers/storage-server/src/fs/ext4/bgd.rs @@ -7,6 +7,7 @@ pub struct BlockGroupDescriptor { pub inode_table: u32, pub free_blocks_count: u16, pub free_inodes_count: u16, + pub used_dirs_count: u16, } impl BlockGroupDescriptor { @@ -21,6 +22,7 @@ impl BlockGroupDescriptor { inode_table: read_u32(&buf, off + 8), free_blocks_count: read_u16(&buf, off + 12), free_inodes_count: read_u16(&buf, off + 14), + used_dirs_count: read_u16(&buf, off + 16), }) } @@ -34,6 +36,7 @@ impl BlockGroupDescriptor { write_u32(&mut buf, off + 8, self.inode_table); write_u16(&mut buf, off + 12, self.free_blocks_count); write_u16(&mut buf, off + 14, self.free_inodes_count); + write_u16(&mut buf, off + 16, self.used_dirs_count); write_block(device, gdt_block as u64, spb, &buf) } } diff --git a/crates/servers/storage-server/src/fs/ext4/inode.rs b/crates/servers/storage-server/src/fs/ext4/inode.rs index 0429eb1..07da25b 100644 --- a/crates/servers/storage-server/src/fs/ext4/inode.rs +++ b/crates/servers/storage-server/src/fs/ext4/inode.rs @@ -110,6 +110,29 @@ impl Ext4 { io::write_block(&mut self.device, block_num, self.sb.sectors_per_block(), &block_buf) } + /// Zeroes `inode_num`'s on-disk record: how ext4 marks an inode unused, + /// so nothing still points at the blocks it had. + pub(super) fn clear_inode(&mut self, inode_num: u32) -> bool { + let Some(bgd) = BlockGroupDescriptor::read( + &mut self.device, + self.gdt_block, + self.sb.inode_block_group(inode_num), + self.sb.sectors_per_block(), + ) else { + return false; + }; + let size = self.sb.inode_size as usize; + let inode_offset = self.sb.inode_index_in_group(inode_num) as usize * size; + let block_num = (bgd.inode_table + (inode_offset / self.sb.block_size) as u32) as u64; + let at = inode_offset % self.sb.block_size; + let mut block_buf = [0u8; 4096]; + if !io::read_block(&mut self.device, block_num, self.sb.sectors_per_block(), &mut block_buf) { + return false; + } + block_buf[at..at + size].fill(0); + io::write_block(&mut self.device, block_num, self.sb.sectors_per_block(), &block_buf) + } + pub fn init_empty_inode(mode: u16) -> Inode { let mut block_data = [0u8; 60]; io::write_u16(&mut block_data, 0, EXTENT_MAGIC); diff --git a/crates/servers/storage-server/src/fs/ext4/mod.rs b/crates/servers/storage-server/src/fs/ext4/mod.rs index 3d1aca8..07917b6 100644 --- a/crates/servers/storage-server/src/fs/ext4/mod.rs +++ b/crates/servers/storage-server/src/fs/ext4/mod.rs @@ -50,9 +50,27 @@ impl Ext4 { open_files: [const { None }; MAX_OPEN_FILES], }) } + + /// One more directory in `inode`'s block group (e2fsck checks the count). + fn count_dir(&mut self, inode: u32) { + let (bg, spb) = (self.sb.inode_block_group(inode), self.sb.sectors_per_block()); + if let Some(mut d) = bgd::BlockGroupDescriptor::read(&mut self.device, self.gdt_block, bg, spb) { + d.used_dirs_count += 1; + d.write_back(&mut self.device, self.gdt_block, bg, spb); + } + } + + /// The disk, for a test to look at what was written. + pub fn into_device(self) -> B { + self.device + } } impl FileSystem for Ext4 { + fn writable(&self) -> bool { + self.sb.writable && self.device.writable() + } + fn open(&mut self, path: &[u8]) -> Option { let inode_num = self.resolve_path(path)?; let inode = self.read_inode(inode_num)?; @@ -73,13 +91,9 @@ impl FileSystem for Ext4 { let file = *self.open_files[handle.raw() as usize].as_ref()?; if file.offset >= file.size { return Some(0); } - let remaining = file.size - file.offset; - let to_read = buf.len().min(remaining); + // As much as `buf` holds, across blocks: a short read means the end. + let to_read = buf.len().min(file.size - file.offset); let bs = self.sb.block_size; - - let logical_block = (file.offset / bs) as u32; - let byte_in_block = file.offset % bs; - let inode = Inode { mode: 0, size: file.size as u64, @@ -88,17 +102,22 @@ impl FileSystem for Ext4 { blocks_count: 0, block_data: file.block_data, }; - let phys_block = self.map_logical_block(&inode, logical_block)?; - + let mut done = 0; let mut block_buf = [0u8; 4096]; - read_block(&mut self.device, phys_block, self.sb.sectors_per_block(), &mut block_buf); + while done < to_read { + let offset = file.offset + done; + let phys_block = self.map_logical_block(&inode, (offset / bs) as u32)?; + if !read_block(&mut self.device, phys_block, self.sb.sectors_per_block(), &mut block_buf) { + return None; + } + let byte_in_block = offset % bs; + let chunk = (to_read - done).min(bs - byte_in_block); + buf[done..done + chunk].copy_from_slice(&block_buf[byte_in_block..byte_in_block + chunk]); + done += chunk; + } - let available = bs - byte_in_block; - let chunk = to_read.min(available); - buf[..chunk].copy_from_slice(&block_buf[byte_in_block..byte_in_block + chunk]); - - self.open_files[handle.raw() as usize].as_mut().map(|f| f.offset += chunk); - Some(chunk) + self.open_files[handle.raw() as usize].as_mut().map(|f| f.offset += done); + Some(done) } fn write(&mut self, handle: FileHandle, buf: &[u8]) -> Option { @@ -262,6 +281,7 @@ impl FileSystem for Ext4 { if let Some(child_inode) = self.read_inode(child_inode_num) { self.free_all_blocks(&child_inode); } + self.clear_inode(child_inode_num); self.free_inode(child_inode_num); for slot in self.open_files.iter_mut() { if let Some(f) = slot { @@ -331,6 +351,7 @@ impl FileSystem for Ext4 { pi.links_count += 1; self.write_inode(parent_inode_num, &pi); } + self.count_dir(new_inode_num); true } diff --git a/crates/servers/storage-server/src/fs/ext4/superblock.rs b/crates/servers/storage-server/src/fs/ext4/superblock.rs index 95cd40c..6db55e7 100644 --- a/crates/servers/storage-server/src/fs/ext4/superblock.rs +++ b/crates/servers/storage-server/src/fs/ext4/superblock.rs @@ -3,6 +3,22 @@ use super::io::{read_u32, write_u32, read_u16, write_u16, SECTOR_SIZE}; const EXT4_MAGIC: u16 = 0xEF53; +// What this driver understands. A disk with any other incompatible feature +// is not mounted; with any other read-only-compatible one, or a journal or +// hashed directories it would not keep up to date, it is mounted read-only. +const INCOMPAT_FILETYPE: u32 = 0x2; +const INCOMPAT_EXTENTS: u32 = 0x40; +const INCOMPAT_FLEX_BG: u32 = 0x200; +const KNOWN_INCOMPAT: u32 = INCOMPAT_FILETYPE | INCOMPAT_EXTENTS | INCOMPAT_FLEX_BG; +const RO_COMPAT_SPARSE_SUPER: u32 = 0x1; +const RO_COMPAT_LARGE_FILE: u32 = 0x2; +const RO_COMPAT_DIR_NLINK: u32 = 0x20; +const RO_COMPAT_EXTRA_ISIZE: u32 = 0x40; +const KNOWN_RO_COMPAT: u32 = + RO_COMPAT_SPARSE_SUPER | RO_COMPAT_LARGE_FILE | RO_COMPAT_DIR_NLINK | RO_COMPAT_EXTRA_ISIZE; +const COMPAT_HAS_JOURNAL: u32 = 0x4; +const COMPAT_DIR_INDEX: u32 = 0x20; + pub struct Superblock { pub inodes_count: u32, pub blocks_count: u32, @@ -13,6 +29,9 @@ pub struct Superblock { pub first_data_block: u32, pub free_blocks_count: u32, pub free_inodes_count: u32, + /// False when a feature it does not maintain means writing would + /// leave the disk inconsistent. + pub writable: bool, } impl Superblock { @@ -22,7 +41,13 @@ impl Superblock { if magic != EXT4_MAGIC { return None; } let log_block_size = read_u32(data, 0x18); + let (compat, incompat, ro_compat) = (read_u32(data, 0x5C), read_u32(data, 0x60), read_u32(data, 0x64)); + if incompat & !KNOWN_INCOMPAT != 0 || log_block_size > 2 { + return None; + } + let writable = ro_compat & !KNOWN_RO_COMPAT == 0 && compat & (COMPAT_HAS_JOURNAL | COMPAT_DIR_INDEX) == 0; Some(Self { + writable, inodes_count: read_u32(data, 0x00), blocks_count: read_u32(data, 0x04), block_size: 1024 << log_block_size, diff --git a/crates/servers/storage-server/src/fs/fat16/bpb.rs b/crates/servers/storage-server/src/fs/fat16/bpb.rs deleted file mode 100644 index c685fc0..0000000 --- a/crates/servers/storage-server/src/fs/fat16/bpb.rs +++ /dev/null @@ -1,68 +0,0 @@ -//! BIOS parameter block: disk geometry. - -use super::SECTOR_SIZE; - -pub(super) struct Bpb { - pub(super) bytes_per_sector: u16, - pub(super) sectors_per_cluster: u8, - pub(super) reserved_sectors: u16, - pub(super) num_fats: u8, - pub(super) root_entry_count: u16, - pub(super) total_sectors: u16, - pub(super) sectors_per_fat: u16, -} - -impl Bpb { - /// Reads the BPB, refusing geometry the rest of this module cannot - /// work with: the disk is untrusted input like any client request. - pub(super) fn parse(sector: &[u8]) -> Option { - if sector.len() < SECTOR_SIZE { - return None; - } - if sector[510] != 0x55 || sector[511] != 0xAA { - return None; - } - let bpb = Self { - bytes_per_sector: u16::from_le_bytes([sector[11], sector[12]]), - sectors_per_cluster: sector[13], - reserved_sectors: u16::from_le_bytes([sector[14], sector[15]]), - num_fats: sector[16], - root_entry_count: u16::from_le_bytes([sector[17], sector[18]]), - total_sectors: u16::from_le_bytes([sector[19], sector[20]]), - sectors_per_fat: u16::from_le_bytes([sector[22], sector[23]]), - }; - let usable = bpb.bytes_per_sector as usize == SECTOR_SIZE - && bpb.sectors_per_cluster.is_power_of_two() - && bpb.num_fats > 0 - && bpb.sectors_per_fat > 0; - usable.then_some(bpb) - } - - pub(super) fn fat_start(&self) -> u32 { - self.reserved_sectors as u32 - } - - pub(super) fn root_dir_start(&self) -> u32 { - self.fat_start() + (self.num_fats as u32) * (self.sectors_per_fat as u32) - } - - pub(super) fn root_dir_sectors(&self) -> u32 { - ((self.root_entry_count as u32) * 32 + (self.bytes_per_sector as u32) - 1) - / (self.bytes_per_sector as u32) - } - - pub(super) fn data_start(&self) -> u32 { - self.root_dir_start() + self.root_dir_sectors() - } - - /// The first sector of data cluster `cluster`. Clusters 0 and 1 are - /// reserved and hold no data. - pub(super) fn cluster_to_sector(&self, cluster: u16) -> Option { - let index = (cluster as u32).checked_sub(2)?; - Some(self.data_start() + index * (self.sectors_per_cluster as u32)) - } - - pub(super) fn cluster_size(&self) -> usize { - self.sectors_per_cluster as usize * SECTOR_SIZE - } -} diff --git a/crates/servers/storage-server/src/fs/fat16/dir.rs b/crates/servers/storage-server/src/fs/fat16/dir.rs deleted file mode 100644 index f7a8bdd..0000000 --- a/crates/servers/storage-server/src/fs/fat16/dir.rs +++ /dev/null @@ -1,151 +0,0 @@ -//! Root directory entries and 8.3 names. - -use super::Fat16; -use super::SECTOR_SIZE; -use crate::filesystem::FileName; -use port_block_device::{BlockDevice, SectorId}; - -impl Fat16 { - pub(super) fn find_in_root_dir(&mut self, name: &[u8]) -> Option<(u16, usize)> { - let mut buf = [0u8; SECTOR_SIZE]; - let fat_name = to_fat_name(name); - for s in 0..self.bpb.root_dir_sectors() { - if !self - .device - .read_sector(SectorId::new(self.bpb.root_dir_start() + s), &mut buf) - { - continue; - } - for i in 0..(SECTOR_SIZE / 32) { - let off = i * 32; - if buf[off] == 0x00 || buf[off] == 0xE5 { - continue; - } - if buf[off..off + 11] == fat_name { - let cluster = u16::from_le_bytes([buf[off + 26], buf[off + 27]]); - let size = u32::from_le_bytes([ - buf[off + 28], - buf[off + 29], - buf[off + 30], - buf[off + 31], - ]); - return Some((cluster, size as usize)); - } - } - } - None - } - - pub(super) fn create_dir_entry(&mut self, name: &[u8], cluster: u16) -> bool { - let mut buf = [0u8; SECTOR_SIZE]; - let fat_name = to_fat_name(name); - for s in 0..self.bpb.root_dir_sectors() { - let sector = self.bpb.root_dir_start() + s; - if !self.device.read_sector(SectorId::new(sector), &mut buf) { - continue; - } - for i in 0..(SECTOR_SIZE / 32) { - let off = i * 32; - if buf[off] == 0x00 || buf[off] == 0xE5 { - buf[off..off + 11].copy_from_slice(&fat_name); - buf[off + 11] = 0x20; - for b in &mut buf[off + 12..off + 26] { - *b = 0; - } - buf[off + 26] = cluster as u8; - buf[off + 27] = (cluster >> 8) as u8; - for b in &mut buf[off + 28..off + 32] { - *b = 0; - } - self.device.write_sector(SectorId::new(sector), &buf); - return true; - } - } - } - false - } - - pub(super) fn delete_dir_entry(&mut self, name: &[u8]) -> Option { - let mut buf = [0u8; SECTOR_SIZE]; - let fat_name = to_fat_name(name); - for s in 0..self.bpb.root_dir_sectors() { - let sector = self.bpb.root_dir_start() + s; - if !self.device.read_sector(SectorId::new(sector), &mut buf) { - continue; - } - for i in 0..(SECTOR_SIZE / 32) { - let off = i * 32; - if buf[off..off + 11] == fat_name { - let cluster = u16::from_le_bytes([buf[off + 26], buf[off + 27]]); - buf[off] = 0xE5; - self.device.write_sector(SectorId::new(sector), &buf); - return Some(cluster); - } - } - } - None - } - - pub(super) fn update_dir_size(&mut self, name: &[u8], size: u32, cluster: u16) { - let mut buf = [0u8; SECTOR_SIZE]; - let fat_name = to_fat_name(name); - for s in 0..self.bpb.root_dir_sectors() { - let sector = self.bpb.root_dir_start() + s; - if !self.device.read_sector(SectorId::new(sector), &mut buf) { - continue; - } - for i in 0..(SECTOR_SIZE / 32) { - let off = i * 32; - if buf[off..off + 11] == fat_name { - buf[off + 28..off + 32].copy_from_slice(&size.to_le_bytes()); - buf[off + 26] = cluster as u8; - buf[off + 27] = (cluster >> 8) as u8; - self.device.write_sector(SectorId::new(sector), &buf); - return; - } - } - } - } -} - -pub(super) fn to_fat_name(name: &[u8]) -> [u8; 11] { - let mut fat = [b' '; 11]; - let dot_pos = name.iter().position(|&b| b == b'.').unwrap_or(name.len()); - for i in 0..dot_pos.min(8) { - fat[i] = name[i].to_ascii_uppercase(); - } - if dot_pos < name.len() { - for i in 0..(name.len() - dot_pos - 1).min(3) { - fat[8 + i] = name[dot_pos + 1 + i].to_ascii_uppercase(); - } - } - fat -} - -pub(super) fn from_fat_name(raw: &[u8]) -> FileName { - let mut buf = [0u8; 32]; - let mut pos = 0; - let name_end = raw[..8] - .iter() - .rposition(|&b| b != b' ') - .map(|i| i + 1) - .unwrap_or(0); - for i in 0..name_end { - buf[pos] = raw[i].to_ascii_lowercase(); - pos += 1; - } - let ext_end = raw[8..11] - .iter() - .rposition(|&b| b != b' ') - .map(|i| i + 1) - .unwrap_or(0); - if ext_end > 0 { - buf[pos] = b'.'; - pos += 1; - for i in 0..ext_end { - buf[pos] = raw[8 + i].to_ascii_lowercase(); - pos += 1; - } - } - FileName::from_bytes(&buf[..pos]) -} diff --git a/crates/servers/storage-server/src/fs/fat16/fat.rs b/crates/servers/storage-server/src/fs/fat16/fat.rs deleted file mode 100644 index f1db602..0000000 --- a/crates/servers/storage-server/src/fs/fat16/fat.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! FAT table: cluster allocation and chains. - -use super::Fat16; -use super::SECTOR_SIZE; -use port_block_device::{BlockDevice, SectorId}; - -impl Fat16 { - pub(super) fn read_fat_entry(&mut self, cluster: u16) -> u16 { - let fat_offset = (cluster as u32) * 2; - let sector = self.bpb.fat_start() + fat_offset / (SECTOR_SIZE as u32); - let offset = (fat_offset % (SECTOR_SIZE as u32)) as usize; - let mut buf = [0u8; SECTOR_SIZE]; - if !self.device.read_sector(SectorId::new(sector), &mut buf) { - return 0xFFFF; - } - u16::from_le_bytes([buf[offset], buf[offset + 1]]) - } - - pub(super) fn write_fat_entry(&mut self, cluster: u16, value: u16) { - let fat_offset = (cluster as u32) * 2; - let sector = self.bpb.fat_start() + fat_offset / (SECTOR_SIZE as u32); - let offset = (fat_offset % (SECTOR_SIZE as u32)) as usize; - let mut buf = [0u8; SECTOR_SIZE]; - self.device.read_sector(SectorId::new(sector), &mut buf); - buf[offset] = value as u8; - buf[offset + 1] = (value >> 8) as u8; - self.device.write_sector(SectorId::new(sector), &buf); - } - - pub(super) fn alloc_cluster(&mut self) -> Option { - let max = self.bpb.total_sectors as u16 / self.bpb.sectors_per_cluster as u16; - for i in 2..max { - if self.read_fat_entry(i) == 0 { - self.write_fat_entry(i, 0xFFFF); - return Some(i); - } - } - None - } - - pub(super) fn free_chain(&mut self, start: u16) { - let mut cluster = start; - loop { - if cluster < 2 || cluster >= 0xFFF8 { - break; - } - let next = self.read_fat_entry(cluster); - self.write_fat_entry(cluster, 0); - cluster = next; - } - } - - pub(super) fn follow_chain(&mut self, start: u16, skip_clusters: usize) -> u16 { - let mut cluster = start; - for _ in 0..skip_clusters { - let next = self.read_fat_entry(cluster); - if next >= 0xFFF8 { - return cluster; - } - cluster = next; - } - cluster - } -} diff --git a/crates/servers/storage-server/src/fs/fat16/mod.rs b/crates/servers/storage-server/src/fs/fat16/mod.rs deleted file mode 100644 index 4789443..0000000 --- a/crates/servers/storage-server/src/fs/fat16/mod.rs +++ /dev/null @@ -1,275 +0,0 @@ -//! FAT16 filesystem over a block device (root directory only). - -mod bpb; -mod dir; -mod fat; - -use crate::filesystem::{DirEntry, FileHandle, FileName, FileSystem}; -use bpb::Bpb; -use dir::from_fat_name; -use port_block_device::{BlockDevice, SectorId}; - -const SECTOR_SIZE: usize = 512; -const MAX_OPEN_FILES: usize = 8; - -#[derive(Clone, Copy)] -struct OpenFile { - first_cluster: u16, - size: usize, - offset: usize, - name: FileName, -} - -pub struct Fat16 { - device: B, - bpb: Bpb, - open_files: [Option; MAX_OPEN_FILES], -} - -impl Fat16 { - pub fn mount(mut device: B) -> Option { - let mut buf = [0u8; SECTOR_SIZE]; - if !device.read_sector(SectorId::new(0), &mut buf) { - return None; - } - let bpb = Bpb::parse(&buf)?; - Some(Self { - device, - bpb, - open_files: [const { None }; MAX_OPEN_FILES], - }) - } - - /// The open-file slot `handle` names; `None` for handles past the - /// table, which come straight from clients. - fn slot(&mut self, handle: FileHandle) -> Option<&mut Option> { - self.open_files.get_mut(handle.raw() as usize) - } - - fn alloc_handle(&self) -> Option { - for i in 0..MAX_OPEN_FILES { - if self.open_files[i].is_none() { - return Some(FileHandle::new(i as u16)); - } - } - None - } -} - -impl FileSystem for Fat16 { - fn open(&mut self, path: &[u8]) -> Option { - let (cluster, size) = self.find_in_root_dir(path)?; - let handle = self.alloc_handle()?; - *self.slot(handle)? = Some(OpenFile { - first_cluster: cluster, - size, - offset: 0, - name: FileName::from_bytes(path), - }); - Some(handle) - } - - fn read(&mut self, handle: FileHandle, buf: &mut [u8]) -> Option { - let file = (*self.slot(handle)?)?; - if file.offset >= file.size { - return Some(0); - } - - let remaining = file.size - file.offset; - let to_read = buf.len().min(remaining); - let cs = self.bpb.cluster_size(); - - let skip_clusters = file.offset / cs; - let mut cluster = self.follow_chain(file.first_cluster, skip_clusters); - let mut skip = file.offset % cs; - let mut read = 0; - let mut sector_buf = [0u8; SECTOR_SIZE]; - - while read < to_read { - let sector_in_cluster = skip / SECTOR_SIZE; - let byte_in_sector = skip % SECTOR_SIZE; - let Some(base) = self.bpb.cluster_to_sector(cluster) else { - break; - }; - let sector = base + sector_in_cluster as u32; - if !self - .device - .read_sector(SectorId::new(sector), &mut sector_buf) - { - break; - } - - let chunk = (SECTOR_SIZE - byte_in_sector).min(to_read - read); - buf[read..read + chunk] - .copy_from_slice(§or_buf[byte_in_sector..byte_in_sector + chunk]); - read += chunk; - skip += chunk; - - if skip >= cs { - let next = self.read_fat_entry(cluster); - if next >= 0xFFF8 { - break; - } - cluster = next; - skip = 0; - } - } - - if let Some(Some(f)) = self.slot(handle) { - f.offset += read; - } - Some(read) - } - - fn write(&mut self, handle: FileHandle, buf: &[u8]) -> Option { - let mut file = (*self.slot(handle)?)?; - let cs = self.bpb.cluster_size(); - - if file.first_cluster < 2 { - let c = self.alloc_cluster()?; - file.first_cluster = c; - } - - let skip_clusters = file.offset / cs; - let mut cluster = self.follow_chain(file.first_cluster, skip_clusters); - let mut pos = file.offset % cs; - let mut written = 0; - let mut sector_buf = [0u8; SECTOR_SIZE]; - - while written < buf.len() { - let sector_in_cluster = pos / SECTOR_SIZE; - let byte_in_sector = pos % SECTOR_SIZE; - let Some(base) = self.bpb.cluster_to_sector(cluster) else { - break; - }; - let sector = base + sector_in_cluster as u32; - 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]); - // 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; - - if pos >= cs { - let next = self.read_fat_entry(cluster); - if next >= 0xFFF8 { - if written < buf.len() { - if let Some(new_c) = self.alloc_cluster() { - self.write_fat_entry(cluster, new_c); - cluster = new_c; - } else { - break; - } - } - } else { - cluster = next; - } - pos = 0; - } - } - - file.offset += written; - if file.offset > file.size { - file.size = file.offset; - } - *self.slot(handle)? = Some(file); - Some(written) - } - - fn close(&mut self, handle: FileHandle) { - let Some(file) = self.slot(handle).and_then(Option::take) else { - return; - }; - let mut name_buf = [0u8; 32]; - let name_bytes = file.name.as_bytes(); - let len = name_bytes.len().min(32); - name_buf[..len].copy_from_slice(&name_bytes[..len]); - self.update_dir_size(&name_buf[..len], file.size as u32, file.first_cluster); - } - - fn list_dir(&mut self, _path: &[u8], entries: &mut [DirEntry]) -> Option { - let mut buf = [0u8; SECTOR_SIZE]; - let mut count = 0; - for s in 0..self.bpb.root_dir_sectors() { - if !self - .device - .read_sector(SectorId::new(self.bpb.root_dir_start() + s), &mut buf) - { - continue; - } - for i in 0..(SECTOR_SIZE / 32) { - if count >= entries.len() { - return Some(count); - } - let off = i * 32; - if buf[off] == 0x00 { - return Some(count); - } - if buf[off] == 0xE5 || buf[off + 11] & 0x08 != 0 { - continue; - } - let size = u32::from_le_bytes([ - buf[off + 28], - buf[off + 29], - buf[off + 30], - buf[off + 31], - ]); - entries[count] = DirEntry { - name: from_fat_name(&buf[off..off + 11]), - size: size as usize, - is_dir: buf[off + 11] & 0x10 != 0, - }; - count += 1; - } - } - Some(count) - } - - fn create(&mut self, path: &[u8]) -> Option { - if self.find_in_root_dir(path).is_some() { - return None; - } - let cluster = self.alloc_cluster()?; - if !self.create_dir_entry(path, cluster) { - self.write_fat_entry(cluster, 0); - return None; - } - let handle = self.alloc_handle()?; - *self.slot(handle)? = Some(OpenFile { - first_cluster: cluster, - size: 0, - offset: 0, - name: FileName::from_bytes(path), - }); - Some(handle) - } - - fn delete(&mut self, path: &[u8]) -> bool { - if let Some(cluster) = self.delete_dir_entry(path) { - self.free_chain(cluster); - true - } else { - false - } - } - - fn file_size(&mut self, handle: FileHandle) -> Option { - self.slot(handle)?.as_ref().map(|f| f.size) - } - - fn mkdir(&mut self, _path: &[u8]) -> bool { - false - } - - fn writable(&self) -> bool { - self.device.writable() - } -} diff --git a/crates/servers/storage-server/src/fs/mod.rs b/crates/servers/storage-server/src/fs/mod.rs index f40d6d1..82e7cd4 100644 --- a/crates/servers/storage-server/src/fs/mod.rs +++ b/crates/servers/storage-server/src/fs/mod.rs @@ -1,2 +1 @@ pub mod ext4; -pub mod fat16; diff --git a/crates/servers/storage-server/src/lib.rs b/crates/servers/storage-server/src/lib.rs index 3a42d55..f3cdeff 100644 --- a/crates/servers/storage-server/src/lib.rs +++ b/crates/servers/storage-server/src/lib.rs @@ -48,11 +48,10 @@ pub fn run() -> ! { libkos::println!("storage: no block device, exiting"); syscall::exit(); }; - let Some(fat16) = fs::fat16::Fat16::mount(device) else { - libkos::println!("storage: FAT16 mount failed, exiting"); + let Some(ext4) = fs::ext4::Ext4::mount(device) else { + libkos::println!("storage: no ext4 filesystem it can read, exiting"); syscall::exit(); }; - libkos::println!("storage: FAT16 mounted"); - let mut srv = server::StorageServer::new(fat16, RegionBuffers); + let mut srv = server::StorageServer::new(ext4, RegionBuffers); server_framework::serve(&mut srv, ep) } diff --git a/crates/servers/storage-server/src/tests/ext4_real.rs b/crates/servers/storage-server/src/tests/ext4_real.rs new file mode 100644 index 0000000..2fe385f --- /dev/null +++ b/crates/servers/storage-server/src/tests/ext4_real.rs @@ -0,0 +1,172 @@ +//! The ext4 driver against images made by the real `mkfs.ext4`, checked by +//! the real `e2fsck`. Skipped (with a note) where e2fsprogs is missing. + +extern crate std; + +use crate::filesystem::{DirEntry, FileSystem}; +use crate::fs::ext4::Ext4; +use port_block_device::{BlockDevice, SectorId}; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::vec::Vec; + +/// The features the driver does not handle, switched off; see +/// scripts/make-disk-image.sh, which formats the real disk the same way. +pub const MKFS_FEATURES: &str = "^has_journal,^metadata_csum,^64bit,^dir_index,^huge_file,^orphan_file,^metadata_csum_seed"; + +struct FileDisk(Vec); + +impl BlockDevice for FileDisk { + fn sector_size(&self) -> usize { + 512 + } + fn sector_count(&self) -> usize { + self.0.len() / 512 + } + fn read_sector(&mut self, s: SectorId, buf: &mut [u8]) -> bool { + let at = s.raw() as usize * 512; + match self.0.get(at..at + 512) { + Some(src) => { + buf[..512].copy_from_slice(src); + true + } + None => false, + } + } + fn write_sector(&mut self, s: SectorId, buf: &[u8]) -> bool { + let at = s.raw() as usize * 512; + match self.0.get_mut(at..at + 512) { + Some(dst) => { + dst.copy_from_slice(&buf[..512]); + true + } + None => false, + } + } +} + +fn scratch(name: &str) -> PathBuf { + let dir = std::env::temp_dir().join(std::format!("kos-ext4-{}-{}", name, std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(dir.join("root/docs")).unwrap(); + dir +} + +/// An 8 MiB image of `root`, or `None` without mkfs.ext4. +fn mkfs(dir: &Path, features: &str) -> Option> { + let img = dir.join("disk.img"); + let ok = Command::new("mkfs.ext4") + .args(["-q", "-F", "-b", "4096", "-O", features, "-d"]) + .arg(dir.join("root")) + .arg(&img) + .arg("8M") + .status() + .ok()? + .success(); + assert!(ok, "mkfs.ext4 failed"); + Some(std::fs::read(img).unwrap()) +} + +/// e2fsck's verdict on `image`: true when it finds nothing to fix. +fn fsck_clean(dir: &Path, image: &[u8]) -> (bool, std::string::String) { + let img = dir.join("after.img"); + std::fs::write(&img, image).unwrap(); + let out = Command::new("e2fsck").args(["-fn"]).arg(&img).output().unwrap(); + (out.status.success(), std::string::String::from_utf8_lossy(&out.stdout).into_owned()) +} + +fn setup(name: &str) -> Option<(PathBuf, Vec)> { + let dir = scratch(name); + std::fs::write(dir.join("root/hello.txt"), b"hello ext4\n").unwrap(); + let numbers: std::string::String = (1..=3000).map(|i| std::format!("{}\n", i)).collect(); + std::fs::write(dir.join("root/docs/numbers.txt"), numbers).unwrap(); + match mkfs(&dir, MKFS_FEATURES) { + Some(img) => Some((dir, img)), + None => { + std::eprintln!("mkfs.ext4 not found; skipping"); + None + } + } +} + +fn names(fs: &mut Ext4, path: &[u8]) -> Vec { + let mut entries = [DirEntry::empty(); 32]; + let n = fs.list_dir(path, &mut entries).expect("list"); + entries[..n].iter().map(|e| std::string::String::from_utf8_lossy(e.name.as_bytes()).into_owned()).collect() +} + +fn read_all(fs: &mut Ext4, path: &[u8]) -> Vec { + let h = fs.open(path).expect("open"); + let mut out = Vec::new(); + let mut buf = [0u8; 1000]; + loop { + let n = fs.read(h, &mut buf).expect("read"); + if n == 0 { + break; + } + out.extend_from_slice(&buf[..n]); + } + fs.close(h); + out +} + +#[test] +fn reads_what_mkfs_wrote() { + let Some((_, img)) = setup("read") else { return }; + let mut fs = Ext4::mount(FileDisk(img)).expect("mount"); + let root = names(&mut fs, b"/"); + assert!(root.iter().any(|n| n == "hello.txt") && root.iter().any(|n| n == "docs"), "{root:?}"); + assert_eq!(read_all(&mut fs, b"/hello.txt"), b"hello ext4\n"); + let numbers = read_all(&mut fs, b"/docs/numbers.txt"); + assert_eq!(numbers.len(), (1..=3000).map(|i: u32| std::format!("{i}").len() + 1).sum::()); + assert!(numbers.ends_with(b"2999\n3000\n")); +} + +#[test] +fn what_it_writes_passes_e2fsck() { + let Some((dir, img)) = setup("write") else { return }; + let mut fs = Ext4::mount(FileDisk(img)).expect("mount"); + assert!(fs.mkdir(b"/notes")); + let h = fs.create(b"/notes/today.txt").expect("create"); + let text: Vec = (0..9000).map(|i| b'a' + (i % 26) as u8).collect(); + assert_eq!(fs.write(h, &text), Some(text.len())); + fs.close(h); + assert!(fs.delete(b"/hello.txt")); + assert_eq!(read_all(&mut fs, b"/notes/today.txt"), text); + let image = fs.into_device().0; + let (clean, report) = fsck_clean(&dir, &image); + assert!(clean, "e2fsck found problems:\n{report}"); +} + +#[test] +fn a_disk_with_features_it_cannot_handle_is_refused() { + let dir = scratch("refuse"); + std::fs::write(dir.join("root/x"), b"x").unwrap(); + let Some(img) = mkfs(&dir, "metadata_csum,64bit") else { return }; + assert!(Ext4::mount(FileDisk(img)).is_none()); +} + +#[test] +fn a_disk_it_could_read_but_not_keep_consistent_is_read_only() { + let dir = scratch("readonly"); + std::fs::write(dir.join("root/x"), b"x").unwrap(); + // Checksums it would not update, and a journal. + let Some(img) = mkfs(&dir, "^64bit,^metadata_csum_seed,metadata_csum,has_journal") else { return }; + let mut fs = Ext4::mount(FileDisk(img)).expect("readable"); + assert!(!fs.writable()); + assert_eq!(read_all(&mut fs, b"/x"), b"x"); + let Some((_, img)) = setup("writable") else { return }; + assert!(Ext4::mount(FileDisk(img)).unwrap().writable()); +} + +#[test] +fn one_read_fills_the_buffer_across_blocks() { + let Some((_, img)) = setup("bigread") else { return }; + let mut fs = Ext4::mount(FileDisk(img)).expect("mount"); + let h = fs.open(b"docs/numbers.txt").expect("open"); + let mut buf = std::vec![0u8; 10_000]; + assert_eq!(fs.read(h, &mut buf), Some(10_000)); + let total = fs.file_size(h).unwrap(); + assert_eq!(fs.read(h, &mut buf), Some(total - 10_000)); + assert_eq!(fs.read(h, &mut buf), Some(0)); +} diff --git a/crates/servers/storage-server/src/tests/fat16.rs b/crates/servers/storage-server/src/tests/fat16.rs deleted file mode 100644 index 39ea62b..0000000 --- a/crates/servers/storage-server/src/tests/fat16.rs +++ /dev/null @@ -1,188 +0,0 @@ -use crate::adapters::mock_block::MockBlockDevice; -use crate::filesystem::{DirEntry, FileSystem}; -use crate::fs::fat16::Fat16; -use port_block_device::{BlockDevice, SectorId}; - -fn format_fat16_disk(sectors: usize) -> MockBlockDevice { - let mut dev = MockBlockDevice::new(sectors); - let mut boot = [0u8; 512]; - - // BPB - boot[11] = 0x00; - boot[12] = 0x02; // bytes_per_sector = 512 - boot[13] = 1; // sectors_per_cluster = 1 - boot[14] = 1; - boot[15] = 0; // reserved_sectors = 1 - boot[16] = 1; // num_fats = 1 - boot[17] = 16; - boot[18] = 0; // root_entry_count = 16 - boot[19] = sectors as u8; // total_sectors - boot[20] = (sectors >> 8) as u8; - boot[22] = 1; - boot[23] = 0; // sectors_per_fat = 1 - - // boot signature - boot[510] = 0x55; - boot[511] = 0xAA; - - dev.write_sector(SectorId::new(0), &boot); - - // FAT sector (sector 1) — all zeros = all free - let fat = [0u8; 512]; - dev.write_sector(SectorId::new(1), &fat); - - dev -} - -fn mount_test_disk() -> Fat16 { - let dev = format_fat16_disk(32); - Fat16::mount(dev).expect("mount failed") -} - -#[test] -fn mount_valid_disk() { - let dev = format_fat16_disk(32); - assert!(Fat16::mount(dev).is_some()); -} - -#[test] -fn mount_invalid_disk() { - let dev = MockBlockDevice::new(32); // no boot signature - assert!(Fat16::mount(dev).is_none()); -} - -#[test] -fn create_and_open_file() { - let mut fs = mount_test_disk(); - let h = fs.create(b"hello.txt").expect("create"); - fs.close(h); - - let h2 = fs.open(b"hello.txt").expect("open"); - assert_eq!(fs.file_size(h2), Some(0)); - fs.close(h2); -} - -#[test] -fn write_and_read_file() { - let mut fs = mount_test_disk(); - let h = fs.create(b"data.bin").expect("create"); - - let data = b"Hello, FAT16!"; - let written = fs.write(h, data).expect("write"); - assert_eq!(written, data.len()); - - fs.close(h); - - let h2 = fs.open(b"data.bin").expect("open"); - assert_eq!(fs.file_size(h2), Some(data.len())); - - let mut buf = [0u8; 32]; - let read = fs.read(h2, &mut buf).expect("read"); - assert_eq!(read, data.len()); - assert_eq!(&buf[..read], data); - fs.close(h2); -} - -#[test] -fn delete_file() { - let mut fs = mount_test_disk(); - fs.create(b"temp.txt").map(|h| fs.close(h)); - assert!(fs.delete(b"temp.txt")); - assert!(fs.open(b"temp.txt").is_none()); -} - -#[test] -fn list_directory() { - let mut fs = mount_test_disk(); - fs.create(b"a.txt").map(|h| fs.close(h)); - fs.create(b"b.txt").map(|h| fs.close(h)); - - let mut entries = [DirEntry::empty(); 16]; - let count = fs.list_dir(b"/", &mut entries).expect("list"); - assert_eq!(count, 2); -} - -#[test] -fn open_nonexistent_returns_none() { - let mut fs = mount_test_disk(); - assert!(fs.open(b"nope.txt").is_none()); -} - -#[test] -fn create_duplicate_returns_none() { - let mut fs = mount_test_disk(); - fs.create(b"dup.txt").map(|h| fs.close(h)); - assert!(fs.create(b"dup.txt").is_none()); -} - -// ---- hostile input: handles from clients, geometry and chains from disk ---- - -fn with_boot_byte(sectors: usize, index: usize, value: u8) -> MockBlockDevice { - let mut dev = format_fat16_disk(sectors); - let mut boot = [0u8; 512]; - dev.read_sector(SectorId::new(0), &mut boot); - boot[index] = value; - dev.write_sector(SectorId::new(0), &boot); - dev -} - -#[test] -fn mount_rejects_geometry_the_driver_cannot_use() { - // sectors_per_cluster 0 or 3, bytes_per_sector 513 or 0, no FATs, no - // FAT sectors - for (index, value) in [(13, 0), (13, 3), (11, 1), (12, 0), (16, 0), (22, 0)] { - let dev = with_boot_byte(32, index, value); - assert!( - Fat16::mount(dev).is_none(), - "byte {index} = {value} accepted" - ); - } -} - -#[test] -fn handles_past_the_table_are_rejected_not_indexed() { - use crate::filesystem::FileHandle; - let mut fs = mount_test_disk(); - let mut buf = [0u8; 16]; - for raw in [8, 9, u16::MAX] { - let h = FileHandle::new(raw); - assert_eq!(fs.read(h, &mut buf), None); - assert_eq!(fs.write(h, b"x"), None); - assert_eq!(fs.file_size(h), None); - fs.close(h); - } -} - -/// Points a root-directory entry for `name` at `cluster` with `size`. -fn plant_entry(dev: &mut MockBlockDevice, name: &[u8; 11], cluster: u16, size: u32) { - // root dir starts after 1 reserved + 1 FAT sector - let mut dir = [0u8; 512]; - dir[..11].copy_from_slice(name); - dir[11] = 0x20; - dir[26..28].copy_from_slice(&cluster.to_le_bytes()); - dir[28..32].copy_from_slice(&size.to_le_bytes()); - dev.write_sector(SectorId::new(2), &dir); -} - -#[test] -fn a_file_pointing_at_cluster_zero_reads_nothing() { - let mut dev = format_fat16_disk(32); - plant_entry(&mut dev, b"BAD BIN", 0, 4096); - let mut fs = Fat16::mount(dev).unwrap(); - let h = fs.open(b"bad.bin").expect("open"); - let mut buf = [0u8; 64]; - assert_eq!(fs.read(h, &mut buf), Some(0)); -} - -#[test] -fn deleting_a_file_with_a_cyclic_chain_terminates() { - let mut dev = format_fat16_disk(32); - // cluster 2 -> 3 -> 2 -> ... - let mut fat = [0u8; 512]; - fat[4..6].copy_from_slice(&3u16.to_le_bytes()); - fat[6..8].copy_from_slice(&2u16.to_le_bytes()); - dev.write_sector(SectorId::new(1), &fat); - plant_entry(&mut dev, b"LOOP BIN", 2, 4096); - let mut fs = Fat16::mount(dev).unwrap(); - assert!(fs.delete(b"loop.bin")); -} diff --git a/crates/servers/storage-server/src/tests/mod.rs b/crates/servers/storage-server/src/tests/mod.rs index 4aea490..24766d0 100644 --- a/crates/servers/storage-server/src/tests/mod.rs +++ b/crates/servers/storage-server/src/tests/mod.rs @@ -1,4 +1,4 @@ mod ext4; -mod fat16; +mod ext4_real; mod protocol; mod server; diff --git a/crates/types/src/spawn.rs b/crates/types/src/spawn.rs index 1e2022c..d886366 100644 --- a/crates/types/src/spawn.rs +++ b/crates/types/src/spawn.rs @@ -9,7 +9,7 @@ use crate::budget::Limits; pub const SPAWN: usize = 0; -/// Program file names: FAT 8.3, with room to spare. +/// The longest program file name a spawn request carries. pub const MAX_NAME: usize = 16; /// Where init puts the child's stack top (its stack ends at the image). pub const STACK_TOP: usize = 0x10000; diff --git a/docs/ipc.md b/docs/ipc.md index a3a6627..dbbfee6 100644 --- a/docs/ipc.md +++ b/docs/ipc.md @@ -76,7 +76,10 @@ A thread made by `SpawnThread`/`SpawnThreadIn` starts with the caller's `arg` in `a0`. A process's first thread reads it as `types::start_arg`: the slots of the console and log capabilities it was handed. -Boot loads only init and storage (init needs it to read anything else): +Boot loads only init and storage, both packed into the kernel image (they +are what reads the disk, so they cannot come from it). The disk is one ext4 +filesystem; storage refuses one with features it does not understand, and +mounts read-only one it could not keep consistent. init gets storage's process and endpoint in slots 1 and 2, before the board capabilities. Everything else init starts from its manifest (`init/src/manifest.rs`), handing each program only the endpoints, device diff --git a/scripts/make-disk-image.sh b/scripts/make-disk-image.sh index d2f0140..18fb371 100755 --- a/scripts/make-disk-image.sh +++ b/scripts/make-disk-image.sh @@ -1,45 +1,43 @@ #!/bin/bash +# Builds the k-os disk: one ext4 filesystem holding every program init may +# start. Init and storage themselves are packed into the kernel image. set -euo pipefail IMG="${IMG:-boot.img}" -IMG_SIZE_KB=4096 +SIZE="${SIZE:-16M}" TARGET="riscv64gc-unknown-none-elf" BUILD_DIR="target/${TARGET}/release" +# What the storage server's ext4 driver does not maintain, switched off so it +# can write to the disk (see storage-server's tests/ext4_real.rs). +FEATURES="^has_journal,^metadata_csum,^64bit,^dir_index,^huge_file,^orphan_file,^metadata_csum_seed" -echo "=== Creating k-os boot disk image ===" +ROOT="$(mktemp -d)" +trap 'rm -rf "${ROOT}"' EXIT -dd if=/dev/zero of="${IMG}" bs=1K count=${IMG_SIZE_KB} 2>/dev/null -mformat -i "${IMG}" -t 8 -h 2 -s 32 -r 16 -c 1 -M 512 :: -echo "Created and formatted ${IMG} (${IMG_SIZE_KB}KB, FAT16)" - -# Copy with 8.3 names (no long filenames, no tilde mangling) copy() { local src="$1" dst="$2" if [ -f "${BUILD_DIR}/${src}" ]; then - mcopy -i "${IMG}" "${BUILD_DIR}/${src}" "::${dst}" - echo " ${dst}" + cp "${BUILD_DIR}/${src}" "${ROOT}/${dst}" fi } -copy init "init.elf" -copy echo "echo.elf" -copy snake "snake.elf" -copy display-qemu "display.elf" -copy input-qemu "input.elf" -copy log-server "log.elf" +copy echo "echo.elf" +copy snake "snake.elf" +copy display-qemu "display.elf" +copy input-qemu "input.elf" +copy log-server "log.elf" copy permission-manager "permis.elf" -copy storage-qemu "storage.elf" -copy session-server "session.elf" -copy terminal-server "terminal.elf" -copy shell "shell.elf" -# Only the test image: its presence makes init run the SMP self-test. +copy session-server "session.elf" +copy terminal-server "terminal.elf" +copy shell "shell.elf" +# Only the test image: its presence makes init run the self-test. if [ -n "${SMPTEST:-}" ]; then - copy smptest "smptest.elf" - copy spawntest "spawntst.elf" - copy hello "hello.elf" + copy smptest "smptest.elf" + copy spawntest "spawntst.elf" + copy hello "hello.elf" fi -echo "" -echo "Disk contents:" -mdir -i "${IMG}" :: -echo "=== Done: ${IMG} ===" +rm -f "${IMG}" +mkfs.ext4 -q -F -b 4096 -L k-os -O "${FEATURES}" -d "${ROOT}" "${IMG}" "${SIZE}" +echo "=== ${IMG}: ext4, ${SIZE} ===" +ls -1 "${ROOT}" diff --git a/scripts/test-qemu.sh b/scripts/test-qemu.sh index 1d67706..bb20b06 100755 --- a/scripts/test-qemu.sh +++ b/scripts/test-qemu.sh @@ -17,7 +17,19 @@ timeout ${QT:-120} qemu-system-riscv64 -machine virt -smp "${SMP}" -bios default status=$? grep -a "smptest:\|spawntest:\|Hello\|SMPTEST" "${LOG}" -if grep -aq "SMPTEST PASS" "${LOG}"; then +# What k-os wrote must be ext4 that the real tools accept. +disk_ok=1 +if command -v e2fsck >/dev/null && command -v debugfs >/dev/null; then + if ! e2fsck -fn "${IMAGE}" >"${LOG}.fsck" 2>&1; then + echo "test-qemu: e2fsck rejects the disk:"; cat "${LOG}.fsck"; disk_ok=0 + elif [ "$(debugfs -R 'cat /selftest.txt' "${IMAGE}" 2>/dev/null)" != "written by k-os on ext4" ]; then + echo "test-qemu: /selftest.txt is not on the disk"; disk_ok=0 + else + echo "test-qemu: e2fsck clean, file on disk" + fi + rm -f "${LOG}.fsck" +fi +if [ "${disk_ok}" -eq 1 ] && grep -aq "SMPTEST PASS" "${LOG}"; then echo "test-qemu: pass (${SMP} harts)" rm -f "${LOG}" exit 0