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

This commit is contained in:
2026-10-27 17:09:35 +01:00
parent 4030dd4315
commit 0fd568cdad
24 changed files with 390 additions and 809 deletions

View File

@@ -11,7 +11,10 @@ fn main() {
let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
let bins = manifest.join("../../target/riscv64gc-unknown-none-elf/release"); let bins = manifest.join("../../target/riscv64gc-unknown-none-elf/release");
let out = PathBuf::from(std::env::var("OUT_DIR").unwrap()); 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); let path = bins.join(bin);
println!("cargo:rerun-if-changed={}", path.display()); println!("cargo:rerun-if-changed={}", path.display());
let path = if path.is_file() { let path = if path.is_file() {

View File

@@ -12,9 +12,15 @@ use libkos::syscall;
use types::bounded::CapSlot; use types::bounded::CapSlot;
use types::budget::Limits; use types::budget::Limits;
/// Where init put them (see init's manifest: spawn endpoint, then control). /// Where init put them (see init's manifest: `uses`, the spawn endpoint,
const INIT: CapSlot = CapSlot::const_new(0); /// then control).
const CONTROL: CapSlot = CapSlot::const_new(1); 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 /// Over half the allowance (128 pages), so a second one fits only after
/// the first has died and its limits came back. /// the first has died and its limits came back.
@@ -31,7 +37,28 @@ fn run_hello(died: CapSlot) -> Result<(), &'static str> {
Ok(()) 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> { 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")?; let died = syscall::create_notification().map_err(|_| "no notification")?;
run_hello(died)?; run_hello(died)?;
libkos::println!("spawntest: spawned program runs and its death is heard: ok"); libkos::println!("spawntest: spawned program runs and its death is heard: ok");

View File

@@ -26,6 +26,7 @@ pub struct VirtioBlockDevice {
mmio: UserMmio, mmio: UserMmio,
queue: VirtioQueue, queue: VirtioQueue,
dma: DmaAddrs, dma: DmaAddrs,
sectors: usize,
} }
impl VirtioBlockDevice { impl VirtioBlockDevice {
@@ -52,20 +53,23 @@ impl VirtioBlockDevice {
data_pa: PhysicalAddress::from_raw(hdr_pa + 32), data_pa: PhysicalAddress::from_raw(hdr_pa + 32),
data_va: VirtualAddress::from_raw(REQ_HDR_VA + 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 { Some(Self {
mmio: UserMmio, mmio: UserMmio,
queue: VirtioQueue::new(QueueAddrs::from_base(VQUEUE_VA)), queue: VirtioQueue::new(QueueAddrs::from_base(VQUEUE_VA)),
dma, dma,
sectors: usize::try_from(capacity).ok()?,
}) })
} }
} }
impl BlockDevice for VirtioBlockDevice { impl BlockDevice for VirtioBlockDevice {
fn sector_size(&self) -> usize { SECTOR_SIZE } 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 { 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]; let mut sector_buf = [0u8; SECTOR_SIZE];
if !block::read_sector(&self.mmio, &self.queue, &self.dma, sector.raw(), &mut sector_buf) { if !block::read_sector(&self.mmio, &self.queue, &self.dma, sector.raw(), &mut sector_buf) {
return false; return false;
@@ -74,8 +78,8 @@ impl BlockDevice for VirtioBlockDevice {
true true
} }
fn write_sector(&mut self, _sector: SectorId, _buf: &[u8]) -> bool { false } fn write_sector(&mut self, sector: SectorId, buf: &[u8]) -> bool {
if sector.raw() as usize >= self.sectors { return false; }
/// Writing is not implemented yet. block::write_sector(&self.mmio, &self.queue, &self.dma, sector.raw(), buf)
fn writable(&self) -> bool { false } }
} }

View File

@@ -60,3 +60,37 @@ pub fn read_sector(
true 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 }
}

View File

@@ -44,7 +44,10 @@ impl<'a> Launcher<'a> {
) -> Result<Self, &'static str> { ) -> Result<Self, &'static str> {
let spawn_ep = syscall::create_endpoint().ok_or("no spawn endpoint")?; let spawn_ep = syscall::create_endpoint().ok_or("no spawn endpoint")?;
let deaths = syscall::create_notification().map_err(|_| "no death notification")?; 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]) { pub(crate) fn storage(&mut self) -> (&mut Storage, &mut [u8]) {

View File

@@ -18,7 +18,7 @@ use types::permission::{Permission, PermissionSet};
/// Where the buffer storage shares with init is mapped. /// Where the buffer storage shares with init is mapped.
const STORAGE_BUF_VA: usize = 0x0180_0000; const STORAGE_BUF_VA: usize = 0x0180_0000;
/// Boot loads storage before init and leaves its endpoint here. /// 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() -> ! { pub fn run() -> ! {
libkos::start::set_console(devices::console()); libkos::start::set_console(devices::console());

View File

@@ -70,6 +70,7 @@ pub const SERVERS: &[Program] = &[
/// spawns through init, then reports and halts. /// spawns through init, then reports and halts.
pub const TESTS: &[Program] = &[Program { pub const TESTS: &[Program] = &[Program {
serves: false, serves: false,
uses: &["storage"],
spawns: Some(Limits { pages: 128, endpoints: 2, notifications: 2, regions: 2 }), spawns: Some(Limits { pages: 128, endpoints: 2, notifications: 2, regions: 2 }),
system_control: true, system_control: true,
..server("spawntest", b"spawntst.elf", SMALL) ..server("spawntest", b"spawntst.elf", SMALL)

View File

@@ -7,6 +7,7 @@ pub struct BlockGroupDescriptor {
pub inode_table: u32, pub inode_table: u32,
pub free_blocks_count: u16, pub free_blocks_count: u16,
pub free_inodes_count: u16, pub free_inodes_count: u16,
pub used_dirs_count: u16,
} }
impl BlockGroupDescriptor { impl BlockGroupDescriptor {
@@ -21,6 +22,7 @@ impl BlockGroupDescriptor {
inode_table: read_u32(&buf, off + 8), inode_table: read_u32(&buf, off + 8),
free_blocks_count: read_u16(&buf, off + 12), free_blocks_count: read_u16(&buf, off + 12),
free_inodes_count: read_u16(&buf, off + 14), 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_u32(&mut buf, off + 8, self.inode_table);
write_u16(&mut buf, off + 12, self.free_blocks_count); 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 + 14, self.free_inodes_count);
write_u16(&mut buf, off + 16, self.used_dirs_count);
write_block(device, gdt_block as u64, spb, &buf) write_block(device, gdt_block as u64, spb, &buf)
} }
} }

View File

@@ -110,6 +110,29 @@ impl<B: BlockDevice> Ext4<B> {
io::write_block(&mut self.device, block_num, self.sb.sectors_per_block(), &block_buf) 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 { pub fn init_empty_inode(mode: u16) -> Inode {
let mut block_data = [0u8; 60]; let mut block_data = [0u8; 60];
io::write_u16(&mut block_data, 0, EXTENT_MAGIC); io::write_u16(&mut block_data, 0, EXTENT_MAGIC);

View File

@@ -50,9 +50,27 @@ impl<B: BlockDevice> Ext4<B> {
open_files: [const { None }; MAX_OPEN_FILES], 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<B: BlockDevice> FileSystem for Ext4<B> { impl<B: BlockDevice> FileSystem for Ext4<B> {
fn writable(&self) -> bool {
self.sb.writable && self.device.writable()
}
fn open(&mut self, path: &[u8]) -> Option<FileHandle> { fn open(&mut self, path: &[u8]) -> Option<FileHandle> {
let inode_num = self.resolve_path(path)?; let inode_num = self.resolve_path(path)?;
let inode = self.read_inode(inode_num)?; let inode = self.read_inode(inode_num)?;
@@ -73,13 +91,9 @@ impl<B: BlockDevice> FileSystem for Ext4<B> {
let file = *self.open_files[handle.raw() as usize].as_ref()?; let file = *self.open_files[handle.raw() as usize].as_ref()?;
if file.offset >= file.size { return Some(0); } if file.offset >= file.size { return Some(0); }
let remaining = file.size - file.offset; // As much as `buf` holds, across blocks: a short read means the end.
let to_read = buf.len().min(remaining); let to_read = buf.len().min(file.size - file.offset);
let bs = self.sb.block_size; let bs = self.sb.block_size;
let logical_block = (file.offset / bs) as u32;
let byte_in_block = file.offset % bs;
let inode = Inode { let inode = Inode {
mode: 0, mode: 0,
size: file.size as u64, size: file.size as u64,
@@ -88,17 +102,22 @@ impl<B: BlockDevice> FileSystem for Ext4<B> {
blocks_count: 0, blocks_count: 0,
block_data: file.block_data, 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]; 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; self.open_files[handle.raw() as usize].as_mut().map(|f| f.offset += done);
let chunk = to_read.min(available); Some(done)
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)
} }
fn write(&mut self, handle: FileHandle, buf: &[u8]) -> Option<usize> { fn write(&mut self, handle: FileHandle, buf: &[u8]) -> Option<usize> {
@@ -262,6 +281,7 @@ impl<B: BlockDevice> FileSystem for Ext4<B> {
if let Some(child_inode) = self.read_inode(child_inode_num) { if let Some(child_inode) = self.read_inode(child_inode_num) {
self.free_all_blocks(&child_inode); self.free_all_blocks(&child_inode);
} }
self.clear_inode(child_inode_num);
self.free_inode(child_inode_num); self.free_inode(child_inode_num);
for slot in self.open_files.iter_mut() { for slot in self.open_files.iter_mut() {
if let Some(f) = slot { if let Some(f) = slot {
@@ -331,6 +351,7 @@ impl<B: BlockDevice> FileSystem for Ext4<B> {
pi.links_count += 1; pi.links_count += 1;
self.write_inode(parent_inode_num, &pi); self.write_inode(parent_inode_num, &pi);
} }
self.count_dir(new_inode_num);
true true
} }

View File

@@ -3,6 +3,22 @@ use super::io::{read_u32, write_u32, read_u16, write_u16, SECTOR_SIZE};
const EXT4_MAGIC: u16 = 0xEF53; 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 struct Superblock {
pub inodes_count: u32, pub inodes_count: u32,
pub blocks_count: u32, pub blocks_count: u32,
@@ -13,6 +29,9 @@ pub struct Superblock {
pub first_data_block: u32, pub first_data_block: u32,
pub free_blocks_count: u32, pub free_blocks_count: u32,
pub free_inodes_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 { impl Superblock {
@@ -22,7 +41,13 @@ impl Superblock {
if magic != EXT4_MAGIC { return None; } if magic != EXT4_MAGIC { return None; }
let log_block_size = read_u32(data, 0x18); 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 { Some(Self {
writable,
inodes_count: read_u32(data, 0x00), inodes_count: read_u32(data, 0x00),
blocks_count: read_u32(data, 0x04), blocks_count: read_u32(data, 0x04),
block_size: 1024 << log_block_size, block_size: 1024 << log_block_size,

View File

@@ -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<Self> {
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<u32> {
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
}
}

View File

@@ -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<B: BlockDevice> Fat16<B> {
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<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 {
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])
}

View File

@@ -1,64 +0,0 @@
//! FAT table: cluster allocation and chains.
use super::Fat16;
use super::SECTOR_SIZE;
use port_block_device::{BlockDevice, SectorId};
impl<B: BlockDevice> Fat16<B> {
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<u16> {
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
}
}

View File

@@ -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<B: BlockDevice> {
device: B,
bpb: Bpb,
open_files: [Option<OpenFile>; MAX_OPEN_FILES],
}
impl<B: BlockDevice> Fat16<B> {
pub fn mount(mut device: B) -> Option<Self> {
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<OpenFile>> {
self.open_files.get_mut(handle.raw() as usize)
}
fn alloc_handle(&self) -> Option<FileHandle> {
for i in 0..MAX_OPEN_FILES {
if self.open_files[i].is_none() {
return Some(FileHandle::new(i as u16));
}
}
None
}
}
impl<B: BlockDevice> FileSystem for Fat16<B> {
fn open(&mut self, path: &[u8]) -> Option<FileHandle> {
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<usize> {
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(&sector_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<usize> {
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), &sector_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<usize> {
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<FileHandle> {
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<usize> {
self.slot(handle)?.as_ref().map(|f| f.size)
}
fn mkdir(&mut self, _path: &[u8]) -> bool {
false
}
fn writable(&self) -> bool {
self.device.writable()
}
}

View File

@@ -1,2 +1 @@
pub mod ext4; pub mod ext4;
pub mod fat16;

View File

@@ -48,11 +48,10 @@ pub fn run() -> ! {
libkos::println!("storage: no block device, exiting"); libkos::println!("storage: no block device, exiting");
syscall::exit(); syscall::exit();
}; };
let Some(fat16) = fs::fat16::Fat16::mount(device) else { let Some(ext4) = fs::ext4::Ext4::mount(device) else {
libkos::println!("storage: FAT16 mount failed, exiting"); libkos::println!("storage: no ext4 filesystem it can read, exiting");
syscall::exit(); syscall::exit();
}; };
libkos::println!("storage: FAT16 mounted"); let mut srv = server::StorageServer::new(ext4, RegionBuffers);
let mut srv = server::StorageServer::new(fat16, RegionBuffers);
server_framework::serve(&mut srv, ep) server_framework::serve(&mut srv, ep)
} }

View File

@@ -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<u8>);
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<Vec<u8>> {
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<u8>)> {
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<FileDisk>, path: &[u8]) -> Vec<std::string::String> {
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<FileDisk>, path: &[u8]) -> Vec<u8> {
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::<usize>());
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<u8> = (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));
}

View File

@@ -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<MockBlockDevice> {
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"));
}

View File

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

View File

@@ -9,7 +9,7 @@
use crate::budget::Limits; use crate::budget::Limits;
pub const SPAWN: usize = 0; 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; pub const MAX_NAME: usize = 16;
/// Where init puts the child's stack top (its stack ends at the image). /// Where init puts the child's stack top (its stack ends at the image).
pub const STACK_TOP: usize = 0x10000; pub const STACK_TOP: usize = 0x10000;

View File

@@ -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`: `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. 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 init gets storage's process and endpoint in slots 1 and 2, before the
board capabilities. Everything else init starts from its manifest board capabilities. Everything else init starts from its manifest
(`init/src/manifest.rs`), handing each program only the endpoints, device (`init/src/manifest.rs`), handing each program only the endpoints, device

View File

@@ -1,45 +1,43 @@
#!/bin/bash #!/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 set -euo pipefail
IMG="${IMG:-boot.img}" IMG="${IMG:-boot.img}"
IMG_SIZE_KB=4096 SIZE="${SIZE:-16M}"
TARGET="riscv64gc-unknown-none-elf" TARGET="riscv64gc-unknown-none-elf"
BUILD_DIR="target/${TARGET}/release" 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() { copy() {
local src="$1" dst="$2" local src="$1" dst="$2"
if [ -f "${BUILD_DIR}/${src}" ]; then if [ -f "${BUILD_DIR}/${src}" ]; then
mcopy -i "${IMG}" "${BUILD_DIR}/${src}" "::${dst}" cp "${BUILD_DIR}/${src}" "${ROOT}/${dst}"
echo " ${dst}"
fi fi
} }
copy init "init.elf" copy echo "echo.elf"
copy echo "echo.elf" copy snake "snake.elf"
copy snake "snake.elf" copy display-qemu "display.elf"
copy display-qemu "display.elf" copy input-qemu "input.elf"
copy input-qemu "input.elf" copy log-server "log.elf"
copy log-server "log.elf"
copy permission-manager "permis.elf" copy permission-manager "permis.elf"
copy storage-qemu "storage.elf" copy session-server "session.elf"
copy session-server "session.elf" copy terminal-server "terminal.elf"
copy terminal-server "terminal.elf" copy shell "shell.elf"
copy shell "shell.elf" # Only the test image: its presence makes init run the self-test.
# Only the test image: its presence makes init run the SMP self-test.
if [ -n "${SMPTEST:-}" ]; then if [ -n "${SMPTEST:-}" ]; then
copy smptest "smptest.elf" copy smptest "smptest.elf"
copy spawntest "spawntst.elf" copy spawntest "spawntst.elf"
copy hello "hello.elf" copy hello "hello.elf"
fi fi
echo "" rm -f "${IMG}"
echo "Disk contents:" mkfs.ext4 -q -F -b 4096 -L k-os -O "${FEATURES}" -d "${ROOT}" "${IMG}" "${SIZE}"
mdir -i "${IMG}" :: echo "=== ${IMG}: ext4, ${SIZE} ==="
echo "=== Done: ${IMG} ===" ls -1 "${ROOT}"

View File

@@ -17,7 +17,19 @@ timeout ${QT:-120} qemu-system-riscv64 -machine virt -smp "${SMP}" -bios default
status=$? status=$?
grep -a "smptest:\|spawntest:\|Hello\|SMPTEST" "${LOG}" 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)" echo "test-qemu: pass (${SMP} harts)"
rm -f "${LOG}" rm -f "${LOG}"
exit 0 exit 0